Merge branch 'main' into storefront-nuxt

This commit is contained in:
Alexandr SaVBaD Waltz 2025-08-19 17:49:44 +03:00
commit 408dee727e
353 changed files with 24535 additions and 18260 deletions

View file

@ -11,6 +11,7 @@ coverage.*
*.cover *.cover
*.py,cover *.py,cover
nosetests.xml nosetests.xml
desktop.ini
# Cache directories # Cache directories
__pycache__/ __pycache__/
@ -37,7 +38,6 @@ __pypackages__/
# Packaging and distribution # Packaging and distribution
# ────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
build/ build/
dist/
dist-ssr/ dist-ssr/
*.egg *.egg
*.egg-info/ *.egg-info/

7
.gitignore vendored
View file

@ -64,8 +64,9 @@ htmlcov/
.pybuilder/ .pybuilder/
# Storefronts # Storefronts
.astro/
.nuxt/ .nuxt/
.next/
next-env.d.ts
# Celery # Celery
celerybeat-schedule celerybeat-schedule
@ -141,6 +142,10 @@ cypress/screenshots/
# JetBrains # JetBrains
.idea/ .idea/
!.idea/icon.svg
!.idea/externalDependencies.xml
!.idea/evibes.iml
!.idea/evibes.ico
# Microsoft # Microsoft
*.suo *.suo

BIN
.idea/evibes.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

35
.idea/evibes.iml Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="evibes/settings/__init__.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/blog/templates" />
<option value="$MODULE_DIR$/core/templates" />
<option value="$MODULE_DIR$/payments/templates" />
<option value="$MODULE_DIR$/vibes_auth/templates" />
</list>
</option>
</component>
</module>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalDependencies">
<plugin id="Docker" />
<plugin id="Git4Idea" />
<plugin id="Pythonid" />
<plugin id="com.github.xepozz.gitcodeowners" />
<plugin id="com.intellij.python.django" />
<plugin id="com.koxudaxi.ruff" />
<plugin id="com.leinardi.pycharm.mypy" />
<plugin id="com.mallowigi" />
<plugin id="com.nasller.CodeGlancePro" />
<plugin id="dev.meanmail.plugin.nginx-intellij-plugin" />
<plugin id="mobi.hsz.idea.gitignore" />
<plugin id="org.jetbrains.plugins.astro" />
<plugin id="ru.adelf.idea.dotenv" />
</component>
</project>

25
.idea/icon.svg Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="100.000000pt" height="100.000000pt" viewBox="0 0 100.000000 100.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,100.000000) scale(0.100000,-0.100000)"
fill="#7965D1" stroke="none">
<path d="M678 935 c-73 -50 -88 -121 -38 -175 29 -31 50 -35 57 -13 2 6 -5 14
-16 18 -30 9 -26 48 9 88 63 72 130 72 149 -1 18 -67 -6 -117 -89 -182 -97
-76 -142 -97 -235 -109 -121 -16 -324 -29 -380 -24 -48 5 -49 4 -33 -13 26
-26 108 -34 248 -23 308 23 362 40 480 147 l65 59 0 64 c0 79 -17 114 -72 152
-61 41 -100 44 -145 12z"/>
<path d="M327 912 c-10 -10 -17 -27 -17 -38 0 -24 35 -64 55 -64 18 0 19 12 3
28 -16 16 19 54 46 50 17 -2 22 -11 24 -45 4 -55 -38 -105 -105 -124 -50 -14
-179 -17 -225 -6 -34 9 -36 -3 -6 -23 55 -35 251 -29 327 10 95 48 92 168 -6
219 -33 17 -78 13 -96 -7z"/>
<path d="M475 435 c-60 -8 -171 -19 -245 -25 -74 -7 -137 -14 -139 -16 -2 -2
9 -9 25 -16 35 -15 179 -13 309 3 50 7 146 12 215 13 186 2 223 -22 185 -119
-20 -53 -49 -78 -115 -100 -37 -12 -54 -14 -69 -5 -41 21 -16 91 36 105 27 6
27 7 9 21 -31 22 -69 17 -99 -14 -15 -15 -27 -34 -27 -42 0 -23 52 -90 81
-106 43 -22 73 -17 144 22 73 40 93 64 102 118 21 131 -138 193 -412 161z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -4,8 +4,7 @@ README.md @fureunoir contact@fureunoir.com
LICENSE @fureunoir contact@fureunoir.com LICENSE @fureunoir contact@fureunoir.com
Dockerfile.app @fureunoir contact@fureunoir.com Dockerfiles @@maintainer
Dockerfile.storefront @SaVBaD savbad@wiseless.xyz
docker-compose.yml @@maintainer docker-compose.yml @@maintainer
.gitignore @@maintainer .gitignore @@maintainer
.dockerignore @@maintainer .dockerignore @@maintainer
@ -13,14 +12,6 @@ nginx @@maintainer
pyproject.toml @fureunoir contact@fureunoir.com pyproject.toml @fureunoir contact@fureunoir.com
poetry.lock @fureunoir contact@fureunoir.com poetry.lock @fureunoir contact@fureunoir.com
blog/ @fureunoir contact@fureunoir.com
core/ @fureunoir contact@fureunoir.com
evibes/ @fureunoir contact@fureunoir.com
payments/ @fureunoir contact@fureunoir.com
scripts/ @fureunoir contact@fureunoir.com
vibes_auth/ @fureunoir contact@fureunoir.com
storefront/ @SaVBaD savbad@wiseless.xyz
*.py @fureunoir contact@fureunoir.com *.py @fureunoir contact@fureunoir.com
*.bat @fureunoir contact@fureunoir.com *.bat @fureunoir contact@fureunoir.com
*.sh @fureunoir contact@fureunoir.com *.sh @fureunoir contact@fureunoir.com
@ -31,5 +22,13 @@ storefront/ @SaVBaD savbad@wiseless.xyz
*.mjs @SaVBaD savbad@wiseless.xyz *.mjs @SaVBaD savbad@wiseless.xyz
*.cjs @SaVBaD savbad@wiseless.xyz *.cjs @SaVBaD savbad@wiseless.xyz
*.vue @SaVBaD savbad@wiseless.xyz *.vue @SaVBaD savbad@wiseless.xyz
*.astro @SaVBaD savbad@wiseless.xyz
*.scss @SaVBaD savbad@wiseless.xyz *.scss @SaVBaD savbad@wiseless.xyz
blog/ @fureunoir contact@fureunoir.com
core/ @fureunoir contact@fureunoir.com
evibes/ @fureunoir contact@fureunoir.com
payments/ @fureunoir contact@fureunoir.com
scripts/ @fureunoir contact@fureunoir.com
vibes_auth/ @fureunoir contact@fureunoir.com
storefront/ @SaVBaD savbad@wiseless.xyz

View file

@ -26,6 +26,7 @@ RUN set -eux; \
graphviz-dev \ graphviz-dev \
libgts-dev \ libgts-dev \
libpq5 \ libpq5 \
chrony \
graphviz \ graphviz \
binutils \ binutils \
libproj-dev \ libproj-dev \
@ -46,4 +47,4 @@ RUN chmod +x /usr/local/bin/app-entrypoint.sh
COPY . . COPY . .
ENTRYPOINT ["app-entrypoint.sh"] ENTRYPOINT ["/usr/bin/bash", "app-entrypoint.sh"]

View file

@ -26,6 +26,7 @@ RUN set -eux; \
graphviz-dev \ graphviz-dev \
libgts-dev \ libgts-dev \
libpq5 \ libpq5 \
chrony \
graphviz \ graphviz \
binutils \ binutils \
libproj-dev \ libproj-dev \
@ -46,4 +47,4 @@ RUN chmod +x /usr/local/bin/beat-entrypoint.sh
COPY . . COPY . .
ENTRYPOINT ["beat-entrypoint.sh"] ENTRYPOINT ["/usr/bin/bash", "beat-entrypoint.sh"]

View file

@ -0,0 +1,50 @@
# syntax=docker/dockerfile:1
FROM python:3.12-bookworm
LABEL authors="fureunoir"
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
LANG=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive \
PATH="/root/.local/bin:$PATH"
WORKDIR /app
RUN set -eux; \
sed -i 's|https://deb.debian.org/debian|https://ftp.uk.debian.org/debian|g' /etc/apt/sources.list.d/debian.sources; \
apt-get update; \
apt-get install -y --no-install-recommends wget gnupg; \
wget -qO - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -; \
echo "deb http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list; \
apt-get update; \
apt-get install -y --no-install-recommends --fix-missing \
build-essential \
libpq-dev \
gettext \
libgettextpo-dev \
graphviz-dev \
libgts-dev \
libpq5 \
chrony \
graphviz \
binutils \
libproj-dev \
postgresql-client-17 \
gdal-bin; \
rm -rf /var/lib/apt/lists/*; \
pip install --upgrade pip; \
curl -sSL https://install.python-poetry.org | python3
COPY pyproject.toml pyproject.toml
COPY poetry.lock poetry.lock
RUN poetry config virtualenvs.create false
RUN poetry install --extras="worker openai" --no-interaction --no-ansi
COPY ./scripts/Docker/stock-updater-entrypoint.sh /usr/local/bin/stock-updater-entrypoint.sh
RUN chmod +x /usr/local/bin/stock-updater-entrypoint.sh
COPY . .
ENTRYPOINT ["/usr/bin/bash", "stock-updater-entrypoint.sh"]

View file

@ -26,6 +26,7 @@ RUN set -eux; \
graphviz-dev \ graphviz-dev \
libgts-dev \ libgts-dev \
libpq5 \ libpq5 \
chrony \
graphviz \ graphviz \
binutils \ binutils \
libproj-dev \ libproj-dev \
@ -46,4 +47,4 @@ RUN chmod +x /usr/local/bin/worker-entrypoint.sh
COPY . . COPY . .
ENTRYPOINT ["worker-entrypoint.sh"] ENTRYPOINT ["/usr/bin/bash", "worker-entrypoint.sh"]

View file

@ -44,10 +44,11 @@ extension.
cd eVibes cd eVibes
``` ```
2. Choose the storefront. By default, `main` branch has Astro one. Skip this step if you're OK with Astro. 2. Choose the storefront. By default, `main` branch has no storefront included.
Skip this step if you're OK with that and plan to only use API or develop your own storefront.
```bash ```bash
git checkout storefront-<option: astro, nuxt, remix, svelte, solid, analog > git checkout storefront-<options: nuxt, next, sk, qwik >
``` ```
3. Generate your .env file. Check and confirm the contents afterward. 3. Generate your .env file. Check and confirm the contents afterward.

View file

@ -1,37 +1,40 @@
from django.contrib import admin from django.contrib.admin import ModelAdmin, register
from django_summernote.admin import SummernoteModelAdmin from django_summernote.admin import SummernoteModelAdminMixin
from core.admin import ActivationActionsMixin, FieldsetsMixin
from .models import Post, PostTag from .models import Post, PostTag
@admin.register(Post) @register(Post)
class PostAdmin(SummernoteModelAdmin): class PostAdmin(SummernoteModelAdminMixin, FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
list_display = ("title", "author", "slug", "created", "modified") list_display = ("title", "author", "slug", "created", "modified")
list_filter = ("author", "tags", "created", "modified") list_filter = ("author", "tags", "created", "modified")
search_fields = ("title", "content") search_fields = ("title", "content", "slug")
filter_horizontal = ("tags",) filter_horizontal = ("tags",)
date_hierarchy = "created" date_hierarchy = "created"
autocomplete_fields = ("author", "tags") autocomplete_fields = ("author", "tags")
summernote_fields = ("content",) readonly_fields = (
"uuid",
"slug",
"modified",
"created",
)
fieldsets = ( summernote_fields = ("content",)
( general_fields = [
None,
{
"fields": (
"author",
"title", "title",
"author",
"content", "content",
"file", "file",
]
relation_fields = [
"tags", "tags",
) ]
},
),
)
@admin.register(PostTag) @register(PostTag)
class PostTagAdmin(admin.ModelAdmin): class PostTagAdmin(ModelAdmin):
list_display = ("tag_name", "name") list_display = ("tag_name", "name")
search_fields = ("tag_name", "name") search_fields = ("tag_name", "name")
ordering = ("tag_name",) ordering = ("tag_name",)

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,55 +17,52 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "المدونة" msgstr "المدونة"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "عنوان المنشور" msgstr "عنوان المنشور"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "العنوان" msgstr "العنوان"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "المنشور" msgstr "المنشور"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "المنشورات" msgstr "المنشورات"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"ملفات تخفيض السعر غير مدعومة Yer - استخدم محتوى تخفيض السعر بدلاً من ذلك!" "ملفات تخفيض السعر غير مدعومة Yer - استخدم محتوى تخفيض السعر بدلاً من ذلك!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "يجب توفير ملف ترميز أو محتوى ترميز مخفض - متنافيان" msgstr "يجب توفير ملف ترميز أو محتوى ترميز مخفض - متنافيان"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "معرّف العلامة الداخلي لعلامة المنشور" msgstr "معرّف العلامة الداخلي لعلامة المنشور"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "اسم العلامة" msgstr "اسم العلامة"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "اسم سهل الاستخدام لعلامة المنشور" msgstr "اسم سهل الاستخدام لعلامة المنشور"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "اسم عرض العلامة" msgstr "اسم عرض العلامة"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "علامة المشاركة" msgstr "علامة المشاركة"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "علامات المشاركة" msgstr "علامات المشاركة"
#~ msgid "eVibes Engine"
#~ msgstr "محرك eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,57 +17,54 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Název příspěvku" msgstr "Název příspěvku"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Název" msgstr "Název"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Příspěvek" msgstr "Příspěvek"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Příspěvky" msgstr "Příspěvky"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Soubory Markdown nejsou podporovány - místo toho použijte obsah Markdown!" "Soubory Markdown nejsou podporovány - místo toho použijte obsah Markdown!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"musí být poskytnut soubor markdown nebo obsah markdown - vzájemně se " "musí být poskytnut soubor markdown nebo obsah markdown - vzájemně se "
"vylučují." "vylučují."
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "interní identifikátor tagu pro tag příspěvku" msgstr "interní identifikátor tagu pro tag příspěvku"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Název štítku" msgstr "Název štítku"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Uživatelsky přívětivý název pro značku příspěvku" msgstr "Uživatelsky přívětivý název pro značku příspěvku"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Zobrazení názvu štítku" msgstr "Zobrazení názvu štítku"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Označení příspěvku" msgstr "Označení příspěvku"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Štítky příspěvků" msgstr "Štítky příspěvků"
#~ msgid "eVibes Engine"
#~ msgstr "Motor eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,55 +17,52 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Indlæggets titel" msgstr "Indlæggets titel"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Titel" msgstr "Titel"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Indlæg" msgstr "Indlæg"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Indlæg" msgstr "Indlæg"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "Markdown-filer understøttes ikke - brug markdown-indhold i stedet!" msgstr "Markdown-filer understøttes ikke - brug markdown-indhold i stedet!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"en markdown-fil eller markdown-indhold skal leveres - gensidigt udelukkende" "en markdown-fil eller markdown-indhold skal leveres - gensidigt udelukkende"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "intern tag-identifikator for indlægs-tagget" msgstr "intern tag-identifikator for indlægs-tagget"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Tag-navn" msgstr "Tag-navn"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Brugervenligt navn til posttagget" msgstr "Brugervenligt navn til posttagget"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Navn på tag-visning" msgstr "Navn på tag-visning"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Tag til indlæg" msgstr "Tag til indlæg"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Tags til indlæg" msgstr "Tags til indlæg"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes-motor"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,58 +17,55 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Titel des Beitrags" msgstr "Titel des Beitrags"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Titel" msgstr "Titel"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Beitrag" msgstr "Beitrag"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Beiträge" msgstr "Beiträge"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Markdown-Dateien werden nicht unterstützt - verwenden Sie stattdessen " "Markdown-Dateien werden nicht unterstützt - verwenden Sie stattdessen "
"Markdown-Inhalte!" "Markdown-Inhalte!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"eine Markdown-Datei oder ein Markdown-Inhalt muss bereitgestellt werden - " "eine Markdown-Datei oder ein Markdown-Inhalt muss bereitgestellt werden - "
"beide schließen sich gegenseitig aus" "beide schließen sich gegenseitig aus"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "interner Tag-Bezeichner für den Post-Tag" msgstr "interner Tag-Bezeichner für den Post-Tag"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Tag name" msgstr "Tag name"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Benutzerfreundlicher Name für das Post-Tag" msgstr "Benutzerfreundlicher Name für das Post-Tag"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Tag-Anzeigename" msgstr "Tag-Anzeigename"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Tag eintragen" msgstr "Tag eintragen"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Tags eintragen" msgstr "Tags eintragen"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes Motor"

View file

@ -5,9 +5,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -21,61 +21,52 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Post's title" msgstr "Post's title"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Title" msgstr "Title"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Post" msgstr "Post"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Posts" msgstr "Posts"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "Markdown files are not supported yer - use markdown content instead!" msgstr "Markdown files are not supported yer - use markdown content instead!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "internal tag identifier for the post tag" msgstr "internal tag identifier for the post tag"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Tag name" msgstr "Tag name"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "User-friendly name for the post tag" msgstr "User-friendly name for the post tag"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Tag display name" msgstr "Tag display name"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Post tag" msgstr "Post tag"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Post tags" msgstr "Post tags"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes Engine"
#~ msgid "(no content yet)"
#~ msgstr "(no content yet)"
#~ msgid "rendered HTML"
#~ msgstr "Rendered HTML"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,55 +17,52 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Post's title" msgstr "Post's title"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Title" msgstr "Title"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Post" msgstr "Post"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Posts" msgstr "Posts"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "Markdown files are not supported yer - use markdown content instead!" msgstr "Markdown files are not supported yer - use markdown content instead!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "internal tag identifier for the post tag" msgstr "internal tag identifier for the post tag"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Tag name" msgstr "Tag name"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "User-friendly name for the post tag" msgstr "User-friendly name for the post tag"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Tag display name" msgstr "Tag display name"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Post tag" msgstr "Post tag"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Post tags" msgstr "Post tags"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes Engine"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,57 +17,54 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Título del mensaje" msgstr "Título del mensaje"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Título" msgstr "Título"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Publicar en" msgstr "Publicar en"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Puestos" msgstr "Puestos"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"No se admiten archivos Markdown - ¡utiliza contenido Markdown en su lugar!" "No se admiten archivos Markdown - ¡utiliza contenido Markdown en su lugar!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"se debe proporcionar un archivo markdown o contenido markdown - mutuamente " "se debe proporcionar un archivo markdown o contenido markdown - mutuamente "
"excluyentes" "excluyentes"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "identificador interno de la etiqueta post" msgstr "identificador interno de la etiqueta post"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Nombre de la etiqueta" msgstr "Nombre de la etiqueta"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Nombre fácil de usar para la etiqueta de la entrada" msgstr "Nombre fácil de usar para la etiqueta de la entrada"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Nombre de la etiqueta" msgstr "Nombre de la etiqueta"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Etiqueta postal" msgstr "Etiqueta postal"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Etiquetas" msgstr "Etiquetas"
#~ msgid "eVibes Engine"
#~ msgstr "Motor eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,58 +17,55 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Titre du message" msgstr "Titre du message"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Titre" msgstr "Titre"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Poste" msgstr "Poste"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Postes" msgstr "Postes"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Les fichiers Markdown ne sont pas pris en charge - utilisez plutôt du " "Les fichiers Markdown ne sont pas pris en charge - utilisez plutôt du "
"contenu Markdown !" "contenu Markdown !"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"un fichier markdown ou un contenu markdown doit être fourni - ils s'excluent " "un fichier markdown ou un contenu markdown doit être fourni - ils s'excluent "
"mutuellement" "mutuellement"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "identifiant interne de la balise post" msgstr "identifiant interne de la balise post"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Nom du jour" msgstr "Nom du jour"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Nom convivial pour la balise post" msgstr "Nom convivial pour la balise post"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Nom d'affichage de l'étiquette" msgstr "Nom d'affichage de l'étiquette"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Tag de poste" msgstr "Tag de poste"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Tags de la poste" msgstr "Tags de la poste"
#~ msgid "eVibes Engine"
#~ msgstr "Moteur eVibes"

View file

@ -5,9 +5,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -20,51 +20,51 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "" msgstr ""
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "" msgstr ""
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "" msgstr ""
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "" msgstr ""
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "" msgstr ""
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "" msgstr ""
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "" msgstr ""
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "" msgstr ""
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "" msgstr ""
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "" msgstr ""
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "" msgstr ""

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,56 +17,53 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Titolo del post" msgstr "Titolo del post"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Titolo" msgstr "Titolo"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Posta" msgstr "Posta"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Messaggi" msgstr "Messaggi"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "I file Markdown non sono supportati: usa invece i contenuti Markdown!" msgstr "I file Markdown non sono supportati: usa invece i contenuti Markdown!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"deve essere fornito un file markdown o un contenuto markdown - si escludono " "deve essere fornito un file markdown o un contenuto markdown - si escludono "
"a vicenda" "a vicenda"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "identificatore interno del tag post" msgstr "identificatore interno del tag post"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Nome del tag" msgstr "Nome del tag"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Nome intuitivo per il tag del post" msgstr "Nome intuitivo per il tag del post"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Nome del tag" msgstr "Nome del tag"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Post tag" msgstr "Post tag"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Tag dei post" msgstr "Tag dei post"
#~ msgid "eVibes Engine"
#~ msgstr "Motore eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,58 +17,55 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "ブログ" msgstr "ブログ"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "投稿タイトル" msgstr "投稿タイトル"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "タイトル" msgstr "タイトル"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "ポスト" msgstr "ポスト"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "投稿" msgstr "投稿"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"マークダウン・ファイルはサポートされていません - 代わりにマークダウン・コンテ" "マークダウン・ファイルはサポートされていません - 代わりにマークダウン・コンテ"
"ンツを使用してください!" "ンツを使用してください!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"マークダウン・ファイルまたはマークダウン・コンテンツを提供しなければならな" "マークダウン・ファイルまたはマークダウン・コンテンツを提供しなければならな"
"い。" "い。"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "投稿タグの内部タグ識別子" msgstr "投稿タグの内部タグ識別子"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "タグ名" msgstr "タグ名"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "投稿タグのユーザーフレンドリーな名前" msgstr "投稿タグのユーザーフレンドリーな名前"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "タグ表示名" msgstr "タグ表示名"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "投稿タグ" msgstr "投稿タグ"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "投稿タグ" msgstr "投稿タグ"
#~ msgid "eVibes Engine"
#~ msgstr "eVibesエンジン"

View file

@ -5,9 +5,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -20,51 +20,51 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "" msgstr ""
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "" msgstr ""
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "" msgstr ""
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "" msgstr ""
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "" msgstr ""
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "" msgstr ""
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "" msgstr ""
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "" msgstr ""
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "" msgstr ""
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "" msgstr ""
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "" msgstr ""

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,58 +17,55 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Titel van de post" msgstr "Titel van de post"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Titel" msgstr "Titel"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Plaats" msgstr "Plaats"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Berichten" msgstr "Berichten"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Markdown-bestanden worden niet ondersteund - gebruik in plaats daarvan " "Markdown-bestanden worden niet ondersteund - gebruik in plaats daarvan "
"markdown-inhoud!" "markdown-inhoud!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"er moet een markdown-bestand of markdown-inhoud worden geleverd - wederzijds " "er moet een markdown-bestand of markdown-inhoud worden geleverd - wederzijds "
"exclusief" "exclusief"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "interne tagidentifier voor de posttag" msgstr "interne tagidentifier voor de posttag"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Tag naam" msgstr "Tag naam"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Gebruiksvriendelijke naam voor de posttag" msgstr "Gebruiksvriendelijke naam voor de posttag"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Tag weergavenaam" msgstr "Tag weergavenaam"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Post tag" msgstr "Post tag"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Post tags" msgstr "Post tags"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes motor"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,57 +17,54 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Tytuł postu" msgstr "Tytuł postu"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Tytuł" msgstr "Tytuł"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Post" msgstr "Post"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Posty" msgstr "Posty"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Pliki Markdown nie są obsługiwane - zamiast tego użyj zawartości Markdown!" "Pliki Markdown nie są obsługiwane - zamiast tego użyj zawartości Markdown!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"należy dostarczyć plik markdown lub zawartość markdown - wzajemnie się " "należy dostarczyć plik markdown lub zawartość markdown - wzajemnie się "
"wykluczające" "wykluczające"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "wewnętrzny identyfikator tagu posta" msgstr "wewnętrzny identyfikator tagu posta"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Nazwa tagu" msgstr "Nazwa tagu"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Przyjazna dla użytkownika nazwa tagu posta" msgstr "Przyjazna dla użytkownika nazwa tagu posta"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Wyświetlana nazwa znacznika" msgstr "Wyświetlana nazwa znacznika"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Tag posta" msgstr "Tag posta"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Tagi postów" msgstr "Tagi postów"
#~ msgid "eVibes Engine"
#~ msgstr "Silnik eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,56 +17,53 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Título da postagem" msgstr "Título da postagem"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Título" msgstr "Título"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Postar" msgstr "Postar"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Publicações" msgstr "Publicações"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Os arquivos markdown não são suportados - use conteúdo markdown em vez disso!" "Os arquivos markdown não são suportados - use conteúdo markdown em vez disso!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"um arquivo ou conteúdo de markdown deve ser fornecido - mutuamente exclusivo" "um arquivo ou conteúdo de markdown deve ser fornecido - mutuamente exclusivo"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "identificador de tag interno para a tag de postagem" msgstr "identificador de tag interno para a tag de postagem"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Nome da etiqueta" msgstr "Nome da etiqueta"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Nome de fácil utilização para a tag de postagem" msgstr "Nome de fácil utilização para a tag de postagem"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Nome de exibição da tag" msgstr "Nome de exibição da tag"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Etiqueta de postagem" msgstr "Etiqueta de postagem"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Tags de postagem" msgstr "Tags de postagem"
#~ msgid "eVibes Engine"
#~ msgstr "Motor eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,57 +17,54 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Blog" msgstr "Blog"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Titlul postului" msgstr "Titlul postului"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Titlul" msgstr "Titlul"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Post" msgstr "Post"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Mesaje" msgstr "Mesaje"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Fișierele Markdown nu sunt acceptate - utilizați în schimb conținut Markdown!" "Fișierele Markdown nu sunt acceptate - utilizați în schimb conținut Markdown!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"trebuie furnizat un fișier markdown sau conținut markdown - se exclud " "trebuie furnizat un fișier markdown sau conținut markdown - se exclud "
"reciproc" "reciproc"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "identificator intern de etichetă pentru eticheta postului" msgstr "identificator intern de etichetă pentru eticheta postului"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Nume etichetă" msgstr "Nume etichetă"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Nume ușor de utilizat pentru eticheta postului" msgstr "Nume ușor de utilizat pentru eticheta postului"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Nume afișare etichetă" msgstr "Nume afișare etichetă"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Etichetă post" msgstr "Etichetă post"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Etichete poștale" msgstr "Etichete poștale"
#~ msgid "eVibes Engine"
#~ msgstr "Motorul eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,58 +17,55 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "Блог" msgstr "Блог"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "Заголовок сообщения" msgstr "Заголовок сообщения"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "Название" msgstr "Название"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "Пост" msgstr "Пост"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "Посты" msgstr "Посты"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "" msgstr ""
"Файлы в формате Markdown не поддерживаются - используйте вместо них " "Файлы в формате Markdown не поддерживаются - используйте вместо них "
"содержимое в формате Markdown!" "содержимое в формате Markdown!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "" msgstr ""
"необходимо предоставить файл разметки или содержимое разметки - " "необходимо предоставить файл разметки или содержимое разметки - "
"взаимоисключающие варианты" "взаимоисключающие варианты"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "внутренний идентификатор тега для тега post" msgstr "внутренний идентификатор тега для тега post"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "Название тега" msgstr "Название тега"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "Удобное для пользователя название тега поста" msgstr "Удобное для пользователя название тега поста"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "Отображаемое имя тега" msgstr "Отображаемое имя тега"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "Тэг поста" msgstr "Тэг поста"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "Тэги постов" msgstr "Тэги постов"
#~ msgid "eVibes Engine"
#~ msgstr "Движок eVibes"

View file

@ -1,9 +1,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n" "Project-Id-Version: EVIBES 2.9.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 22:42+0100\n" "POT-Creation-Date: 2025-07-03 18:33+0300\n"
"PO-Revision-Date: 2025-06-16 08:59+0100\n" "PO-Revision-Date: 2025-06-16 08:59+0100\n"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n" "Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n" "Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,54 +17,51 @@ msgstr ""
msgid "blog" msgid "blog"
msgstr "博客" msgstr "博客"
#: blog/models.py:17 #: blog/models.py:36
msgid "post title" msgid "post title"
msgstr "帖子标题" msgstr "帖子标题"
#: blog/models.py:17 #: blog/models.py:36
msgid "title" msgid "title"
msgstr "标题" msgstr "标题"
#: blog/models.py:64 #: blog/models.py:83
msgid "post" msgid "post"
msgstr "职位" msgstr "职位"
#: blog/models.py:65 #: blog/models.py:84
msgid "posts" msgid "posts"
msgstr "职位" msgstr "职位"
#: blog/models.py:69 #: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead" msgid "markdown files are not supported yet - use markdown content instead"
msgstr "不支持 Markdown 文件,请使用 Markdown 内容!" msgstr "不支持 Markdown 文件,请使用 Markdown 内容!"
#: blog/models.py:71 #: blog/models.py:90
msgid "" msgid ""
"a markdown file or markdown content must be provided - mutually exclusive" "a markdown file or markdown content must be provided - mutually exclusive"
msgstr "必须提供标记符文件或标记符内容 - 相互排斥" msgstr "必须提供标记符文件或标记符内容 - 相互排斥"
#: blog/models.py:82 #: blog/models.py:122
msgid "internal tag identifier for the post tag" msgid "internal tag identifier for the post tag"
msgstr "职位标签的内部标签标识符" msgstr "职位标签的内部标签标识符"
#: blog/models.py:83 #: blog/models.py:123
msgid "tag name" msgid "tag name"
msgstr "标签名称" msgstr "标签名称"
#: blog/models.py:87 #: blog/models.py:127
msgid "user-friendly name for the post tag" msgid "user-friendly name for the post tag"
msgstr "方便用户使用的帖子标签名称" msgstr "方便用户使用的帖子标签名称"
#: blog/models.py:88 #: blog/models.py:128
msgid "tag display name" msgid "tag display name"
msgstr "标签显示名称" msgstr "标签显示名称"
#: blog/models.py:96 #: blog/models.py:136
msgid "post tag" msgid "post tag"
msgstr "职位标签" msgstr "职位标签"
#: blog/models.py:97 #: blog/models.py:137
msgid "post tags" msgid "post tags"
msgstr "帖子标签" msgstr "帖子标签"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes 引擎"

View file

@ -18,56 +18,113 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='PostTag', name="PostTag",
fields=[ fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, (
help_text='unique id is used to surely identify any database object', "uuid",
primary_key=True, serialize=False, verbose_name='unique id')), models.UUIDField(
('is_active', models.BooleanField(default=True, 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", help_text="if set to false, this object can't be seen by users without needed permission",
verbose_name='is active')), 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, "created",
help_text='when the object was last modified', django_extensions.db.fields.CreationDateTimeField(
verbose_name='modified')), auto_now_add=True,
('tag_name', models.CharField(help_text='internal tag identifier for the post tag', max_length=255, help_text="when the object first appeared on the database",
verbose_name='tag name')), verbose_name="created",
('name', models.CharField(help_text='user-friendly name for the post tag', max_length=255, unique=True, ),
verbose_name='tag display name')), ),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
),
),
(
"tag_name",
models.CharField(
help_text="internal tag identifier for the post tag", max_length=255, verbose_name="tag name"
),
),
(
"name",
models.CharField(
help_text="user-friendly name for the post tag",
max_length=255,
unique=True,
verbose_name="tag display name",
),
),
], ],
options={ options={
'verbose_name': 'post tag', "verbose_name": "post tag",
'verbose_name_plural': 'post tags', "verbose_name_plural": "post tags",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Post', name="Post",
fields=[ fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, (
help_text='unique id is used to surely identify any database object', "uuid",
primary_key=True, serialize=False, verbose_name='unique id')), models.UUIDField(
('is_active', models.BooleanField(default=True, 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", help_text="if set to false, this object can't be seen by users without needed permission",
verbose_name='is active')), 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, "created",
help_text='when the object was last modified', django_extensions.db.fields.CreationDateTimeField(
verbose_name='modified')), auto_now_add=True,
('title', models.CharField()), help_text="when the object first appeared on the database",
('content', markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name='content')), verbose_name="created",
('file', models.FileField(blank=True, null=True, upload_to='posts/')), ),
('slug', models.SlugField(allow_unicode=True)), ),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', (
to=settings.AUTH_USER_MODEL)), "modified",
('tags', models.ManyToManyField(to='blog.posttag')), django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
),
),
("title", models.CharField()),
("content", markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content")),
("file", models.FileField(blank=True, null=True, upload_to="posts/")),
("slug", models.SlugField(allow_unicode=True)),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="posts", to=settings.AUTH_USER_MODEL
),
),
("tags", models.ManyToManyField(to="blog.posttag")),
], ],
options={ options={
'verbose_name': 'post', "verbose_name": "post",
'verbose_name_plural': 'posts', "verbose_name_plural": "posts",
}, },
), ),
] ]

View file

@ -5,20 +5,21 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0001_initial'), ("blog", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='post', model_name="post",
name='slug', name="slug",
field=django_extensions.db.fields.AutoSlugField(allow_unicode=True, blank=True, editable=False, populate_from='title', unique=True), field=django_extensions.db.fields.AutoSlugField(
allow_unicode=True, blank=True, editable=False, populate_from="title", unique=True
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='post', model_name="post",
name='title', name="title",
field=models.CharField(help_text='post title', max_length=128, unique=True, verbose_name='title'), field=models.CharField(help_text="post title", max_length=128, unique=True, verbose_name="title"),
), ),
] ]

View file

@ -4,15 +4,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0002_alter_post_slug_alter_post_title'), ("blog", "0002_alter_post_slug_alter_post_title"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='post', model_name="post",
name='tags', name="tags",
field=models.ManyToManyField(blank=True, related_name='posts', to='blog.posttag'), field=models.ManyToManyField(blank=True, related_name="posts", to="blog.posttag"),
), ),
] ]

View file

@ -0,0 +1,301 @@
# Generated by Django 5.2 on 2025-06-29 13:09
import markdown_field.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("blog", "0003_alter_post_tags"),
]
operations = [
migrations.AddField(
model_name="post",
name="content_ar_ar",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_cs_cz",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_da_dk",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_de_de",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_en_gb",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_en_us",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_es_es",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_fr_fr",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_hi_in",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_it_it",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_ja_jp",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_kk_kz",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_nl_nl",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_pl_pl",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_pt_br",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_ro_ro",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_ru_ru",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="content_zh_hans",
field=markdown_field.fields.MarkdownField(blank=True, null=True, verbose_name="content"),
),
migrations.AddField(
model_name="post",
name="title_ar_ar",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_cs_cz",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_da_dk",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_de_de",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_en_gb",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_en_us",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_es_es",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_fr_fr",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_hi_in",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_it_it",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_ja_jp",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_kk_kz",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_nl_nl",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_pl_pl",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_pt_br",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_ro_ro",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_ru_ru",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
migrations.AddField(
model_name="post",
name="title_zh_hans",
field=models.CharField(
help_text="post title",
max_length=128,
null=True,
unique=True,
verbose_name="title",
),
),
]

View file

@ -7,13 +7,32 @@ from markdown_field import MarkdownField
from core.abstract import NiceModel from core.abstract import NiceModel
class Post(NiceModel): class Post(NiceModel): # type: ignore [django-manager-missing]
"""
Represents a blog post model extending NiceModel.
The Post class defines the structure and behavior of a blog post. It includes
attributes for author, title, content, optional file attachment, slug,
and associated tags. The class enforces constraints such as requiring either
content or a file attachment but not both simultaneously. It also supports
automatic slug generation based on the title. This model can be used in
a blogging platform to manage posts created by users.
Attributes:
is_publicly_visible (bool): Specifies whether the post is visible to the public.
author (ForeignKey): A reference to the user who authored the post.
title (CharField): The title of the post. Must be unique and non-empty.
content (MarkdownField): The content of the post written in Markdown format.
file (FileField): An optional file attachment for the post.
slug (AutoSlugField): A unique, automatically generated slug based on the title.
tags (ManyToManyField): Tags associated with the post for categorization.
"""
is_publicly_visible = True is_publicly_visible = True
author: ForeignKey = ForeignKey( author = ForeignKey(to="vibes_auth.User", on_delete=CASCADE, blank=False, null=False, related_name="posts")
to="vibes_auth.User", on_delete=CASCADE, blank=False, null=False, related_name="posts" title = CharField(
)
title: CharField = CharField(
unique=True, max_length=128, blank=False, null=False, help_text=_("post title"), verbose_name=_("title") unique=True, max_length=128, blank=False, null=False, help_text=_("post title"), verbose_name=_("title")
) )
content: MarkdownField = MarkdownField( content: MarkdownField = MarkdownField(
@ -53,9 +72,9 @@ class Post(NiceModel):
blank=True, blank=True,
null=True, null=True,
) )
file: FileField = FileField(upload_to="posts/", blank=True, null=True) file = FileField(upload_to="posts/", blank=True, null=True)
slug: AutoSlugField = AutoSlugField(populate_from="title", allow_unicode=True, unique=True, editable=False) slug = AutoSlugField(populate_from="title", allow_unicode=True, unique=True, editable=False)
tags: ManyToManyField = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts") tags = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts")
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}"
@ -73,16 +92,37 @@ class Post(NiceModel):
class PostTag(NiceModel): class PostTag(NiceModel):
is_publicly_visible = True """
Represents a tag associated with a post.
tag_name: CharField = CharField( The PostTag class is used to define and manage tags that can be assigned
to posts. These tags include an internal identifier and a user-friendly
display name. The class supports internationalization for both the internal
identifier and the display name.
Attributes:
is_publicly_visible (bool): Determines if the tag is visible publicly.
tag_name (CharField): An internal tag identifier for the post's tag. It is a required
field with a maximum length of 255 characters.
name (CharField): A user-friendly, unique display name for the post's tag
with a maximum length of 255 characters.
Meta:
verbose_name (str): Human-readable singular name of the PostTag model.
verbose_name_plural (str): Human-readable plural name of the PostTag model.
"""
is_publicly_visible = True
posts: "Post"
tag_name = CharField(
blank=False, blank=False,
null=False, null=False,
max_length=255, max_length=255,
help_text=_("internal tag identifier for the post tag"), help_text=_("internal tag identifier for the post tag"),
verbose_name=_("tag name"), verbose_name=_("tag name"),
) )
name: CharField = CharField( name = CharField(
max_length=255, max_length=255,
help_text=_("user-friendly name for the post tag"), help_text=_("user-friendly name for the post tag"),
verbose_name=_("tag display name"), verbose_name=_("tag display name"),

9
blog/translation.py Normal file
View file

@ -0,0 +1,9 @@
from modeltranslation.decorators import register
from modeltranslation.translator import TranslationOptions
from blog.models import Post
@register(Post)
class PostOptions(TranslationOptions):
fields = ("title", "content")

View file

@ -3,6 +3,8 @@ from rest_framework.routers import DefaultRouter
from blog.viewsets import PostViewSet from blog.viewsets import PostViewSet
app_name = "blog"
payment_router = DefaultRouter() payment_router = DefaultRouter()
payment_router.register(prefix=r"posts", viewset=PostViewSet, basename="posts") payment_router.register(prefix=r"posts", viewset=PostViewSet, basename="posts")

View file

@ -1,3 +1,3 @@
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger("django")

View file

@ -8,6 +8,22 @@ from core.permissions import EvibesPermission
class PostViewSet(ReadOnlyModelViewSet): class PostViewSet(ReadOnlyModelViewSet):
"""
Encapsulates operations for managing and retrieving Post entities in a read-only model view set.
This class is tailored to handle Post objects that are active and allows filtration based on defined
filters. It integrates with Django's backend filtering system and ensures operations align with the
defined permissions. The view set also includes an additional "retrieve" permission configuration.
Attributes:
serializer_class: Specifies the serializer to be used for Post objects.
permission_classes: Defines the permissions required to interact with this view set.
queryset: Determines the initial queryset, filtered to include only active Post objects.
filter_backends: Lists the backends to be used for filtering querysets.
filterset_class: Defines the set of filters used for filtering Post objects.
additional: Contains additional configuration, such as specific action permissions.
"""
serializer_class = PostSerializer serializer_class = PostSerializer
permission_classes = (EvibesPermission,) permission_classes = (EvibesPermission,)
queryset = Post.objects.filter(is_active=True) queryset = Post.objects.filter(is_active=True)

View file

@ -1,5 +1,4 @@
import uuid import uuid
from datetime import datetime
from django.db.models import BooleanField, Model, UUIDField from django.db.models import BooleanField, Model, UUIDField
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -7,32 +6,24 @@ from django_extensions.db.fields import CreationDateTimeField, ModificationDateT
class NiceModel(Model): class NiceModel(Model):
id = None id: None = None
uuid: uuid = UUIDField( # type: ignore uuid = UUIDField(
verbose_name=_("unique id"), verbose_name=_("unique id"),
help_text=_("unique id is used to surely identify any database object"), help_text=_("unique id is used to surely identify any database object"),
primary_key=True, primary_key=True,
default=uuid.uuid4, default=uuid.uuid4,
editable=False, editable=False,
) )
is_active: bool = BooleanField( # type: ignore is_active = BooleanField(
default=True, default=True,
verbose_name=_("is active"), verbose_name=_("is active"),
help_text=_( help_text=_("if set to false, this object can't be seen by users without needed permission"),
"if set to false, this object can't be seen by users without needed permission"
),
)
created: datetime = CreationDateTimeField( # type: ignore
_("created"), help_text=_("when the object first appeared on the database")
)
modified: datetime = ModificationDateTimeField( # type: ignore
_("modified"), help_text=_("when the object was last modified")
) )
created = CreationDateTimeField(_("created"), help_text=_("when the object first appeared on the database"))
modified = ModificationDateTimeField(_("modified"), help_text=_("when the object was last modified"))
def save(self, **kwargs): def save(self, **kwargs):
self.update_modified = kwargs.pop( self.update_modified = kwargs.pop("update_modified", getattr(self, "update_modified", True))
"update_modified", getattr(self, "update_modified", True)
)
super().save(**kwargs) super().save(**kwargs)
class Meta: class Meta:

View file

@ -1,21 +1,20 @@
from contextlib import suppress from contextlib import suppress
from typing import ClassVar, Type
from constance.admin import Config from constance.admin import Config
from constance.admin import ConstanceAdmin as BaseConstanceAdmin from constance.admin import ConstanceAdmin as BaseConstanceAdmin
from django.apps import apps from django.apps import apps
from django.contrib import admin from django.contrib.admin import ModelAdmin, TabularInline, action, register, site
from django.contrib.admin import ModelAdmin, TabularInline
from django.contrib.gis.admin import GISModelAdmin 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 import Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from modeltranslation.translator import NotRegistered, translator from modeltranslation.translator import NotRegistered, translator
from modeltranslation.utils import get_translation_fields from modeltranslation.utils import get_translation_fields
from mptt.admin import DraggableMPTTAdmin from mptt.admin import DraggableMPTTAdmin
from evibes.settings import CONSTANCE_CONFIG from core.forms import OrderForm, OrderProductForm, VendorForm
from core.models import (
from .forms import OrderForm, OrderProductForm, VendorForm
from .models import (
Address, Address,
Attribute, Attribute,
AttributeGroup, AttributeGroup,
@ -35,14 +34,21 @@ from .models import (
Vendor, Vendor,
Wishlist, Wishlist,
) )
from evibes.settings import CONSTANCE_CONFIG
class FieldsetsMixin: class FieldsetsMixin:
general_fields: list = [] general_fields: list = []
relation_fields: list = [] relation_fields: list = []
model: Model model: ClassVar[Type[Model]]
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
if request:
pass
if obj:
pass
fieldsets = [] fieldsets = []
def add_translations_fieldset(fss): def add_translations_fieldset(fss):
@ -52,9 +58,7 @@ class FieldsetsMixin:
for orig in transoptions.local_fields: for orig in transoptions.local_fields:
translation_fields += get_translation_fields(orig) translation_fields += get_translation_fields(orig)
if translation_fields: if translation_fields:
fss = list(fss) + [ fss = list(fss) + [(_("translations"), {"fields": translation_fields})]
(_("translations"), {"fields": translation_fields})
]
return fss return fss
if self.general_fields: if self.general_fields:
@ -79,34 +83,37 @@ class FieldsetsMixin:
return fieldsets return fieldsets
class BasicModelAdmin(ModelAdmin): # noinspection PyUnresolvedReferences
@admin.action(description=str(_("activate selected %(verbose_name_plural)s"))) class ActivationActionsMixin:
def activate_selected(self, request, queryset) -> str: actions_on_top = True
if request: actions_on_bottom = True
pass actions = [
queryset.update(is_active=True) "delete_selected",
return str(_("%(verbose_name_plural)s activated successfully!"))
@admin.action(description=str(_("deactivate selected %(verbose_name_plural)s")))
def deactivate_selected(self, request, queryset) -> str:
if request:
pass
queryset.update(is_active=False)
return str(_("%(verbose_name_plural)s deactivated successfully."))
def get_actions(self, request):
actions = super().get_actions(request)
actions["activate_selected"] = (
self.activate_selected,
"activate_selected", "activate_selected",
str(_("activate selected %(verbose_name_plural)s")),
)
actions["deactivate_selected"] = (
self.deactivate_selected,
"deactivate_selected", "deactivate_selected",
str(_("deactivate selected %(verbose_name_plural)s")), ]
@action(description=_("activate selected %(verbose_name_plural)s").lower(), permissions=["change"])
def activate_selected(self, request, queryset):
try:
queryset.update(is_active=True)
self.message_user(
request=request, message=_("selected items have been activated.").lower(), level=messages.SUCCESS
) )
return actions
except Exception as e:
self.message_user(request=request, message=str(e), level=messages.ERROR)
@action(description=_("deactivate selected %(verbose_name_plural)s").lower(), permissions=["change"])
def deactivate_selected(self, request, queryset):
try:
queryset.update(is_active=False)
self.message_user(
request=request, message=_("selected items have been deactivated.").lower(), level=messages.SUCCESS
)
except Exception as e:
self.message_user(request=request, message=str(e), level=messages.ERROR)
class AttributeValueInline(TabularInline): class AttributeValueInline(TabularInline):
@ -148,12 +155,7 @@ class OrderProductInline(TabularInline):
icon = "fa-solid fa-boxes-packing" icon = "fa-solid fa-boxes-packing"
def get_queryset(self, request): def get_queryset(self, request):
return ( return super().get_queryset(request).select_related("product").only("product__name")
super()
.get_queryset(request)
.select_related("product")
.only("product__name")
)
class CategoryChildrenInline(TabularInline): class CategoryChildrenInline(TabularInline):
@ -167,9 +169,10 @@ class CategoryChildrenInline(TabularInline):
icon = "fa-solid fa-leaf" icon = "fa-solid fa-leaf"
@admin.register(AttributeGroup) @register(AttributeGroup)
class AttributeGroupAdmin(FieldsetsMixin, BasicModelAdmin): class AttributeGroupAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = AttributeGroup # type: ignore # noinspection PyClassVar
model = AttributeGroup # type: ignore [misc]
list_display = ("name", "modified") list_display = ("name", "modified")
search_fields = ("uuid", "name") search_fields = ("uuid", "name")
readonly_fields = ("uuid", "modified", "created") readonly_fields = ("uuid", "modified", "created")
@ -178,9 +181,10 @@ class AttributeGroupAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = [] relation_fields = []
@admin.register(Attribute) @register(Attribute)
class AttributeAdmin(FieldsetsMixin, BasicModelAdmin): class AttributeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Attribute # type: ignore # noinspection PyClassVar
model = Attribute # type: ignore [misc]
list_display = ("name", "group", "value_type", "modified") list_display = ("name", "group", "value_type", "modified")
list_filter = ("value_type", "group", "is_active") list_filter = ("value_type", "group", "is_active")
search_fields = ("uuid", "name", "group__name") search_fields = ("uuid", "name", "group__name")
@ -191,9 +195,10 @@ class AttributeAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["group", "categories"] relation_fields = ["group", "categories"]
@admin.register(AttributeValue) @register(AttributeValue)
class AttributeValueAdmin(FieldsetsMixin, BasicModelAdmin): class AttributeValueAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = AttributeValue # type: ignore # noinspection PyClassVar
model = AttributeValue # type: ignore [misc]
list_display = ("attribute", "value", "modified") list_display = ("attribute", "value", "modified")
list_filter = ("attribute__group", "is_active") list_filter = ("attribute__group", "is_active")
search_fields = ("uuid", "value", "attribute__name") search_fields = ("uuid", "value", "attribute__name")
@ -204,35 +209,39 @@ class AttributeValueAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["attribute", "product"] relation_fields = ["attribute", "product"]
@admin.register(Category) @register(Category)
class CategoryAdmin(FieldsetsMixin, DraggableMPTTAdmin, BasicModelAdmin): class CategoryAdmin(FieldsetsMixin, ActivationActionsMixin, DraggableMPTTAdmin):
model = Category # type: ignore # noinspection PyClassVar
model = Category
list_display = ("indented_title", "parent", "is_active", "modified") list_display = ("indented_title", "parent", "is_active", "modified")
# noinspection PyUnresolvedReferences
list_filter = ("is_active", "level", "created", "modified") list_filter = ("is_active", "level", "created", "modified")
search_fields = ("uuid", "name") search_fields = ("uuid", "name")
inlines = [CategoryChildrenInline] inlines = [CategoryChildrenInline]
autocomplete_fields = ["parent", "tags"] autocomplete_fields = ["parent", "tags"]
readonly_fields = ("slug", "uuid", "modified", "created") readonly_fields = ("slug", "uuid", "modified", "created")
general_fields = ["is_active", "name", "description", "image", "markup_percent"] general_fields = ["is_active", "name", "description", "image", "markup_percent", "priority"]
relation_fields = ["parent", "tags"] relation_fields = ["parent", "tags"]
@admin.register(Brand) @register(Brand)
class BrandAdmin(FieldsetsMixin, BasicModelAdmin): class BrandAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Brand # type: ignore # noinspection PyClassVar
model = Brand # type: ignore [misc]
list_display = ("name",) list_display = ("name",)
list_filter = ("categories", "is_active") list_filter = ("categories", "is_active")
search_fields = ("uuid", "name", "categories__name") search_fields = ("uuid", "name", "categories__name")
readonly_fields = ("uuid", "modified", "created") readonly_fields = ("uuid", "slug", "modified", "created")
general_fields = ["is_active", "name", "description"] general_fields = ["is_active", "name", "description", "priority"]
relation_fields = ["small_logo", "big_logo", "categories"] relation_fields = ["small_logo", "big_logo", "categories"]
@admin.register(Product) @register(Product)
class ProductAdmin(FieldsetsMixin, BasicModelAdmin): class ProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Product # type: ignore # noinspection PyClassVar
model = Product # type: ignore [misc]
list_display = ( list_display = (
"name", "name",
"partnumber", "partnumber",
@ -246,8 +255,8 @@ class ProductAdmin(FieldsetsMixin, BasicModelAdmin):
list_filter = ( list_filter = (
"is_active", "is_active",
"is_digital", "is_digital",
("tags", admin.RelatedOnlyFieldListFilter), "stocks__vendor",
("stocks__vendor", admin.RelatedOnlyFieldListFilter), "tags__name",
"created", "created",
"modified", "modified",
) )
@ -255,7 +264,9 @@ class ProductAdmin(FieldsetsMixin, BasicModelAdmin):
"name", "name",
"partnumber", "partnumber",
"brand__name", "brand__name",
"brand__slug",
"category__name", "category__name",
"category__slug",
"uuid", "uuid",
"slug", "slug",
) )
@ -263,13 +274,14 @@ class ProductAdmin(FieldsetsMixin, BasicModelAdmin):
autocomplete_fields = ("category", "brand", "tags") autocomplete_fields = ("category", "brand", "tags")
inlines = [AttributeValueInline, ProductImageInline, StockInline] inlines = [AttributeValueInline, ProductImageInline, StockInline]
general_fields = ["is_active", "name", "partnumber", "is_active", "is_digital"] general_fields = ["is_active", "name", "partnumber", "is_digital"]
relation_fields = ["category", "brand", "tags"] relation_fields = ["category", "brand", "tags"]
@admin.register(ProductTag) @register(ProductTag)
class ProductTagAdmin(FieldsetsMixin, BasicModelAdmin): class ProductTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = ProductTag # type: ignore # noinspection PyClassVar
model = ProductTag # type: ignore [misc]
list_display = ("tag_name",) list_display = ("tag_name",)
search_fields = ("tag_name",) search_fields = ("tag_name",)
readonly_fields = ("uuid", "modified", "created") readonly_fields = ("uuid", "modified", "created")
@ -278,9 +290,10 @@ class ProductTagAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = [] relation_fields = []
@admin.register(CategoryTag) @register(CategoryTag)
class CategoryTagAdmin(FieldsetsMixin, BasicModelAdmin): class CategoryTagAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = CategoryTag # type: ignore # noinspection PyClassVar
model = CategoryTag # type: ignore [misc]
list_display = ("tag_name",) list_display = ("tag_name",)
search_fields = ("tag_name",) search_fields = ("tag_name",)
readonly_fields = ("uuid", "modified", "created") readonly_fields = ("uuid", "modified", "created")
@ -289,22 +302,24 @@ class CategoryTagAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = [] relation_fields = []
@admin.register(Vendor) @register(Vendor)
class VendorAdmin(FieldsetsMixin, BasicModelAdmin): class VendorAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Vendor # type: ignore # noinspection PyClassVar
model = Vendor # type: ignore [misc]
list_display = ("name", "markup_percent", "modified") list_display = ("name", "markup_percent", "modified")
list_filter = ("markup_percent", "is_active") list_filter = ("markup_percent", "is_active")
search_fields = ("name",) search_fields = ("name",)
readonly_fields = ("uuid", "modified", "created") readonly_fields = ("uuid", "modified", "created")
form = VendorForm form = VendorForm
general_fields = ["is_active", "name", "markup_percent", "authentication"] general_fields = ["is_active", "name", "markup_percent", "authentication", "b2b_auth_token"]
relation_fields = [] relation_fields = ["users"]
@admin.register(Feedback) @register(Feedback)
class FeedbackAdmin(FieldsetsMixin, BasicModelAdmin): class FeedbackAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Feedback # type: ignore # noinspection PyClassVar
model = Feedback # type: ignore [misc]
list_display = ("order_product", "rating", "comment", "modified") list_display = ("order_product", "rating", "comment", "modified")
list_filter = ("rating", "is_active") list_filter = ("rating", "is_active")
search_fields = ("order_product__product__name", "comment") search_fields = ("order_product__product__name", "comment")
@ -314,9 +329,10 @@ class FeedbackAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["order_product"] relation_fields = ["order_product"]
@admin.register(Order) @register(Order)
class OrderAdmin(FieldsetsMixin, BasicModelAdmin): class OrderAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Order # type: ignore # noinspection PyClassVar
model = Order # type: ignore [misc]
list_display = ( list_display = (
"human_readable_id", "human_readable_id",
"user", "user",
@ -338,13 +354,14 @@ class OrderAdmin(FieldsetsMixin, BasicModelAdmin):
inlines = [OrderProductInline] inlines = [OrderProductInline]
form = OrderForm form = OrderForm
general_fields = ["is_active", "user", "status"] general_fields = ["is_active", "user", "status", "notifications", "attributes", "buy_time"]
relation_fields = ["promo_code", "billing_address", "shipping_address"] relation_fields = ["promo_code", "billing_address", "shipping_address"]
@admin.register(OrderProduct) @register(OrderProduct)
class OrderProductAdmin(FieldsetsMixin, BasicModelAdmin): class OrderProductAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = OrderProduct # type: ignore # noinspection PyClassVar
model = OrderProduct # type: ignore [misc]
list_display = ("order", "product", "quantity", "buy_price", "status", "modified") list_display = ("order", "product", "quantity", "buy_price", "status", "modified")
list_filter = ("status",) list_filter = ("status",)
search_fields = ("order__user__email", "product__name") search_fields = ("order__user__email", "product__name")
@ -355,9 +372,10 @@ class OrderProductAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["order", "product"] relation_fields = ["order", "product"]
@admin.register(PromoCode) @register(PromoCode)
class PromoCodeAdmin(FieldsetsMixin, BasicModelAdmin): class PromoCodeAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = PromoCode # type: ignore # noinspection PyClassVar
model = PromoCode # type: ignore [misc]
list_display = ( list_display = (
"code", "code",
"discount_percent", "discount_percent",
@ -383,9 +401,10 @@ class PromoCodeAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["user"] relation_fields = ["user"]
@admin.register(Promotion) @register(Promotion)
class PromotionAdmin(FieldsetsMixin, BasicModelAdmin): class PromotionAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Promotion # type: ignore # noinspection PyClassVar
model = Promotion # type: ignore [misc]
list_display = ("name", "discount_percent", "modified") list_display = ("name", "discount_percent", "modified")
search_fields = ("name",) search_fields = ("name",)
readonly_fields = ("uuid", "modified", "created") readonly_fields = ("uuid", "modified", "created")
@ -395,9 +414,10 @@ class PromotionAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["products"] relation_fields = ["products"]
@admin.register(Stock) @register(Stock)
class StockAdmin(FieldsetsMixin, BasicModelAdmin): class StockAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Stock # type: ignore # noinspection PyClassVar
model = Stock # type: ignore [misc]
list_display = ("product", "vendor", "sku", "quantity", "price", "modified") list_display = ("product", "vendor", "sku", "quantity", "price", "modified")
list_filter = ("vendor", "quantity") list_filter = ("vendor", "quantity")
search_fields = ("product__name", "vendor__name", "sku") search_fields = ("product__name", "vendor__name", "sku")
@ -415,9 +435,10 @@ class StockAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["product", "vendor"] relation_fields = ["product", "vendor"]
@admin.register(Wishlist) @register(Wishlist)
class WishlistAdmin(FieldsetsMixin, BasicModelAdmin): class WishlistAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = Wishlist # type: ignore # noinspection PyClassVar
model = Wishlist # type: ignore [misc]
list_display = ("user", "modified") list_display = ("user", "modified")
search_fields = ("user__email",) search_fields = ("user__email",)
readonly_fields = ("uuid", "modified", "created") readonly_fields = ("uuid", "modified", "created")
@ -426,9 +447,10 @@ class WishlistAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["products"] relation_fields = ["products"]
@admin.register(ProductImage) @register(ProductImage)
class ProductImageAdmin(FieldsetsMixin, BasicModelAdmin): class ProductImageAdmin(FieldsetsMixin, ActivationActionsMixin, ModelAdmin): # type: ignore [misc]
model = ProductImage # type: ignore # noinspection PyClassVar
model = ProductImage # type: ignore [misc]
list_display = ("alt", "product", "priority", "modified") list_display = ("alt", "product", "priority", "modified")
list_filter = ("priority",) list_filter = ("priority",)
search_fields = ("alt", "product__name") search_fields = ("alt", "product__name")
@ -439,9 +461,10 @@ class ProductImageAdmin(FieldsetsMixin, BasicModelAdmin):
relation_fields = ["product"] relation_fields = ["product"]
@admin.register(Address) @register(Address)
class AddressAdmin(FieldsetsMixin, GISModelAdmin): class AddressAdmin(FieldsetsMixin, GISModelAdmin):
model = Address # type: ignore # noinspection PyClassVar
model = Address # type: ignore [misc]
list_display = ("street", "city", "region", "country", "user") list_display = ("street", "city", "region", "country", "user")
list_filter = ("country", "region") list_filter = ("country", "region")
search_fields = ("street", "city", "postal_code", "user__email") search_fields = ("street", "city", "postal_code", "user__email")
@ -501,8 +524,10 @@ class ConstanceConfig:
_meta = Meta() _meta = Meta()
admin.site.unregister([Config]) # type: ignore # noinspection PyTypeChecker
admin.site.register([ConstanceConfig], BaseConstanceAdmin) # type: ignore site.unregister([Config])
admin.site.site_title = CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore # noinspection PyTypeChecker
admin.site.site_header = "eVibes" site.register([ConstanceConfig], BaseConstanceAdmin) # type: ignore [list-item]
admin.site.index_title = CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore site.site_title = CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]
site.site_header = "eVibes"
site.index_title = CONSTANCE_CONFIG["PROJECT_NAME"][0] # type: ignore [assignment]

View file

@ -30,6 +30,8 @@ from core.viewsets import (
WishlistViewSet, WishlistViewSet,
) )
app_name = "core"
core_router = DefaultRouter() core_router = DefaultRouter()
core_router.register(r"products", ProductViewSet, basename="products") core_router.register(r"products", ProductViewSet, basename="products")
core_router.register(r"orders", OrderViewSet, basename="orders") core_router.register(r"orders", OrderViewSet, basename="orders")
@ -55,10 +57,26 @@ sitemaps = {
urlpatterns = [ urlpatterns = [
path("core/", include(core_router.urls)), path("core/", include(core_router.urls)),
path( path(
"sitemap.xml", sitemap_index, {"sitemaps": sitemaps, "sitemap_url_name": "sitemap-detail"}, name="sitemap-index" "sitemap.xml",
sitemap_index,
{
"sitemaps": sitemaps,
"sitemap_url_name": "core:sitemap-detail",
},
name="sitemap-index",
),
path(
"sitemap-<section>.xml",
sitemap_detail,
{"sitemaps": sitemaps},
name="sitemap-detail",
),
path(
"sitemap-<section>-<int:page>.xml",
sitemap_detail,
{"sitemaps": sitemaps},
name="sitemap-detail",
), ),
path("sitemap-<section>.xml", sitemap_detail, {"sitemaps": sitemaps}, name="sitemap-detail"),
path("sitemap-<section>-<int:page>.xml", sitemap_detail, {"sitemaps": sitemaps}, name="sitemap-detail"),
path("download/<str:order_product_uuid>/", download_digital_asset_view, name="download_digital_asset"), path("download/<str:order_product_uuid>/", download_digital_asset_view, name="download_digital_asset"),
path("search/", GlobalSearchView.as_view(), name="global_search"), path("search/", GlobalSearchView.as_view(), name="global_search"),
path("app/cache/", CacheOperatorView.as_view(), name="cache_operator"), path("app/cache/", CacheOperatorView.as_view(), name="cache_operator"),

View file

@ -5,6 +5,8 @@ from core.views import (
GlobalSearchView, GlobalSearchView,
) )
app_name = "core"
urlpatterns = [ urlpatterns = [
path("search/", GlobalSearchView.as_view(), name="global_search"), path("search/", GlobalSearchView.as_view(), name="global_search"),
path("orders/buy_as_business/", BuyAsBusinessView.as_view(), name="request_cursed_url"), path("orders/buy_as_business/", BuyAsBusinessView.as_view(), name="request_cursed_url"),

View file

@ -35,6 +35,7 @@ from core.serializers import (
WishlistDetailSerializer, WishlistDetailSerializer,
WishlistSimpleSerializer, WishlistSimpleSerializer,
) )
from core.serializers.seo import SeoSnapshotSerializer
from core.serializers.utility import AddressCreateSerializer, AddressSuggestionSerializer, DoFeedbackSerializer from core.serializers.utility import AddressCreateSerializer, AddressSuggestionSerializer, DoFeedbackSerializer
from payments.serializers import TransactionProcessSerializer from payments.serializers import TransactionProcessSerializer
@ -122,28 +123,50 @@ ATTRIBUTE_VALUE_SCHEMA = {
CATEGORY_SCHEMA = { CATEGORY_SCHEMA = {
"list": extend_schema( "list": extend_schema(
summary=_("list all categories (simple view)"), summary=_("list all categories (simple view)"),
description=_("list all categories (simple view)"),
responses={status.HTTP_200_OK: CategorySimpleSerializer(many=True), **BASE_ERRORS}, responses={status.HTTP_200_OK: CategorySimpleSerializer(many=True), **BASE_ERRORS},
), ),
"retrieve": extend_schema( "retrieve": extend_schema(
summary=_("retrieve a single category (detailed view)"), summary=_("retrieve a single category (detailed view)"),
description=_("retrieve a single category (detailed view)"),
responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS}, responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS},
), ),
"create": extend_schema( "create": extend_schema(
summary=_("create a category"), summary=_("create a category"),
description=_("create a category"),
responses={status.HTTP_201_CREATED: CategoryDetailSerializer(), **BASE_ERRORS}, responses={status.HTTP_201_CREATED: CategoryDetailSerializer(), **BASE_ERRORS},
), ),
"destroy": extend_schema( "destroy": extend_schema(
summary=_("delete a category"), summary=_("delete a category"),
description=_("delete a category"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
), ),
"update": extend_schema( "update": extend_schema(
summary=_("rewrite an existing category saving non-editables"), summary=_("rewrite an existing category saving non-editables"),
description=_("rewrite an existing category saving non-editables"),
responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS}, responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS},
), ),
"partial_update": extend_schema( "partial_update": extend_schema(
summary=_("rewrite some fields of an existing category saving non-editables"), summary=_("rewrite some fields of an existing category saving non-editables"),
description=_("rewrite some fields of an existing category saving non-editables"),
responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS}, responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS},
), ),
"seo": extend_schema(
summary=_("SEO Meta Snapshot"),
description=_("returns a snapshot of the category's SEO meta data"),
parameters=[
OpenApiParameter(
name="lookup",
location="path",
description=_("Category UUID or slug"),
type=str,
),
],
responses={
status.HTTP_200_OK: SeoSnapshotSerializer(),
**BASE_ERRORS,
},
),
} }
ORDER_SCHEMA = { ORDER_SCHEMA = {
@ -347,6 +370,7 @@ ATTRIBUTES_DESC = _(
PRODUCT_SCHEMA = { PRODUCT_SCHEMA = {
"list": extend_schema( "list": extend_schema(
summary=_("list all products (simple view)"), summary=_("list all products (simple view)"),
description=_("list all products (simple view)"),
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="uuid", name="uuid",
@ -439,6 +463,7 @@ PRODUCT_SCHEMA = {
), ),
"retrieve": extend_schema( "retrieve": extend_schema(
summary=_("retrieve a single product (detailed view)"), summary=_("retrieve a single product (detailed view)"),
description=_("retrieve a single product (detailed view)"),
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="lookup_value", name="lookup_value",
@ -454,6 +479,7 @@ PRODUCT_SCHEMA = {
), ),
"create": extend_schema( "create": extend_schema(
summary=_("create a product"), summary=_("create a product"),
description=_("create a product"),
responses={ responses={
status.HTTP_201_CREATED: ProductDetailSerializer(), status.HTTP_201_CREATED: ProductDetailSerializer(),
**BASE_ERRORS, **BASE_ERRORS,
@ -461,6 +487,7 @@ PRODUCT_SCHEMA = {
), ),
"update": extend_schema( "update": extend_schema(
summary=_("rewrite an existing product, preserving non-editable fields"), summary=_("rewrite an existing product, preserving non-editable fields"),
description=_("rewrite an existing product, preserving non-editable fields"),
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="lookup", name="lookup",
@ -476,6 +503,7 @@ PRODUCT_SCHEMA = {
), ),
"partial_update": extend_schema( "partial_update": extend_schema(
summary=_("update some fields of an existing product, preserving non-editable fields"), summary=_("update some fields of an existing product, preserving non-editable fields"),
description=_("update some fields of an existing product, preserving non-editable fields"),
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="lookup", name="lookup",
@ -491,6 +519,7 @@ PRODUCT_SCHEMA = {
), ),
"destroy": extend_schema( "destroy": extend_schema(
summary=_("delete a product"), summary=_("delete a product"),
description=_("delete a product"),
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="lookup", name="lookup",
@ -506,6 +535,7 @@ PRODUCT_SCHEMA = {
), ),
"feedbacks": extend_schema( "feedbacks": extend_schema(
summary=_("lists all permitted feedbacks for a product"), summary=_("lists all permitted feedbacks for a product"),
description=_("lists all permitted feedbacks for a product"),
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="lookup", name="lookup",
@ -519,6 +549,22 @@ PRODUCT_SCHEMA = {
**BASE_ERRORS, **BASE_ERRORS,
}, },
), ),
"seo": extend_schema(
summary=_("SEO Meta Snapshot"),
description=_("returns a snapshot of the product's SEO meta data"),
parameters=[
OpenApiParameter(
name="lookup",
location="path",
description=_("Product UUID or slug"),
type=str,
),
],
responses={
status.HTTP_200_OK: SeoSnapshotSerializer(),
**BASE_ERRORS,
},
),
} }
ADDRESS_SCHEMA = { ADDRESS_SCHEMA = {

View file

@ -15,19 +15,15 @@ SMART_FIELDS = [
"name^6", "name^6",
"name.ngram^5", "name.ngram^5",
"name.phonetic", "name.phonetic",
"title^4", "title^4",
"title.ngram^3", "title.ngram^3",
"title.phonetic", "title.phonetic",
"description^2", "description^2",
"description.ngram", "description.ngram",
"description.phonetic", "description.phonetic",
"brand__name^3", "brand__name^3",
"brand__name.ngram", "brand__name.ngram",
"brand__name.auto", "brand__name.auto",
"category__name^2", "category__name^2",
"category__name.ngram", "category__name.ngram",
"category__name.auto", "category__name.auto",
@ -71,7 +67,6 @@ functions = [
"missing": 0, "missing": 0,
}, },
}, },
# category-level boost when searching for categories # category-level boost when searching for categories
{ {
"filter": Q("term", **{"_index": "categories"}), "filter": Q("term", **{"_index": "categories"}),
@ -82,7 +77,6 @@ functions = [
"missing": 0, "missing": 0,
}, },
}, },
# brand-level boost when searching for brands # brand-level boost when searching for brands
{ {
"filter": Q("term", **{"_index": "brands"}), "filter": Q("term", **{"_index": "brands"}),
@ -191,8 +185,8 @@ def process_query(query: str = "", request: Request | None = None) -> dict[str,
results[idx].append(hit_result) results[idx].append(hit_result)
return results return results
except NotFoundError: except NotFoundError as nfe:
raise Http404 raise Http404 from nfe
LANGUAGE_ANALYZER_MAP = { LANGUAGE_ANALYZER_MAP = {

View file

@ -12,13 +12,9 @@ class _BaseDoc(ActiveOnlyMixin, Document):
analyzer="standard", analyzer="standard",
fields={ fields={
"raw": fields.KeywordField(ignore_above=256), "raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField( "ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"), "phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField( "auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
analyzer="autocomplete", search_analyzer="autocomplete_search"
),
}, },
) )
description = fields.TextField( description = fields.TextField(
@ -26,13 +22,9 @@ class _BaseDoc(ActiveOnlyMixin, Document):
analyzer="standard", analyzer="standard",
fields={ fields={
"raw": fields.KeywordField(ignore_above=256), "raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField( "ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"), "phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField( "auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
analyzer="autocomplete", search_analyzer="autocomplete_search"
),
}, },
) )
slug = fields.KeywordField(attr="slug", index=False) slug = fields.KeywordField(attr="slug", index=False)

View file

@ -32,7 +32,7 @@ from django_filters import (
from core.elasticsearch import process_query from core.elasticsearch import process_query
from core.models import Address, Brand, Category, Feedback, Order, Product, Wishlist from core.models import Address, Brand, Category, Feedback, Order, Product, Wishlist
logger = logging.getLogger(__name__) logger = logging.getLogger("django")
class CaseInsensitiveListFilter(BaseInFilter, CharFilter): class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
@ -363,6 +363,7 @@ class CategoryFilter(FilterSet):
order_by = OrderingFilter( order_by = OrderingFilter(
fields=( fields=(
("priority", "priority"),
("uuid", "uuid"), ("uuid", "uuid"),
("name", "name"), ("name", "name"),
("?", "random"), ("?", "random"),
@ -430,6 +431,7 @@ class BrandFilter(FilterSet):
order_by = OrderingFilter( order_by = OrderingFilter(
fields=( fields=(
("priority", "priority"),
("uuid", "uuid"), ("uuid", "uuid"),
("name", "name"), ("name", "name"),
("?", "random"), ("?", "random"),

View file

@ -1,10 +1,10 @@
from graphene import Mutation from graphene import Mutation
class BaseMutation(Mutation): class BaseMutation(Mutation): # type: ignore [misc]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@staticmethod @staticmethod
def mutate(**kwargs): def mutate(**kwargs) -> None:
pass pass

View file

@ -13,13 +13,14 @@ from core.elasticsearch import process_query
from core.graphene import BaseMutation from core.graphene import BaseMutation
from core.graphene.object_types import ( from core.graphene.object_types import (
AddressType, AddressType,
BulkActionOrderProductInput, BulkProductInput,
OrderType, OrderType,
ProductType, ProductType,
SearchResultsType, SearchResultsType,
WishlistType, WishlistType,
FeedbackType,
) )
from core.models import Address, Category, Order, Product, Wishlist from core.models import Address, Category, Order, Product, Wishlist, OrderProduct
from core.utils import format_attributes, is_url_safe from core.utils import format_attributes, is_url_safe
from core.utils.caching import web_cache from core.utils.caching import web_cache
from core.utils.emailing import contact_us_email from core.utils.emailing import contact_us_email
@ -27,7 +28,7 @@ from core.utils.messages import permission_denied_message
from core.utils.nominatim import fetch_address_suggestions from core.utils.nominatim import fetch_address_suggestions
from payments.graphene.object_types import TransactionType from payments.graphene.object_types import TransactionType
logger = logging.getLogger(__name__) logger = logging.getLogger("django")
class CacheOperator(BaseMutation): class CacheOperator(BaseMutation):
@ -96,8 +97,8 @@ class AddOrderProduct(BaseMutation):
order = order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes)) order = order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
return AddOrderProduct(order=order) return AddOrderProduct(order=order)
except Order.DoesNotExist: except Order.DoesNotExist as dne:
raise Http404(_(f"order {order_uuid} not found")) raise Http404(_(f"order {order_uuid} not found")) from dne
class RemoveOrderProduct(BaseMutation): class RemoveOrderProduct(BaseMutation):
@ -122,8 +123,8 @@ class RemoveOrderProduct(BaseMutation):
order = order.remove_product(product_uuid=product_uuid, attributes=format_attributes(attributes)) order = order.remove_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
return AddOrderProduct(order=order) return AddOrderProduct(order=order)
except Order.DoesNotExist: except Order.DoesNotExist as dne:
raise Http404(_(f"order {order_uuid} not found")) raise Http404(_(f"order {order_uuid} not found")) from dne
class RemoveAllOrderProducts(BaseMutation): class RemoveAllOrderProducts(BaseMutation):
@ -174,13 +175,14 @@ class BuyOrder(BaseMutation):
description = _("buy an order") description = _("buy an order")
class Arguments: class Arguments:
order_uuid = UUID(required=False) order_uuid = String(required=False)
order_hr_id = String(required=False) order_hr_id = String(required=False)
force_balance = Boolean(required=False) force_balance = Boolean(required=False)
force_payment = Boolean(required=False) force_payment = Boolean(required=False)
promocode_uuid = UUID(required=False) promocode_uuid = String(required=False)
shipping_address = UUID(required=False) shipping_address = String(required=False)
billing_address = UUID(required=False) billing_address = String(required=False)
chosen_products = List(BulkProductInput, required=False)
order = Field(OrderType, required=False) order = Field(OrderType, required=False)
transaction = Field(TransactionType, required=False) transaction = Field(TransactionType, required=False)
@ -196,6 +198,7 @@ class BuyOrder(BaseMutation):
promocode_uuid=None, promocode_uuid=None,
shipping_address=None, shipping_address=None,
billing_address=None, billing_address=None,
chosen_products=None,
): ):
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]): if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]):
raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive")) raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive"))
@ -214,6 +217,7 @@ class BuyOrder(BaseMutation):
promocode_uuid=promocode_uuid, promocode_uuid=promocode_uuid,
shipping_address=shipping_address, shipping_address=shipping_address,
billing_address=billing_address, billing_address=billing_address,
chosen_products=chosen_products,
) )
match str(type(instance)): match str(type(instance)):
@ -224,8 +228,8 @@ class BuyOrder(BaseMutation):
case _: case _:
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}")) raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
except Order.DoesNotExist: except Order.DoesNotExist as dne:
raise Http404(_(f"order {order_uuid} not found")) raise Http404(_(f"order {order_uuid} not found")) from dne
class BulkOrderAction(BaseMutation): class BulkOrderAction(BaseMutation):
@ -236,7 +240,7 @@ class BulkOrderAction(BaseMutation):
order_uuid = UUID(required=False) order_uuid = UUID(required=False)
order_hr_id = String(required=False) order_hr_id = String(required=False)
action = String(required=True, description=_("remove/add")) action = String(required=True, description=_("remove/add"))
products = List(BulkActionOrderProductInput, required=True) products = List(BulkProductInput, required=True)
order = Field(OrderType, required=False) order = Field(OrderType, required=False)
@ -271,8 +275,48 @@ class BulkOrderAction(BaseMutation):
return BulkOrderAction(order=order) return BulkOrderAction(order=order)
except Order.DoesNotExist: except Order.DoesNotExist as dne:
raise Http404(_(f"order {order_uuid} not found")) raise Http404(_(f"order {order_uuid} not found")) from dne
class BulkWishlistAction(BaseMutation):
class Meta:
description = _("perform an action on a list of products in the wishlist")
class Arguments:
wishlist_uuid = UUID(required=False)
action = String(required=True, description="remove/add")
products = List(BulkProductInput, required=True)
wishlist = Field(WishlistType, required=False)
@staticmethod
def mutate(
_parent,
info,
action,
products,
wishlist_uuid=None,
):
if not wishlist_uuid:
raise BadRequest(_("please provide wishlist_uuid value"))
user = info.context.user
try:
wishlist = Wishlist.objects.get(user=user, uuid=wishlist_uuid)
# noinspection PyUnreachableCode
match action:
case "add":
wishlist = wishlist.bulk_add_products(products)
case "remove":
wishlist = wishlist.bulk_remove_products(products)
case _:
raise BadRequest(_("action must be either add or remove"))
return BulkWishlistAction(wishlist=wishlist)
except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class BuyUnregisteredOrder(BaseMutation): class BuyUnregisteredOrder(BaseMutation):
@ -344,8 +388,8 @@ class AddWishlistProduct(BaseMutation):
return AddWishlistProduct(wishlist=wishlist) return AddWishlistProduct(wishlist=wishlist)
except Wishlist.DoesNotExist: except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class RemoveWishlistProduct(BaseMutation): class RemoveWishlistProduct(BaseMutation):
@ -371,8 +415,8 @@ class RemoveWishlistProduct(BaseMutation):
return RemoveWishlistProduct(wishlist=wishlist) return RemoveWishlistProduct(wishlist=wishlist)
except Wishlist.DoesNotExist: except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class RemoveAllWishlistProducts(BaseMutation): class RemoveAllWishlistProducts(BaseMutation):
@ -398,8 +442,8 @@ class RemoveAllWishlistProducts(BaseMutation):
return RemoveAllWishlistProducts(wishlist=wishlist) return RemoveAllWishlistProducts(wishlist=wishlist)
except Wishlist.DoesNotExist: except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class BuyWishlist(BaseMutation): class BuyWishlist(BaseMutation):
@ -441,8 +485,8 @@ class BuyWishlist(BaseMutation):
case _: case _:
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}")) raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
except Wishlist.DoesNotExist: except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class BuyProduct(BaseMutation): class BuyProduct(BaseMutation):
@ -483,6 +527,37 @@ class BuyProduct(BaseMutation):
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}")) raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
class FeedbackProductAction(BaseMutation):
class Meta:
description = _("add or delete a feedback for orderproduct")
class Arguments:
order_product_uuid = UUID(required=True)
action = String(required=True, description="add/remove")
comment = String(required=False)
rating = Int(required=False)
feedback = Field(FeedbackType, required=False)
@staticmethod
def mutate(_parent, info, order_product_uuid, action, comment=None, rating=None):
user = info.context.user
try:
order_product = OrderProduct.objects.get(uuid=order_product_uuid)
if user != order_product.order.user or not user.has_perm("core.change_orderproduct"):
raise PermissionDenied(permission_denied_message)
match action:
case "add":
feedback = order_product.do_feedback(comment=comment, rating=rating, action="add")
case "remove":
feedback = order_product.do_feedback(action="remove")
case _:
raise BadRequest(_("action must be either `add` or `remove`"))
return FeedbackProductAction(feedback=feedback)
except OrderProduct.DoesNotExist as dne:
raise Http404(_(f"order product {order_product_uuid} not found")) from dne
class CreateProduct(BaseMutation): class CreateProduct(BaseMutation):
class Arguments: class Arguments:
name = String(required=True) name = String(required=True)
@ -575,9 +650,9 @@ class DeleteAddress(BaseMutation):
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
except Address.DoesNotExist: except Address.DoesNotExist as dne:
name = "Address" name = "Address"
raise Http404(_(f"{name} does not exist: {uuid}")) raise Http404(_(f"{name} does not exist: {uuid}")) from dne
class AutocompleteAddress(BaseMutation): class AutocompleteAddress(BaseMutation):

View file

@ -1,5 +1,8 @@
import logging
from constance import config
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Max, Min from django.db.models import Max, Min, QuerySet
from django.db.models.functions import Length from django.db.models.functions import Length
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import ( from graphene import (
@ -13,6 +16,7 @@ from graphene import (
ObjectType, ObjectType,
String, String,
relay, relay,
Boolean,
) )
from graphene.types.generic import GenericScalar from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
@ -40,8 +44,31 @@ from core.models import (
Vendor, Vendor,
Wishlist, Wishlist,
) )
from core.utils import graphene_current_lang, graphene_abs
from core.utils.seo_builders import (
org_schema,
breadcrumb_schema,
brand_schema,
website_schema,
category_schema,
item_list_schema,
product_schema,
)
from payments.graphene.object_types import TransactionType
from payments.models import Transaction
logger = __import__("logging").getLogger(__name__) logger = logging.getLogger("django")
class SEOMetaType(ObjectType):
title = String()
description = String()
canonical = String()
robots = String()
open_graph = GenericScalar()
twitter = GenericScalar()
json_ld = List(GenericScalar)
hreflang = List(GenericScalar)
class AttributeType(DjangoObjectType): class AttributeType(DjangoObjectType):
@ -87,6 +114,7 @@ class AttributeGroupType(DjangoObjectType):
class BrandType(DjangoObjectType): class BrandType(DjangoObjectType):
categories = List(lambda: CategoryType, description=_("categories")) categories = List(lambda: CategoryType, description=_("categories"))
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
class Meta: class Meta:
model = Brand model = Brand
@ -106,6 +134,48 @@ class BrandType(DjangoObjectType):
def resolve_small_logo(self: Brand, info): def resolve_small_logo(self: Brand, info):
return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else "" return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else ""
def resolve_seo_meta(self: Brand, info):
lang = graphene_current_lang()
base = f"https://{config.BASE_DOMAIN}"
canonical = f"{base}/{lang}/brand/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
description = (self.description or "")[:180]
logo_url = None
if getattr(self, "big_logo", None):
logo_url = graphene_abs(info.context, self.big_logo.url)
elif getattr(self, "small_logo", None):
logo_url = graphene_abs(info.context, self.small_logo.url)
og = {
"title": title,
"description": description,
"type": "website",
"url": canonical,
"image": logo_url or "",
}
tw = {"card": "summary_large_image", "title": title, "description": description, "image": logo_url or ""}
crumbs = [("Home", f"{base}/"), (self.name, canonical)]
json_ld = [
org_schema(),
website_schema(),
breadcrumb_schema(crumbs),
brand_schema(self, canonical, logo_url=logo_url),
]
return {
"title": title,
"description": description,
"canonical": canonical,
"robots": "index,follow",
"open_graph": og,
"twitter": tw,
"json_ld": json_ld,
"hreflang": [],
}
class FilterableAttributeType(ObjectType): class FilterableAttributeType(ObjectType):
attribute_name = String(required=True) attribute_name = String(required=True)
@ -134,6 +204,7 @@ class CategoryType(DjangoObjectType):
) )
tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category")) tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category"))
products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category")) products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category"))
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
class Meta: class Meta:
model = Category model = Category
@ -216,6 +287,58 @@ class CategoryType(DjangoObjectType):
"max_price": min_max_prices["max_price"], "max_price": min_max_prices["max_price"],
} }
def resolve_seo_meta(self: Category, info):
lang = graphene_current_lang()
base = f"https://{config.BASE_DOMAIN}"
canonical = f"{base}/{lang}/catalog/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
description = (self.description or "")[:180]
og_image = graphene_abs(info.context, self.image.url) if getattr(self, "image", None) else ""
og = {
"title": title,
"description": description,
"type": "website",
"url": canonical,
"image": og_image,
}
tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}
crumbs = [("Home", f"{base}/")]
for c in self.get_ancestors():
crumbs.append((c.name, f"{base}/{lang}/catalog/{c.slug}"))
crumbs.append((self.name, canonical))
json_ld = [org_schema(), website_schema(), breadcrumb_schema(crumbs), category_schema(self, canonical)]
product_urls = []
qs = (
Product.objects.filter(
is_active=True,
category=self,
brand__is_active=True,
stocks__vendor__is_active=True,
)
.only("slug")
.distinct()[:24]
)
for p in qs:
product_urls.append(f"{base}/{lang}/product/{p.slug}")
if product_urls:
json_ld.append(item_list_schema(product_urls))
return {
"title": title,
"description": description,
"canonical": canonical,
"robots": "index,follow",
"open_graph": og,
"twitter": tw,
"json_ld": json_ld,
"hreflang": [],
}
class VendorType(DjangoObjectType): class VendorType(DjangoObjectType):
markup_percent = Float(description=_("markup percentage")) markup_percent = Float(description=_("markup percentage"))
@ -234,6 +357,7 @@ class AddressType(DjangoObjectType):
class Meta: class Meta:
model = Address model = Address
interfaces = (relay.Node,)
fields = ( fields = (
"uuid", "uuid",
"street", "street",
@ -313,6 +437,7 @@ class OrderType(DjangoObjectType):
is_whole_digital = Float(description=_("are all products in the order digital")) is_whole_digital = Float(description=_("are all products in the order digital"))
attributes = GenericScalar(description=_("attributes")) attributes = GenericScalar(description=_("attributes"))
notifications = GenericScalar(description=_("notifications")) notifications = GenericScalar(description=_("notifications"))
payments_transactions = Field(TransactionType, description=_("transactions for this order"))
class Meta: class Meta:
model = Order model = Order
@ -329,21 +454,27 @@ class OrderType(DjangoObjectType):
"total_quantity", "total_quantity",
"is_whole_digital", "is_whole_digital",
"human_readable_id", "human_readable_id",
"payments_transactions",
) )
description = _("orders") description = _("orders")
def resolve_total_price(self, _info): def resolve_total_price(self: Order, _info):
return self.total_price return self.total_price
def resolve_total_quantity(self, _info): def resolve_total_quantity(self: Order, _info):
return self.total_quantity return self.total_quantity
def resolve_notifications(self, _info): def resolve_notifications(self: Order, _info):
return camelize(self.notifications) return camelize(self.notifications)
def resolve_attributes(self, _info): def resolve_attributes(self: Order, _info):
return camelize(self.attributes) return camelize(self.attributes)
def resolve_payments_transactions(self: Order, _info) -> QuerySet[Transaction] | None:
if self.payments_transactions:
return self.payments_transactions.all()
return None
class ProductImageType(DjangoObjectType): class ProductImageType(DjangoObjectType):
image = String(description=_("image url")) image = String(description=_("image url"))
@ -368,6 +499,8 @@ class ProductType(DjangoObjectType):
price = Float(description=_("price")) price = Float(description=_("price"))
quantity = Float(description=_("quantity")) quantity = Float(description=_("quantity"))
feedbacks_count = Int(description=_("number of feedbacks")) feedbacks_count = Int(description=_("number of feedbacks"))
personal_orders_only = Boolean(description=_("only available for personal orders"))
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
class Meta: class Meta:
model = Product model = Product
@ -381,13 +514,17 @@ class ProductType(DjangoObjectType):
"slug", "slug",
"description", "description",
"feedbacks", "feedbacks",
"feedbacks_count",
"personal_orders_only",
"quantity",
"attribute_groups",
"images", "images",
"price", "price",
) )
filter_fields = ["uuid", "name"] filter_fields = ["uuid", "name"]
description = _("products") description = _("products")
def resolve_price(self, _info) -> float: def resolve_price(self: Product, _info) -> float:
return self.price or 0.0 return self.price or 0.0
def resolve_feedbacks(self: Product, _info): def resolve_feedbacks(self: Product, _info):
@ -395,7 +532,7 @@ class ProductType(DjangoObjectType):
return Feedback.objects.filter(order_product__product=self) return Feedback.objects.filter(order_product__product=self)
return Feedback.objects.filter(order_product__product=self, is_active=True) return Feedback.objects.filter(order_product__product=self, is_active=True)
def resolve_feedbacks_count(self, _info) -> int: def resolve_feedbacks_count(self: Product, _info) -> int:
return self.feedbacks_count or 0 return self.feedbacks_count or 0
def resolve_attribute_groups(self: Product, info): def resolve_attribute_groups(self: Product, info):
@ -403,9 +540,58 @@ class ProductType(DjangoObjectType):
return AttributeGroup.objects.filter(attributes__values__product=self).distinct() return AttributeGroup.objects.filter(attributes__values__product=self).distinct()
def resolve_quantity(self, _info) -> int: def resolve_quantity(self: Product, _info) -> int:
return self.quantity or 0 return self.quantity or 0
def resolve_personal_orders_only(self: Product, _info) -> bool:
return False or self.personal_orders_only
def resolve_seo_meta(self: Product, info):
lang = graphene_current_lang()
base = f"https://{config.BASE_DOMAIN}"
canonical = f"{base}/{lang}/product/{self.slug}"
title = f"{self.name} | {config.PROJECT_NAME}"
description = (self.description or "")[:180]
first_img = self.images.order_by("priority").first()
og_image = graphene_abs(info.context, first_img.image.url) if first_img else ""
og = {
"title": title,
"description": description,
"type": "product",
"url": canonical,
"image": og_image,
}
tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}
crumbs = [("Home", f"{base}/")]
if self.category:
for c in self.category.get_ancestors(include_self=True):
crumbs.append((c.name, f"{base}/{lang}/catalog/{c.slug}"))
crumbs.append((self.name, canonical))
images = list(self.images.all()[:6])
rating = {"value": self.rating, "count": self.feedbacks_count}
json_ld = [
org_schema(),
website_schema(),
breadcrumb_schema(crumbs),
product_schema(self, images, rating=rating),
]
return {
"title": title,
"description": description,
"canonical": canonical,
"robots": "index,follow",
"open_graph": og,
"twitter": tw,
"json_ld": json_ld,
"hreflang": [],
}
class AttributeValueType(DjangoObjectType): class AttributeValueType(DjangoObjectType):
value = String(description=_("attribute value")) value = String(description=_("attribute value"))
@ -436,7 +622,7 @@ class PromoCodeType(DjangoObjectType):
description = _("promocodes") description = _("promocodes")
def resolve_discount(self: PromoCode, _info) -> float: def resolve_discount(self: PromoCode, _info) -> float:
return float(self.discount_percent) if self.discount_percent else float(self.discount_amount) return float(self.discount_percent) if self.discount_percent else float(self.discount_amount) # type: ignore [arg-type]
def resolve_discount_type(self: PromoCode, _info) -> str: def resolve_discount_type(self: PromoCode, _info) -> str:
return "percent" if self.discount_percent else "amount" return "percent" if self.discount_percent else "amount"
@ -557,6 +743,6 @@ class SearchResultsType(ObjectType):
posts = List(description=_("posts search results"), of_type=SearchPostsResultsType) posts = List(description=_("posts search results"), of_type=SearchPostsResultsType)
class BulkActionOrderProductInput(InputObjectType): class BulkProductInput(InputObjectType):
uuid = UUID(required=True) uuid = UUID(required=True)
attributes = GenericScalar(required=False) attributes = GenericScalar(required=False)

View file

@ -1,6 +1,7 @@
import logging import logging
from django.core.cache import cache from django.core.cache import cache
from django.utils import timezone
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from graphene import Field, List, ObjectType, Schema from graphene import Field, List, ObjectType, Schema
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
@ -14,12 +15,14 @@ from core.filters import (
OrderFilter, OrderFilter,
ProductFilter, ProductFilter,
WishlistFilter, WishlistFilter,
AddressFilter,
) )
from core.graphene.mutations import ( from core.graphene.mutations import (
AddOrderProduct, AddOrderProduct,
AddWishlistProduct, AddWishlistProduct,
AutocompleteAddress, AutocompleteAddress,
BulkOrderAction, BulkOrderAction,
BulkWishlistAction,
BuyOrder, BuyOrder,
BuyProduct, BuyProduct,
BuyWishlist, BuyWishlist,
@ -29,6 +32,7 @@ from core.graphene.mutations import (
CreateProduct, CreateProduct,
DeleteAddress, DeleteAddress,
DeleteProduct, DeleteProduct,
FeedbackProductAction,
RemoveAllOrderProducts, RemoveAllOrderProducts,
RemoveAllWishlistProducts, RemoveAllWishlistProducts,
RemoveOrderProduct, RemoveOrderProduct,
@ -56,6 +60,7 @@ from core.graphene.object_types import (
StockType, StockType,
VendorType, VendorType,
WishlistType, WishlistType,
AddressType,
) )
from core.models import ( from core.models import (
AttributeGroup, AttributeGroup,
@ -73,6 +78,7 @@ from core.models import (
Stock, Stock,
Vendor, Vendor,
Wishlist, Wishlist,
Address,
) )
from core.utils import get_project_parameters from core.utils import get_project_parameters
from core.utils.languages import get_flag_by_language from core.utils.languages import get_flag_by_language
@ -95,7 +101,7 @@ from vibes_auth.graphene.mutations import (
from vibes_auth.graphene.object_types import UserType from vibes_auth.graphene.object_types import UserType
from vibes_auth.models import User from vibes_auth.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger("django")
class Query(ObjectType): class Query(ObjectType):
@ -104,6 +110,7 @@ class Query(ObjectType):
products = DjangoFilterConnectionField(ProductType, filterset_class=ProductFilter) products = DjangoFilterConnectionField(ProductType, filterset_class=ProductFilter)
orders = DjangoFilterConnectionField(OrderType, filterset_class=OrderFilter) orders = DjangoFilterConnectionField(OrderType, filterset_class=OrderFilter)
users = DjangoFilterConnectionField(UserType, filterset_class=UserFilter) users = DjangoFilterConnectionField(UserType, filterset_class=UserFilter)
addresses = DjangoFilterConnectionField(AddressType, filterset_class=AddressFilter)
attribute_groups = DjangoFilterConnectionField(AttributeGroupType) attribute_groups = DjangoFilterConnectionField(AttributeGroupType)
categories = DjangoFilterConnectionField(CategoryType, filterset_class=CategoryFilter) categories = DjangoFilterConnectionField(CategoryType, filterset_class=CategoryFilter)
vendors = DjangoFilterConnectionField(VendorType) vendors = DjangoFilterConnectionField(VendorType)
@ -145,7 +152,10 @@ class Query(ObjectType):
Product.objects.all().select_related("brand", "category").prefetch_related("images", "stocks") Product.objects.all().select_related("brand", "category").prefetch_related("images", "stocks")
if info.context.user.has_perm("core.view_product") if info.context.user.has_perm("core.view_product")
else Product.objects.filter( else Product.objects.filter(
is_active=True, brand__is_active=True, category__is_active=True, stocks__isnull=False is_active=True,
brand__is_active=True,
category__is_active=True,
stocks__vendor__is_active=True,
) )
.select_related("brand", "category") .select_related("brand", "category")
.prefetch_related("images", "stocks") .prefetch_related("images", "stocks")
@ -284,7 +294,12 @@ class Query(ObjectType):
promocodes = PromoCode.objects promocodes = PromoCode.objects
if info.context.user.has_perm("core.view_promocode"): if info.context.user.has_perm("core.view_promocode"):
return promocodes.filter(user__uuid=kwargs.get("user_uuid")) or promocodes.all() return promocodes.filter(user__uuid=kwargs.get("user_uuid")) or promocodes.all()
return promocodes.filter(is_active=True, user=info.context.user) return promocodes.filter(
is_active=True,
user=info.context.user,
used_on__isnull=True,
start_time__lte=timezone.now(),
)
@staticmethod @staticmethod
def resolve_product_tags(_parent, info, **_kwargs): def resolve_product_tags(_parent, info, **_kwargs):
@ -298,6 +313,12 @@ class Query(ObjectType):
return CategoryTag.objects.all() return CategoryTag.objects.all()
return CategoryTag.objects.filter(is_active=True) return CategoryTag.objects.filter(is_active=True)
@staticmethod
def resolve_addresses(_parent, info, **_kwargs):
if info.context.user.has_perm("core.view_address"):
return Address.objects.all()
return Address.objects.filter(is_active=True, user=info.context.user)
class Mutation(ObjectType): class Mutation(ObjectType):
search = Search.Field() search = Search.Field()
@ -314,6 +335,8 @@ class Mutation(ObjectType):
remove_order_products_of_a_kind = RemoveOrderProductsOfAKind.Field() remove_order_products_of_a_kind = RemoveOrderProductsOfAKind.Field()
buy_order = BuyOrder.Field() buy_order = BuyOrder.Field()
bulk_order_action = BulkOrderAction.Field() bulk_order_action = BulkOrderAction.Field()
bulk_wishlist_action = BulkWishlistAction.Field()
feedback_product_action = FeedbackProductAction.Field()
deposit = Deposit.Field() deposit = Deposit.Field()
obtain_jwt_token = ObtainJSONWebToken.Field() obtain_jwt_token = ObtainJSONWebToken.Field()
refresh_jwt_token = RefreshJSONWebToken.Field() refresh_jwt_token = RefreshJSONWebToken.Field()

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show more