Features: 1) Introduce ALLOW validation for generic actions in permissions; 2) Add AllowAny permission class to seo_meta endpoints; 3) Update SEO URL patterns and breadcrumbs with language codes; 4) Improve compatibility of attributes in serializers with ListField and DictField.;

Fixes: 1) Correct dynamic URL generation for images in Open Graph (`og_image` and `logo_url`); 2) Address potential missing or empty `crumbs` handling in breadcrumbs generation;

Extra: 1) Refactor redundant action names (e.g., `seo` to `seo_meta`) for better consistency; 2) Clean up and optimize serializer structures and Graphene object fields; 3) Improve readability and maintainability of action and object implementations.;
This commit is contained in:
Egor Pavlovich Gorbunov 2025-08-18 15:20:47 +03:00
parent 733b249643
commit ea22ca69a3
4 changed files with 49 additions and 23 deletions

View file

@ -554,7 +554,7 @@ class ProductType(DjangoObjectType):
description = (self.description or "")[:180] description = (self.description or "")[:180]
first_img = self.images.order_by("priority").first() first_img = self.images.order_by("priority").first()
og_image = _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 ""
og = { og = {
"title": title, "title": title,

View file

@ -47,6 +47,9 @@ class EvibesPermission(permissions.BasePermission):
app_label = model._meta.app_label app_label = model._meta.app_label
model_name = model._meta.model_name model_name = model._meta.model_name
if view.additional.get(action) == "ALLOW":
return True
if action == "create" and view.additional.get("create") == "ALLOW": if action == "create" and view.additional.get("create") == "ALLOW":
return True return True

View file

@ -122,7 +122,7 @@ class RecursiveField(Field):
class AddOrderProductSerializer(Serializer): class AddOrderProductSerializer(Serializer):
product_uuid = CharField(required=True) product_uuid = CharField(required=True)
attributes = JSONField(required=False, default=dict) attributes = ListField(required=False, child=DictField(), default=list)
class BulkAddOrderProductsSerializer(Serializer): class BulkAddOrderProductsSerializer(Serializer):

View file

@ -17,6 +17,7 @@ from drf_spectacular.utils import extend_schema_view
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import AllowAny
from rest_framework.renderers import MultiPartRenderer from rest_framework.renderers import MultiPartRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
@ -283,7 +284,7 @@ class CategoryViewSet(EvibesViewSet):
action_serializer_classes = { action_serializer_classes = {
"list": CategorySimpleSerializer, "list": CategorySimpleSerializer,
} }
additional = {"seo": "ALLOW"} additional = {"seo_meta": "ALLOW"}
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
@ -291,8 +292,15 @@ class CategoryViewSet(EvibesViewSet):
return qs return qs
return qs.filter(is_active=True) return qs.filter(is_active=True)
@action(detail=True, methods=["get"], url_path="seo") @action(
def seo(self, request, **kwargs): detail=True,
methods=["get"],
url_path="meta",
permission_classes=[
AllowAny,
],
)
def seo_meta(self, request, **kwargs):
lookup_key = getattr(self, "lookup_url_kwarg", "pk") lookup_key = getattr(self, "lookup_url_kwarg", "pk")
lookup_val = kwargs.get(lookup_key) lookup_val = kwargs.get(lookup_key)
@ -304,13 +312,14 @@ class CategoryViewSet(EvibesViewSet):
title = f"{category.name} | {config.PROJECT_NAME}" title = f"{category.name} | {config.PROJECT_NAME}"
description = (category.description or "")[:180] description = (category.description or "")[:180]
canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}" canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
og_image = request.build_absolute_uri(category.image.url) if getattr(category, "image", None) else ""
og = { og = {
"title": title, "title": title,
"description": description, "description": description,
"type": "website", "type": "website",
"url": canonical, "url": canonical,
"image": category.image.url if getattr(category, "image", None) else "", "image": og_image,
} }
tw = {"card": "summary_large_image", "title": title, "description": description} tw = {"card": "summary_large_image", "title": title, "description": description}
@ -379,7 +388,7 @@ class BrandViewSet(EvibesViewSet):
action_serializer_classes = { action_serializer_classes = {
"list": BrandSimpleSerializer, "list": BrandSimpleSerializer,
} }
additional = {"seo": "ALLOW"} additional = {"seo_meta": "ALLOW"}
def get_queryset(self): def get_queryset(self):
queryset = Brand.objects.all() queryset = Brand.objects.all()
@ -393,8 +402,15 @@ class BrandViewSet(EvibesViewSet):
return queryset return queryset
@action(detail=True, methods=["get"], url_path="seo") @action(
def seo(self, request, **kwargs): detail=True,
methods=["get"],
url_path="meta",
permission_classes=[
AllowAny,
],
)
def seo_meta(self, request, **kwargs):
lookup_key = getattr(self, "lookup_url_kwarg", "pk") lookup_key = getattr(self, "lookup_url_kwarg", "pk")
lookup_val = kwargs.get(lookup_key) lookup_val = kwargs.get(lookup_key)
brand = get_object_or_404(Brand, slug=str(lookup_val)) brand = get_object_or_404(Brand, slug=str(lookup_val))
@ -406,9 +422,11 @@ class BrandViewSet(EvibesViewSet):
canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}" canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}"
logo_url = ( logo_url = (
brand.big_logo.url request.build_absolute_uri(brand.big_logo.url)
if getattr(brand, "big_logo", None) if getattr(brand, "big_logo", None)
else (brand.small_logo.url if getattr(brand, "small_logo", None) else None) else request.build_absolute_uri(brand.small_logo.url)
if getattr(brand, "small_logo", None)
else ""
) )
og = { og = {
@ -416,7 +434,7 @@ class BrandViewSet(EvibesViewSet):
"description": description, "description": description,
"type": "website", "type": "website",
"url": canonical, "url": canonical,
"image": logo_url or "", "image": logo_url,
} }
tw = {"card": "summary_large_image", "title": title, "description": description} tw = {"card": "summary_large_image", "title": title, "description": description}
@ -479,7 +497,7 @@ class ProductViewSet(EvibesViewSet):
} }
lookup_field = "lookup_value" lookup_field = "lookup_value"
lookup_url_kwarg = "lookup_value" lookup_url_kwarg = "lookup_value"
additional = {"seo": "ALLOW"} additional = {"seo_meta": "ALLOW"}
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
@ -530,29 +548,34 @@ class ProductViewSet(EvibesViewSet):
name = "Product" name = "Product"
return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": _(f"{name} does not exist: {uuid}")}) return Response(status=status.HTTP_404_NOT_FOUND, data={"detail": _(f"{name} does not exist: {uuid}")})
@action(detail=True, methods=["get"], url_path="seo") @action(
def seo(self, request, slug): detail=True,
p = get_object_or_404(Product.objects.select_related("brand", "category"), slug=slug) methods=["get"],
url_path="meta",
permission_classes=[
AllowAny,
],
)
def seo_meta(self, request, **kwargs):
p = self.get_object()
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} | {config.PROJECT_NAME}" title = f"{p.name} | {config.PROJECT_NAME}"
description = (p.description or "")[:180] description = (p.description or "")[:180]
canonical = f"https://{config.BASE_DOMAIN}/products/{p.slug}" canonical = f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
og = { og = {
"title": title, "title": title,
"description": description, "description": description,
"type": "product", "type": "product",
"url": canonical, "url": canonical,
"image": images[0].image.url if images else "", "image": request.build_absolute_uri(images[0].image.url) if images else "",
} }
tw = {"card": "summary_large_image", "title": title, "description": description} tw = {"card": "summary_large_image", "title": title, "description": description}
crumbs = [] crumbs = [("Home", f"https://{config.BASE_DOMAIN}/")]
if p.category: if p.category:
crumbs.append(("Home", f"https://{config.BASE_DOMAIN}/"))
for c in p.category.get_ancestors(include_self=True): for c in p.category.get_ancestors(include_self=True):
crumbs.append((c.name, f"https://{config.BASE_DOMAIN}/c/{c.slug}")) crumbs.append((c.name, f"https://{config.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{c.slug}"))
crumbs.append((p.name, canonical)) crumbs.append((p.name, canonical))
json_ld = [org_schema(), website_schema()] json_ld = [org_schema(), website_schema()]