Skip to content

Prevent admins to be able to view carousel in shops#14364

Merged
rioug merged 6 commits into
openfoodfoundation:masterfrom
chahmedejaz:task/14157-prevent-admin-from-adding-multiple-images
Jul 3, 2026
Merged

Prevent admins to be able to view carousel in shops#14364
rioug merged 6 commits into
openfoodfoundation:masterfrom
chahmedejaz:task/14157-prevent-admin-from-adding-multiple-images

Conversation

@chahmedejaz

@chahmedejaz chahmedejaz commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

What? Why?

As part of the multiple product images roadmap, the carousel functionality is being developed before backoffice support for managing multiple images is available.

Currently, it is possible for products to have multiple images associated with them through the image settings page, even though this functionality is not intended to be available to users yet. To keep production data aligned with the current user experience, this PR:

  1. Adds a data migration that removes secondary images from products, ensuring each product has at most one product-level image.
  2. Removes the ability to create additional product images from the product's image page by hiding the "New Image" action.

Users can still replace an existing product image through the product list and image settings page, so there is no change to the intended image management workflow. A new image can only be added through the product list page.

What should we test?

Scenario 1 - Existing product image management

  1. Create or open a product that already has an image.
  2. Navigate to the product's Images page.
  3. Verify there is no New Image button.
  4. Edit the existing image.
  5. Upload a replacement image and save.
  6. Verify the image is updated successfully.

Scenario 2 - Product without an image

  1. Create or open a product with no image.
  2. Navigate to the product's Images page.
  3. Verify the "no images found" state is displayed.
  4. Verify there is no option to create an additional image from this page.
  5. From the product list, upload an image for the product.
  6. Verify the image appears correctly on the Images page.

Scenario 3 - Migration

  1. Run the migration against data containing products with multiple product images.
  2. Verify that only the first product image record is retained. i.e. There should not be any visual change of image for the product in the app before and after the migration.
  3. Verify that additional product images are removed.

Release notes

Changelog Category (reviewers may add a label for the release notes):

  • User facing changes
  • API changes (V0, V1, DFC or Webhook)
  • Technical changes only
  • Feature toggled

Dependencies

Need the following PR to be merged along with this one:

@github-project-automation github-project-automation Bot moved this to All the things 💤 in OFN Delivery board Jun 2, 2026
@chahmedejaz chahmedejaz force-pushed the task/14157-prevent-admin-from-adding-multiple-images branch 3 times, most recently from 73a5fe9 to 17d5bcb Compare June 3, 2026 20:25
@chahmedejaz chahmedejaz moved this from All the things 💤 to In Progress ⚙ in OFN Delivery board Jun 3, 2026
@chahmedejaz chahmedejaz moved this from In Progress ⚙ to Code review 🔎 in OFN Delivery board Jun 3, 2026
@chahmedejaz chahmedejaz added dependencies user facing changes Thes pull requests affect the user experience labels Jun 3, 2026
@chahmedejaz chahmedejaz marked this pull request as ready for review June 3, 2026 22:46

@rioug rioug left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good, I just have some reservation around the migration testing.

let(:product) { create(:product) }

it 'keeps the first image and removes additional images for a product' do
first_image = described_class::SpreeImage.create!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be using the actual app model instead of the one defined in the migration ? I think technically it should not make a difference but I feel like we are not testing a real production scenario where database entries would be created by a different model. Thought ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, @rioug. I considered using the application Spree::Image model initially as well, but I ended up avoiding it because migrations should ideally be tested independently of the application code.
But your point is valid as well. That way, we may not be testing the actual production scenario. I've addressed it here: b7f8de2

@chahmedejaz chahmedejaz requested a review from rioug June 5, 2026 00:51

@rioug rioug left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice ! thanks for updating the migration spec 👍

@mkllnk mkllnk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Ideally we would modify the UX first and then delete old data in a separate pull request to avoid uploads during deploy. But it's very unlikely. So this is okay.

I have one suggestion below but you can also just leave it as is. Not important.


RSpec.describe EnsureSingleProductImage, type: :migration do
let(:migration) { described_class.new }
let(:attachment) { Rack::Test::UploadedFile.new(Rails.root.join('app/webpacker/images/logo-white.png'), "image/png") }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a helper for this:

Suggested change
let(:attachment) { Rack::Test::UploadedFile.new(Rails.root.join('app/webpacker/images/logo-white.png'), "image/png") }
include FileHelper
let(:attachment) { white_logo_file }

@mkllnk mkllnk moved this from Code review 🔎 to Test Ready 🧪 in OFN Delivery board Jun 5, 2026
@mariocarabotta

mariocarabotta commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

@chahmedejaz
In the description,what do you mean by "Verify that only the earliest product image is retained"?
I might have not made it clear in the original issue, but I believe the image we want to keep is the latest one uploaded, the most recent - maybe that's what you mean by earliest? This is because anytime a new image gets uploaded in the Images section, it's the one that gets displayed in the frontshop.

Also, just wondering: are you planning to directly delete the images or they're still going to be retained for a while in case for very unlikely reasons some revert is needed?

Thank you!

@chahmedejaz

chahmedejaz commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator Author

Hey @mariocarabotta - you're right, but that's the behavior when uploading an image from the Products listing page, where the upload updates the earliest image record.

By "Verify that only the earliest product image is retained", I was referring to the multiple images created when using the New Image button on the Images page. In that scenario, only the first image is actually displayed.

Sorry for the confusion - I just realized the wording is misleading. I'll update the testing instructions. Thanks for pointing it out!

Also, just wondering: are you planning to directly delete the images or they're still going to be retained for a while in case for very unlikely reasons some revert is needed?

Hmm... that's a good callout. My initial thought was to remove the image records that aren't actually being displayed by the application. Before doing that, though, we could take a database backup so we have an easy rollback path if we need to restore anything later.

Edit:
I've updated the instructions with the following:
Verify that only the first product image record is retained. i.e. There should not be any visual change of image for the product in the app before and after the migration.
Please review it looks good. Thanks!

@mariocarabotta

Copy link
Copy Markdown
Collaborator

thanks @chahmedejaz for clarifying, that was actually my mistake, and your approach is correct.
Just to double check, a few simple tests I've done. These are done without the carousel being staged.

Add first image to product

  • Create product
  • Add image A
  • product displays image A

Add new image from settings

  • image A currently displayed
  • add image B by clicking button on Image settings and save
  • image A is still displayed >>> you are correct, the earliest image is retained as primary

Replace image

  • click on image in product list
  • upload image file C
  • product displays new file C, but it's still a single image record (one image in Images settings)
  • Same behaviour happens if I replace the file from the Imags settings

I would still suggest to do a snapshot of the database for backup. Since this is a bug and I am not entirely sure what the software was doing in the past (and why those leftovers are still there :D ), it feels a bit safer to keep the data for a while.

Thank you again!

@chahmedejaz

Copy link
Copy Markdown
Collaborator Author

Thanks @mariocarabotta for the detailed testing. And sure we will make sure to keep the snapshot of the database just in case.

@AMEA-LYON AMEA-LYON self-assigned this Jun 8, 2026
@AMEA-LYON AMEA-LYON added the pr-staged-uk staging.openfoodnetwork.org.uk label Jun 8, 2026
@AMEA-LYON

Copy link
Copy Markdown

Tested successfully scenario 1 & 2...
How do I proceed to test migration scenario ?

@chahmedejaz

Copy link
Copy Markdown
Collaborator Author

Thanks @AMEA-LYON - For the migration scenario, we just need to make sure that before and after the PR the visible images on the products should remain intact. For example we added multiple images on a product. And only the first one is visible on the page. After the migration, the first image should still be visible.

Deletion of images can only be verified when its dependent PR is merged. So, I think we can verify this scenario in the release testing.
Please let me know if you have any questions on this. Thanks

@AMEA-LYON AMEA-LYON added no-staging-UK A tag which does not trigger deployments, indicating a server is being used pr-staged-uk staging.openfoodnetwork.org.uk and removed pr-staged-uk staging.openfoodnetwork.org.uk no-staging-UK A tag which does not trigger deployments, indicating a server is being used labels Jun 8, 2026
@AMEA-LYON

Copy link
Copy Markdown

Thanks for the explanations @chahmedejaz ,

so I deployed 5.5.0, and added 2 images on a product,
when I deployed the PR, the first image was still visible.
I then deleted the image, the second one was still present... You will have to check after deplying the related PR...

@chahmedejaz chahmedejaz force-pushed the task/14157-prevent-admin-from-adding-multiple-images branch from fd32335 to b12596d Compare June 17, 2026 20:26
@chahmedejaz chahmedejaz force-pushed the task/14157-prevent-admin-from-adding-multiple-images branch from b12596d to af31d7a Compare June 24, 2026 01:26
@chahmedejaz

Copy link
Copy Markdown
Collaborator Author

Hi @rioug - the conflict has been resolved.

…first by ID

- Use SpreeProduct model with `has_one :image` to preserve the product's associated image. Since the association has no default order, the first image by ID may differ from the associated image at runtime
- Batch process products in slices of 1000 to reduce memory usage.
- Update spec to rely on the product-image association instead of creation-order assumptions
@chahmedejaz

chahmedejaz commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

I tested this on staging and found an issue with the migration.

The migration was keeping the first image because that's the behavior I observed locally and with newly created products on staging. However, after testing older products, I found that this assumption isn't always correct.

The reason is that the product currently displays its image through a has_one association that doesn't define any ordering. As a result, the database returns an arbitrary matching record, so the "current" image isn't necessarily the first image that was created. Which image is returned depends on the SQL query plan and is effectively undefined.

I've updated the migration to preserve the image that's currently associated with the product (i.e. product.image) and soft-delete all other images. This ensures we keep the image that users currently see, regardless of the order in which the underlying records were created.

Here's the commit for the above change: 33b723c

@chahmedejaz chahmedejaz requested a review from rioug June 28, 2026 20:31
images.where.not(id: image_to_keep.id).update_all(deleted_at: Time.current)
viewable_id: product.id
).where.not(id: image_to_keep.id)
.update_all(deleted_at: Time.current)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that update_all bypasses usual ActiveRecord callback, does the associated physical image gets deleted ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope @rioug - The associated physical image doesn't get deleted in this case. It's preserved for us to restore if required.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying, I had forgotten we wanted to keep the image for potential restore.

@mkllnk mkllnk moved this from Code review 🔎 to Test Ready 🧪 in OFN Delivery board Jun 29, 2026
@AMEA-LYON AMEA-LYON added no-staging-FR A tag which does not trigger deployments, indicating a server is being used pr-staged-fr staging.coopcircuits.fr and removed no-staging-FR A tag which does not trigger deployments, indicating a server is being used labels Jul 1, 2026
@AMEA-LYON

Copy link
Copy Markdown

Tested the 3 scenario successfully ! Congratulations !! 🎉 🎉 🎉

@AMEA-LYON AMEA-LYON removed the pr-staged-fr staging.coopcircuits.fr label Jul 1, 2026
@AMEA-LYON AMEA-LYON moved this from Test Ready 🧪 to Ready to go 🚀 in OFN Delivery board Jul 1, 2026
@sigmundpetersen

Copy link
Copy Markdown
Contributor

@chahmedejaz there's a conflict

@chahmedejaz

Copy link
Copy Markdown
Collaborator Author

Thanks @sigmundpetersen - It's resolved!

@rioug rioug merged commit a9f8577 into openfoodfoundation:master Jul 3, 2026
31 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to go 🚀 to Done in OFN Delivery board Jul 3, 2026
@chahmedejaz

chahmedejaz commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

Hi @rioug - Sorry but this is not meant to be deployed to production yet. It needs to go with its dependent PR mentioned in the description, that's still up. Let me revert this and redraft the release. Sorry for the inconvenience. 😞

@chahmedejaz

Copy link
Copy Markdown
Collaborator Author

I've redrafted the release including this PR's revert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies user facing changes Thes pull requests affect the user experience

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Prevent admins to be able to display carousel in shops

7 participants