Compare commits
No commits in common. "faea55c2572a11308df96f95be5288c0d4e1d1b6" and "1e1d0ef39717c0bfd5f7d8619e1eed3c79b2469a" have entirely different histories.
faea55c257
...
1e1d0ef397
25 changed files with 185 additions and 775 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib.admin import register
|
from django.contrib.admin import register
|
||||||
from django.db.models import TextField
|
from django.db.models import TextField
|
||||||
from unfold.admin import ModelAdmin
|
from unfold.admin import ModelAdmin
|
||||||
from unfold_markdown import MarkdownWidget
|
from unfold.contrib.forms.widgets import WysiwygWidget
|
||||||
|
|
||||||
from engine.blog.models import Post, PostTag
|
from engine.blog.models import Post, PostTag
|
||||||
from engine.core.admin import ActivationActionsMixin, FieldsetsMixin
|
from engine.core.admin import ActivationActionsMixin, FieldsetsMixin
|
||||||
|
|
@ -15,7 +15,7 @@ class PostAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
|
||||||
filter_horizontal = ("tags",)
|
filter_horizontal = ("tags",)
|
||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
autocomplete_fields = ("tags",)
|
autocomplete_fields = ("tags",)
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
formfield_overrides = {TextField: {"widget": WysiwygWidget}}
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
"uuid",
|
"uuid",
|
||||||
"slug",
|
"slug",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ from graphene import List, String, relay
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from engine.blog.models import Post, PostTag
|
from engine.blog.models import Post, PostTag
|
||||||
from engine.core.utils.markdown import render_markdown
|
|
||||||
|
|
||||||
|
|
||||||
class PostType(DjangoObjectType):
|
class PostType(DjangoObjectType):
|
||||||
|
|
@ -16,7 +15,7 @@ class PostType(DjangoObjectType):
|
||||||
interfaces = (relay.Node,)
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
def resolve_content(self: Post, _info: HttpRequest) -> str:
|
def resolve_content(self: Post, _info: HttpRequest) -> str:
|
||||||
return render_markdown(self.content or "")
|
return self.content or ""
|
||||||
|
|
||||||
|
|
||||||
class PostTagType(DjangoObjectType):
|
class PostTagType(DjangoObjectType):
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,10 @@ from django.db.models import (
|
||||||
ManyToManyField,
|
ManyToManyField,
|
||||||
TextField,
|
TextField,
|
||||||
)
|
)
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from engine.core.abstract import NiceModel
|
from engine.core.abstract import NiceModel
|
||||||
from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function
|
from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function
|
||||||
from engine.core.utils.markdown import strip_markdown
|
|
||||||
|
|
||||||
|
|
||||||
class Post(NiceModel):
|
class Post(NiceModel):
|
||||||
|
|
@ -72,10 +70,6 @@ class Post(NiceModel):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title} | {self.author.first_name} {self.author.last_name}"
|
return f"{self.title} | {self.author.first_name} {self.author.last_name}"
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def seo_description(self) -> str:
|
|
||||||
return strip_markdown(self.content or "")[:180]
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("post")
|
verbose_name = _("post")
|
||||||
verbose_name_plural = _("posts")
|
verbose_name_plural = _("posts")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
from engine.blog.models import Post, PostTag
|
from engine.blog.models import Post, PostTag
|
||||||
from engine.core.utils.markdown import render_markdown
|
|
||||||
|
|
||||||
|
|
||||||
class PostTagSerializer(ModelSerializer):
|
class PostTagSerializer(ModelSerializer):
|
||||||
|
|
@ -20,4 +19,4 @@ class PostSerializer(ModelSerializer):
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
def get_content(self, obj: Post) -> str:
|
def get_content(self, obj: Post) -> str:
|
||||||
return render_markdown(obj.content or "")
|
return obj.content or ""
|
||||||
|
|
|
||||||
51
engine/blog/widgets.py
Normal file
51
engine/blog/widgets.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.forms.renderers import BaseRenderer
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownEditorWidget(forms.Textarea):
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
"all": (
|
||||||
|
"https://cdnjs.cloudflare.com/ajax/libs/easymde/2.14.0/easymde.min.css",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
js = ("https://cdnjs.cloudflare.com/ajax/libs/easymde/2.14.0/easymde.min.js",)
|
||||||
|
|
||||||
|
def render(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
value: str,
|
||||||
|
attrs: dict[Any, Any] | None = None,
|
||||||
|
renderer: BaseRenderer | None = None,
|
||||||
|
):
|
||||||
|
if not attrs:
|
||||||
|
attrs = {}
|
||||||
|
attrs["class"] = "markdown-editor"
|
||||||
|
textarea_html = super().render(name, value, attrs, renderer)
|
||||||
|
textarea_id = attrs.get("id", f"id_{name}")
|
||||||
|
init_js = f"""
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {{
|
||||||
|
var el = document.getElementById("{textarea_id}");
|
||||||
|
if (!el || !window.EasyMDE) return;
|
||||||
|
new EasyMDE({{
|
||||||
|
element: el,
|
||||||
|
spellChecker: false,
|
||||||
|
renderingConfig: {{ singleLineBreaks: false }},
|
||||||
|
autoDownloadFontAwesome: false,
|
||||||
|
toolbar: [
|
||||||
|
"bold","italic","heading","|",
|
||||||
|
"quote","unordered-list","ordered-list","|",
|
||||||
|
"link","image","|",
|
||||||
|
"preview","side-by-side","fullscreen","|",
|
||||||
|
"guide"
|
||||||
|
]
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
# noinspection DjangoSafeString
|
||||||
|
return mark_safe(textarea_html + init_js)
|
||||||
|
|
@ -5,14 +5,12 @@ from constance.admin import Config
|
||||||
from constance.admin import ConstanceAdmin as BaseConstanceAdmin
|
from constance.admin import ConstanceAdmin as BaseConstanceAdmin
|
||||||
from django.apps import AppConfig, apps
|
from django.apps import AppConfig, apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.admin import register, site
|
from django.contrib.admin import register, site
|
||||||
from django.contrib.gis.admin import GISModelAdmin
|
from django.contrib.gis.admin import GISModelAdmin
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
from django.db.models import Model, TextField
|
from django.db.models import Model
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin
|
from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin
|
||||||
from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin
|
from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin
|
||||||
|
|
@ -35,7 +33,6 @@ from unfold.contrib.import_export.forms import ExportForm, ImportForm
|
||||||
from unfold.decorators import action
|
from unfold.decorators import action
|
||||||
from unfold.typing import FieldsetsType
|
from unfold.typing import FieldsetsType
|
||||||
from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget
|
from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget
|
||||||
from unfold_markdown import MarkdownWidget
|
|
||||||
|
|
||||||
from engine.core.forms import (
|
from engine.core.forms import (
|
||||||
CRMForm,
|
CRMForm,
|
||||||
|
|
@ -57,7 +54,6 @@ from engine.core.models import (
|
||||||
Order,
|
Order,
|
||||||
OrderCrmLink,
|
OrderCrmLink,
|
||||||
OrderProduct,
|
OrderProduct,
|
||||||
PastedImage,
|
|
||||||
Product,
|
Product,
|
||||||
ProductImage,
|
ProductImage,
|
||||||
ProductTag,
|
ProductTag,
|
||||||
|
|
@ -369,7 +365,6 @@ class CategoryAdmin(
|
||||||
):
|
):
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
model = Category
|
model = Category
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"indented_title",
|
"indented_title",
|
||||||
"parent",
|
"parent",
|
||||||
|
|
@ -421,7 +416,6 @@ class BrandAdmin(
|
||||||
):
|
):
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
model = Brand
|
model = Brand
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"name",
|
"name",
|
||||||
"priority",
|
"priority",
|
||||||
|
|
@ -457,7 +451,6 @@ class ProductAdmin(
|
||||||
):
|
):
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
model = Product
|
model = Product
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
|
||||||
actions = ActivationActionsMixin.actions + [
|
actions = ActivationActionsMixin.actions + [
|
||||||
"export_to_marketplaces",
|
"export_to_marketplaces",
|
||||||
"ban_from_marketplaces",
|
"ban_from_marketplaces",
|
||||||
|
|
@ -863,7 +856,6 @@ class PromotionAdmin(
|
||||||
):
|
):
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
model = Promotion
|
model = Promotion
|
||||||
formfield_overrides = {TextField: {"widget": MarkdownWidget}}
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"name",
|
"name",
|
||||||
"discount_percent",
|
"discount_percent",
|
||||||
|
|
@ -1003,38 +995,6 @@ class ProductImageAdmin(
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@register(PastedImage)
|
|
||||||
class PastedImageAdmin(ActivationActionsMixin, ModelAdmin):
|
|
||||||
list_display = ("name", "image_preview", "alt_text", "is_active", "created")
|
|
||||||
list_filter = ("is_active",)
|
|
||||||
search_fields = ("name", "alt_text")
|
|
||||||
readonly_fields = ("uuid", "created", "modified", "image_preview_large")
|
|
||||||
|
|
||||||
fieldsets = (
|
|
||||||
(None, {"fields": ("name", "image", "alt_text")}),
|
|
||||||
(_("Preview"), {"fields": ("image_preview_large",)}),
|
|
||||||
(_("Metadata"), {"fields": ("uuid", "is_active", "created", "modified")}),
|
|
||||||
)
|
|
||||||
|
|
||||||
@admin.display(description=_("preview"))
|
|
||||||
def image_preview(self, obj: PastedImage) -> str:
|
|
||||||
if obj.image:
|
|
||||||
return format_html(
|
|
||||||
'<img src="{}" style="max-height: 50px; max-width: 100px;" />',
|
|
||||||
obj.image.url,
|
|
||||||
)
|
|
||||||
return "-"
|
|
||||||
|
|
||||||
@admin.display(description=_("image preview"))
|
|
||||||
def image_preview_large(self, obj: PastedImage) -> str:
|
|
||||||
if obj.image:
|
|
||||||
return format_html(
|
|
||||||
'<img src="{}" style="max-height: 300px; max-width: 500px;" />',
|
|
||||||
obj.image.url,
|
|
||||||
)
|
|
||||||
return "-"
|
|
||||||
|
|
||||||
|
|
||||||
@register(Address)
|
@register(Address)
|
||||||
class AddressAdmin(DjangoQLSearchMixin, FieldsetsMixin, GISModelAdmin):
|
class AddressAdmin(DjangoQLSearchMixin, FieldsetsMixin, GISModelAdmin):
|
||||||
# noinspection PyClassVar
|
# noinspection PyClassVar
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ from engine.core.models import (
|
||||||
Wishlist,
|
Wishlist,
|
||||||
)
|
)
|
||||||
from engine.core.utils import graphene_abs, graphene_current_lang
|
from engine.core.utils import graphene_abs, graphene_current_lang
|
||||||
from engine.core.utils.markdown import render_markdown
|
|
||||||
from engine.core.utils.seo_builders import (
|
from engine.core.utils.seo_builders import (
|
||||||
brand_schema,
|
brand_schema,
|
||||||
breadcrumb_schema,
|
breadcrumb_schema,
|
||||||
|
|
@ -133,9 +132,6 @@ class BrandType(DjangoObjectType):
|
||||||
filter_fields = ["uuid", "name"]
|
filter_fields = ["uuid", "name"]
|
||||||
description = _("brands")
|
description = _("brands")
|
||||||
|
|
||||||
def resolve_description(self: Brand, _info) -> str:
|
|
||||||
return render_markdown(self.description or "")
|
|
||||||
|
|
||||||
def resolve_categories(self: Brand, info) -> QuerySet[Category]:
|
def resolve_categories(self: Brand, info) -> QuerySet[Category]:
|
||||||
if info.context.user.has_perm("core.view_category"):
|
if info.context.user.has_perm("core.view_category"):
|
||||||
return self.categories.all()
|
return self.categories.all()
|
||||||
|
|
@ -160,7 +156,7 @@ class BrandType(DjangoObjectType):
|
||||||
base = f"https://{settings.BASE_DOMAIN}"
|
base = f"https://{settings.BASE_DOMAIN}"
|
||||||
canonical = f"{base}/{lang}/brand/{self.slug}"
|
canonical = f"{base}/{lang}/brand/{self.slug}"
|
||||||
title = f"{self.name} | {settings.PROJECT_NAME}"
|
title = f"{self.name} | {settings.PROJECT_NAME}"
|
||||||
description = self.seo_description
|
description = (self.description or "")[:180]
|
||||||
|
|
||||||
logo_url = None
|
logo_url = None
|
||||||
if getattr(self, "big_logo", None):
|
if getattr(self, "big_logo", None):
|
||||||
|
|
@ -259,9 +255,6 @@ class CategoryType(DjangoObjectType):
|
||||||
filter_fields = ["uuid"]
|
filter_fields = ["uuid"]
|
||||||
description = _("categories")
|
description = _("categories")
|
||||||
|
|
||||||
def resolve_description(self: Category, _info) -> str:
|
|
||||||
return render_markdown(self.description or "")
|
|
||||||
|
|
||||||
def resolve_children(self, info) -> TreeQuerySet | list[Category]:
|
def resolve_children(self, info) -> TreeQuerySet | list[Category]:
|
||||||
categories = Category.objects.filter(parent=self)
|
categories = Category.objects.filter(parent=self)
|
||||||
if not info.context.user.has_perm("core.view_category"):
|
if not info.context.user.has_perm("core.view_category"):
|
||||||
|
|
@ -299,7 +292,7 @@ class CategoryType(DjangoObjectType):
|
||||||
base = f"https://{settings.BASE_DOMAIN}"
|
base = f"https://{settings.BASE_DOMAIN}"
|
||||||
canonical = f"{base}/{lang}/catalog/{self.slug}"
|
canonical = f"{base}/{lang}/catalog/{self.slug}"
|
||||||
title = f"{self.name} | {settings.PROJECT_NAME}"
|
title = f"{self.name} | {settings.PROJECT_NAME}"
|
||||||
description = self.seo_description
|
description = (self.description or "")[:180]
|
||||||
|
|
||||||
og_image = (
|
og_image = (
|
||||||
graphene_abs(info.context, self.image.url)
|
graphene_abs(info.context, self.image.url)
|
||||||
|
|
@ -567,9 +560,6 @@ class ProductType(DjangoObjectType):
|
||||||
filter_fields = ["uuid", "name"]
|
filter_fields = ["uuid", "name"]
|
||||||
description = _("products")
|
description = _("products")
|
||||||
|
|
||||||
def resolve_description(self: Product, _info) -> str:
|
|
||||||
return render_markdown(self.description or "")
|
|
||||||
|
|
||||||
def resolve_price(self: Product, _info) -> float:
|
def resolve_price(self: Product, _info) -> float:
|
||||||
return self.price or 0.0
|
return self.price or 0.0
|
||||||
|
|
||||||
|
|
@ -602,7 +592,7 @@ class ProductType(DjangoObjectType):
|
||||||
base = f"https://{settings.BASE_DOMAIN}"
|
base = f"https://{settings.BASE_DOMAIN}"
|
||||||
canonical = f"{base}/{lang}/product/{self.slug}"
|
canonical = f"{base}/{lang}/product/{self.slug}"
|
||||||
title = f"{self.name} | {settings.PROJECT_NAME}"
|
title = f"{self.name} | {settings.PROJECT_NAME}"
|
||||||
description = self.seo_description
|
description = (self.description or "")[:180]
|
||||||
|
|
||||||
first_img = self.images.order_by("priority").first()
|
first_img = self.images.order_by("priority").first()
|
||||||
og_image = graphene_abs(info.context, first_img.image.url) if first_img else ""
|
og_image = graphene_abs(info.context, first_img.image.url) if first_img else ""
|
||||||
|
|
@ -699,19 +689,10 @@ class PromotionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Promotion
|
model = Promotion
|
||||||
interfaces = (relay.Node,)
|
interfaces = (relay.Node,)
|
||||||
fields = (
|
fields = ("uuid", "name", "discount_percent", "products")
|
||||||
"uuid",
|
|
||||||
"name",
|
|
||||||
"discount_percent",
|
|
||||||
"description",
|
|
||||||
"products",
|
|
||||||
)
|
|
||||||
filter_fields = ["uuid"]
|
filter_fields = ["uuid"]
|
||||||
description = _("promotions")
|
description = _("promotions")
|
||||||
|
|
||||||
def resolve_description(self: Promotion, _info) -> str:
|
|
||||||
return render_markdown(self.description or "")
|
|
||||||
|
|
||||||
|
|
||||||
class StockType(DjangoObjectType):
|
class StockType(DjangoObjectType):
|
||||||
vendor = Field(VendorType, description=_("vendor"))
|
vendor = Field(VendorType, description=_("vendor"))
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
self._load_demo_data()
|
self._load_demo_data()
|
||||||
|
|
||||||
with override("en-gb"):
|
with override("en"):
|
||||||
if action == "install":
|
if action == "install":
|
||||||
self._install(options)
|
self._install(options)
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
|
|
@ -257,8 +257,8 @@ class Command(BaseCommand):
|
||||||
Attribute.objects.filter(group__name__in=group_names).delete()
|
Attribute.objects.filter(group__name__in=group_names).delete()
|
||||||
AttributeGroup.objects.filter(name__in=group_names).delete()
|
AttributeGroup.objects.filter(name__in=group_names).delete()
|
||||||
|
|
||||||
User.objects.filter(email=f"staff@{DEMO_EMAIL_DOMAIN}").delete()
|
self.staff_user.delete()
|
||||||
User.objects.filter(email=f"super@{DEMO_EMAIL_DOMAIN}").delete()
|
self.super_user.delete()
|
||||||
|
|
||||||
self.stdout.write("")
|
self.stdout.write("")
|
||||||
self.stdout.write(self.style.SUCCESS("=" * 50))
|
self.stdout.write(self.style.SUCCESS("=" * 50))
|
||||||
|
|
@ -409,15 +409,13 @@ class Command(BaseCommand):
|
||||||
product.description_ru_ru = prod_data["description_ru"] # ty: ignore[invalid-assignment]
|
product.description_ru_ru = prod_data["description_ru"] # ty: ignore[invalid-assignment]
|
||||||
product.save()
|
product.save()
|
||||||
|
|
||||||
Stock.objects.get_or_create(
|
Stock.objects.create(
|
||||||
vendor=vendor,
|
vendor=vendor,
|
||||||
product=product,
|
product=product,
|
||||||
defaults={
|
sku=f"GS-{prod_data['partnumber']}",
|
||||||
"sku": f"GS-{prod_data['partnumber']}",
|
price=prod_data["price"],
|
||||||
"price": prod_data["price"],
|
purchase_price=prod_data["purchase_price"],
|
||||||
"purchase_price": prod_data["purchase_price"],
|
quantity=prod_data["quantity"],
|
||||||
"quantity": prod_data["quantity"],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add product image
|
# Add product image
|
||||||
|
|
@ -438,11 +436,8 @@ class Command(BaseCommand):
|
||||||
attribute=attr,
|
attribute=attr,
|
||||||
defaults={"value": value},
|
defaults={"value": value},
|
||||||
)
|
)
|
||||||
if created:
|
if created and value_ru:
|
||||||
if value_ru:
|
av.value_ru_ru = value_ru # ty:ignore[invalid-assignment]
|
||||||
av.value_ru_ru = value_ru # ty:ignore[invalid-assignment]
|
|
||||||
else:
|
|
||||||
av.value_ru_ru = value # ty:ignore[invalid-assignment]
|
|
||||||
av.save()
|
av.save()
|
||||||
|
|
||||||
def _find_image(self, partnumber: str, suffix: str = "") -> Path | None:
|
def _find_image(self, partnumber: str, suffix: str = "") -> Path | None:
|
||||||
|
|
@ -477,9 +472,6 @@ class Command(BaseCommand):
|
||||||
def _save_product_image(
|
def _save_product_image(
|
||||||
self, product: Product, image_path: Path, priority: int
|
self, product: Product, image_path: Path, priority: int
|
||||||
) -> None:
|
) -> None:
|
||||||
if product.images.filter(priority=priority).exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(image_path, "rb") as f:
|
with open(image_path, "rb") as f:
|
||||||
image_content = f.read()
|
image_content = f.read()
|
||||||
|
|
||||||
|
|
@ -518,18 +510,16 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
existing_emails.add(email)
|
existing_emails.add(email)
|
||||||
|
|
||||||
user, created = User.objects.get_or_create(
|
# Create user
|
||||||
|
user = User(
|
||||||
email=email,
|
email=email,
|
||||||
defaults={
|
first_name=first_name,
|
||||||
"first_name": first_name,
|
last_name=last_name,
|
||||||
"last_name": last_name,
|
is_active=True,
|
||||||
"is_active": True,
|
is_verified=True,
|
||||||
"is_verified": True,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if created:
|
user.set_password(password)
|
||||||
user.set_password(password)
|
user.save()
|
||||||
user.save()
|
|
||||||
|
|
||||||
Balance.objects.get_or_create(
|
Balance.objects.get_or_create(
|
||||||
user=user,
|
user=user,
|
||||||
|
|
@ -601,14 +591,12 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
address = Address.objects.filter(user=user).first()
|
address = Address.objects.filter(user=user).first()
|
||||||
|
|
||||||
order, _ = Order.objects.get_or_create(
|
order = Order.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
status=status,
|
status=status,
|
||||||
buy_time=order_date,
|
buy_time=order_date,
|
||||||
defaults={
|
billing_address=address,
|
||||||
"billing_address": address,
|
shipping_address=address,
|
||||||
"shipping_address": address,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Order.objects.filter(pk=order.pk).update(created=order_date)
|
Order.objects.filter(pk=order.pk).update(created=order_date)
|
||||||
|
|
@ -629,14 +617,12 @@ class Command(BaseCommand):
|
||||||
else:
|
else:
|
||||||
op_status = random.choice(["ACCEPTED", "PENDING"])
|
op_status = random.choice(["ACCEPTED", "PENDING"])
|
||||||
|
|
||||||
OrderProduct.objects.get_or_create(
|
OrderProduct.objects.create(
|
||||||
order=order,
|
order=order,
|
||||||
product=product,
|
product=product,
|
||||||
defaults={
|
quantity=quantity,
|
||||||
"quantity": quantity,
|
buy_price=round(price, 2),
|
||||||
"buy_price": round(price, 2),
|
status=op_status,
|
||||||
"status": op_status,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
orders.append(order)
|
orders.append(order)
|
||||||
|
|
@ -693,6 +679,9 @@ class Command(BaseCommand):
|
||||||
tag.save()
|
tag.save()
|
||||||
|
|
||||||
for post_data in data.get("blog_posts", []):
|
for post_data in data.get("blog_posts", []):
|
||||||
|
if Post.objects.filter(title=post_data["title"]).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
content_en = self._load_blog_content(post_data["content_file"], "en")
|
content_en = self._load_blog_content(post_data["content_file"], "en")
|
||||||
content_ru = self._load_blog_content(post_data["content_file"], "ru")
|
content_ru = self._load_blog_content(post_data["content_file"], "ru")
|
||||||
|
|
||||||
|
|
@ -704,23 +693,20 @@ class Command(BaseCommand):
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
post, created = Post.objects.get_or_create(
|
post = Post(
|
||||||
|
author=author,
|
||||||
title=post_data["title"],
|
title=post_data["title"],
|
||||||
defaults={
|
content=content_en,
|
||||||
"author": author,
|
meta_description=post_data.get("meta_description", ""),
|
||||||
"content": content_en,
|
is_static_page=post_data.get("is_static_page", False),
|
||||||
"meta_description": post_data.get("meta_description", ""),
|
|
||||||
"is_static_page": post_data.get("is_static_page", False),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if created:
|
if "title_ru" in post_data:
|
||||||
if "title_ru" in post_data:
|
post.title_ru_ru = post_data["title_ru"] # ty:ignore[unresolved-attribute]
|
||||||
post.title_ru_ru = post_data["title_ru"]
|
if content_ru:
|
||||||
if content_ru:
|
post.content_ru_ru = content_ru # ty:ignore[unresolved-attribute]
|
||||||
post.content_ru_ru = content_ru
|
if "meta_description_ru" in post_data:
|
||||||
if "meta_description_ru" in post_data:
|
post.meta_description_ru_ru = post_data["meta_description_ru"] # ty:ignore[unresolved-attribute]
|
||||||
post.meta_description_ru_ru = post_data["meta_description_ru"]
|
post.save()
|
||||||
post.save()
|
|
||||||
|
|
||||||
for tag_name in post_data.get("tags", []):
|
for tag_name in post_data.get("tags", []):
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
# Generated by Django 5.2.11 on 2026-02-27 20:25
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import django_extensions.db.fields
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import engine.core.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("core", "0055_alter_brand_categories_alter_product_slug"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="PastedImage",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"uuid",
|
|
||||||
models.UUIDField(
|
|
||||||
default=uuid.uuid4,
|
|
||||||
editable=False,
|
|
||||||
help_text="unique id is used to surely identify any database object",
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="unique id",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_active",
|
|
||||||
models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
help_text="if set to false, this object can't be seen by users without needed permission",
|
|
||||||
verbose_name="is active",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"created",
|
|
||||||
django_extensions.db.fields.CreationDateTimeField(
|
|
||||||
auto_now_add=True,
|
|
||||||
help_text="when the object first appeared on the database",
|
|
||||||
verbose_name="created",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"modified",
|
|
||||||
django_extensions.db.fields.ModificationDateTimeField(
|
|
||||||
auto_now=True,
|
|
||||||
help_text="when the object was last modified",
|
|
||||||
verbose_name="modified",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"name",
|
|
||||||
models.CharField(
|
|
||||||
help_text="descriptive name for the image",
|
|
||||||
max_length=100,
|
|
||||||
verbose_name="name",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"image",
|
|
||||||
models.ImageField(
|
|
||||||
help_text="image file pasted in the markdown editor",
|
|
||||||
upload_to=engine.core.models.get_pasted_image_path,
|
|
||||||
verbose_name="image",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"alt_text",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
default="",
|
|
||||||
help_text="alternative text for accessibility",
|
|
||||||
max_length=255,
|
|
||||||
verbose_name="alt text",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "pasted image",
|
|
||||||
"verbose_name_plural": "pasted images",
|
|
||||||
"ordering": ("-created",),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -69,7 +69,6 @@ from engine.core.utils import (
|
||||||
)
|
)
|
||||||
from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function
|
from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function
|
||||||
from engine.core.utils.lists import FAILED_STATUSES
|
from engine.core.utils.lists import FAILED_STATUSES
|
||||||
from engine.core.utils.markdown import strip_markdown
|
|
||||||
from engine.core.validators import validate_category_image_dimensions
|
from engine.core.validators import validate_category_image_dimensions
|
||||||
from engine.payments.models import Transaction
|
from engine.payments.models import Transaction
|
||||||
from schon.utils.misc import create_object
|
from schon.utils.misc import create_object
|
||||||
|
|
@ -465,25 +464,17 @@ class Category(NiceModel, MPTTModel):
|
||||||
@cached_property
|
@cached_property
|
||||||
def min_price(self) -> float:
|
def min_price(self) -> float:
|
||||||
return (
|
return (
|
||||||
self.products.filter(is_active=True, stocks__is_active=True).aggregate(
|
self.products.filter(is_active=True).aggregate(Min("price"))["price__min"]
|
||||||
Min("stocks__price")
|
|
||||||
)["stocks__price__min"]
|
|
||||||
or 0.0
|
or 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def max_price(self) -> float:
|
def max_price(self) -> float:
|
||||||
return (
|
return (
|
||||||
self.products.filter(is_active=True, stocks__is_active=True).aggregate(
|
self.products.filter(is_active=True).aggregate(Max("price"))["price__max"]
|
||||||
Max("stocks__price")
|
|
||||||
)["stocks__price__max"]
|
|
||||||
or 0.0
|
or 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def seo_description(self) -> str:
|
|
||||||
return strip_markdown(self.description or "")[:180]
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("category")
|
verbose_name = _("category")
|
||||||
verbose_name_plural = _("categories")
|
verbose_name_plural = _("categories")
|
||||||
|
|
@ -555,10 +546,6 @@ class Brand(NiceModel):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def seo_description(self) -> str:
|
|
||||||
return strip_markdown(self.description or "")[:180]
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("brand")
|
verbose_name = _("brand")
|
||||||
verbose_name_plural = _("brands")
|
verbose_name_plural = _("brands")
|
||||||
|
|
@ -809,10 +796,6 @@ class Product(NiceModel):
|
||||||
def has_images(self):
|
def has_images(self):
|
||||||
return self.images.exists()
|
return self.images.exists()
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def seo_description(self) -> str:
|
|
||||||
return strip_markdown(self.description or "")[:180]
|
|
||||||
|
|
||||||
|
|
||||||
class Attribute(NiceModel):
|
class Attribute(NiceModel):
|
||||||
__doc__ = _(
|
__doc__ = _(
|
||||||
|
|
@ -1003,10 +986,6 @@ class Promotion(NiceModel):
|
||||||
verbose_name=_("included products"),
|
verbose_name=_("included products"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def seo_description(self) -> str:
|
|
||||||
return strip_markdown(self.description or "")[:180]
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("promotion")
|
verbose_name = _("promotion")
|
||||||
verbose_name_plural = _("promotions")
|
verbose_name_plural = _("promotions")
|
||||||
|
|
@ -2194,35 +2173,3 @@ class DigitalAssetDownload(NiceModel):
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return f"https://api.{settings.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}"
|
return f"https://api.{settings.BASE_DOMAIN}/download/{urlsafe_base64_encode(force_bytes(self.order_product.uuid))}"
|
||||||
|
|
||||||
|
|
||||||
def get_pasted_image_path(instance, filename: str) -> str:
|
|
||||||
return f"pasted_images/{instance.uuid}/{filename}"
|
|
||||||
|
|
||||||
|
|
||||||
class PastedImage(NiceModel):
|
|
||||||
name = CharField(
|
|
||||||
max_length=100,
|
|
||||||
verbose_name=_("name"),
|
|
||||||
help_text=_("descriptive name for the image"),
|
|
||||||
)
|
|
||||||
image = ImageField(
|
|
||||||
upload_to=get_pasted_image_path,
|
|
||||||
verbose_name=_("image"),
|
|
||||||
help_text=_("image file pasted in the markdown editor"),
|
|
||||||
)
|
|
||||||
alt_text = CharField(
|
|
||||||
max_length=255,
|
|
||||||
blank=True,
|
|
||||||
default="",
|
|
||||||
verbose_name=_("alt text"),
|
|
||||||
help_text=_("alternative text for accessibility"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("pasted image")
|
|
||||||
verbose_name_plural = _("pasted images")
|
|
||||||
ordering = ("-created",)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ from engine.core.serializers.simple import (
|
||||||
)
|
)
|
||||||
from engine.core.serializers.utility import AddressSerializer
|
from engine.core.serializers.utility import AddressSerializer
|
||||||
from engine.core.typing import FilterableAttribute
|
from engine.core.typing import FilterableAttribute
|
||||||
from engine.core.utils.markdown import render_markdown
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -61,7 +60,6 @@ class CategoryDetailListSerializer(ListSerializer):
|
||||||
|
|
||||||
class CategoryDetailSerializer(ModelSerializer):
|
class CategoryDetailSerializer(ModelSerializer):
|
||||||
children = SerializerMethodField()
|
children = SerializerMethodField()
|
||||||
description = SerializerMethodField()
|
|
||||||
filterable_attributes = SerializerMethodField()
|
filterable_attributes = SerializerMethodField()
|
||||||
brands = BrandSimpleSerializer(many=True, read_only=True)
|
brands = BrandSimpleSerializer(many=True, read_only=True)
|
||||||
min_price = SerializerMethodField()
|
min_price = SerializerMethodField()
|
||||||
|
|
@ -84,9 +82,6 @@ class CategoryDetailSerializer(ModelSerializer):
|
||||||
"modified",
|
"modified",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_description(self, obj: Category) -> str:
|
|
||||||
return render_markdown(obj.description or "")
|
|
||||||
|
|
||||||
def get_filterable_attributes(self, obj: Category) -> list[FilterableAttribute]:
|
def get_filterable_attributes(self, obj: Category) -> list[FilterableAttribute]:
|
||||||
return obj.filterable_attributes
|
return obj.filterable_attributes
|
||||||
|
|
||||||
|
|
@ -114,14 +109,12 @@ class CategoryDetailSerializer(ModelSerializer):
|
||||||
|
|
||||||
class BrandDetailSerializer(ModelSerializer):
|
class BrandDetailSerializer(ModelSerializer):
|
||||||
categories = CategorySimpleSerializer(many=True)
|
categories = CategorySimpleSerializer(many=True)
|
||||||
description = SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Brand
|
model = Brand
|
||||||
fields = [
|
fields = [
|
||||||
"uuid",
|
"uuid",
|
||||||
"name",
|
"name",
|
||||||
"description",
|
|
||||||
"categories",
|
"categories",
|
||||||
"created",
|
"created",
|
||||||
"modified",
|
"modified",
|
||||||
|
|
@ -129,9 +122,6 @@ class BrandDetailSerializer(ModelSerializer):
|
||||||
"small_logo",
|
"small_logo",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_description(self, obj: Brand) -> str:
|
|
||||||
return render_markdown(obj.description or "")
|
|
||||||
|
|
||||||
|
|
||||||
class BrandProductDetailSerializer(ModelSerializer):
|
class BrandProductDetailSerializer(ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -277,7 +267,6 @@ class ProductDetailSerializer(ModelSerializer):
|
||||||
many=True,
|
many=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
description = SerializerMethodField()
|
|
||||||
rating = SerializerMethodField()
|
rating = SerializerMethodField()
|
||||||
price = SerializerMethodField()
|
price = SerializerMethodField()
|
||||||
quantity = SerializerMethodField()
|
quantity = SerializerMethodField()
|
||||||
|
|
@ -310,9 +299,6 @@ class ProductDetailSerializer(ModelSerializer):
|
||||||
"personal_orders_only",
|
"personal_orders_only",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_description(self, obj: Product) -> str:
|
|
||||||
return render_markdown(obj.description or "")
|
|
||||||
|
|
||||||
def get_rating(self, obj: Product) -> float:
|
def get_rating(self, obj: Product) -> float:
|
||||||
return obj.rating
|
return obj.rating
|
||||||
|
|
||||||
|
|
@ -336,7 +322,6 @@ class PromotionDetailSerializer(ModelSerializer):
|
||||||
products = ProductDetailSerializer(
|
products = ProductDetailSerializer(
|
||||||
many=True,
|
many=True,
|
||||||
)
|
)
|
||||||
description = SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Promotion
|
model = Promotion
|
||||||
|
|
@ -350,9 +335,6 @@ class PromotionDetailSerializer(ModelSerializer):
|
||||||
"modified",
|
"modified",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_description(self, obj: Promotion) -> str:
|
|
||||||
return render_markdown(obj.description or "")
|
|
||||||
|
|
||||||
|
|
||||||
class WishlistDetailSerializer(ModelSerializer):
|
class WishlistDetailSerializer(ModelSerializer):
|
||||||
products = ProductSimpleSerializer(
|
products = ProductSimpleSerializer(
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ from engine.core.models import (
|
||||||
Wishlist,
|
Wishlist,
|
||||||
)
|
)
|
||||||
from engine.core.serializers.utility import AddressSerializer
|
from engine.core.serializers.utility import AddressSerializer
|
||||||
from engine.core.utils.markdown import render_markdown
|
|
||||||
|
|
||||||
|
|
||||||
class AttributeGroupSimpleSerializer(ModelSerializer):
|
class AttributeGroupSimpleSerializer(ModelSerializer):
|
||||||
|
|
@ -138,7 +137,6 @@ class ProductSimpleSerializer(ModelSerializer):
|
||||||
|
|
||||||
attributes = AttributeValueSimpleSerializer(many=True, read_only=True)
|
attributes = AttributeValueSimpleSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
description = SerializerMethodField()
|
|
||||||
rating = SerializerMethodField()
|
rating = SerializerMethodField()
|
||||||
price = SerializerMethodField()
|
price = SerializerMethodField()
|
||||||
quantity = SerializerMethodField()
|
quantity = SerializerMethodField()
|
||||||
|
|
@ -169,9 +167,6 @@ class ProductSimpleSerializer(ModelSerializer):
|
||||||
"discount_price",
|
"discount_price",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_description(self, obj: Product) -> str:
|
|
||||||
return render_markdown(obj.description or "")
|
|
||||||
|
|
||||||
def get_rating(self, obj: Product) -> float:
|
def get_rating(self, obj: Product) -> float:
|
||||||
return obj.rating
|
return obj.rating
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from engine.core.views import (
|
||||||
ContactUsView,
|
ContactUsView,
|
||||||
DownloadDigitalAssetView,
|
DownloadDigitalAssetView,
|
||||||
GlobalSearchView,
|
GlobalSearchView,
|
||||||
PastedImageUploadView,
|
|
||||||
RequestCursedURLView,
|
RequestCursedURLView,
|
||||||
SupportedLanguagesView,
|
SupportedLanguagesView,
|
||||||
WebsiteParametersView,
|
WebsiteParametersView,
|
||||||
|
|
@ -183,9 +182,4 @@ urlpatterns = [
|
||||||
RequestCursedURLView.as_view(),
|
RequestCursedURLView.as_view(),
|
||||||
name="request_cursed_url",
|
name="request_cursed_url",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"app/pasted_images/upload/",
|
|
||||||
PastedImageUploadView.as_view(),
|
|
||||||
name="pasted_image_upload",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import re
|
|
||||||
from contextlib import suppress
|
|
||||||
|
|
||||||
import markdown
|
|
||||||
from django.utils.html import strip_tags
|
|
||||||
|
|
||||||
|
|
||||||
def render_markdown(text: str) -> str:
|
|
||||||
"""Render a markdown string to HTML."""
|
|
||||||
with suppress(Exception):
|
|
||||||
return markdown.markdown(text, extensions=["tables", "fenced_code", "nl2br"])
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def strip_markdown(text: str) -> str:
|
|
||||||
"""Render markdown to HTML then strip all tags, collapsing whitespace."""
|
|
||||||
with suppress(Exception):
|
|
||||||
html = render_markdown(text)
|
|
||||||
plain = strip_tags(html)
|
|
||||||
return re.sub(r"\s+", " ", plain).strip()
|
|
||||||
return text
|
|
||||||
|
|
@ -36,8 +36,7 @@ from drf_spectacular.utils import extend_schema_view
|
||||||
from drf_spectacular.views import SpectacularAPIView
|
from drf_spectacular.views import SpectacularAPIView
|
||||||
from graphene_file_upload.django import FileUploadGraphQLView
|
from graphene_file_upload.django import FileUploadGraphQLView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.permissions import AllowAny, IsAdminUser
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
@ -59,7 +58,6 @@ from engine.core.models import (
|
||||||
DigitalAssetDownload,
|
DigitalAssetDownload,
|
||||||
Order,
|
Order,
|
||||||
OrderProduct,
|
OrderProduct,
|
||||||
PastedImage,
|
|
||||||
Product,
|
Product,
|
||||||
Wishlist,
|
Wishlist,
|
||||||
)
|
)
|
||||||
|
|
@ -438,27 +436,6 @@ def version(request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
version.__doc__ = str(_("Returns current version of the Schon. "))
|
version.__doc__ = str(_("Returns current version of the Schon. "))
|
||||||
|
|
||||||
|
|
||||||
class PastedImageUploadView(APIView):
|
|
||||||
permission_classes = [IsAdminUser]
|
|
||||||
parser_classes = [MultiPartParser]
|
|
||||||
|
|
||||||
def post(self, request: Request, *args, **kwargs) -> Response:
|
|
||||||
image = request.FILES.get("image")
|
|
||||||
if not image:
|
|
||||||
return Response(
|
|
||||||
{"error": _("no image file provided")},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
obj = PastedImage.objects.create(
|
|
||||||
name=image.name or "pasted",
|
|
||||||
image=image,
|
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
{"data": {"filePath": request.build_absolute_uri(obj.image.url)}},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
||||||
tf_map: dict[str, int] = {"7": 7, "30": 30, "90": 90, "360": 360}
|
tf_map: dict[str, int] = {"7": 7, "30": 30, "90": 90, "360": 360}
|
||||||
tf_param = str(request.GET.get("tf", "30") or "30")
|
tf_param = str(request.GET.get("tf", "30") or "30")
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ class CategoryViewSet(SchonViewSet):
|
||||||
category = self.get_object()
|
category = self.get_object()
|
||||||
|
|
||||||
title = f"{category.name} | {settings.PROJECT_NAME}"
|
title = f"{category.name} | {settings.PROJECT_NAME}"
|
||||||
description = category.seo_description
|
description = (category.description or "")[:180]
|
||||||
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
|
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
|
||||||
og_image = (
|
og_image = (
|
||||||
request.build_absolute_uri(category.image.url)
|
request.build_absolute_uri(category.image.url)
|
||||||
|
|
@ -409,7 +409,7 @@ class BrandViewSet(SchonViewSet):
|
||||||
brand = self.get_object()
|
brand = self.get_object()
|
||||||
|
|
||||||
title = f"{brand.name} | {settings.PROJECT_NAME}"
|
title = f"{brand.name} | {settings.PROJECT_NAME}"
|
||||||
description = brand.seo_description
|
description = (brand.description or "")[:180]
|
||||||
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}"
|
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}"
|
||||||
|
|
||||||
logo_url = (
|
logo_url = (
|
||||||
|
|
@ -555,7 +555,7 @@ class ProductViewSet(SchonViewSet):
|
||||||
images = list(p.images.all()[:6])
|
images = list(p.images.all()[:6])
|
||||||
rating = {"value": p.rating, "count": p.feedbacks_count}
|
rating = {"value": p.rating, "count": p.feedbacks_count}
|
||||||
title = f"{p.name} | {settings.PROJECT_NAME}"
|
title = f"{p.name} | {settings.PROJECT_NAME}"
|
||||||
description = p.seo_description
|
description = (p.description or "")[:180]
|
||||||
canonical = (
|
canonical = (
|
||||||
f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
|
f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import Prefetch, QuerySet
|
from django.db.models import Prefetch, QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework_simplejwt.token_blacklist.admin import (
|
from rest_framework_simplejwt.token_blacklist.admin import (
|
||||||
BlacklistedTokenAdmin as BaseBlacklistedTokenAdmin,
|
BlacklistedTokenAdmin as BaseBlacklistedTokenAdmin,
|
||||||
|
|
@ -28,8 +29,8 @@ from rest_framework_simplejwt.token_blacklist.models import (
|
||||||
OutstandingToken as BaseOutstandingToken,
|
OutstandingToken as BaseOutstandingToken,
|
||||||
)
|
)
|
||||||
from unfold.admin import ModelAdmin, TabularInline
|
from unfold.admin import ModelAdmin, TabularInline
|
||||||
|
from unfold.contrib.forms.widgets import WysiwygWidget
|
||||||
from unfold.forms import AdminPasswordChangeForm, UserCreationForm
|
from unfold.forms import AdminPasswordChangeForm, UserCreationForm
|
||||||
from unfold_markdown import MarkdownWidget
|
|
||||||
|
|
||||||
from engine.core.admin import ActivationActionsMixin
|
from engine.core.admin import ActivationActionsMixin
|
||||||
from engine.core.models import Order
|
from engine.core.models import Order
|
||||||
|
|
@ -38,6 +39,7 @@ from engine.vibes_auth.emailing.choices import CampaignStatus
|
||||||
from engine.vibes_auth.emailing.models import (
|
from engine.vibes_auth.emailing.models import (
|
||||||
CampaignRecipient,
|
CampaignRecipient,
|
||||||
EmailCampaign,
|
EmailCampaign,
|
||||||
|
EmailImage,
|
||||||
EmailTemplate,
|
EmailTemplate,
|
||||||
)
|
)
|
||||||
from engine.vibes_auth.emailing.tasks import (
|
from engine.vibes_auth.emailing.tasks import (
|
||||||
|
|
@ -194,6 +196,38 @@ class ChatMessageAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ("created", "modified")
|
readonly_fields = ("created", "modified")
|
||||||
|
|
||||||
|
|
||||||
|
@register(EmailImage)
|
||||||
|
class EmailImageAdmin(ActivationActionsMixin, ModelAdmin):
|
||||||
|
list_display = ("name", "image_preview", "alt_text", "is_active", "created")
|
||||||
|
list_filter = ("is_active",)
|
||||||
|
search_fields = ("name", "alt_text")
|
||||||
|
readonly_fields = ("uuid", "created", "modified", "image_preview_large")
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("name", "image", "alt_text")}),
|
||||||
|
(_("Preview"), {"fields": ("image_preview_large",)}),
|
||||||
|
(_("Metadata"), {"fields": ("uuid", "is_active", "created", "modified")}),
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.display(description=_("preview"))
|
||||||
|
def image_preview(self, obj: EmailImage) -> str:
|
||||||
|
if obj.image:
|
||||||
|
return format_html(
|
||||||
|
'<img src="{}" style="max-height: 50px; max-width: 100px;" />',
|
||||||
|
obj.image.url,
|
||||||
|
)
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
@admin.display(description=_("image preview"))
|
||||||
|
def image_preview_large(self, obj: EmailImage) -> str:
|
||||||
|
if obj.image:
|
||||||
|
return format_html(
|
||||||
|
'<img src="{}" style="max-height: 300px; max-width: 500px;" />',
|
||||||
|
obj.image.url,
|
||||||
|
)
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
|
||||||
@register(EmailTemplate)
|
@register(EmailTemplate)
|
||||||
class EmailTemplateAdmin(ActivationActionsMixin, ModelAdmin):
|
class EmailTemplateAdmin(ActivationActionsMixin, ModelAdmin):
|
||||||
list_display = ("name", "slug", "subject", "is_active", "modified")
|
list_display = ("name", "slug", "subject", "is_active", "modified")
|
||||||
|
|
@ -204,7 +238,7 @@ class EmailTemplateAdmin(ActivationActionsMixin, ModelAdmin):
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("name", "slug", "subject")}),
|
(None, {"fields": ("name", "slug", "subject")}),
|
||||||
(_("Content"), {"fields": ("content", "plain_content")}),
|
(_("Content"), {"fields": ("html_content", "plain_content")}),
|
||||||
(
|
(
|
||||||
_("Documentation"),
|
_("Documentation"),
|
||||||
{
|
{
|
||||||
|
|
@ -216,8 +250,8 @@ class EmailTemplateAdmin(ActivationActionsMixin, ModelAdmin):
|
||||||
)
|
)
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||||
if db_field.name == "content":
|
if db_field.name == "html_content":
|
||||||
kwargs["widget"] = MarkdownWidget
|
kwargs["widget"] = WysiwygWidget
|
||||||
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
from engine.vibes_auth.emailing.models import (
|
from engine.vibes_auth.emailing.models import (
|
||||||
CampaignRecipient,
|
CampaignRecipient,
|
||||||
EmailCampaign,
|
EmailCampaign,
|
||||||
|
EmailImage,
|
||||||
EmailTemplate,
|
EmailTemplate,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"EmailImage",
|
||||||
"EmailTemplate",
|
"EmailTemplate",
|
||||||
"EmailCampaign",
|
"EmailCampaign",
|
||||||
"CampaignRecipient",
|
"CampaignRecipient",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from django.db.models import (
|
||||||
CharField,
|
CharField,
|
||||||
DateTimeField,
|
DateTimeField,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
|
ImageField,
|
||||||
Index,
|
Index,
|
||||||
PositiveIntegerField,
|
PositiveIntegerField,
|
||||||
SlugField,
|
SlugField,
|
||||||
|
|
@ -20,10 +21,42 @@ from engine.vibes_auth.emailing.choices import CampaignStatus, RecipientStatus
|
||||||
|
|
||||||
|
|
||||||
def get_email_image_path(instance, filename: str) -> str:
|
def get_email_image_path(instance, filename: str) -> str:
|
||||||
"""Kept for historic migrations that reference this callable."""
|
|
||||||
return f"email_images/{instance.uuid}/{filename}"
|
return f"email_images/{instance.uuid}/{filename}"
|
||||||
|
|
||||||
|
|
||||||
|
class EmailImage(NiceModel):
|
||||||
|
"""
|
||||||
|
Stores images that can be used in email templates.
|
||||||
|
Images are uploaded to filesystem and referenced by URL in templates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name=_("name"),
|
||||||
|
help_text=_("descriptive name for the image"),
|
||||||
|
)
|
||||||
|
image = ImageField(
|
||||||
|
upload_to=get_email_image_path,
|
||||||
|
verbose_name=_("image"),
|
||||||
|
help_text=_("image file to use in email templates"),
|
||||||
|
)
|
||||||
|
alt_text = CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
verbose_name=_("alt text"),
|
||||||
|
help_text=_("alternative text for accessibility"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("email image")
|
||||||
|
verbose_name_plural = _("email images")
|
||||||
|
ordering = ("-created",)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class EmailTemplate(NiceModel):
|
class EmailTemplate(NiceModel):
|
||||||
"""
|
"""
|
||||||
Customizable email template stored in the database.
|
Customizable email template stored in the database.
|
||||||
|
|
@ -48,10 +81,10 @@ class EmailTemplate(NiceModel):
|
||||||
verbose_name=_("subject"),
|
verbose_name=_("subject"),
|
||||||
help_text=_("email subject line - supports {{ variables }}"),
|
help_text=_("email subject line - supports {{ variables }}"),
|
||||||
)
|
)
|
||||||
content = TextField(
|
html_content = TextField(
|
||||||
verbose_name=_("content"),
|
verbose_name=_("HTML content"),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"email body in markdown - supports {{ user.first_name }}, "
|
"email body content - supports {{ user.first_name }}, "
|
||||||
"{{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}"
|
"{{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from django.utils.html import strip_tags
|
||||||
from django.utils.translation import activate
|
from django.utils.translation import activate
|
||||||
|
|
||||||
from engine.core.utils import get_dynamic_email_connection
|
from engine.core.utils import get_dynamic_email_connection
|
||||||
from engine.core.utils.markdown import render_markdown
|
|
||||||
from engine.vibes_auth.emailing.choices import CampaignStatus, RecipientStatus
|
from engine.vibes_auth.emailing.choices import CampaignStatus, RecipientStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -231,8 +230,7 @@ def send_single_campaign_email(campaign, recipient, connection) -> None:
|
||||||
|
|
||||||
# Render subject and content
|
# Render subject and content
|
||||||
subject = render_template_string(template.subject, context)
|
subject = render_template_string(template.subject, context)
|
||||||
rendered_content = render_template_string(template.content, context)
|
html_content = render_template_string(template.html_content, context)
|
||||||
html_content = render_markdown(rendered_content)
|
|
||||||
|
|
||||||
# Add unsubscribe footer to HTML
|
# Add unsubscribe footer to HTML
|
||||||
html_content = add_unsubscribe_footer(html_content, unsubscribe_url)
|
html_content = add_unsubscribe_footer(html_content, unsubscribe_url)
|
||||||
|
|
|
||||||
|
|
@ -1,393 +0,0 @@
|
||||||
# Generated by Django 5.2.11 on 2026-02-27 20:25
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("vibes_auth", "0008_emailtemplate_html_content_ar_ar_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name="EmailImage",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_ar_ar",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_cs_cz",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_da_dk",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_de_de",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_en_gb",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_en_us",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_es_es",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_fa_ir",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_fr_fr",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_he_il",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_hi_in",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_hr_hr",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_id_id",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_it_it",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_ja_jp",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_kk_kz",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_ko_kr",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_nl_nl",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_no_no",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_pl_pl",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_pt_br",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_ro_ro",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_ru_ru",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_sv_se",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_th_th",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_tr_tr",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_vi_vn",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="html_content_zh_hans",
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content",
|
|
||||||
field=models.TextField(
|
|
||||||
default="<p>null</p>",
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_ar_ar",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_cs_cz",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_da_dk",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_de_de",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_en_gb",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_en_us",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_es_es",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_fa_ir",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_fr_fr",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_he_il",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_hi_in",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_hr_hr",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_id_id",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_it_it",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_ja_jp",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_kk_kz",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_ko_kr",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_nl_nl",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_no_no",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_pl_pl",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_pt_br",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_ro_ro",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_ru_ru",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_sv_se",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_th_th",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_tr_tr",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_vi_vn",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="emailtemplate",
|
|
||||||
name="content_zh_hans",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="email body in markdown - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}",
|
|
||||||
null=True,
|
|
||||||
verbose_name="content",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -6,4 +6,4 @@ from engine.vibes_auth.emailing import EmailTemplate
|
||||||
|
|
||||||
@register(EmailTemplate)
|
@register(EmailTemplate)
|
||||||
class EmailTemplateOptions(TranslationOptions):
|
class EmailTemplateOptions(TranslationOptions):
|
||||||
fields = ("subject", "content", "plain_content")
|
fields = ("subject", "html_content", "plain_content")
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ dependencies = [
|
||||||
"django-ratelimit==4.1.0",
|
"django-ratelimit==4.1.0",
|
||||||
"django-storages==1.14.6",
|
"django-storages==1.14.6",
|
||||||
"django-unfold==0.81.0",
|
"django-unfold==0.81.0",
|
||||||
"django-unfold-markdown==0.1.2",
|
|
||||||
"django-debug-toolbar==6.2.0",
|
"django-debug-toolbar==6.2.0",
|
||||||
"django-widget-tweaks==1.5.1",
|
"django-widget-tweaks==1.5.1",
|
||||||
"djangorestframework==3.16.1",
|
"djangorestframework==3.16.1",
|
||||||
|
|
@ -52,7 +51,6 @@ dependencies = [
|
||||||
"graphene-django==3.2.3",
|
"graphene-django==3.2.3",
|
||||||
"graphene-file-upload==1.3.0",
|
"graphene-file-upload==1.3.0",
|
||||||
"httpx==0.28.1",
|
"httpx==0.28.1",
|
||||||
"markdown==3.10.2",
|
|
||||||
"opentelemetry-instrumentation-django==0.60b1",
|
"opentelemetry-instrumentation-django==0.60b1",
|
||||||
"paramiko==4.0.0",
|
"paramiko==4.0.0",
|
||||||
"pillow==12.1.1",
|
"pillow==12.1.1",
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,6 @@ INSTALLED_APPS: list[str] = [
|
||||||
"unfold.contrib.inlines",
|
"unfold.contrib.inlines",
|
||||||
"unfold.contrib.constance",
|
"unfold.contrib.constance",
|
||||||
"unfold.contrib.import_export",
|
"unfold.contrib.import_export",
|
||||||
"unfold_markdown",
|
|
||||||
"constance",
|
"constance",
|
||||||
"modeltranslation",
|
"modeltranslation",
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
|
|
|
||||||
17
uv.lock
17
uv.lock
|
|
@ -1066,19 +1066,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/03/f3b11452636bcb8f8fb4b2daa3301781eca7ea1b2ee5781fdc888e315b43/django_unfold-0.81.0-py3-none-any.whl", hash = "sha256:7a800fcf7ac438ae473ffa51cdfbb22ef0e6e8455dad84ce1e1846ddd21deac9", size = 1226399, upload-time = "2026-02-25T09:48:40.646Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/03/f3b11452636bcb8f8fb4b2daa3301781eca7ea1b2ee5781fdc888e315b43/django_unfold-0.81.0-py3-none-any.whl", hash = "sha256:7a800fcf7ac438ae473ffa51cdfbb22ef0e6e8455dad84ce1e1846ddd21deac9", size = 1226399, upload-time = "2026-02-25T09:48:40.646Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "django-unfold-markdown"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "django" },
|
|
||||||
{ name = "django-unfold" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/c5/3d1e9d43fce9a0b646bf4f40fdce056af45cda39ede35a2d1cdebfb25ddc/django_unfold_markdown-0.1.2.tar.gz", hash = "sha256:6b4d8c627c08901fc6088a4aa85c357510fa4c9e55fcb2e81f2bdf10628076ec", size = 117597, upload-time = "2025-11-30T19:40:02.159Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/6d/c4ded7b49db7e4c8daddafb28b6ec377a5e435879e370e048537342e65c3/django_unfold_markdown-0.1.2-py3-none-any.whl", hash = "sha256:a9614d7eac13c0b6be1288dfe7750987cbb99810372803a880b90b07ee014ce0", size = 119709, upload-time = "2025-11-30T19:40:00.864Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-widget-tweaks"
|
name = "django-widget-tweaks"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
|
|
@ -3364,7 +3351,6 @@ dependencies = [
|
||||||
{ name = "django-redis" },
|
{ name = "django-redis" },
|
||||||
{ name = "django-storages" },
|
{ name = "django-storages" },
|
||||||
{ name = "django-unfold" },
|
{ name = "django-unfold" },
|
||||||
{ name = "django-unfold-markdown" },
|
|
||||||
{ name = "django-widget-tweaks" },
|
{ name = "django-widget-tweaks" },
|
||||||
{ name = "djangoql" },
|
{ name = "djangoql" },
|
||||||
{ name = "djangorestframework" },
|
{ name = "djangorestframework" },
|
||||||
|
|
@ -3382,7 +3368,6 @@ dependencies = [
|
||||||
{ name = "graphene-django" },
|
{ name = "graphene-django" },
|
||||||
{ name = "graphene-file-upload" },
|
{ name = "graphene-file-upload" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "markdown" },
|
|
||||||
{ name = "opentelemetry-instrumentation-django" },
|
{ name = "opentelemetry-instrumentation-django" },
|
||||||
{ name = "paramiko" },
|
{ name = "paramiko" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
|
|
@ -3467,7 +3452,6 @@ requires-dist = [
|
||||||
{ name = "django-storages", specifier = "==1.14.6" },
|
{ name = "django-storages", specifier = "==1.14.6" },
|
||||||
{ name = "django-stubs", marker = "extra == 'linting'", specifier = "==5.2.9" },
|
{ name = "django-stubs", marker = "extra == 'linting'", specifier = "==5.2.9" },
|
||||||
{ name = "django-unfold", specifier = "==0.81.0" },
|
{ name = "django-unfold", specifier = "==0.81.0" },
|
||||||
{ name = "django-unfold-markdown", specifier = "==0.1.2" },
|
|
||||||
{ name = "django-widget-tweaks", specifier = "==1.5.1" },
|
{ name = "django-widget-tweaks", specifier = "==1.5.1" },
|
||||||
{ name = "djangoql", specifier = "==0.19.1" },
|
{ name = "djangoql", specifier = "==0.19.1" },
|
||||||
{ name = "djangorestframework", specifier = "==3.16.1" },
|
{ name = "djangorestframework", specifier = "==3.16.1" },
|
||||||
|
|
@ -3487,7 +3471,6 @@ requires-dist = [
|
||||||
{ name = "graphene-file-upload", specifier = "==1.3.0" },
|
{ name = "graphene-file-upload", specifier = "==1.3.0" },
|
||||||
{ name = "httpx", specifier = "==0.28.1" },
|
{ name = "httpx", specifier = "==0.28.1" },
|
||||||
{ name = "jupyter", marker = "extra == 'jupyter'", specifier = "==1.1.1" },
|
{ name = "jupyter", marker = "extra == 'jupyter'", specifier = "==1.1.1" },
|
||||||
{ name = "markdown", specifier = "==3.10.2" },
|
|
||||||
{ name = "openai", marker = "extra == 'openai'", specifier = "==2.24.0" },
|
{ name = "openai", marker = "extra == 'openai'", specifier = "==2.24.0" },
|
||||||
{ name = "opentelemetry-instrumentation-django", specifier = "==0.60b1" },
|
{ name = "opentelemetry-instrumentation-django", specifier = "==0.60b1" },
|
||||||
{ name = "paramiko", specifier = "==4.0.0" },
|
{ name = "paramiko", specifier = "==4.0.0" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue