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
*.py,cover
nosetests.xml
desktop.ini
# Cache directories
__pycache__/
@ -37,7 +38,6 @@ __pypackages__/
# Packaging and distribution
# ──────────────────────────────────────────────────────────────────────────
build/
dist/
dist-ssr/
*.egg
*.egg-info/

7
.gitignore vendored
View file

@ -64,8 +64,9 @@ htmlcov/
.pybuilder/
# Storefronts
.astro/
.nuxt/
.next/
next-env.d.ts
# Celery
celerybeat-schedule
@ -141,6 +142,10 @@ cypress/screenshots/
# JetBrains
.idea/
!.idea/icon.svg
!.idea/externalDependencies.xml
!.idea/evibes.iml
!.idea/evibes.ico
# Microsoft
*.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
Dockerfile.app @fureunoir contact@fureunoir.com
Dockerfile.storefront @SaVBaD savbad@wiseless.xyz
Dockerfiles @@maintainer
docker-compose.yml @@maintainer
.gitignore @@maintainer
.dockerignore @@maintainer
@ -13,14 +12,6 @@ nginx @@maintainer
pyproject.toml @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
*.bat @fureunoir contact@fureunoir.com
*.sh @fureunoir contact@fureunoir.com
@ -31,5 +22,13 @@ storefront/ @SaVBaD savbad@wiseless.xyz
*.mjs @SaVBaD savbad@wiseless.xyz
*.cjs @SaVBaD savbad@wiseless.xyz
*.vue @SaVBaD savbad@wiseless.xyz
*.astro @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 \
libgts-dev \
libpq5 \
chrony \
graphviz \
binutils \
libproj-dev \
@ -46,4 +47,4 @@ RUN chmod +x /usr/local/bin/app-entrypoint.sh
COPY . .
ENTRYPOINT ["app-entrypoint.sh"]
ENTRYPOINT ["/usr/bin/bash", "app-entrypoint.sh"]

View file

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

View file

@ -44,10 +44,11 @@ extension.
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
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.

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n"
"Project-Id-Version: EVIBES 2.9.2\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"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,55 +17,52 @@ msgstr ""
msgid "blog"
msgstr "Blog"
#: blog/models.py:17
#: blog/models.py:36
msgid "post title"
msgstr "Indlæggets titel"
#: blog/models.py:17
#: blog/models.py:36
msgid "title"
msgstr "Titel"
#: blog/models.py:64
#: blog/models.py:83
msgid "post"
msgstr "Indlæg"
#: blog/models.py:65
#: blog/models.py:84
msgid "posts"
msgstr "Indlæg"
#: blog/models.py:69
#: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead"
msgstr "Markdown-filer understøttes ikke - brug markdown-indhold i stedet!"
#: blog/models.py:71
#: blog/models.py:90
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
"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"
msgstr "intern tag-identifikator for indlægs-tagget"
#: blog/models.py:83
#: blog/models.py:123
msgid "tag name"
msgstr "Tag-navn"
#: blog/models.py:87
#: blog/models.py:127
msgid "user-friendly name for the post tag"
msgstr "Brugervenligt navn til posttagget"
#: blog/models.py:88
#: blog/models.py:128
msgid "tag display name"
msgstr "Navn på tag-visning"
#: blog/models.py:96
#: blog/models.py:136
msgid "post tag"
msgstr "Tag til indlæg"
#: blog/models.py:97
#: blog/models.py:137
msgid "post tags"
msgstr "Tags til indlæg"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes-motor"

View file

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

View file

@ -5,9 +5,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n"
"Project-Id-Version: EVIBES 2.9.2\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"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -21,61 +21,52 @@ msgstr ""
msgid "blog"
msgstr "Blog"
#: blog/models.py:17
#: blog/models.py:36
msgid "post title"
msgstr "Post's title"
#: blog/models.py:17
#: blog/models.py:36
msgid "title"
msgstr "Title"
#: blog/models.py:64
#: blog/models.py:83
msgid "post"
msgstr "Post"
#: blog/models.py:65
#: blog/models.py:84
msgid "posts"
msgstr "Posts"
#: blog/models.py:69
#: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead"
msgstr "Markdown files are not supported yer - use markdown content instead!"
#: blog/models.py:71
#: blog/models.py:90
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
"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"
msgstr "internal tag identifier for the post tag"
#: blog/models.py:83
#: blog/models.py:123
msgid "tag name"
msgstr "Tag name"
#: blog/models.py:87
#: blog/models.py:127
msgid "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"
msgstr "Tag display name"
#: blog/models.py:96
#: blog/models.py:136
msgid "post tag"
msgstr "Post tag"
#: blog/models.py:97
#: blog/models.py:137
msgid "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 ""
msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n"
"Project-Id-Version: EVIBES 2.9.2\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"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,55 +17,52 @@ msgstr ""
msgid "blog"
msgstr "Blog"
#: blog/models.py:17
#: blog/models.py:36
msgid "post title"
msgstr "Post's title"
#: blog/models.py:17
#: blog/models.py:36
msgid "title"
msgstr "Title"
#: blog/models.py:64
#: blog/models.py:83
msgid "post"
msgstr "Post"
#: blog/models.py:65
#: blog/models.py:84
msgid "posts"
msgstr "Posts"
#: blog/models.py:69
#: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead"
msgstr "Markdown files are not supported yer - use markdown content instead!"
#: blog/models.py:71
#: blog/models.py:90
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
"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"
msgstr "internal tag identifier for the post tag"
#: blog/models.py:83
#: blog/models.py:123
msgid "tag name"
msgstr "Tag name"
#: blog/models.py:87
#: blog/models.py:127
msgid "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"
msgstr "Tag display name"
#: blog/models.py:96
#: blog/models.py:136
msgid "post tag"
msgstr "Post tag"
#: blog/models.py:97
#: blog/models.py:137
msgid "post tags"
msgstr "Post tags"
#~ msgid "eVibes Engine"
#~ msgstr "eVibes Engine"

View file

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

View file

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

View file

@ -5,9 +5,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n"
"Project-Id-Version: EVIBES 2.9.2\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"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -20,51 +20,51 @@ msgstr ""
msgid "blog"
msgstr ""
#: blog/models.py:17
#: blog/models.py:36
msgid "post title"
msgstr ""
#: blog/models.py:17
#: blog/models.py:36
msgid "title"
msgstr ""
#: blog/models.py:64
#: blog/models.py:83
msgid "post"
msgstr ""
#: blog/models.py:65
#: blog/models.py:84
msgid "posts"
msgstr ""
#: blog/models.py:69
#: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead"
msgstr ""
#: blog/models.py:71
#: blog/models.py:90
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
#: blog/models.py:82
#: blog/models.py:122
msgid "internal tag identifier for the post tag"
msgstr ""
#: blog/models.py:83
#: blog/models.py:123
msgid "tag name"
msgstr ""
#: blog/models.py:87
#: blog/models.py:127
msgid "user-friendly name for the post tag"
msgstr ""
#: blog/models.py:88
#: blog/models.py:128
msgid "tag display name"
msgstr ""
#: blog/models.py:96
#: blog/models.py:136
msgid "post tag"
msgstr ""
#: blog/models.py:97
#: blog/models.py:137
msgid "post tags"
msgstr ""

View file

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

View file

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

View file

@ -5,9 +5,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n"
"Project-Id-Version: EVIBES 2.9.2\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"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -20,51 +20,51 @@ msgstr ""
msgid "blog"
msgstr ""
#: blog/models.py:17
#: blog/models.py:36
msgid "post title"
msgstr ""
#: blog/models.py:17
#: blog/models.py:36
msgid "title"
msgstr ""
#: blog/models.py:64
#: blog/models.py:83
msgid "post"
msgstr ""
#: blog/models.py:65
#: blog/models.py:84
msgid "posts"
msgstr ""
#: blog/models.py:69
#: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead"
msgstr ""
#: blog/models.py:71
#: blog/models.py:90
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
#: blog/models.py:82
#: blog/models.py:122
msgid "internal tag identifier for the post tag"
msgstr ""
#: blog/models.py:83
#: blog/models.py:123
msgid "tag name"
msgstr ""
#: blog/models.py:87
#: blog/models.py:127
msgid "user-friendly name for the post tag"
msgstr ""
#: blog/models.py:88
#: blog/models.py:128
msgid "tag display name"
msgstr ""
#: blog/models.py:96
#: blog/models.py:136
msgid "post tag"
msgstr ""
#: blog/models.py:97
#: blog/models.py:137
msgid "post tags"
msgstr ""

View file

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

View file

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

View file

@ -1,9 +1,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: EVIBES 2.8.10\n"
"Project-Id-Version: EVIBES 2.9.2\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"
"Last-Translator: EGOR GORBUNOV <CONTACT@FUREUNOIR.COM>\n"
"Language-Team: LANGUAGE <CONTACT@FUREUNOIR.COM>\n"
@ -17,56 +17,53 @@ msgstr ""
msgid "blog"
msgstr "Blog"
#: blog/models.py:17
#: blog/models.py:36
msgid "post title"
msgstr "Título da postagem"
#: blog/models.py:17
#: blog/models.py:36
msgid "title"
msgstr "Título"
#: blog/models.py:64
#: blog/models.py:83
msgid "post"
msgstr "Postar"
#: blog/models.py:65
#: blog/models.py:84
msgid "posts"
msgstr "Publicações"
#: blog/models.py:69
#: blog/models.py:88
msgid "markdown files are not supported yet - use markdown content instead"
msgstr ""
"Os arquivos markdown não são suportados - use conteúdo markdown em vez disso!"
#: blog/models.py:71
#: blog/models.py:90
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
"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"
msgstr "identificador de tag interno para a tag de postagem"
#: blog/models.py:83
#: blog/models.py:123
msgid "tag name"
msgstr "Nome da etiqueta"
#: blog/models.py:87
#: blog/models.py:127
msgid "user-friendly name for the post tag"
msgstr "Nome de fácil utilização para a tag de postagem"
#: blog/models.py:88
#: blog/models.py:128
msgid "tag display name"
msgstr "Nome de exibição da tag"
#: blog/models.py:96
#: blog/models.py:136
msgid "post tag"
msgstr "Etiqueta de postagem"
#: blog/models.py:97
#: blog/models.py:137
msgid "post tags"
msgstr "Tags de postagem"
#~ msgid "eVibes Engine"
#~ msgstr "Motor eVibes"

View file

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

View file

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

View file

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

View file

@ -18,56 +18,113 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='PostTag',
name="PostTag",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False,
help_text='unique id is used to surely identify any database object',
primary_key=True, serialize=False, verbose_name='unique id')),
('is_active', models.BooleanField(default=True,
help_text="if set to false, this object can't be seen by users without needed permission",
verbose_name='is active')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True,
help_text='when the object first appeared on the database',
verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True,
help_text='when the object was last modified',
verbose_name='modified')),
('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')),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="unique id is used to surely identify any database object",
primary_key=True,
serialize=False,
verbose_name="unique id",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="if set to false, this object can't be seen by users without needed permission",
verbose_name="is active",
),
),
(
"created",
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True,
help_text="when the object first appeared on the database",
verbose_name="created",
),
),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
),
),
(
"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={
'verbose_name': 'post tag',
'verbose_name_plural': 'post tags',
"verbose_name": "post tag",
"verbose_name_plural": "post tags",
},
),
migrations.CreateModel(
name='Post',
name="Post",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False,
help_text='unique id is used to surely identify any database object',
primary_key=True, serialize=False, verbose_name='unique id')),
('is_active', models.BooleanField(default=True,
help_text="if set to false, this object can't be seen by users without needed permission",
verbose_name='is active')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True,
help_text='when the object first appeared on the database',
verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True,
help_text='when the object was last modified',
verbose_name='modified')),
('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')),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="unique id is used to surely identify any database object",
primary_key=True,
serialize=False,
verbose_name="unique id",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="if set to false, this object can't be seen by users without needed permission",
verbose_name="is active",
),
),
(
"created",
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True,
help_text="when the object first appeared on the database",
verbose_name="created",
),
),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, help_text="when the object was last modified", verbose_name="modified"
),
),
("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={
'verbose_name': 'post',
'verbose_name_plural': 'posts',
"verbose_name": "post",
"verbose_name_plural": "posts",
},
),
]

View file

@ -5,20 +5,21 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
("blog", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name='post',
name='slug',
field=django_extensions.db.fields.AutoSlugField(allow_unicode=True, blank=True, editable=False, populate_from='title', unique=True),
model_name="post",
name="slug",
field=django_extensions.db.fields.AutoSlugField(
allow_unicode=True, blank=True, editable=False, populate_from="title", unique=True
),
),
migrations.AlterField(
model_name='post',
name='title',
field=models.CharField(help_text='post title', max_length=128, unique=True, verbose_name='title'),
model_name="post",
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):
dependencies = [
('blog', '0002_alter_post_slug_alter_post_title'),
("blog", "0002_alter_post_slug_alter_post_title"),
]
operations = [
migrations.AlterField(
model_name='post',
name='tags',
field=models.ManyToManyField(blank=True, related_name='posts', to='blog.posttag'),
model_name="post",
name="tags",
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
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
author: ForeignKey = ForeignKey(
to="vibes_auth.User", on_delete=CASCADE, blank=False, null=False, related_name="posts"
)
title: CharField = CharField(
author = ForeignKey(to="vibes_auth.User", on_delete=CASCADE, blank=False, null=False, related_name="posts")
title = CharField(
unique=True, max_length=128, blank=False, null=False, help_text=_("post title"), verbose_name=_("title")
)
content: MarkdownField = MarkdownField(
@ -53,9 +72,9 @@ class Post(NiceModel):
blank=True,
null=True,
)
file: FileField = FileField(upload_to="posts/", blank=True, null=True)
slug: AutoSlugField = AutoSlugField(populate_from="title", allow_unicode=True, unique=True, editable=False)
tags: ManyToManyField = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts")
file = FileField(upload_to="posts/", blank=True, null=True)
slug = AutoSlugField(populate_from="title", allow_unicode=True, unique=True, editable=False)
tags = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts")
def __str__(self):
return f"{self.title} | {self.author.first_name} {self.author.last_name}"
@ -73,16 +92,37 @@ class Post(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,
null=False,
max_length=255,
help_text=_("internal tag identifier for the post tag"),
verbose_name=_("tag name"),
)
name: CharField = CharField(
name = CharField(
max_length=255,
help_text=_("user-friendly name for the post tag"),
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
app_name = "blog"
payment_router = DefaultRouter()
payment_router.register(prefix=r"posts", viewset=PostViewSet, basename="posts")

View file

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

View file

@ -8,6 +8,22 @@ from core.permissions import EvibesPermission
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
permission_classes = (EvibesPermission,)
queryset = Post.objects.filter(is_active=True)

View file

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

View file

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

View file

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

View file

@ -35,6 +35,7 @@ from core.serializers import (
WishlistDetailSerializer,
WishlistSimpleSerializer,
)
from core.serializers.seo import SeoSnapshotSerializer
from core.serializers.utility import AddressCreateSerializer, AddressSuggestionSerializer, DoFeedbackSerializer
from payments.serializers import TransactionProcessSerializer
@ -122,28 +123,50 @@ ATTRIBUTE_VALUE_SCHEMA = {
CATEGORY_SCHEMA = {
"list": extend_schema(
summary=_("list all categories (simple view)"),
description=_("list all categories (simple view)"),
responses={status.HTTP_200_OK: CategorySimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single category (detailed view)"),
description=_("retrieve a single category (detailed view)"),
responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a category"),
description=_("create a category"),
responses={status.HTTP_201_CREATED: CategoryDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a category"),
description=_("delete a category"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing category saving non-editables"),
description=_("rewrite an existing category saving non-editables"),
responses={status.HTTP_200_OK: CategoryDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
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},
),
"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 = {
@ -347,6 +370,7 @@ ATTRIBUTES_DESC = _(
PRODUCT_SCHEMA = {
"list": extend_schema(
summary=_("list all products (simple view)"),
description=_("list all products (simple view)"),
parameters=[
OpenApiParameter(
name="uuid",
@ -439,6 +463,7 @@ PRODUCT_SCHEMA = {
),
"retrieve": extend_schema(
summary=_("retrieve a single product (detailed view)"),
description=_("retrieve a single product (detailed view)"),
parameters=[
OpenApiParameter(
name="lookup_value",
@ -454,6 +479,7 @@ PRODUCT_SCHEMA = {
),
"create": extend_schema(
summary=_("create a product"),
description=_("create a product"),
responses={
status.HTTP_201_CREATED: ProductDetailSerializer(),
**BASE_ERRORS,
@ -461,6 +487,7 @@ PRODUCT_SCHEMA = {
),
"update": extend_schema(
summary=_("rewrite an existing product, preserving non-editable fields"),
description=_("rewrite an existing product, preserving non-editable fields"),
parameters=[
OpenApiParameter(
name="lookup",
@ -476,6 +503,7 @@ PRODUCT_SCHEMA = {
),
"partial_update": extend_schema(
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=[
OpenApiParameter(
name="lookup",
@ -491,6 +519,7 @@ PRODUCT_SCHEMA = {
),
"destroy": extend_schema(
summary=_("delete a product"),
description=_("delete a product"),
parameters=[
OpenApiParameter(
name="lookup",
@ -506,6 +535,7 @@ PRODUCT_SCHEMA = {
),
"feedbacks": extend_schema(
summary=_("lists all permitted feedbacks for a product"),
description=_("lists all permitted feedbacks for a product"),
parameters=[
OpenApiParameter(
name="lookup",
@ -519,6 +549,22 @@ PRODUCT_SCHEMA = {
**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 = {

View file

@ -15,19 +15,15 @@ SMART_FIELDS = [
"name^6",
"name.ngram^5",
"name.phonetic",
"title^4",
"title.ngram^3",
"title.phonetic",
"description^2",
"description.ngram",
"description.phonetic",
"brand__name^3",
"brand__name.ngram",
"brand__name.auto",
"category__name^2",
"category__name.ngram",
"category__name.auto",
@ -71,7 +67,6 @@ functions = [
"missing": 0,
},
},
# category-level boost when searching for categories
{
"filter": Q("term", **{"_index": "categories"}),
@ -82,7 +77,6 @@ functions = [
"missing": 0,
},
},
# brand-level boost when searching for 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)
return results
except NotFoundError:
raise Http404
except NotFoundError as nfe:
raise Http404 from nfe
LANGUAGE_ANALYZER_MAP = {

View file

@ -12,13 +12,9 @@ class _BaseDoc(ActiveOnlyMixin, Document):
analyzer="standard",
fields={
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(
analyzer="name_ngram", search_analyzer="query_lc"
),
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField(
analyzer="autocomplete", search_analyzer="autocomplete_search"
),
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
},
)
description = fields.TextField(
@ -26,13 +22,9 @@ class _BaseDoc(ActiveOnlyMixin, Document):
analyzer="standard",
fields={
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(
analyzer="name_ngram", search_analyzer="query_lc"
),
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
"auto": fields.TextField(
analyzer="autocomplete", search_analyzer="autocomplete_search"
),
"auto": fields.TextField(analyzer="autocomplete", search_analyzer="autocomplete_search"),
},
)
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.models import Address, Brand, Category, Feedback, Order, Product, Wishlist
logger = logging.getLogger(__name__)
logger = logging.getLogger("django")
class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
@ -363,6 +363,7 @@ class CategoryFilter(FilterSet):
order_by = OrderingFilter(
fields=(
("priority", "priority"),
("uuid", "uuid"),
("name", "name"),
("?", "random"),
@ -430,6 +431,7 @@ class BrandFilter(FilterSet):
order_by = OrderingFilter(
fields=(
("priority", "priority"),
("uuid", "uuid"),
("name", "name"),
("?", "random"),

View file

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

View file

@ -13,13 +13,14 @@ from core.elasticsearch import process_query
from core.graphene import BaseMutation
from core.graphene.object_types import (
AddressType,
BulkActionOrderProductInput,
BulkProductInput,
OrderType,
ProductType,
SearchResultsType,
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.caching import web_cache
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 payments.graphene.object_types import TransactionType
logger = logging.getLogger(__name__)
logger = logging.getLogger("django")
class CacheOperator(BaseMutation):
@ -96,8 +97,8 @@ class AddOrderProduct(BaseMutation):
order = order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
return AddOrderProduct(order=order)
except Order.DoesNotExist:
raise Http404(_(f"order {order_uuid} not found"))
except Order.DoesNotExist as dne:
raise Http404(_(f"order {order_uuid} not found")) from dne
class RemoveOrderProduct(BaseMutation):
@ -122,8 +123,8 @@ class RemoveOrderProduct(BaseMutation):
order = order.remove_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
return AddOrderProduct(order=order)
except Order.DoesNotExist:
raise Http404(_(f"order {order_uuid} not found"))
except Order.DoesNotExist as dne:
raise Http404(_(f"order {order_uuid} not found")) from dne
class RemoveAllOrderProducts(BaseMutation):
@ -174,13 +175,14 @@ class BuyOrder(BaseMutation):
description = _("buy an order")
class Arguments:
order_uuid = UUID(required=False)
order_uuid = String(required=False)
order_hr_id = String(required=False)
force_balance = Boolean(required=False)
force_payment = Boolean(required=False)
promocode_uuid = UUID(required=False)
shipping_address = UUID(required=False)
billing_address = UUID(required=False)
promocode_uuid = String(required=False)
shipping_address = String(required=False)
billing_address = String(required=False)
chosen_products = List(BulkProductInput, required=False)
order = Field(OrderType, required=False)
transaction = Field(TransactionType, required=False)
@ -196,6 +198,7 @@ class BuyOrder(BaseMutation):
promocode_uuid=None,
shipping_address=None,
billing_address=None,
chosen_products=None,
):
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"))
@ -214,6 +217,7 @@ class BuyOrder(BaseMutation):
promocode_uuid=promocode_uuid,
shipping_address=shipping_address,
billing_address=billing_address,
chosen_products=chosen_products,
)
match str(type(instance)):
@ -224,8 +228,8 @@ class BuyOrder(BaseMutation):
case _:
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
except Order.DoesNotExist:
raise Http404(_(f"order {order_uuid} not found"))
except Order.DoesNotExist as dne:
raise Http404(_(f"order {order_uuid} not found")) from dne
class BulkOrderAction(BaseMutation):
@ -236,7 +240,7 @@ class BulkOrderAction(BaseMutation):
order_uuid = UUID(required=False)
order_hr_id = String(required=False)
action = String(required=True, description=_("remove/add"))
products = List(BulkActionOrderProductInput, required=True)
products = List(BulkProductInput, required=True)
order = Field(OrderType, required=False)
@ -271,8 +275,48 @@ class BulkOrderAction(BaseMutation):
return BulkOrderAction(order=order)
except Order.DoesNotExist:
raise Http404(_(f"order {order_uuid} not found"))
except Order.DoesNotExist as dne:
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):
@ -344,8 +388,8 @@ class AddWishlistProduct(BaseMutation):
return AddWishlistProduct(wishlist=wishlist)
except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found"))
except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class RemoveWishlistProduct(BaseMutation):
@ -371,8 +415,8 @@ class RemoveWishlistProduct(BaseMutation):
return RemoveWishlistProduct(wishlist=wishlist)
except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found"))
except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class RemoveAllWishlistProducts(BaseMutation):
@ -398,8 +442,8 @@ class RemoveAllWishlistProducts(BaseMutation):
return RemoveAllWishlistProducts(wishlist=wishlist)
except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found"))
except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class BuyWishlist(BaseMutation):
@ -441,8 +485,8 @@ class BuyWishlist(BaseMutation):
case _:
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found"))
except Wishlist.DoesNotExist as dne:
raise Http404(_(f"wishlist {wishlist_uuid} not found")) from dne
class BuyProduct(BaseMutation):
@ -483,6 +527,37 @@ class BuyProduct(BaseMutation):
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 Arguments:
name = String(required=True)
@ -575,9 +650,9 @@ class DeleteAddress(BaseMutation):
raise PermissionDenied(permission_denied_message)
except Address.DoesNotExist:
except Address.DoesNotExist as dne:
name = "Address"
raise Http404(_(f"{name} does not exist: {uuid}"))
raise Http404(_(f"{name} does not exist: {uuid}")) from dne
class AutocompleteAddress(BaseMutation):

View file

@ -1,5 +1,8 @@
import logging
from constance import config
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.utils.translation import gettext_lazy as _
from graphene import (
@ -13,6 +16,7 @@ from graphene import (
ObjectType,
String,
relay,
Boolean,
)
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType
@ -40,8 +44,31 @@ from core.models import (
Vendor,
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):
@ -87,6 +114,7 @@ class AttributeGroupType(DjangoObjectType):
class BrandType(DjangoObjectType):
categories = List(lambda: CategoryType, description=_("categories"))
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
class Meta:
model = Brand
@ -106,6 +134,48 @@ class BrandType(DjangoObjectType):
def resolve_small_logo(self: Brand, info):
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):
attribute_name = String(required=True)
@ -134,6 +204,7 @@ class CategoryType(DjangoObjectType):
)
tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category"))
products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category"))
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
class Meta:
model = Category
@ -216,6 +287,58 @@ class CategoryType(DjangoObjectType):
"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):
markup_percent = Float(description=_("markup percentage"))
@ -234,6 +357,7 @@ class AddressType(DjangoObjectType):
class Meta:
model = Address
interfaces = (relay.Node,)
fields = (
"uuid",
"street",
@ -313,6 +437,7 @@ class OrderType(DjangoObjectType):
is_whole_digital = Float(description=_("are all products in the order digital"))
attributes = GenericScalar(description=_("attributes"))
notifications = GenericScalar(description=_("notifications"))
payments_transactions = Field(TransactionType, description=_("transactions for this order"))
class Meta:
model = Order
@ -329,21 +454,27 @@ class OrderType(DjangoObjectType):
"total_quantity",
"is_whole_digital",
"human_readable_id",
"payments_transactions",
)
description = _("orders")
def resolve_total_price(self, _info):
def resolve_total_price(self: Order, _info):
return self.total_price
def resolve_total_quantity(self, _info):
def resolve_total_quantity(self: Order, _info):
return self.total_quantity
def resolve_notifications(self, _info):
def resolve_notifications(self: Order, _info):
return camelize(self.notifications)
def resolve_attributes(self, _info):
def resolve_attributes(self: Order, _info):
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):
image = String(description=_("image url"))
@ -368,6 +499,8 @@ class ProductType(DjangoObjectType):
price = Float(description=_("price"))
quantity = Float(description=_("quantity"))
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:
model = Product
@ -381,13 +514,17 @@ class ProductType(DjangoObjectType):
"slug",
"description",
"feedbacks",
"feedbacks_count",
"personal_orders_only",
"quantity",
"attribute_groups",
"images",
"price",
)
filter_fields = ["uuid", "name"]
description = _("products")
def resolve_price(self, _info) -> float:
def resolve_price(self: Product, _info) -> float:
return self.price or 0.0
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, is_active=True)
def resolve_feedbacks_count(self, _info) -> int:
def resolve_feedbacks_count(self: Product, _info) -> int:
return self.feedbacks_count or 0
def resolve_attribute_groups(self: Product, info):
@ -403,9 +540,58 @@ class ProductType(DjangoObjectType):
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
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):
value = String(description=_("attribute value"))
@ -436,7 +622,7 @@ class PromoCodeType(DjangoObjectType):
description = _("promocodes")
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:
return "percent" if self.discount_percent else "amount"
@ -557,6 +743,6 @@ class SearchResultsType(ObjectType):
posts = List(description=_("posts search results"), of_type=SearchPostsResultsType)
class BulkActionOrderProductInput(InputObjectType):
class BulkProductInput(InputObjectType):
uuid = UUID(required=True)
attributes = GenericScalar(required=False)

View file

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