Skip to content

Move the product read-only flag from ViewBag to the model and remove unread ViewBag values - #1013

Open
KrzysztofPajak wants to merge 9 commits into
developfrom
refactor/remove-viewbag-readonly
Open

KrzysztofPajak wants to merge 9 commits into
developfrom
refactor/remove-viewbag-readonly

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Type: bugfix

Issue

ViewBag.IsReadOnly decides whether a store owner sees the buttons that write to a product. It is untyped, so a misspelled key compiles and turns into "editable", and the views' real input is not visible from their @model. Every write action still checks access on the server, so this concerns the screen only.

Several other ViewBag values were written and read by no view.

Solution

  • ProductModel gets IsReadOnly, set by BaseProductController.Edit (GET). It has [BindNever], so a posted value cannot reach the re-rendered form. The product views read Model.IsReadOnly.
  • Partials/UserFields in the Store panel also serves other entities, so it checks Model is ProductModel { IsReadOnly: true }.
  • The unread writes go: AllLanguages (Blog and News), RefreshPage (OrderTags), and the input names in the required product and course popups. Those popups read the names from the query string, which their forms keep. The action parameters that only fed these writes go too.

Other ViewBag uses stay on purpose: Title, the popups that do use it, plugins, Currency/List and Blog/Comments (no model to move to), and IsPopup on pages, which a theme may read. ClosePage in PermissionController is left to a separate pull request.

Review of this change also found defects on the same screens, fixed here in separate commits:

  • A form that failed validation listed every language twice, because the languages were added to the Locales already bound from the form. The next save stored the duplicates. Fixed in Blog (post, category create and edit) and in address, customer, product and specification attributes.
  • Saving an order tag gave no message and re-rendered the form from the POST, so a refresh submitted it again. It now shows a success message and redirects, like the product tag screen.
  • A product name ending in a backslash broke the course popup's script. Its values are now serialized and the name is set as text.
  • Required product names kept a trailing comma when the panel had no access to the last product.

One new resource, Admin.Orders.OrderTags.Updated, in DefaultLanguage.xml and Upgrade/en_250.xml. No new settings or migrations.

Breaking changes

These affect custom plugins and views only. Nothing in this repository depends on them, and URLs are unchanged.

  • These ViewBag values are no longer set, so a custom plugin or view that reads them gets null: IsReadOnly (product edit), AllLanguages (Blog and News), RefreshPage (OrderTags), productIdsInput (required product popup), and btnId, productIdInput and productNameInput (course product popup).
  • Two action signatures change, so a subclass calling them no longer compiles: BaseProductController.RequiredProductAddPopup no longer takes productIdsInput, and CourseController.AssociateProductToCoursePopup (POST) no longer takes btnId, productIdInput or productNameInput.
  • ProductModel has a new property, IsReadOnly.
  • The OrderTagsController constructor takes IRequestTranslationService, and its Edit POST redirects after a successful save.

Testing

The solution builds. Unit tests pass: Grand.Web.Admin.Tests (1849), Grand.Web.Store.Tests (188), Grand.Web.Vendor.Tests (53), Grand.Mapping.Tests (237). The two Product_ToModel snapshots gain only IsReadOnly: false.

Steps 1-5 were run on a fresh install with sample data. Vendor was covered by unit tests only, and so were the address, customer, product and specification attribute screens. Steps 6-8 were run on a fresh install with a second language.

  1. In Admin, open a sample product. Save, Save and continue and Delete are there.
  2. Create a store manager and sign in as them. Open the same product in /Store/Product/Edit/{id}. Save, Save and continue, Delete and the user field form are gone, and Copy is still there.
  3. In the Store panel, create a product. Its edit screen has all of them.
  4. In Admin, open the required product popup from a product and the product popup from a course. Choosing a product fills the field in the opener.
  5. Open Blog and News Create in the Admin and Store panels.
  6. Add a second language. Open a blog category, clear the name and save with client validation off. The form returns with one row per language and keeps what was typed.
  7. Open an order tag, change the name and save. The page reloads with a success message and the new name.
  8. Rename a product to Foo\ <b>x</b>, then choose it in a course's product popup. The field shows that exact text and the popup closes.

support added 4 commits November 1, 2026 21:10
AllLanguages, ClosePage, OrderTags' RefreshPage and the popup input names
were written and never read: the popup views take the input names from the
query string, which their forms keep through asp-route-*. The action
parameters that only fed those writes go with them.
ViewBag.IsReadOnly decides whether a store owner sees the buttons that write
to a product, yet a misspelled key compiled and turned into "editable". As a
model property the compiler checks every reader; it is never bound, so a
posted value cannot reach the re-rendered form.
Its ClosePage writes sit in code that another pull request changes.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 21:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

support added 5 commits November 2, 2026 08:01
The Locales list is already bound from the form, so adding the languages
again on the invalid path listed each language twice. The next save
stored the duplicates. News and the other screens already return the
bound model as it is.
A successful save re-rendered the form from the POST, which looked the
same as a failed one, and a refresh submitted it again. It now behaves
like the product tag screen.
A product name ending in a backslash broke the script, so the opener's
field stayed empty and the popup stayed open. The name now goes in as
text, the same way the required product popup passes its values.
The separator was chosen by array index, so skipping the last product
because the panel has no access to it left "A, " in the field.
The test only checked that the attribute was present. It now posts a
value through the binder and checks that it does not reach the model.
// actually appended, so skipping the last product still leaves a trailing ", ". Pre-existing
// quirk in both original hosts, not introduced by this migration - characterized, not fixed.
Assert.AreEqual("Allowed, ", GetTextProperty(json.Value));
Assert.AreEqual("Allowed", GetTextProperty(json.Value));

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("Edit", redirect.ActionName);

var redirect = result as RedirectToActionResult;
Assert.IsNotNull(redirect);
Assert.AreEqual("List", redirect.ActionName);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants