From 7e9c0f6dc230df74d2d5594bde28f1eb2edabd6c Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Mon, 9 Mar 2026 18:13:53 +0300 Subject: [PATCH] feat(admin): add reusable submit row with storefront link component refactor submit row logic into a dedicated reusable template (`submit_line_with_storefront_link.html`) to improve maintainability and reduce duplication across admin templates. - Updated `change_form_with_storefront_link.html` to use the new template. - Added missing `formfield` implementation for `EncryptedJSONField` to ensure proper form compatibility. - Enhanced attributes handling in user-related mutation and signal functions for better flexibility and error handling. --- engine/core/signals.py | 13 +-- .../change_form_with_storefront_link.html | 15 +--- .../submit_line_with_storefront_link.html | 79 +++++++++++++++++++ engine/vibes_auth/graphene/mutations.py | 26 +++--- schon/fields.py | 4 + 5 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 engine/core/templates/admin/core/submit_line_with_storefront_link.html diff --git a/engine/core/signals.py b/engine/core/signals.py index 43fe5e65..6e71c485 100644 --- a/engine/core/signals.py +++ b/engine/core/signals.py @@ -70,14 +70,15 @@ def create_wishlist_on_user_creation_signal( def create_promocode_on_user_referring( instance: User, created: bool, **kwargs: dict[Any, Any] ) -> None: - try: - if type(instance.attributes) is not dict: - instance.attributes = {} - instance.save() + if not created: + return - if created and instance.attributes.get("referrer", ""): + try: + attrs = instance.attributes if isinstance(instance.attributes, dict) else {} + + if attrs.get("referrer", ""): referrer_uuid = urlsafe_base64_decode( - instance.attributes.get("referrer", "") + attrs.get("referrer", "") ).decode() referrer = User.objects.get(uuid=referrer_uuid) code = f"WELCOME-{get_random_string(6)}" diff --git a/engine/core/templates/admin/core/change_form_with_storefront_link.html b/engine/core/templates/admin/core/change_form_with_storefront_link.html index c89434e6..71995314 100644 --- a/engine/core/templates/admin/core/change_form_with_storefront_link.html +++ b/engine/core/templates/admin/core/change_form_with_storefront_link.html @@ -1,18 +1,5 @@ {% extends "admin/change_form.html" %} -{% load i18n admin_modify %} {% block submit_buttons_bottom %} - {% submit_row %} - - {% if original and storefront_url %} -
- - open_in_new - {% trans "See on site" %} - -
- {% endif %} + {% include "admin/core/submit_line_with_storefront_link.html" %} {% endblock %} diff --git a/engine/core/templates/admin/core/submit_line_with_storefront_link.html b/engine/core/templates/admin/core/submit_line_with_storefront_link.html new file mode 100644 index 00000000..b9918324 --- /dev/null +++ b/engine/core/templates/admin/core/submit_line_with_storefront_link.html @@ -0,0 +1,79 @@ +{% load i18n admin_urls unfold %} + +
+
+
+ {% block submit-row %} + {% if show_save %} + {% component "unfold/components/button.html" with submit=1 form=opts.model_name|add:"_form" name="_save" class="w-full lg:w-auto" %} + {% trans "Save" %} + {% endcomponent %} + {% endif %} + + {% for action in actions_submit_line %} + {% component "unfold/components/button.html" with submit=1 form=opts.model_name|add:"_form" name=action.attrs.name|default:action.action_name variant="default" class="w-full lg:w-auto" attrs=action.attrs %} + {% if action.icon %} + + {{ action.icon }} + + {% endif %} + + {{ action.description }} + {% endcomponent %} + {% endfor %} + + {% if show_save_and_continue %} + {% component "unfold/components/button.html" with submit=1 form=opts.model_name|add:"_form" name="_continue" variant="default" class="w-full lg:w-auto" %} + {% if can_change %} + {% trans "Save and continue editing" %} + {% else %} + {% trans "Save and view" %} + {% endif %} + {% endcomponent %} + {% endif %} + + {% if show_save_and_add_another %} + {% component "unfold/components/button.html" with submit=1 form=opts.model_name|add:"_form" name="_addanother" variant="default" class="w-full lg:w-auto" %} + {% trans "Save and add another" %} + {% endcomponent %} + {% endif %} + + {% if show_save_as_new %} + {% component "unfold/components/button.html" with submit=1 form=opts.model_name|add:"_form" name="_saveasnew" variant="default" class="w-full lg:w-auto" %} + {% trans "Save as new" %} + {% endcomponent %} + {% endif %} + + {% if original and storefront_url %} + + open_in_new + {% trans "See on site" %} + + {% endif %} + +
+ {% if show_close or adminform.model_admin.change_form_show_cancel_button %} + {% url opts|admin_urlname:'changelist' as changelist_url %} + {% add_preserved_filters changelist_url as link %} + + {% component "unfold/components/button.html" with href=link variant="default" class="w-full lg:w-auto"%} + {% trans "Close" %} + {% endcomponent %} + {% endif %} + + {% if show_delete_link and original %} + {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} + {% add_preserved_filters delete_url as link %} + + {% component "unfold/components/button.html" with href=delete_url variant="danger" class="mr-auto w-full lg:w-auto" %} + {% trans "Delete" %} {{ opts.verbose_name }} + {% endcomponent %} + {% endif %} +
+ {% endblock %} +
+
+
diff --git a/engine/vibes_auth/graphene/mutations.py b/engine/vibes_auth/graphene/mutations.py index 6ab3841a..b51e42ef 100644 --- a/engine/vibes_auth/graphene/mutations.py +++ b/engine/vibes_auth/graphene/mutations.py @@ -153,19 +153,25 @@ class UpdateUser(Mutation): user.set_password(password) user.save() - attribute_pairs = kwargs.pop("attributes", "") + new_attributes = kwargs.pop("attributes", None) - if attribute_pairs: + if new_attributes is not None: if not isinstance(user.attributes, dict): user.attributes = {} - for attribute_pair in attribute_pairs.split(";"): - if "-" in attribute_pair: - attr, value = attribute_pair.split("-", 1) - user.attributes[attr] = value - else: - raise BadRequest( - _(f"Invalid attribute format: {attribute_pair}") - ) + + if isinstance(new_attributes, dict): + user.attributes.update(new_attributes) + elif isinstance(new_attributes, str) and new_attributes: + for attribute_pair in new_attributes.split(";"): + if "-" in attribute_pair: + attr, value = attribute_pair.split("-", 1) + user.attributes[attr] = value + else: + raise BadRequest( + _(f"Invalid attribute format: {attribute_pair}") + ) + else: + raise BadRequest(_("attributes must be a dict or a string")) for attr, value in kwargs.items(): if attr == "password" or attr == "confirm_password": diff --git a/schon/fields.py b/schon/fields.py index 33dfa3cd..c98e9d2d 100644 --- a/schon/fields.py +++ b/schon/fields.py @@ -1,5 +1,6 @@ import orjson from cryptography.fernet import InvalidToken +from django import forms from encrypted_fields.fields import EncryptedTextField @@ -16,6 +17,9 @@ class EncryptedJSONTextField(EncryptedTextField): def get_internal_type(self) -> str: return "TextField" + def formfield(self, **kwargs): + return super().formfield(**{"form_class": forms.JSONField, **kwargs}) + def get_prep_value(self, value): if value is not None and not isinstance(value, str): value = orjson.dumps(value, default=str).decode("utf-8")