Merge branch 'master' into storefront-nuxt
This commit is contained in:
commit
cb92b7ddaa
7 changed files with 133 additions and 29 deletions
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
{% extends "admin/change_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block submit_buttons_bottom %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if storefront_url %}
|
||||
<template id="storefront-link-tpl">
|
||||
<a href="{{ storefront_url }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="storefront-link font-medium inline-flex items-center gap-2 px-3 py-2 rounded-default justify-center whitespace-nowrap cursor-pointer text-white text-sm w-full lg:w-auto"
|
||||
style="background:linear-gradient(135deg,#6366f1,#8b5cf6);box-shadow:0 0 14px rgba(99,102,241,.35);">
|
||||
<span class="material-symbols-outlined text-base">open_in_new</span>
|
||||
{% trans "See on site" %}
|
||||
</a>
|
||||
</template>
|
||||
<script>
|
||||
(function() {
|
||||
var tpl = document.getElementById("storefront-link-tpl");
|
||||
if (!tpl) return;
|
||||
var link = tpl.content.firstElementChild.cloneNode(true);
|
||||
var save = document.querySelector('#submit-row button[name="_save"]');
|
||||
if (save) {
|
||||
save.insertAdjacentElement("afterend", link);
|
||||
}
|
||||
tpl.remove();
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
21
engine/core/templates/unfold/helpers/language_switch.html
Normal file
21
engine/core/templates/unfold/helpers/language_switch.html
Normal file
|
|
@ -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 %}
|
||||
<div class="max-h-[280px] overflow-y-auto">
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
<div class="border-t border-base-200 mt-1 pt-1 dark:border-base-700"></div>
|
||||
{% endif %}
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue