Merge branch 'main' into storefront-nuxt
This commit is contained in:
commit
57e5e49059
266 changed files with 17645 additions and 14124 deletions
38
Dockerfiles/supervisor.Dockerfile
Normal file
38
Dockerfiles/supervisor.Dockerfile
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM node:22-bookworm-slim AS build
|
||||
WORKDIR /app
|
||||
|
||||
ARG EVIBES_BASE_DOMAIN
|
||||
ARG EVIBES_PROJECT_NAME
|
||||
ENV EVIBES_BASE_DOMAIN=$EVIBES_BASE_DOMAIN
|
||||
ENV EVIBES_PROJECT_NAME=$EVIBES_PROJECT_NAME
|
||||
|
||||
COPY ./supervisor/package.json ./supervisor/package-lock.json ./
|
||||
RUN npm ci --include=optional
|
||||
|
||||
COPY ./supervisor ./
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-bookworm-slim AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=7777
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN addgroup --system --gid 1001 nodeapp \
|
||||
&& adduser --system --uid 1001 --ingroup nodeapp --home /home/nodeapp nodeapp
|
||||
USER nodeapp
|
||||
|
||||
COPY --from=build /app/.output/ ./
|
||||
|
||||
RUN install -d -m 0755 -o nodeapp -g nodeapp /home/nodeapp \
|
||||
&& printf '#!/bin/sh\nif [ \"$DEBUG\" = \"1\" ]; then export NODE_ENV=development; else export NODE_ENV=production; fi\nexec node /app/server/index.mjs\n' > /home/nodeapp/start.sh \
|
||||
&& chown nodeapp:nodeapp /home/nodeapp/start.sh \
|
||||
&& chmod +x /home/nodeapp/start.sh
|
||||
|
||||
USER nodeapp
|
||||
CMD ["sh", "/home/nodeapp/start.sh"]
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
from django.contrib.admin import ModelAdmin, register
|
||||
from django.contrib.admin import register
|
||||
from django_summernote.admin import SummernoteModelAdminMixin
|
||||
from unfold.admin import ModelAdmin
|
||||
|
||||
from engine.blog.models import Post, PostTag
|
||||
from engine.core.admin import ActivationActionsMixin, FieldsetsMixin
|
||||
|
||||
from .models import Post, PostTag
|
||||
|
||||
|
||||
@register(Post)
|
||||
class PostAdmin(SummernoteModelAdminMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-06-16 08:59+0100\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
|
|||
|
|
@ -5,16 +5,33 @@ from constance.admin import Config
|
|||
from constance.admin import ConstanceAdmin as BaseConstanceAdmin
|
||||
from django.apps import AppConfig, apps
|
||||
from django.conf import settings
|
||||
from django.contrib.admin import ModelAdmin, TabularInline, action, register, site
|
||||
from django.contrib.admin import register, site
|
||||
from django.contrib.gis.admin import GISModelAdmin
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.db.models import Model
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin
|
||||
from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin
|
||||
from django_celery_beat.admin import PeriodicTaskAdmin as BasePeriodicTaskAdmin
|
||||
from django_celery_beat.admin import PeriodicTaskForm, TaskSelectWidget
|
||||
from django_celery_beat.models import (
|
||||
ClockedSchedule,
|
||||
CrontabSchedule,
|
||||
IntervalSchedule,
|
||||
PeriodicTask,
|
||||
SolarSchedule,
|
||||
)
|
||||
from djangoql.admin import DjangoQLSearchMixin
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
from modeltranslation.translator import NotRegistered, translator
|
||||
from modeltranslation.utils import get_translation_fields
|
||||
from mptt.admin import DraggableMPTTAdmin
|
||||
from unfold.admin import ModelAdmin, TabularInline
|
||||
from unfold.contrib.import_export.forms import ExportForm, ImportForm
|
||||
from unfold.decorators import action
|
||||
from unfold.widgets import UnfoldAdminSelectWidget, UnfoldAdminTextInputWidget
|
||||
|
||||
from engine.core.forms import CRMForm, OrderForm, OrderProductForm, StockForm, VendorForm
|
||||
from engine.core.models import (
|
||||
|
|
@ -65,15 +82,15 @@ class FieldsetsMixin:
|
|||
for orig in transoptions.local_fields:
|
||||
translation_fields += get_translation_fields(orig)
|
||||
if translation_fields:
|
||||
fss = list(fss) + [(_("translations"), {"fields": translation_fields})] # type: ignore [list-item]
|
||||
fss = list(fss) + [(_("translations"), {"classes": ["tab"], "fields": translation_fields})] # type: ignore [list-item]
|
||||
return fss
|
||||
|
||||
if self.general_fields:
|
||||
fieldsets.append((_("general"), {"fields": self.general_fields}))
|
||||
fieldsets.append((_("general"), {"classes": ["tab"], "fields": self.general_fields}))
|
||||
if self.relation_fields:
|
||||
fieldsets.append((_("relations"), {"fields": self.relation_fields}))
|
||||
fieldsets.append((_("relations"), {"classes": ["tab"], "fields": self.relation_fields}))
|
||||
if self.additional_fields:
|
||||
fieldsets.append((_("additional info"), {"fields": self.additional_fields}))
|
||||
fieldsets.append((_("additional info"), {"classes": ["tab"], "fields": self.additional_fields}))
|
||||
opts = self.model._meta
|
||||
|
||||
meta_fields = []
|
||||
|
|
@ -91,14 +108,14 @@ class FieldsetsMixin:
|
|||
meta_fields.append("human_readable_id")
|
||||
|
||||
if meta_fields:
|
||||
fieldsets.append((_("metadata"), {"fields": meta_fields}))
|
||||
fieldsets.append((_("metadata"), {"classes": ["tab"], "fields": meta_fields}))
|
||||
|
||||
ts = []
|
||||
for name in ("created", "modified"):
|
||||
if any(f.name == name for f in opts.fields):
|
||||
ts.append(name)
|
||||
if ts:
|
||||
fieldsets.append((_("timestamps"), {"fields": ts, "classes": ["collapse"]}))
|
||||
fieldsets.append((_("timestamps"), {"classes": ["tab"], "fields": ts}))
|
||||
fieldsets = add_translations_fieldset(fieldsets) # type: ignore [arg-type, assignment]
|
||||
return fieldsets # type: ignore [return-value]
|
||||
|
||||
|
|
@ -140,10 +157,9 @@ class AttributeValueInline(TabularInline): # type: ignore [type-arg]
|
|||
model = AttributeValue
|
||||
extra = 0
|
||||
autocomplete_fields = ["attribute"]
|
||||
is_navtab = True
|
||||
verbose_name = _("attribute value")
|
||||
verbose_name_plural = _("attribute values")
|
||||
icon = "fa-solid fa-list-ul"
|
||||
tab = True
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("attribute", "product")
|
||||
|
|
@ -152,10 +168,9 @@ class AttributeValueInline(TabularInline): # type: ignore [type-arg]
|
|||
class ProductImageInline(TabularInline): # type: ignore [type-arg]
|
||||
model = ProductImage
|
||||
extra = 0
|
||||
is_navtab = True
|
||||
tab = True
|
||||
verbose_name = _("image")
|
||||
verbose_name_plural = _("images")
|
||||
icon = "fa-regular fa-images"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("product")
|
||||
|
|
@ -165,10 +180,9 @@ class StockInline(TabularInline): # type: ignore [type-arg]
|
|||
model = Stock
|
||||
extra = 0
|
||||
form = StockForm
|
||||
is_navtab = True
|
||||
tab = True
|
||||
verbose_name = _("stock")
|
||||
verbose_name_plural = _("stocks")
|
||||
icon = "fa-solid fa-boxes-stacked"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("vendor", "product")
|
||||
|
|
@ -179,10 +193,9 @@ class OrderProductInline(TabularInline): # type: ignore [type-arg]
|
|||
extra = 0
|
||||
readonly_fields = ("product", "quantity", "buy_price")
|
||||
form = OrderProductForm
|
||||
is_navtab = True
|
||||
verbose_name = _("order product")
|
||||
verbose_name_plural = _("order products")
|
||||
icon = "fa-solid fa-boxes-packing"
|
||||
tab = True
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("product").only("product__name")
|
||||
|
|
@ -193,14 +206,13 @@ class CategoryChildrenInline(TabularInline): # type: ignore [type-arg]
|
|||
fk_name = "parent"
|
||||
extra = 0
|
||||
fields = ("name", "description", "is_active", "image", "markup_percent")
|
||||
is_navtab = True
|
||||
tab = True
|
||||
verbose_name = _("children")
|
||||
verbose_name_plural = _("children")
|
||||
icon = "fa-solid fa-leaf"
|
||||
|
||||
|
||||
@register(AttributeGroup)
|
||||
class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class AttributeGroupAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = AttributeGroup # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -224,7 +236,7 @@ class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
|
|||
|
||||
|
||||
@register(Attribute)
|
||||
class AttributeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class AttributeAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Attribute # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -299,7 +311,7 @@ class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin):
|
|||
|
||||
|
||||
@register(Category)
|
||||
class CategoryAdmin(FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin):
|
||||
class CategoryAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin, ModelAdmin):
|
||||
# noinspection PyClassVar
|
||||
model = Category
|
||||
list_display = (
|
||||
|
|
@ -348,7 +360,7 @@ class CategoryAdmin(FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin):
|
|||
|
||||
|
||||
@register(Brand)
|
||||
class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class BrandAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Brand # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -383,7 +395,7 @@ class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: i
|
|||
|
||||
|
||||
@register(Product)
|
||||
class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class ProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin, ImportExportModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Product # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -432,6 +444,8 @@ class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type:
|
|||
ProductImageInline,
|
||||
StockInline,
|
||||
]
|
||||
import_form_class = ImportForm
|
||||
export_form_class = ExportForm
|
||||
|
||||
general_fields = [
|
||||
"is_active",
|
||||
|
|
@ -460,7 +474,7 @@ class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type:
|
|||
|
||||
|
||||
@register(ProductTag)
|
||||
class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class ProductTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = ProductTag # type: ignore [misc]
|
||||
list_display = ("tag_name",)
|
||||
|
|
@ -478,7 +492,7 @@ class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # ty
|
|||
|
||||
|
||||
@register(CategoryTag)
|
||||
class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class CategoryTagAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = CategoryTag # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -504,7 +518,7 @@ class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # t
|
|||
|
||||
|
||||
@register(Vendor)
|
||||
class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class VendorAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Vendor # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -544,7 +558,7 @@ class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type:
|
|||
|
||||
|
||||
@register(Feedback)
|
||||
class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class FeedbackAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Feedback # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -577,7 +591,7 @@ class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type
|
|||
|
||||
|
||||
@register(Order)
|
||||
class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class OrderAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Order # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -628,7 +642,7 @@ class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: i
|
|||
|
||||
|
||||
@register(OrderProduct)
|
||||
class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class OrderProductAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = OrderProduct # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -666,7 +680,7 @@ class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): #
|
|||
|
||||
|
||||
@register(PromoCode)
|
||||
class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class PromoCodeAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = PromoCode # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -710,7 +724,7 @@ class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # typ
|
|||
|
||||
|
||||
@register(Promotion)
|
||||
class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class PromotionAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Promotion # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -737,7 +751,7 @@ class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # typ
|
|||
|
||||
|
||||
@register(Stock)
|
||||
class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class StockAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Stock # type: ignore [misc]
|
||||
form = StockForm
|
||||
|
|
@ -785,7 +799,7 @@ class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: i
|
|||
|
||||
|
||||
@register(Wishlist)
|
||||
class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class WishlistAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = Wishlist # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -811,7 +825,7 @@ class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type
|
|||
|
||||
|
||||
@register(ProductImage)
|
||||
class ProductImageAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class ProductImageAdmin(DjangoQLSearchMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = ProductImage # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -847,7 +861,7 @@ class ProductImageAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): #
|
|||
|
||||
|
||||
@register(Address)
|
||||
class AddressAdmin(FieldsetsMixin, GISModelAdmin): # type: ignore [misc]
|
||||
class AddressAdmin(DjangoQLSearchMixin, FieldsetsMixin, GISModelAdmin): # type: ignore [misc]
|
||||
# noinspection PyClassVar
|
||||
model = Address # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -897,7 +911,7 @@ class AddressAdmin(FieldsetsMixin, GISModelAdmin): # type: ignore [misc]
|
|||
|
||||
|
||||
@register(CustomerRelationshipManagementProvider)
|
||||
class CustomerRelationshipManagementProviderAdmin(FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class CustomerRelationshipManagementProviderAdmin(DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = CustomerRelationshipManagementProvider # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -926,7 +940,7 @@ class CustomerRelationshipManagementProviderAdmin(FieldsetsMixin, ModelAdmin):
|
|||
|
||||
|
||||
@register(OrderCrmLink)
|
||||
class OrderCrmLinkAdmin(FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
class OrderCrmLinkAdmin(DjangoQLSearchMixin, FieldsetsMixin, ModelAdmin): # type: ignore [misc, type-arg]
|
||||
# noinspection PyClassVar
|
||||
model = OrderCrmLink # type: ignore [misc]
|
||||
list_display = (
|
||||
|
|
@ -993,6 +1007,49 @@ class ConstanceConfig:
|
|||
site.unregister([Config])
|
||||
# noinspection PyTypeChecker
|
||||
site.register([ConstanceConfig], BaseConstanceAdmin) # type: ignore [list-item]
|
||||
site.site_title = settings.CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]
|
||||
site.site_title = settings.PROJECT_NAME
|
||||
site.site_header = "eVibes"
|
||||
site.index_title = settings.CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]
|
||||
site.index_title = settings.PROJECT_NAME
|
||||
|
||||
|
||||
site.unregister(PeriodicTask)
|
||||
site.unregister(IntervalSchedule)
|
||||
site.unregister(CrontabSchedule)
|
||||
site.unregister(SolarSchedule)
|
||||
site.unregister(ClockedSchedule)
|
||||
|
||||
|
||||
class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget):
|
||||
pass
|
||||
|
||||
|
||||
class UnfoldPeriodicTaskForm(PeriodicTaskForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["task"].widget = UnfoldAdminTextInputWidget()
|
||||
self.fields["regtask"].widget = UnfoldTaskSelectWidget()
|
||||
|
||||
|
||||
@register(PeriodicTask)
|
||||
class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
|
||||
form = UnfoldPeriodicTaskForm
|
||||
|
||||
|
||||
@register(IntervalSchedule)
|
||||
class IntervalScheduleAdmin(ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@register(CrontabSchedule)
|
||||
class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@register(SolarSchedule)
|
||||
class SolarScheduleAdmin(ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@register(ClockedSchedule)
|
||||
class ClockedScheduleAdmin(BaseClockedScheduleAdmin, ModelAdmin):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import logging
|
|||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
from constance import config
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Max, Min, QuerySet
|
||||
|
|
@ -139,7 +138,7 @@ class BrandType(DjangoObjectType): # type: ignore [misc]
|
|||
lang = graphene_current_lang()
|
||||
base = f"https://{settings.BASE_DOMAIN}"
|
||||
canonical = f"{base}/{lang}/brand/{self.slug}"
|
||||
title = f"{self.name} | {config.PROJECT_NAME}"
|
||||
title = f"{self.name} | {settings.PROJECT_NAME}"
|
||||
description = (self.description or "")[:180]
|
||||
|
||||
logo_url = None
|
||||
|
|
@ -265,7 +264,7 @@ class CategoryType(DjangoObjectType): # type: ignore [misc]
|
|||
lang = graphene_current_lang()
|
||||
base = f"https://{settings.BASE_DOMAIN}"
|
||||
canonical = f"{base}/{lang}/catalog/{self.slug}"
|
||||
title = f"{self.name} | {config.PROJECT_NAME}"
|
||||
title = f"{self.name} | {settings.PROJECT_NAME}"
|
||||
description = (self.description or "")[:180]
|
||||
|
||||
og_image = graphene_abs(info.context, self.image.url) if getattr(self, "image", None) else ""
|
||||
|
|
@ -537,7 +536,7 @@ class ProductType(DjangoObjectType): # type: ignore [misc]
|
|||
lang = graphene_current_lang()
|
||||
base = f"https://{settings.BASE_DOMAIN}"
|
||||
canonical = f"{base}/{lang}/product/{self.slug}"
|
||||
title = f"{self.name} | {config.PROJECT_NAME}"
|
||||
title = f"{self.name} | {settings.PROJECT_NAME}"
|
||||
description = (self.description or "")[:180]
|
||||
|
||||
first_img = self.images.order_by("priority").first()
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
1
engine/core/static/graphql.svg
Normal file
1
engine/core/static/graphql.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#e10098"><style>svg{fill:color(display-p3 0.8824 0 0.5961);}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M50 6.90308L87.323 28.4515V71.5484L50 93.0968L12.677 71.5484V28.4515L50 6.90308ZM16.8647 30.8693V62.5251L44.2795 15.0414L16.8647 30.8693ZM50 13.5086L18.3975 68.2457H81.6025L50 13.5086ZM77.4148 72.4334H22.5852L50 88.2613L77.4148 72.4334ZM83.1353 62.5251L55.7205 15.0414L83.1353 30.8693V62.5251Z"/><circle cx="50" cy="9.3209" r="8.82"/><circle cx="85.2292" cy="29.6605" r="8.82"/><circle cx="85.2292" cy="70.3396" r="8.82"/><circle cx="50" cy="90.6791" r="8.82"/><circle cx="14.7659" cy="70.3396" r="8.82"/><circle cx="14.7659" cy="29.6605" r="8.82"/></svg>
|
||||
|
After Width: | Height: | Size: 740 B |
5
engine/core/static/redoc.svg
Normal file
5
engine/core/static/redoc.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
|
||||
<path d="M0 0 C4.85630418 -0.09938127 9.71200765 -0.17170794 14.5690918 -0.21972656 C16.21893637 -0.23975797 17.8687107 -0.26699142 19.51831055 -0.30175781 C21.89910274 -0.35065357 24.278951 -0.3730528 26.66015625 -0.390625 C27.38960953 -0.41127014 28.11906281 -0.43191528 28.87062073 -0.45318604 C34.98996246 -0.45582643 39.16046149 1.39602858 43.7421875 5.59765625 C48.39070216 11.60866658 48.56284028 16.60873893 48 24 C46.25230248 29.44870404 42.60721838 32.78541808 38 36 C34.30459598 37.23180134 30.89696622 37.12412112 27.05078125 37.09765625 C26.27816452 37.0962413 25.50554779 37.09482635 24.70951843 37.09336853 C22.24380611 37.08777466 19.77818485 37.07522333 17.3125 37.0625 C15.63997527 37.05748421 13.96744912 37.05292144 12.29492188 37.04882812 C8.19657954 37.03780724 4.09830521 37.02054684 0 37 C0.33 36.01 0.66 35.02 1 34 C2.97285682 33.30950011 4.94642158 32.62085067 6.92578125 31.94921875 C10.37674801 30.36996277 11.86671615 28.10295833 14 25 C15.56840852 20.29477445 15.54895539 16.47351184 13.8125 11.8125 C10.19941703 6.20599194 6.23939638 4.46809327 0 3 C0 2.01 0 1.02 0 0 Z " fill="#ffffff" transform="translate(8,1)"/>
|
||||
<path d="M0 0 C4.69557647 -0.09954745 9.39052663 -0.17178467 14.08691406 -0.21972656 C15.68142408 -0.23973053 17.27586178 -0.26694301 18.87011719 -0.30175781 C36.87926136 -0.68493109 36.87926136 -0.68493109 44.9375 6.6875 C47 10 47 10 48 19 C37.44 19 26.88 19 16 19 C15.01 16.03 14.02 13.06 13 10 C9.007668 5.73546355 5.53762935 4.21827846 0 3 C0 2.01 0 1.02 0 0 Z " fill="#ffffff" transform="translate(8,44)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
2
engine/core/static/swagger.svg
Normal file
2
engine/core/static/swagger.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_swagger</title><path d="M30,16a14,14,0,1,1-4.1-9.9A13.956,13.956,0,0,1,30,16Z" style="fill:#fff"/><path d="M27.9,16a11.9,11.9,0,1,1-3.485-8.415A11.863,11.863,0,0,1,27.9,16Z" style="fill:#6d9a00"/><path d="M11.66,15.983a.938.938,0,0,1,.977-.976.976.976,0,1,1-.977.976Z" style="fill:#fff"/><path d="M15.031,15.983a.938.938,0,0,1,.977-.976.976.976,0,1,1-.977.976Z" style="fill:#fff"/><path d="M18.4,15.983a.938.938,0,0,1,.977-.976.976.976,0,1,1-.977.976Z" style="fill:#fff"/><path d="M7.619,16.89V15.142A2.824,2.824,0,0,0,8.5,15a1.126,1.126,0,0,0,.439-.441,2.1,2.1,0,0,0,.254-.776,9.08,9.08,0,0,0,.055-1.216,10.547,10.547,0,0,1,.123-1.97,1.847,1.847,0,0,1,.446-.9,1.72,1.72,0,0,1,.81-.552,4.788,4.788,0,0,1,1.316-.131h.363v1.437a3.177,3.177,0,0,0-.977.091.63.63,0,0,0-.319.277,3.372,3.372,0,0,0-.1.941q0,.459-.062,1.741a4.639,4.639,0,0,1-.178,1.169,2.435,2.435,0,0,1-.367.739,2.939,2.939,0,0,1-.682.6,2.432,2.432,0,0,1,.662.579,2.377,2.377,0,0,1,.394.8,5.8,5.8,0,0,1,.178,1.267q.048,1.209.048,1.544a3.034,3.034,0,0,0,.11.932.694.694,0,0,0,.333.288,2.927,2.927,0,0,0,.963.1v1.486h-.363a3.843,3.843,0,0,1-1.292-.192A1.905,1.905,0,0,1,9.82,22.3a1.875,1.875,0,0,1-.456-.9,8.724,8.724,0,0,1-.117-1.686,8.414,8.414,0,0,0-.11-1.741,1.553,1.553,0,0,0-.456-.834A2.106,2.106,0,0,0,7.619,16.89Z" style="fill:#fff"/><path d="M23.285,17.143a1.553,1.553,0,0,0-.456.834,8.414,8.414,0,0,0-.11,1.741A8.724,8.724,0,0,1,22.6,21.4a1.875,1.875,0,0,1-.456.9,1.905,1.905,0,0,1-.833.521,3.843,3.843,0,0,1-1.292.192h-.363V21.53a2.927,2.927,0,0,0,.963-.1.694.694,0,0,0,.333-.288,3.034,3.034,0,0,0,.11-.932q0-.335.048-1.544A5.8,5.8,0,0,1,21.29,17.4a2.377,2.377,0,0,1,.394-.8,2.432,2.432,0,0,1,.662-.579,2.939,2.939,0,0,1-.682-.6,2.435,2.435,0,0,1-.367-.739,4.639,4.639,0,0,1-.178-1.169q-.062-1.282-.062-1.741a3.372,3.372,0,0,0-.1-.941.63.63,0,0,0-.319-.277,3.177,3.177,0,0,0-.977-.091V9.016h.363a4.788,4.788,0,0,1,1.316.131,1.72,1.72,0,0,1,.81.552,1.847,1.847,0,0,1,.446.9,10.547,10.547,0,0,1,.123,1.97,9.08,9.08,0,0,0,.055,1.216,2.1,2.1,0,0,0,.254.776,1.126,1.126,0,0,0,.439.441,2.824,2.824,0,0,0,.883.144V16.89A2.106,2.106,0,0,0,23.285,17.143Z" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
167
engine/core/templates/admin/index.html
Normal file
167
engine/core/templates/admin/index.html
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% load i18n unfold %}
|
||||
|
||||
{% block title %}
|
||||
{% if subtitle %}
|
||||
{{ subtitle }} |
|
||||
{% endif %}
|
||||
|
||||
{{ title }} | {{ site_title|default:_('Django site admin') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
{% include "unfold/helpers/site_branding.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% component "unfold/components/container.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Dashboard" %}
|
||||
<br/>
|
||||
{% endcomponent %}
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Revenue (gross, 30d)" %}
|
||||
{% endcomponent %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{{ revenue_gross_30|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Revenue (net, 30d)" %}
|
||||
{% endcomponent %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{{ revenue_net_30|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Returns (30d)" %}
|
||||
{% endcomponent %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{{ returns_30|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Processed orders (30d)" %}
|
||||
{% endcomponent %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{{ processed_orders_30|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 items-start">
|
||||
{% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 %}
|
||||
{% with total=gross|add:returns %}
|
||||
{% component "unfold/components/card.html" with class="xl:col-span-2" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Sales vs Returns (30d)" %}
|
||||
{% endcomponent %}
|
||||
{% if total and total > 0 %}
|
||||
{% widthratio gross total 360 as gross_deg %}
|
||||
<div class="flex flex-col sm:flex-row items-center gap-6">
|
||||
<div class="relative w-40 h-40">
|
||||
<div class="w-40 h-40 rounded-full"
|
||||
style="background:
|
||||
conic-gradient(
|
||||
rgb(34,197,94) 0 {{ gross_deg }}deg,
|
||||
rgb(239,68,68) {{ gross_deg }}deg 360deg
|
||||
);">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm"
|
||||
style="background:rgb(34,197,94)"></span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Gross" %}:</span>
|
||||
<span class="font-semibold">{{ gross }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm"
|
||||
style="background:rgb(239,68,68)"></span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Returns" %}:</span>
|
||||
<span class="font-semibold">{{ returns }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Not enough data for chart yet." %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Quick Links" %}
|
||||
{% endcomponent %}
|
||||
{% if quick_links %}
|
||||
{% component "unfold/components/navigation.html" with class="flex flex-col gap-1" items=quick_links %}
|
||||
{% endcomponent %}
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "No links available." %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Most wished product" %}
|
||||
{% endcomponent %}
|
||||
{% if most_wished_product %}
|
||||
<a href="{{ most_wished_product.admin_url }}" class="flex items-center gap-4">
|
||||
<img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}"
|
||||
class="w-16 h-16 object-cover rounded"/>
|
||||
<span class="font-medium">{{ most_wished_product.name }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "No data yet." %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Most popular product" %}
|
||||
{% endcomponent %}
|
||||
{% if most_popular_product %}
|
||||
<a href="{{ most_popular_product.admin_url }}" class="flex items-center gap-4">
|
||||
<img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}"
|
||||
class="w-16 h-16 object-cover rounded"/>
|
||||
<span class="font-medium">{{ most_popular_product.name }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "No data yet." %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
|
||||
{% component "unfold/components/separator.html" %}
|
||||
{% endcomponent %}
|
||||
|
||||
<div class="mt-4">
|
||||
<br/>
|
||||
{% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %}
|
||||
eVibes {{ evibes_version }} · Wiseless Team
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
{% endcomponent %}
|
||||
{% endblock %}
|
||||
|
|
@ -30,4 +30,5 @@ class DRFCoreViewsTests(TestCase):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
return serializer.validated_data["access_token"]
|
||||
|
||||
|
||||
# TODO: create tests for every possible HTTP method in core module with DRF stack
|
||||
|
|
|
|||
52
engine/core/utils/commerce.py
Normal file
52
engine/core/utils/commerce.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.db.models import F, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import now
|
||||
from constance import config
|
||||
from engine.core.models import Order, OrderProduct
|
||||
|
||||
|
||||
def get_period_order_products(period: timedelta = timedelta(days=30), statuses: list[str] | None = None):
|
||||
if statuses is None:
|
||||
statuses = ["FINISHED"]
|
||||
current = now()
|
||||
perioded = current - period
|
||||
orders = Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=perioded)
|
||||
return OrderProduct.objects.filter(status__in=statuses, order__in=orders)
|
||||
|
||||
|
||||
def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)):
|
||||
order_products = get_period_order_products(period)
|
||||
total: float = (
|
||||
order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0
|
||||
)
|
||||
try:
|
||||
total = float(total)
|
||||
except (TypeError, ValueError):
|
||||
total = 0.0
|
||||
|
||||
if clear:
|
||||
try:
|
||||
tax_rate = float(config.TAX_RATE or 0)
|
||||
except (TypeError, ValueError):
|
||||
tax_rate = 0.0
|
||||
net = total * (1 - tax_rate / 100.0)
|
||||
return round(net, 2)
|
||||
else:
|
||||
return round(float(total), 2)
|
||||
|
||||
|
||||
def get_returns(period: timedelta = timedelta(days=30)):
|
||||
order_products = get_period_order_products(period, ["RETURNED"])
|
||||
total_returns: float = (
|
||||
order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0
|
||||
)
|
||||
try:
|
||||
return round(float(total_returns), 2)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_total_processed_orders(period: timedelta = timedelta(days=30)):
|
||||
return get_period_order_products(period, ["RETURNED", "FINISHED"]).count()
|
||||
|
|
@ -24,7 +24,7 @@ def contact_us_email(contact_info) -> tuple[bool, str]:
|
|||
)
|
||||
|
||||
email = EmailMessage(
|
||||
_(f"{config.PROJECT_NAME} | contact us initiated"),
|
||||
_(f"{settings.PROJECT_NAME} | contact us initiated"),
|
||||
render_to_string(
|
||||
"../templates/contact_us_email.html",
|
||||
{
|
||||
|
|
@ -37,7 +37,7 @@ def contact_us_email(contact_info) -> tuple[bool, str]:
|
|||
},
|
||||
),
|
||||
to=[config.EMAIL_FROM],
|
||||
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
connection=get_dynamic_email_connection(),
|
||||
)
|
||||
email.content_subtype = "html"
|
||||
|
|
@ -70,7 +70,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
|
|||
|
||||
if not order.is_whole_digital:
|
||||
email = EmailMessage(
|
||||
_(f"{config.PROJECT_NAME} | order confirmation"),
|
||||
_(f"{settings.PROJECT_NAME} | order confirmation"),
|
||||
render_to_string(
|
||||
"digital_order_created_email.html" if order.is_whole_digital else "shipped_order_created_email.html",
|
||||
{
|
||||
|
|
@ -81,7 +81,7 @@ def send_order_created_email(order_pk: str) -> tuple[bool, str]:
|
|||
},
|
||||
),
|
||||
to=[recipient],
|
||||
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
connection=get_dynamic_email_connection(),
|
||||
)
|
||||
email.content_subtype = "html"
|
||||
|
|
@ -102,14 +102,14 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
|
|||
activate(order.user.language)
|
||||
|
||||
email = EmailMessage(
|
||||
_(f"{config.PROJECT_NAME} | order delivered"),
|
||||
_(f"{settings.PROJECT_NAME} | order delivered"),
|
||||
render_to_string(
|
||||
template_name="../templates/digital_order_delivered_email.html",
|
||||
context={
|
||||
"order_uuid": order.human_readable_id,
|
||||
"user_first_name": "" or order.user.first_name,
|
||||
"order_products": ops,
|
||||
"project_name": config.PROJECT_NAME,
|
||||
"project_name": settings.PROJECT_NAME,
|
||||
"contact_email": config.EMAIL_FROM,
|
||||
"total_price": round(sum(0.0 or op.buy_price for op in ops), 2), # type: ignore [misc]
|
||||
"display_system_attributes": order.user.has_perm("core.view_order"),
|
||||
|
|
@ -117,7 +117,7 @@ def send_order_finished_email(order_pk: str) -> tuple[bool, str]:
|
|||
},
|
||||
),
|
||||
to=[order.user.email],
|
||||
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
connection=get_dynamic_email_connection(),
|
||||
)
|
||||
email.content_subtype = "html"
|
||||
|
|
@ -185,20 +185,20 @@ def send_promocode_created_email(promocode_pk: str) -> tuple[bool, str]:
|
|||
activate(promocode.user.language)
|
||||
|
||||
email = EmailMessage(
|
||||
_(f"{config.PROJECT_NAME} | promocode granted"),
|
||||
_(f"{settings.PROJECT_NAME} | promocode granted"),
|
||||
render_to_string(
|
||||
template_name="../templates/promocode_granted_email.html",
|
||||
context={
|
||||
"promocode": promocode,
|
||||
"user_first_name": "" or promocode.user.first_name,
|
||||
"project_name": config.PROJECT_NAME,
|
||||
"project_name": settings.PROJECT_NAME,
|
||||
"contact_email": config.EMAIL_FROM,
|
||||
"today": datetime.today(),
|
||||
"currency": settings.CURRENCY_CODE,
|
||||
},
|
||||
),
|
||||
to=[promocode.user.email],
|
||||
from_email=f"{config.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
from_email=f"{settings.PROJECT_NAME} <{config.EMAIL_FROM}>",
|
||||
connection=get_dynamic_email_connection(),
|
||||
)
|
||||
email.content_subtype = "html"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ def website_schema():
|
|||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": config.PROJECT_NAME,
|
||||
"name": settings.PROJECT_NAME,
|
||||
"url": f"https://{settings.BASE_DOMAIN}/",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import logging
|
|||
import mimetypes
|
||||
import os
|
||||
import traceback
|
||||
from contextlib import suppress
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
|
@ -9,8 +10,10 @@ from django.contrib.sitemaps.views import index as _sitemap_index_view
|
|||
from django.contrib.sitemaps.views import sitemap as _sitemap_detail_view
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.db.models import Count, Sum
|
||||
from django.http import FileResponse, Http404, HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -44,7 +47,7 @@ from engine.core.docs.drf.views import (
|
|||
SEARCH_SCHEMA,
|
||||
)
|
||||
from engine.core.elasticsearch import process_query
|
||||
from engine.core.models import DigitalAssetDownload, Order, OrderProduct
|
||||
from engine.core.models import DigitalAssetDownload, Order, OrderProduct, Product, Wishlist
|
||||
from engine.core.serializers import (
|
||||
BuyAsBusinessOrderSerializer,
|
||||
CacheOperatorSerializer,
|
||||
|
|
@ -53,6 +56,7 @@ from engine.core.serializers import (
|
|||
)
|
||||
from engine.core.utils import get_project_parameters, is_url_safe
|
||||
from engine.core.utils.caching import web_cache
|
||||
from engine.core.utils.commerce import get_returns, get_revenue, get_total_processed_orders
|
||||
from engine.core.utils.emailing import contact_us_email
|
||||
from engine.core.utils.languages import get_flag_by_language
|
||||
from engine.payments.serializers import TransactionProcessSerializer
|
||||
|
|
@ -410,3 +414,86 @@ def version(request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|||
version.__doc__ = _( # type: ignore [assignment]
|
||||
"Returns current version of the eVibes. "
|
||||
)
|
||||
|
||||
|
||||
def dashboard_callback(request, context):
|
||||
revenue_gross_30 = get_revenue(clear=False)
|
||||
revenue_net_30 = get_revenue(clear=True)
|
||||
returns_30 = get_returns()
|
||||
processed_orders_30 = get_total_processed_orders()
|
||||
|
||||
quick_links: list[dict[str, str]] = []
|
||||
with suppress(Exception):
|
||||
quick_links_section = settings.UNFOLD.get("SIDEBAR", {}).get("navigation", [])[1] # type: ignore[assignment]
|
||||
for item in quick_links_section.get("items", []):
|
||||
title = item.get("title")
|
||||
link = item.get("link")
|
||||
if not title or not link:
|
||||
continue
|
||||
quick_links.append(
|
||||
{
|
||||
"title": str(title),
|
||||
"link": str(link),
|
||||
**({"icon": item.get("icon")} if item.get("icon") else {}),
|
||||
}
|
||||
)
|
||||
|
||||
most_wished: dict[str, str | int | float | None] | None = None
|
||||
with suppress(Exception):
|
||||
wished = (
|
||||
Wishlist.objects.filter(user__is_active=True, user__is_staff=False)
|
||||
.values("products")
|
||||
.exclude(products__isnull=True)
|
||||
.annotate(cnt=Count("products"))
|
||||
.order_by("-cnt")
|
||||
.first()
|
||||
)
|
||||
if wished and wished.get("products"):
|
||||
product = Product.objects.filter(pk=wished["products"]).first()
|
||||
if product:
|
||||
img = product.images.first().image_url if product.images.exists() else ""
|
||||
most_wished = {
|
||||
"name": product.name,
|
||||
"image": img,
|
||||
"admin_url": reverse("admin:core_product_change", args=[product.pk]),
|
||||
}
|
||||
|
||||
most_popular: dict[str, str | int | float | None] | None = None
|
||||
with suppress(Exception):
|
||||
popular = (
|
||||
OrderProduct.objects.filter(status="FINISHED", order__status="FINISHED", product__isnull=False)
|
||||
.values("product")
|
||||
.annotate(total_qty=Sum("quantity"))
|
||||
.order_by("-total_qty")
|
||||
.first()
|
||||
)
|
||||
if popular and popular.get("product"):
|
||||
product = Product.objects.filter(pk=popular["product"]).first()
|
||||
if product:
|
||||
img = product.images.first().image_url if product.images.exists() else ""
|
||||
most_popular = {
|
||||
"name": product.name,
|
||||
"image": img,
|
||||
"admin_url": reverse("admin:core_product_change", args=[product.pk]),
|
||||
}
|
||||
|
||||
context.update(
|
||||
{
|
||||
"custom_variable": "value",
|
||||
"revenue_gross_30": revenue_gross_30,
|
||||
"revenue_net_30": revenue_net_30,
|
||||
"returns_30": returns_30,
|
||||
"processed_orders_30": processed_orders_30,
|
||||
"evibes_version": settings.EVIBES_VERSION,
|
||||
"quick_links": quick_links,
|
||||
"most_wished_product": most_wished,
|
||||
"most_popular_product": most_popular,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
dashboard_callback.__doc__ = _( # type: ignore [assignment]
|
||||
"Returns custom variables for Dashboard. "
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import uuid
|
|||
from typing import Type
|
||||
from uuid import UUID
|
||||
|
||||
from constance import config
|
||||
from django.conf import settings
|
||||
from django.db.models import Prefetch, Q, OuterRef, Exists
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.decorators import method_decorator
|
||||
|
|
@ -270,7 +269,7 @@ class CategoryViewSet(EvibesViewSet):
|
|||
def seo_meta(self, request: Request, *args, **kwargs) -> Response:
|
||||
category = self.get_object()
|
||||
|
||||
title = f"{category.name} | {config.PROJECT_NAME}"
|
||||
title = f"{category.name} | {settings.PROJECT_NAME}"
|
||||
description = (category.description or "")[:180]
|
||||
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/catalog/{category.slug}"
|
||||
og_image = request.build_absolute_uri(category.image.url) if getattr(category, "image", None) else ""
|
||||
|
|
@ -387,7 +386,7 @@ class BrandViewSet(EvibesViewSet):
|
|||
def seo_meta(self, request: Request, *args, **kwargs) -> Response:
|
||||
brand = self.get_object()
|
||||
|
||||
title = f"{brand.name} | {config.PROJECT_NAME}"
|
||||
title = f"{brand.name} | {settings.PROJECT_NAME}"
|
||||
description = (brand.description or "")[:180]
|
||||
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/brand/{brand.slug}"
|
||||
|
||||
|
|
@ -529,7 +528,7 @@ class ProductViewSet(EvibesViewSet):
|
|||
p = self.get_object()
|
||||
images = list(p.images.all()[:6])
|
||||
rating = {"value": p.rating, "count": p.feedbacks_count}
|
||||
title = f"{p.name} | {config.PROJECT_NAME}"
|
||||
title = f"{p.name} | {settings.PROJECT_NAME}"
|
||||
description = (p.description or "")[:180]
|
||||
canonical = f"https://{settings.BASE_DOMAIN}/{settings.LANGUAGE_CODE}/product/{p.slug}"
|
||||
og = {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.admin import ModelAdmin, register
|
||||
from django.contrib.admin import register
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from unfold.admin import ModelAdmin, TabularInline
|
||||
|
||||
from engine.core.admin import ActivationActionsMixin
|
||||
from engine.payments.forms import GatewayForm, TransactionForm
|
||||
from engine.payments.models import Balance, Transaction, Gateway
|
||||
from engine.payments.models import Balance, Gateway, Transaction
|
||||
|
||||
|
||||
class TransactionInline(admin.TabularInline): # type: ignore [type-arg]
|
||||
class TransactionInline(TabularInline): # type: ignore [type-arg]
|
||||
model = Transaction
|
||||
form = TransactionForm
|
||||
extra = 1
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,9 +1,9 @@
|
|||
#
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-01-30 03:27+0000\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: BRITISH ENGLISH <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
@ -138,6 +138,10 @@ msgstr "لم يتم تعيين مسار تكامل البوابة"
|
|||
msgid "invalid integration path: %(path)s"
|
||||
msgstr "مسار تكامل غير صالح: %(path)s"
|
||||
|
||||
#: engine/payments/signals.py:41
|
||||
msgid "the transaction amount didn't fit into allowed limits: "
|
||||
msgstr "لم يتناسب مبلغ المعاملة مع الحدود المسموح بها:"
|
||||
|
||||
#: engine/payments/templates/balance_deposit_email.html:6
|
||||
#: engine/payments/templates/balance_deposit_email.html:93
|
||||
msgid "balance deposit"
|
||||
|
|
@ -188,10 +192,36 @@ msgstr "مطلوب مزود للحصول على الأسعار من"
|
|||
msgid "couldn't find provider {provider}"
|
||||
msgstr "تعذر العثور على مزود {provider}"
|
||||
|
||||
#: engine/payments/utils/emailing.py:27
|
||||
#: engine/payments/utils/emailing.py:28
|
||||
#, python-brace-format
|
||||
msgid "{config.PROJECT_NAME} | balance deposit"
|
||||
msgstr "{config.PROJECT_NAME} | إيداع الرصيد"
|
||||
msgid "{settings.PROJECT_NAME} | balance deposit"
|
||||
msgstr "{settings.PROJECT_NAME} | إيداع الرصيد"
|
||||
|
||||
#: engine/payments/views.py:23
|
||||
msgid ""
|
||||
"This class provides an API endpoint to handle deposit transactions.\n"
|
||||
"It supports the creation of a deposit transaction after validating the provided data. If the user is not authenticated, an appropriate response is returned. On successful validation and execution, a response with the transaction details is provided."
|
||||
msgstr ""
|
||||
"توفر هذه الفئة نقطة نهاية API للتعامل مع معاملات الإيداع.\n"
|
||||
"وهي تدعم إنشاء معاملة إيداع بعد التحقق من صحة البيانات المقدمة. إذا لم تتم مصادقة المستخدم، يتم إرجاع استجابة مناسبة. عند التحقق والتنفيذ بنجاح، يتم توفير استجابة بتفاصيل المعاملة."
|
||||
|
||||
#: engine/payments/views.py:49
|
||||
msgid ""
|
||||
"Handles incoming callback requests to the API.\n"
|
||||
"This class processes and routes incoming HTTP POST requests to the appropriate pgateway handler based on the provided gateway parameter. It is designed to handle callback events coming from external systems and provide an appropriate HTTP response indicating success or failure."
|
||||
msgstr ""
|
||||
"يعالج طلبات رد الاتصال الواردة إلى واجهة برمجة التطبيقات.\n"
|
||||
"يقوم هذا الصنف بمعالجة طلبات HTTP POST الواردة وتوجيهها إلى معالج pgateway المناسب بناءً على معلمة البوابة المقدمة. وهو مصمم للتعامل مع أحداث رد الاتصال الواردة من أنظمة خارجية وتوفير استجابة HTTP مناسبة تشير إلى النجاح أو الفشل."
|
||||
|
||||
#: engine/payments/views.py:60
|
||||
#, python-brace-format
|
||||
msgid "Transaction {transaction.uuid} has no gateway"
|
||||
msgstr "لا تحتوي المعاملة {transaction.uuid} على بوابة"
|
||||
|
||||
#: engine/payments/views.py:63
|
||||
#, python-brace-format
|
||||
msgid "Gateway {transaction.gateway} has no integration"
|
||||
msgstr "البوابة {transaction.gateway} ليس لها تكامل"
|
||||
|
||||
#: engine/payments/viewsets.py:14
|
||||
msgid ""
|
||||
|
|
@ -205,4 +235,3 @@ msgstr ""
|
|||
"للقراءة فقط للتفاعل مع بيانات المعاملات. وتستخدم أداة TransactionSerializer "
|
||||
"لتسلسل البيانات وإلغاء تسلسلها. تضمن الفئة أن المستخدمين المصرح لهم فقط، "
|
||||
"الذين يستوفون أذونات محددة، يمكنهم الوصول إلى المعاملات."
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,9 +1,9 @@
|
|||
#
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: EVIBES 2025.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-11-12 15:44+0300\n"
|
||||
"POT-Creation-Date: 2025-11-15 16:53+0300\n"
|
||||
"PO-Revision-Date: 2025-01-30 03:27+0000\n"
|
||||
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
|
||||
"Language-Team: BRITISH ENGLISH <CONTACT@FUREUNOIR.COM>\n"
|
||||
|
|
@ -138,6 +138,10 @@ msgstr "cesta integrace brány není nastavena"
|
|||
msgid "invalid integration path: %(path)s"
|
||||
msgstr "neplatná cesta integrace: %(path)s"
|
||||
|
||||
#: engine/payments/signals.py:41
|
||||
msgid "the transaction amount didn't fit into allowed limits: "
|
||||
msgstr "Částka transakce se nevešla do povolených limitů:"
|
||||
|
||||
#: engine/payments/templates/balance_deposit_email.html:6
|
||||
#: engine/payments/templates/balance_deposit_email.html:93
|
||||
msgid "balance deposit"
|
||||
|
|
@ -188,10 +192,36 @@ msgstr "Je třeba mít poskytovatele, od kterého lze získat sazby"
|
|||
msgid "couldn't find provider {provider}"
|
||||
msgstr "Nepodařilo se najít poskytovatele {provider}"
|
||||
|
||||
#: engine/payments/utils/emailing.py:27
|
||||
#: engine/payments/utils/emailing.py:28
|
||||
#, python-brace-format
|
||||
msgid "{config.PROJECT_NAME} | balance deposit"
|
||||
msgstr "{config.PROJECT_NAME} | Zůstatek vkladu"
|
||||
msgid "{settings.PROJECT_NAME} | balance deposit"
|
||||
msgstr "{settings.PROJECT_NAME} | zůstatek vkladu"
|
||||
|
||||
#: engine/payments/views.py:23
|
||||
msgid ""
|
||||
"This class provides an API endpoint to handle deposit transactions.\n"
|
||||
"It supports the creation of a deposit transaction after validating the provided data. If the user is not authenticated, an appropriate response is returned. On successful validation and execution, a response with the transaction details is provided."
|
||||
msgstr ""
|
||||
"Tato třída poskytuje koncový bod API pro zpracování vkladových transakcí.\n"
|
||||
"Podporuje vytvoření vkladové transakce po ověření zadaných údajů. Pokud uživatel není ověřen, je vrácena odpovídající odpověď. Při úspěšném ověření a provedení je poskytnuta odpověď s údaji o transakci."
|
||||
|
||||
#: engine/payments/views.py:49
|
||||
msgid ""
|
||||
"Handles incoming callback requests to the API.\n"
|
||||
"This class processes and routes incoming HTTP POST requests to the appropriate pgateway handler based on the provided gateway parameter. It is designed to handle callback events coming from external systems and provide an appropriate HTTP response indicating success or failure."
|
||||
msgstr ""
|
||||
"Zpracovává příchozí požadavky na zpětné volání rozhraní API.\n"
|
||||
"Tato třída zpracovává a směruje příchozí požadavky HTTP POST na příslušnou obsluhu pgateway na základě zadaného parametru brány. Je navržena tak, aby zpracovávala události zpětného volání přicházející z externích systémů a poskytovala příslušnou odpověď HTTP označující úspěch nebo selhání."
|
||||
|
||||
#: engine/payments/views.py:60
|
||||
#, python-brace-format
|
||||
msgid "Transaction {transaction.uuid} has no gateway"
|
||||
msgstr "Transakce {transaction.uuid} nemá žádnou bránu"
|
||||
|
||||
#: engine/payments/views.py:63
|
||||
#, python-brace-format
|
||||
msgid "Gateway {transaction.gateway} has no integration"
|
||||
msgstr "Brána {transaction.gateway} nemá žádnou integraci"
|
||||
|
||||
#: engine/payments/viewsets.py:14
|
||||
msgid ""
|
||||
|
|
@ -206,4 +236,3 @@ msgstr ""
|
|||
"Pro serializaci a deserializaci dat používá TransactionSerializer. Třída "
|
||||
"zajišťuje, že k transakcím mohou přistupovat pouze oprávnění uživatelé, "
|
||||
"kteří splňují určitá oprávnění."
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue