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/signals.py b/engine/core/signals.py index 43fe5e65..9fce91da 100644 --- a/engine/core/signals.py +++ b/engine/core/signals.py @@ -70,15 +70,14 @@ 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", ""): - referrer_uuid = urlsafe_base64_decode( - instance.attributes.get("referrer", "") - ).decode() + try: + attrs = instance.attributes if isinstance(instance.attributes, dict) else {} + + if attrs.get("referrer", ""): + referrer_uuid = urlsafe_base64_decode(attrs.get("referrer", "")).decode() referrer = User.objects.get(uuid=referrer_uuid) code = f"WELCOME-{get_random_string(6)}" PromoCode.objects.create( 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..83cb0272 --- /dev/null +++ b/engine/core/templates/admin/core/change_form_with_storefront_link.html @@ -0,0 +1,31 @@ +{% extends "admin/change_form.html" %} +{% load i18n %} + +{% block submit_buttons_bottom %} + {{ block.super }} + + {% if storefront_url %} + + + {% 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..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: - 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}) - else: - raise BadRequest( - _(f"Invalid attribute format: {attribute_pair}") - ) + if new_attributes is not None: + if not isinstance(user.attributes, dict): + user.attributes = {} + + 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..2dd7a482 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,10 +17,21 @@ 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): + # Encrypt directly instead of delegating to super().get_prep_value(). + # The parent chain (EncryptedFieldMixin → CharField) calls + # self.to_python() which parses the JSON string back to a dict, + # then str(dict) produces Python repr with single quotes — not JSON. + if value is None: + return None + if not isinstance(value, str): value = orjson.dumps(value, default=str).decode("utf-8") - return super().get_prep_value(value) + if not value: + return None + return self.f.encrypt(value.encode("utf-8")).decode("utf-8") def from_db_value(self, value, expression, connection): if value is None: 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",