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:
|
class FieldsetsMixin:
|
||||||
general_fields: list[str] | None = []
|
general_fields: list[str] | None = []
|
||||||
relation_fields: list[str] | None = []
|
relation_fields: list[str] | None = []
|
||||||
|
|
@ -361,12 +379,14 @@ class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
|
||||||
|
|
||||||
@register(Category)
|
@register(Category)
|
||||||
class CategoryAdmin(
|
class CategoryAdmin(
|
||||||
|
StorefrontLinkMixin,
|
||||||
DjangoQLSearchMixin,
|
DjangoQLSearchMixin,
|
||||||
FieldsetsMixin,
|
FieldsetsMixin,
|
||||||
ActivationActionsMixin,
|
ActivationActionsMixin,
|
||||||
DraggableMPTTAdmin,
|
DraggableMPTTAdmin,
|
||||||
ModelAdmin,
|
ModelAdmin,
|
||||||
):
|
):
|
||||||
|
storefront_path_prefix = "catalog"
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
model = Category
|
model = Category
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
||||||
|
|
@ -417,8 +437,13 @@ class CategoryAdmin(
|
||||||
|
|
||||||
@register(Brand)
|
@register(Brand)
|
||||||
class BrandAdmin(
|
class BrandAdmin(
|
||||||
DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin
|
StorefrontLinkMixin,
|
||||||
|
DjangoQLSearchMixin,
|
||||||
|
FieldsetsMixin,
|
||||||
|
ActivationActionsMixin,
|
||||||
|
ModelAdmin,
|
||||||
):
|
):
|
||||||
|
storefront_path_prefix = "brand"
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
model = Brand
|
model = Brand
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
||||||
|
|
@ -449,12 +474,14 @@ class BrandAdmin(
|
||||||
|
|
||||||
@register(Product)
|
@register(Product)
|
||||||
class ProductAdmin(
|
class ProductAdmin(
|
||||||
|
StorefrontLinkMixin,
|
||||||
DjangoQLSearchMixin,
|
DjangoQLSearchMixin,
|
||||||
FieldsetsMixin,
|
FieldsetsMixin,
|
||||||
ActivationActionsMixin,
|
ActivationActionsMixin,
|
||||||
ModelAdmin,
|
ModelAdmin,
|
||||||
ImportExportModelAdmin,
|
ImportExportModelAdmin,
|
||||||
):
|
):
|
||||||
|
storefront_path_prefix = "product"
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
model = Product
|
model = Product
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
||||||
|
|
|
||||||
|
|
@ -70,15 +70,14 @@ def create_wishlist_on_user_creation_signal(
|
||||||
def create_promocode_on_user_referring(
|
def create_promocode_on_user_referring(
|
||||||
instance: User, created: bool, **kwargs: dict[Any, Any]
|
instance: User, created: bool, **kwargs: dict[Any, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
if not created:
|
||||||
if type(instance.attributes) is not dict:
|
return
|
||||||
instance.attributes = {}
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
if created and instance.attributes.get("referrer", ""):
|
try:
|
||||||
referrer_uuid = urlsafe_base64_decode(
|
attrs = instance.attributes if isinstance(instance.attributes, dict) else {}
|
||||||
instance.attributes.get("referrer", "")
|
|
||||||
).decode()
|
if attrs.get("referrer", ""):
|
||||||
|
referrer_uuid = urlsafe_base64_decode(attrs.get("referrer", "")).decode()
|
||||||
referrer = User.objects.get(uuid=referrer_uuid)
|
referrer = User.objects.get(uuid=referrer_uuid)
|
||||||
code = f"WELCOME-{get_random_string(6)}"
|
code = f"WELCOME-{get_random_string(6)}"
|
||||||
PromoCode.objects.create(
|
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.set_password(password)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
attribute_pairs = kwargs.pop("attributes", "")
|
new_attributes = kwargs.pop("attributes", None)
|
||||||
|
|
||||||
if attribute_pairs:
|
if new_attributes is not None:
|
||||||
for attribute_pair in attribute_pairs.split(";"):
|
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:
|
if "-" in attribute_pair:
|
||||||
attr, value = attribute_pair.split("-", 1)
|
attr, value = attribute_pair.split("-", 1)
|
||||||
if not user.attributes:
|
user.attributes[attr] = value
|
||||||
user.attributes = {}
|
|
||||||
user.attributes.update({attr: value})
|
|
||||||
else:
|
else:
|
||||||
raise BadRequest(
|
raise BadRequest(
|
||||||
_(f"Invalid attribute format: {attribute_pair}")
|
_(f"Invalid attribute format: {attribute_pair}")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
raise BadRequest(_("attributes must be a dict or a string"))
|
||||||
|
|
||||||
for attr, value in kwargs.items():
|
for attr, value in kwargs.items():
|
||||||
if attr == "password" or attr == "confirm_password":
|
if attr == "password" or attr == "confirm_password":
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import orjson
|
import orjson
|
||||||
from cryptography.fernet import InvalidToken
|
from cryptography.fernet import InvalidToken
|
||||||
|
from django import forms
|
||||||
from encrypted_fields.fields import EncryptedTextField
|
from encrypted_fields.fields import EncryptedTextField
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,10 +17,21 @@ class EncryptedJSONTextField(EncryptedTextField):
|
||||||
def get_internal_type(self) -> str:
|
def get_internal_type(self) -> str:
|
||||||
return "TextField"
|
return "TextField"
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
return super().formfield(**{"form_class": forms.JSONField, **kwargs})
|
||||||
|
|
||||||
def get_prep_value(self, value):
|
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")
|
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):
|
def from_db_value(self, value, expression, connection):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from os import getenv
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
|
@ -31,7 +32,7 @@ UNFOLD: dict[str, Any] = {
|
||||||
"950": "#22282d",
|
"950": "#22282d",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"SITE_URL": STOREFRONT_DOMAIN,
|
"SITE_URL": f"https://{STOREFRONT_DOMAIN}",
|
||||||
"SITE_TITLE": f"{PROJECT_NAME} Dashboard",
|
"SITE_TITLE": f"{PROJECT_NAME} Dashboard",
|
||||||
"SITE_HEADER": PROJECT_NAME,
|
"SITE_HEADER": PROJECT_NAME,
|
||||||
"SITE_LOGO": lambda request: static("favicon.png"),
|
"SITE_LOGO": lambda request: static("favicon.png"),
|
||||||
|
|
@ -120,11 +121,18 @@ UNFOLD: dict[str, Any] = {
|
||||||
"icon": "api",
|
"icon": "api",
|
||||||
"link": reverse_lazy("rapidoc-platform"),
|
"link": reverse_lazy("rapidoc-platform"),
|
||||||
},
|
},
|
||||||
|
*(
|
||||||
|
[
|
||||||
{
|
{
|
||||||
"title": "GraphQL",
|
"title": "GraphQL",
|
||||||
"icon": "graph_5",
|
"icon": "graph_5",
|
||||||
"link": reverse_lazy("graphql-platform"),
|
"link": reverse_lazy("graphql-platform"),
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
if getenv("GRAPHQL_INTROSPECTION", "").lower()
|
||||||
|
in ("1", "true", "yes")
|
||||||
|
else []
|
||||||
|
),
|
||||||
{
|
{
|
||||||
"title": _("Taskboard"),
|
"title": _("Taskboard"),
|
||||||
"icon": "view_kanban",
|
"icon": "view_kanban",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue