From af69abf8e3ceed778f4110409212e9a700f5cb52 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Mon, 9 Mar 2026 17:38:28 +0300 Subject: [PATCH] feat(admin): add "See on site" link in change forms for better navigation Integrates a "See on site" button in the admin change forms for `Category`, `Brand`, and `Product` models. This allows users to navigate directly to the corresponding storefront page of the object being edited, improving efficiency and user experience. - Introduced `StorefrontLinkMixin` for reusable storefront URL logic. - Added custom template `change_form_with_storefront_link.html` for rendering the button. --- engine/core/admin.py | 29 ++++++++++++++++++- .../change_form_with_storefront_link.html | 18 ++++++++++++ .../unfold/helpers/language_switch.html | 21 ++++++++++++++ engine/vibes_auth/graphene/mutations.py | 6 ++-- schon/settings/unfold.py | 20 +++++++++---- 5 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 engine/core/templates/admin/core/change_form_with_storefront_link.html create mode 100644 engine/core/templates/unfold/helpers/language_switch.html diff --git a/engine/core/admin.py b/engine/core/admin.py index 93be8b04..9fbf6ebc 100644 --- a/engine/core/admin.py +++ b/engine/core/admin.py @@ -69,6 +69,24 @@ from engine.core.models import ( ) +class StorefrontLinkMixin: + """Adds a 'See on site' link button to the change form submit row.""" + + change_form_template = "admin/core/change_form_with_storefront_link.html" + storefront_path_prefix: str = "" + + def changeform_view(self, request, object_id=None, form_url="", extra_context=None): + extra_context = extra_context or {} + if object_id: + obj = self.get_object(request, object_id) + if obj and hasattr(obj, "slug"): + extra_context["storefront_url"] = ( + f"https://{settings.STOREFRONT_DOMAIN}" + f"/{self.storefront_path_prefix}/{obj.slug}" + ) + return super().changeform_view(request, object_id, form_url, extra_context) + + class FieldsetsMixin: general_fields: list[str] | None = [] relation_fields: list[str] | None = [] @@ -361,12 +379,14 @@ class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): @register(Category) class CategoryAdmin( + StorefrontLinkMixin, DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin, ModelAdmin, ): + storefront_path_prefix = "catalog" # noinspection PyClassVar model = Category formfield_overrides = {TextField: {"widget": MarkdownWidget}} @@ -417,8 +437,13 @@ class CategoryAdmin( @register(Brand) class BrandAdmin( - DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin + StorefrontLinkMixin, + DjangoQLSearchMixin, + FieldsetsMixin, + ActivationActionsMixin, + ModelAdmin, ): + storefront_path_prefix = "brand" # noinspection PyClassVar model = Brand formfield_overrides = {TextField: {"widget": MarkdownWidget}} @@ -449,12 +474,14 @@ class BrandAdmin( @register(Product) class ProductAdmin( + StorefrontLinkMixin, DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin, ImportExportModelAdmin, ): + storefront_path_prefix = "product" # noinspection PyClassVar model = Product formfield_overrides = {TextField: {"widget": MarkdownWidget}} 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 new file mode 100644 index 00000000..c89434e6 --- /dev/null +++ b/engine/core/templates/admin/core/change_form_with_storefront_link.html @@ -0,0 +1,18 @@ +{% 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 %} +{% endblock %} diff --git a/engine/core/templates/unfold/helpers/language_switch.html b/engine/core/templates/unfold/helpers/language_switch.html new file mode 100644 index 00000000..05f12b43 --- /dev/null +++ b/engine/core/templates/unfold/helpers/language_switch.html @@ -0,0 +1,21 @@ +{% load i18n %} + +{% get_current_language as LANGUAGE_CODE %} +{% get_available_languages as LANGUAGES %} +{% get_language_info_list for LANGUAGES as languages %} + +{% if show_languages %} +
+ {% if languages_list %} + {% for language in languages_list %} + {% include "unfold/helpers/language_form.html" with language=language %} + {% endfor %} + {% else %} + {% for language in languages %} + {% include "unfold/helpers/language_form.html" with language=language %} + {% endfor %} + {% endif %} +
+ +
+{% endif %} diff --git a/engine/vibes_auth/graphene/mutations.py b/engine/vibes_auth/graphene/mutations.py index 4563b5c9..6ab3841a 100644 --- a/engine/vibes_auth/graphene/mutations.py +++ b/engine/vibes_auth/graphene/mutations.py @@ -156,12 +156,12 @@ class UpdateUser(Mutation): attribute_pairs = kwargs.pop("attributes", "") if attribute_pairs: + 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) - if not user.attributes: - user.attributes = {} - user.attributes.update({attr: value}) + user.attributes[attr] = value else: raise BadRequest( _(f"Invalid attribute format: {attribute_pair}") diff --git a/schon/settings/unfold.py b/schon/settings/unfold.py index 69d4d1a0..02beb794 100644 --- a/schon/settings/unfold.py +++ b/schon/settings/unfold.py @@ -1,3 +1,4 @@ +from os import getenv from typing import Any from django.templatetags.static import static @@ -31,7 +32,7 @@ UNFOLD: dict[str, Any] = { "950": "#22282d", }, }, - "SITE_URL": STOREFRONT_DOMAIN, + "SITE_URL": f"https://{STOREFRONT_DOMAIN}", "SITE_TITLE": f"{PROJECT_NAME} Dashboard", "SITE_HEADER": PROJECT_NAME, "SITE_LOGO": lambda request: static("favicon.png"), @@ -120,11 +121,18 @@ UNFOLD: dict[str, Any] = { "icon": "api", "link": reverse_lazy("rapidoc-platform"), }, - { - "title": "GraphQL", - "icon": "graph_5", - "link": reverse_lazy("graphql-platform"), - }, + *( + [ + { + "title": "GraphQL", + "icon": "graph_5", + "link": reverse_lazy("graphql-platform"), + }, + ] + if getenv("GRAPHQL_INTROSPECTION", "").lower() + in ("1", "true", "yes") + else [] + ), { "title": _("Taskboard"), "icon": "view_kanban",