Initial Commit

This commit is contained in:
Egor Pavlovich Gorbunov 2025-04-29 22:40:30 +03:00
commit 68febcdb08
460 changed files with 117847 additions and 0 deletions

35
.dockerignore Normal file
View file

@ -0,0 +1,35 @@
*.log
*.sqlite3
*.pyo
.Python
env/
venv/
ENV/
env.bak/
venv.bak/
ENV.bak/
*.egg-info/
*.egg
*.eggs
*.db
.git/
htmlcov/
.coverage
.tox/
.DS_Store
.idea/
.vscode/
Dockerfile
docker-compose.yml
secrets/
.env
services_data/
static/
media/
node_modules//geo/data/admin1CodesASCII.txt
/geo/data/admin2Codes.txt
/geo/data/allCountries.zip
/geo/data/alternateNames.zip
/geo/data/cities5000.zip
/geo/data/countryInfo.txt
/geo/data/hierarchy.zip

89
.gitignore vendored Normal file
View file

@ -0,0 +1,89 @@
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
.idea
.env
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.manifest
*.spec
pip-log.txt
pip-delete-this-directory.txt
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
instance/
.webassets-cache
.scrapy
docs/_build/
.pybuilder/
target/
.ipynb_checkpoints
profile_default/
ipython_config.py
.pdm.toml
__pypackages__/
celerybeat-schedule
celerybeat.pid
*.sage.py
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.spyderproject
.spyproject
.ropeproject
/site
.mypy_cache/
.dmypy.json
dmypy.json
.pyre/
.pytype/
cython_debug/
services_data
services_data/postgres/*
services_data/redis/*
./static
!core/static/*
!geo/static/*
!payments/static/*
!vibes_auth/static/*
media
debug.log
errors.log
test.json

36
Dockerfile Normal file
View file

@ -0,0 +1,36 @@
# syntax=docker/dockerfile:1
FROM python:3.12-bookworm
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 update; \
apt install -y --no-install-recommends --fix-missing \
build-essential \
libpq-dev \
gettext \
libgettextpo-dev \
graphviz-dev \
libgts-dev \
libpq5 \
graphviz \
binutils \
libproj-dev \
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 install -E graph -E worker -E AI -E sentry
COPY . .

58
LICENSE Normal file
View file

@ -0,0 +1,58 @@
eVibes License Version 2.0, April 29, 2025
Copyright (c) 2025 Egor “fureunoir” Gorbunov
Permission is hereby granted, free of charge, to any person obtaining a copy of the eVibes software and associated documentation (the “Software”), to use, copy, modify, merge, publish, distribute, and sublicense the Software, subject to the terms and conditions below. Any distribution of the Software (in source or binary form) must include a copy of this License and preserve the above copyright notice. By using the Software, you indicate your acceptance of these terms. If you do not agree to these terms, you have no rights to use the Software.
1. Non-Commercial Use
The Software is provided at no cost for personal, academic, or other non-commercial purposes. “Non-Commercial Use” means any use of the Software that does not generate income (directly or indirectly) and is not part of a for-profit or revenue-generating activity. For Non-Commercial Use, the Software is provided “AS IS” and without any warranty or liability. You may freely use and modify the Software for Non-Commercial purposes, and you may distribute it for non-commercial ends as long as this License is included and the same terms apply to all recipients.
2. Commercial Use
Any use of the Software in a commercial context or as part of any project or service that earns money or other compensation is considered “Commercial Use.” This includes using the Software to develop products or services for sale, using it in a business environment to facilitate revenue, or any use that results in financial gain to you or your organization. For any Commercial Use, the following conditions apply:
a. Prior Authorization
You must obtain written permission from the author (Egor “fureunoir” Gorbunov) and enter into a separate commercial license agreement before using the Software for Commercial Use.
b. Automatic Royalty Obligation
If you use the Software commercially without first obtaining a separate signed agreement with the author, you are automatically bound by this License to pay a royalty fee of 8 % (eight percent) of all Income generated through such use of the Software. This obligation is self-executing and enforceable: by proceeding to use the Software commercially, you agree that 8 % of your income derived from the Software is due to the author as a royalty. This royalty is owed without any further action by the author, aside from providing payment instructions, and failure to pay it constitutes a breach of this License.
c. Definition of Income
For the purposes of this License, “Income” means any and all revenue, earnings, or profit obtained as a result of using the Software. This includes (but is not limited to) sales receipts, subscription or service fees, advertising revenue, cost savings that translate into greater profits, or any other form of monetary benefit derived from the Softwares use.
d. Payment and Reporting
Royalty payments of 8 % of Income shall be calculated and paid quarterly (every three months). Within 15 days after the end of each calendar quarter, you must provide the author with a written report detailing the total Income generated from your use of the Software during that quarter and remit the 8 % royalty payment for that period. Reports and payment inquiries shall be sent to the author at the contact email provided below.
e. Audit Rights
You agree to maintain accurate financial records of revenue relating to your use of the Software. The author (or an auditor appointed by the author) has the right to request an audit of relevant records to verify the accuracy of reported Income and royalties. Such an audit will be at the authors expense, unless a material underpayment of fees by you (more than 5 %) is discovered, in which case you must reimburse the reasonable audit costs.
f. Warranty for Authorized Commercial Users
The author is pleased to offer a limited warranty to those commercial users who have contacted the author and entered into a separate written agreement. The scope, duration, and terms of any such warranty shall be set forth in that separate agreement. In the absence of such an agreement, no warranty is provided.
g. No Unauthorized Commercial Use
Except as explicitly allowed under this Section, any Commercial Use without the authors prior consent or without paying the required royalties is prohibited. Using the Software in a commercial setting without honoring these terms violates the authors intellectual property rights and will result in termination of your rights (see Section 5) and potential legal liability.
3. Compliance and Enforcement
You, as a user of the Software, agree to strictly comply with all terms of this License. Failure to comply with any condition (for example, using the Software commercially without authorization or non-payment of the required fees) constitutes a material breach of this License. In the event of a breach, the author is entitled to enforce these terms through appropriate legal means. Specifically, you acknowledge that the royalty obligation in Section 2 is a binding debt enforceable in a court of law, and that the author may pursue legal action, injunctions, and damages to collect unpaid fees or to prevent unauthorized use. All rights granted to you under this License are conditioned on your continued compliance.
4. Warranty Disclaimer and Liability Limitation
Except as expressly agreed in a separate written agreement for authorized commercial users (Section 2 f), the Software is provided “AS IS,” without warranty of any kind. The author disclaims all warranties, express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement. You assume all risks associated with using the Software. In no event shall the author or any copyright holder be liable for any direct, indirect, incidental, special, exemplary, or consequential damages arising in any way out of use of the Software, even if advised of the possibility of such damage.
5. Termination of Rights
This License (and all rights granted to you hereunder) will automatically terminate if you fail to comply with any of its terms and conditions. Termination is effective immediately and without prior notice. Upon termination, you must cease all use of the Software, destroy or delete any copies in your possession, and confirm in writing to the author that you have complied. Termination does not relieve you of your obligation to pay any outstanding royalty fees. The provisions concerning fees, audits, enforcement, disclaimers, and limitations of liability survive termination.
6. Governing Law and Jurisdiction
This License is intended to be enforceable worldwide. Governing Law: By default, this License shall be governed by and construed in accordance with the substantive laws of the Russian Federation. However, the obligations and rights herein shall be interpreted to be consistent with and enforceable under the laws of any country in which a user operates or where enforcement is sought. Jurisdiction: You agree that the author may bring legal action to enforce this License in any court of competent jurisdiction, including but not limited to Moscow (Russian Federation), Berlin (Germany), or London (United Kingdom). You waive any objections based on inconvenient forum. In any action to enforce this License, the prevailing party shall be entitled to an award of its reasonable attorneys fees and costs.
7. Miscellaneous Provisions
• Severability: If any provision is held invalid or unenforceable, the remaining provisions remain in full force and effect.
• No Waiver: The authors failure to enforce any right or provision does not constitute a waiver of future enforcement.
• No Transfer: You may not assign this License without the authors prior written consent. The author may assign his rights to a successor or assignee at any time by notice.
• Entire Agreement: This License constitutes the entire agreement between you and the author regarding the Software and supersedes any prior understandings. No amendment is binding unless in writing and signed by the author.
Contact Information:
For inquiries, contract requests, reports, or notices, please contact:
Egor “fureunoir” Gorbunov
Email: contact@fureunoir.com
Telegram: https://t.me/fureunoir
By using the eVibes framework, you acknowledge that you have read and understood this License and agree to be bound by its terms.

134
README.md Normal file
View file

@ -0,0 +1,134 @@
# eVibes
![LOGO](core/docs/images/evibes-big.png)
eVibes is an eCommerce backend service built with Django. Its designed for flexibility, making it ideal for various use cases and learning Django skills. The project is easy to customize, allowing for straightforward editing and extension.
## Table of Contents
- [Features](#features)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Usage](#usage)
- [Contact](#contact)
## Features
- **Modular Architecture**: Easily extend and customize the backend to fit your needs.
- **Dockerized Deployment**: Quick setup and deployment using Docker and Docker Compose.
- **Asynchronous Task Processing**: Integrated Celery workers and beat scheduler for background tasks.
- **GraphQL and REST APIs**: Supports both GraphQL and RESTful API endpoints.
- **Internationalization**: Multilingual support using modeltranslate.
- **Advanced Caching**: Utilizes Redis for caching and task queuing.
- **Security**: Implements JWT authentication and rate limiting.
## Getting Started
### Prerequisites
- Docker and Docker Compose installed on your machine.
- Python 3.12 if running locally without Docker.
### Installation
1. Clone the repository:
```bash
git clone https://gitlab.com/wiseless.xyz/eVibes.git
cd eVibes
```
2. Copy the example environment file and configure it.
3. Build and start the services:
```bash
docker-compose up -d --build
```
This command will build the Docker images and start all the services defined in the `docker-compose.yml` file.
## Configuration
### Dockerfile
Don't forget to change the `RUN sed -i 's|https://deb.debian.org/debian|https://ftp.<locale>.debian.org/debian|g' /etc/apt/sources.list.d/debian.sources`
### Environment Variables
The project uses environment variables for configuration. Below is an example of the `.env` file:
```plaintext
PROJECT_NAME="eVibes"
FRONTEND_DOMAIN="evibes.com"
BASE_DOMAIN="evibes.com"
SENTRY_DSN=""
DEBUG=1
SECRET_KEY="SUPERSECRETKEY"
JWT_SIGNING_KEY="SUPERSECRETJWTSIGNINGKEY"
ALLOWED_HOSTS="localhost 127.0.0.1 evibes.com api.evibes.com b2b.evibes.com"
CSRF_TRUSTED_ORIGINS="http://api.localhost http://127.0.0.1 https://evibes.com https://api.evibes.com https://www.evibes.com https://b2b.evibes.com"
CORS_ALLOWED_ORIGINS="http://api.localhost http://127.0.0.1 https://evibes.com https://api.evibes.com https://www.evibes.com https://b2b.evibes.com"
POSTGRES_DB="evibes"
POSTGRES_USER="evibes_user"
POSTGRES_PASSWORD="SUPERSECRETPOSTGRESPASSWORD"
ELASTIC_PASSWORD="SUPERSECRETELASTICPASSWORD"
REDIS_PASSWORD="SUPERSECRETREDISPASSWORD"
CELERY_BROKER_URL="redis://:SUPERSECRETREDISPASSWORD@redis:6379/0"
CELERY_RESULT_BACKEND="redis://:SUPERSECRETREDISPASSWORD@redis:6379/0"
FLOWER_USER=evibes
FLOWER_PASSWORD="SUPERSECRETFLOWERPASSWORD"
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST="smtp.whatever.evibes.com"
EMAIL_PORT="465"
EMAIL_USE_TLS=0
EMAIL_USE_SSL=1
EMAIL_HOST_USER="your-email-user@whatever.evibes.com"
EMAIL_HOST_PASSWORD="SUPERSECRETEMAILHOSTPASSWORD"
EMAIL_FROM="your-email-user@whatever.evibes.com"
COMPANY_NAME="eVibes, Inc."
COMPANY_PHONE_NUMBER="+888888888888"
COMPANY_ADDRESS="The place that does not exist"
OPENAI_API_KEY="Haha, really?"
ABSTRACT_API_KEY="Haha, really? x2"
```
**Note**: Replace all placeholder values (e.g., `your-secret-key`, `your-database-name`) with your actual configuration.
## Usage
Add these lines to your hosts-file to use django-hosts functionality on localhost:
```hosts
127.0.0.1 api.localhost
127.0.0.1 b2b.localhost
```
Otherwise, add needed subdomains to DNS-settings of your domain.
Once the services are up and running, you can access the application at `http://api.localhost:8000`.
- **Django Admin**: `http://api.localhost:8000/admin/`
- **API Endpoints**:
- REST API: `http://api.localhost:8000/docs/swagger` or `http://api.localhost:8000/docs/redoc`
- GraphQL API: `http://api.localhost:8000/graphql/`
## Contact
- **Author**: Egor "fureunoir" Gorbunov
- Email: contact@fureunoir.com
- Telegram: [@fureunoir](https://t.me/fureunoir)
![FAVICON](core/docs/images/evibes.png)

0
blog/__init__.py Normal file
View file

43
blog/admin.py Normal file
View file

@ -0,0 +1,43 @@
from django.contrib import admin
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from .forms import PostAdminForm
from .models import Post, PostTag
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
form = PostAdminForm
list_display = ("title", "author", "slug", "created", "modified")
list_filter = ("author", "tags", "created", "modified")
search_fields = ("title", "content")
filter_horizontal = ("tags",)
date_hierarchy = "created"
autocomplete_fields = ("author", "tags")
readonly_fields = ("preview_html",)
fieldsets = (
(None, {
"fields": (
"author", "title",
"content",
"preview_html",
"file",
"tags",
)
}),
)
def preview_html(self, obj):
html = obj.content.html or "<em>{}</em>".format(_("(no content yet)"))
return mark_safe(html)
preview_html.short_description = _("rendered HTML")
@admin.register(PostTag)
class PostTagAdmin(admin.ModelAdmin):
list_display = ("tag_name", "name")
search_fields = ("tag_name", "name")
ordering = ("tag_name",)

12
blog/apps.py Normal file
View file

@ -0,0 +1,12 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
verbose_name = _("blog")
def ready(self):
import blog.elasticsearch.documents
import blog.signals # noqa: F401

0
blog/docs/__init__.py Normal file
View file

View file

0
blog/docs/drf/views.py Normal file
View file

View file

View file

@ -0,0 +1,35 @@
from django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registry
from blog.models import Post
from core.elasticsearch import COMMON_ANALYSIS, ActiveOnlyMixin
class PostDocument(ActiveOnlyMixin, Document):
title = fields.TextField(
attr="title",
analyzer="standard",
fields={
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"),
},
)
class Index:
name = "posts"
settings = {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": COMMON_ANALYSIS,
"index": {"max_ngram_diff": 18},
}
class Django:
model = Post
fields = ["uuid"]
registry.register_document(PostDocument)

26
blog/filters.py Normal file
View file

@ -0,0 +1,26 @@
from django_filters import CharFilter, FilterSet, OrderingFilter, UUIDFilter
from blog.models import Post
from core.filters import CaseInsensitiveListFilter
class PostFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
slug = CharFilter(field_name="slug", lookup_expr="exact")
author = UUIDFilter(field_name="author__uuid", lookup_expr="exact")
tags = CaseInsensitiveListFilter(field_name="tags__tag_name", label="Tags")
order_by = OrderingFilter(
fields=(
("uuid", "uuid"),
("slug", "slug"),
("author__uuid", "author"),
("created", "created"),
("modified", "modified"),
("?", "random"),
)
)
class Meta:
model = Post
fields = ["uuid", "slug", "author", "tags", "order_by"]

13
blog/forms.py Normal file
View file

@ -0,0 +1,13 @@
from django import forms
from blog.models import Post
from blog.widgets import MarkdownEditorWidget
class PostAdminForm(forms.ModelForm):
class Meta:
model = Post
fields = ("author", "content", "tags", "title")
widgets = {
"content": MarkdownEditorWidget(attrs={"style": "min-height: 500px;"}),
}

View file

View file

@ -0,0 +1,27 @@
import graphene
from graphene import relay
from graphene_django import DjangoObjectType
from blog.models import Post, PostTag
class PostType(DjangoObjectType):
tags = graphene.List(lambda: PostTagType)
content = graphene.String()
class Meta:
model = Post
fields = "__all__"
interfaces = (relay.Node,)
filter_fields = ["is_active"]
def resolve_content(self, info):
return self.content.html.replace("\n", "<br/>")
class PostTagType(DjangoObjectType):
class Meta:
model = PostTag
fields = "__all__"
interfaces = (relay.Node,)
filter_fields = ["is_active"]

Binary file not shown.

View file

@ -0,0 +1,71 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: ar-AR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(لا يوجد محتوى بعد)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "تم تقديمه بتنسيق HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "المدونة"
#: blog/models.py:16
msgid "post title"
msgstr "عنوان المنشور"
#: blog/models.py:17
msgid "title"
msgstr "العنوان"
#: blog/models.py:64
msgid "post"
msgstr "المنشور"
#: blog/models.py:65
msgid "posts"
msgstr "المنشورات"
#: blog/models.py:69
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr "يجب توفير ملف ترميز أو محتوى ترميز مخفض - متنافيان"
#: blog/models.py:80
msgid "internal tag identifier for the post tag"
msgstr "معرّف العلامة الداخلي لعلامة المنشور"
#: blog/models.py:81
msgid "tag name"
msgstr "اسم العلامة"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "اسم سهل الاستخدام لعلامة المنشور"
#: blog/models.py:86
msgid "tag display name"
msgstr "اسم عرض العلامة"
#: blog/models.py:94
msgid "post tag"
msgstr "علامة المشاركة"
#: blog/models.py:95
msgid "post tags"
msgstr "علامات المشاركة"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: cs-CZ\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(zatím bez obsahu)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "Vykreslené HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Název příspěvku"
#: blog/models.py:17
msgid "title"
msgstr "Název"
#: blog/models.py:64
msgid "post"
msgstr "Příspěvek"
#: blog/models.py:65
msgid "posts"
msgstr "Příspěvky"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "interní identifikátor tagu pro tag příspěvku"
#: blog/models.py:81
msgid "tag name"
msgstr "Název štítku"
#: blog/models.py:85
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:86
msgid "tag display name"
msgstr "Zobrazení názvu štítku"
#: blog/models.py:94
msgid "post tag"
msgstr "Označení příspěvku"
#: blog/models.py:95
msgid "post tags"
msgstr "Štítky příspěvků"

Binary file not shown.

View file

@ -0,0 +1,72 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: da-DK\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(intet indhold endnu)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "Rendered HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Indlæggets titel"
#: blog/models.py:17
msgid "title"
msgstr "Titel"
#: blog/models.py:64
msgid "post"
msgstr "Indlæg"
#: blog/models.py:65
msgid "posts"
msgstr "Indlæg"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "intern tag-identifikator for indlægs-tagget"
#: blog/models.py:81
msgid "tag name"
msgstr "Tag-navn"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Brugervenligt navn til posttagget"
#: blog/models.py:86
msgid "tag display name"
msgstr "Navn på tag-visning"
#: blog/models.py:94
msgid "post tag"
msgstr "Tag til indlæg"
#: blog/models.py:95
msgid "post tags"
msgstr "Tags til indlæg"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de-DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(noch kein Inhalt)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "Gerendertes HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Titel des Beitrags"
#: blog/models.py:17
msgid "title"
msgstr "Titel"
#: blog/models.py:64
msgid "post"
msgstr "Beitrag"
#: blog/models.py:65
msgid "posts"
msgstr "Beiträge"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "interner Tag-Bezeichner für den Post-Tag"
#: blog/models.py:81
msgid "tag name"
msgstr "Tag name"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Benutzerfreundlicher Name für das Post-Tag"
#: blog/models.py:86
msgid "tag display name"
msgstr "Tag-Anzeigename"
#: blog/models.py:94
msgid "post tag"
msgstr "Tag eintragen"
#: blog/models.py:95
msgid "post tags"
msgstr "Tags eintragen"

Binary file not shown.

View file

@ -0,0 +1,77 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(no content yet)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "Rendered HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Post's title"
#: blog/models.py:17
msgid "title"
msgstr "Title"
#: blog/models.py:64
msgid "post"
msgstr "Post"
#: blog/models.py:65
msgid "posts"
msgstr "Posts"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "internal tag identifier for the post tag"
#: blog/models.py:81
msgid "tag name"
msgstr "Tag name"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "User-friendly name for the post tag"
#: blog/models.py:86
msgid "tag display name"
msgstr "Tag display name"
#: blog/models.py:94
msgid "post tag"
msgstr "Post tag"
#: blog/models.py:95
msgid "post tags"
msgstr "Post tags"

Binary file not shown.

View file

@ -0,0 +1,72 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: en-US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(no content yet)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "Rendered HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Post's title"
#: blog/models.py:17
msgid "title"
msgstr "Title"
#: blog/models.py:64
msgid "post"
msgstr "Post"
#: blog/models.py:65
msgid "posts"
msgstr "Posts"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "internal tag identifier for the post tag"
#: blog/models.py:81
msgid "tag name"
msgstr "Tag name"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "User-friendly name for the post tag"
#: blog/models.py:86
msgid "tag display name"
msgstr "Tag display name"
#: blog/models.py:94
msgid "post tag"
msgstr "Post tag"
#: blog/models.py:95
msgid "post tags"
msgstr "Post tags"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: es-ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(sin contenido aún)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "HTML renderizado"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Título del mensaje"
#: blog/models.py:17
msgid "title"
msgstr "Título"
#: blog/models.py:64
msgid "post"
msgstr "Publicar en"
#: blog/models.py:65
msgid "posts"
msgstr "Puestos"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "identificador interno de la etiqueta post"
#: blog/models.py:81
msgid "tag name"
msgstr "Nombre de la etiqueta"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Nombre fácil de usar para la etiqueta de la entrada"
#: blog/models.py:86
msgid "tag display name"
msgstr "Nombre de la etiqueta"
#: blog/models.py:94
msgid "post tag"
msgstr "Etiqueta postal"
#: blog/models.py:95
msgid "post tags"
msgstr "Etiquetas"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: fr-FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(pas encore de contenu)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "HTML rendu"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Titre du message"
#: blog/models.py:17
msgid "title"
msgstr "Titre"
#: blog/models.py:64
msgid "post"
msgstr "Poste"
#: blog/models.py:65
msgid "posts"
msgstr "Postes"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "identifiant interne de la balise post"
#: blog/models.py:81
msgid "tag name"
msgstr "Nom du jour"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Nom convivial pour la balise post"
#: blog/models.py:86
msgid "tag display name"
msgstr "Nom d'affichage de l'étiquette"
#: blog/models.py:94
msgid "post tag"
msgstr "Tag de poste"
#: blog/models.py:95
msgid "post tags"
msgstr "Tags de la poste"

Binary file not shown.

View file

@ -0,0 +1,75 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr ""
#: blog/admin.py:36
msgid "rendered HTML"
msgstr ""
#: blog/apps.py:8
msgid "blog"
msgstr ""
#: blog/models.py:16
msgid "post title"
msgstr ""
#: blog/models.py:17
msgid "title"
msgstr ""
#: blog/models.py:64
msgid "post"
msgstr ""
#: blog/models.py:65
msgid "posts"
msgstr ""
#: blog/models.py:69
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
#: blog/models.py:80
msgid "internal tag identifier for the post tag"
msgstr ""
#: blog/models.py:81
msgid "tag name"
msgstr ""
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr ""
#: blog/models.py:86
msgid "tag display name"
msgstr ""
#: blog/models.py:94
msgid "post tag"
msgstr ""
#: blog/models.py:95
msgid "post tags"
msgstr ""

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: it-IT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(ancora senza contenuti)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "HTML renderizzato"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Titolo del post"
#: blog/models.py:17
msgid "title"
msgstr "Titolo"
#: blog/models.py:64
msgid "post"
msgstr "Posta"
#: blog/models.py:65
msgid "posts"
msgstr "Messaggi"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "identificatore interno del tag post"
#: blog/models.py:81
msgid "tag name"
msgstr "Nome del tag"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Nome intuitivo per il tag del post"
#: blog/models.py:86
msgid "tag display name"
msgstr "Nome del tag"
#: blog/models.py:94
msgid "post tag"
msgstr "Post tag"
#: blog/models.py:95
msgid "post tags"
msgstr "Tag dei post"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: ja-JP\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(内容はまだありません)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "レンダリングされたHTML"
#: blog/apps.py:8
msgid "blog"
msgstr "ブログ"
#: blog/models.py:16
msgid "post title"
msgstr "投稿タイトル"
#: blog/models.py:17
msgid "title"
msgstr "タイトル"
#: blog/models.py:64
msgid "post"
msgstr "ポスト"
#: blog/models.py:65
msgid "posts"
msgstr "投稿"
#: blog/models.py:69
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
"マークダウン・ファイルまたはマークダウン・コンテンツを提供しなければならな"
"い。"
#: blog/models.py:80
msgid "internal tag identifier for the post tag"
msgstr "投稿タグの内部タグ識別子"
#: blog/models.py:81
msgid "tag name"
msgstr "タグ名"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "投稿タグのユーザーフレンドリーな名前"
#: blog/models.py:86
msgid "tag display name"
msgstr "タグ表示名"
#: blog/models.py:94
msgid "post tag"
msgstr "投稿タグ"
#: blog/models.py:95
msgid "post tags"
msgstr "投稿タグ"

Binary file not shown.

View file

@ -0,0 +1,75 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr ""
#: blog/admin.py:36
msgid "rendered HTML"
msgstr ""
#: blog/apps.py:8
msgid "blog"
msgstr ""
#: blog/models.py:16
msgid "post title"
msgstr ""
#: blog/models.py:17
msgid "title"
msgstr ""
#: blog/models.py:64
msgid "post"
msgstr ""
#: blog/models.py:65
msgid "posts"
msgstr ""
#: blog/models.py:69
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
#: blog/models.py:80
msgid "internal tag identifier for the post tag"
msgstr ""
#: blog/models.py:81
msgid "tag name"
msgstr ""
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr ""
#: blog/models.py:86
msgid "tag display name"
msgstr ""
#: blog/models.py:94
msgid "post tag"
msgstr ""
#: blog/models.py:95
msgid "post tags"
msgstr ""

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: nl-NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(nog geen inhoud)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "HTML weergeven"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Titel van de post"
#: blog/models.py:17
msgid "title"
msgstr "Titel"
#: blog/models.py:64
msgid "post"
msgstr "Plaats"
#: blog/models.py:65
msgid "posts"
msgstr "Berichten"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "interne tagidentifier voor de posttag"
#: blog/models.py:81
msgid "tag name"
msgstr "Tag naam"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Gebruiksvriendelijke naam voor de posttag"
#: blog/models.py:86
msgid "tag display name"
msgstr "Tag weergavenaam"
#: blog/models.py:94
msgid "post tag"
msgstr "Post tag"
#: blog/models.py:95
msgid "post tags"
msgstr "Post tags"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: pl-PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(brak treści)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "Renderowany HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Tytuł postu"
#: blog/models.py:17
msgid "title"
msgstr "Tytuł"
#: blog/models.py:64
msgid "post"
msgstr "Post"
#: blog/models.py:65
msgid "posts"
msgstr "Posty"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "wewnętrzny identyfikator tagu posta"
#: blog/models.py:81
msgid "tag name"
msgstr "Nazwa tagu"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Przyjazna dla użytkownika nazwa tagu posta"
#: blog/models.py:86
msgid "tag display name"
msgstr "Wyświetlana nazwa znacznika"
#: blog/models.py:94
msgid "post tag"
msgstr "Tag posta"
#: blog/models.py:95
msgid "post tags"
msgstr "Tagi postów"

Binary file not shown.

View file

@ -0,0 +1,72 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: pt-BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(ainda não há conteúdo)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "HTML renderizado"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Título da postagem"
#: blog/models.py:17
msgid "title"
msgstr "Título"
#: blog/models.py:64
msgid "post"
msgstr "Postar"
#: blog/models.py:65
msgid "posts"
msgstr "Publicações"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "identificador de tag interno para a tag de postagem"
#: blog/models.py:81
msgid "tag name"
msgstr "Nome da etiqueta"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Nome de fácil utilização para a tag de postagem"
#: blog/models.py:86
msgid "tag display name"
msgstr "Nome de exibição da tag"
#: blog/models.py:94
msgid "post tag"
msgstr "Etiqueta de postagem"
#: blog/models.py:95
msgid "post tags"
msgstr "Tags de postagem"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: ro-RO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(fără conținut încă)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "HTML redat"
#: blog/apps.py:8
msgid "blog"
msgstr "Blog"
#: blog/models.py:16
msgid "post title"
msgstr "Titlul postului"
#: blog/models.py:17
msgid "title"
msgstr "Titlul"
#: blog/models.py:64
msgid "post"
msgstr "Post"
#: blog/models.py:65
msgid "posts"
msgstr "Mesaje"
#: blog/models.py:69
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:80
msgid "internal tag identifier for the post tag"
msgstr "identificator intern de etichetă pentru eticheta postului"
#: blog/models.py:81
msgid "tag name"
msgstr "Nume etichetă"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Nume ușor de utilizat pentru eticheta postului"
#: blog/models.py:86
msgid "tag display name"
msgstr "Nume afișare etichetă"
#: blog/models.py:94
msgid "post tag"
msgstr "Etichetă post"
#: blog/models.py:95
msgid "post tags"
msgstr "Etichete poștale"

Binary file not shown.

View file

@ -0,0 +1,73 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: ru-RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(пока без содержания)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "Рендеринг HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "Блог"
#: blog/models.py:16
msgid "post title"
msgstr "Заголовок сообщения"
#: blog/models.py:17
msgid "title"
msgstr "Название"
#: blog/models.py:64
msgid "post"
msgstr "Пост"
#: blog/models.py:65
msgid "posts"
msgstr "Посты"
#: blog/models.py:69
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr ""
"необходимо предоставить файл разметки или содержимое разметки - "
"взаимоисключающие варианты"
#: blog/models.py:80
msgid "internal tag identifier for the post tag"
msgstr "внутренний идентификатор тега для тега post"
#: blog/models.py:81
msgid "tag name"
msgstr "Название тега"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "Удобное для пользователя название тега поста"
#: blog/models.py:86
msgid "tag display name"
msgstr "Отображаемое имя тега"
#: blog/models.py:94
msgid "post tag"
msgstr "Тэг поста"
#: blog/models.py:95
msgid "post tags"
msgstr "Тэги постов"

Binary file not shown.

View file

@ -0,0 +1,71 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-29 12:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: zh-hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: blog/admin.py:33
msgid "(no content yet)"
msgstr "(暂无内容)"
#: blog/admin.py:36
msgid "rendered HTML"
msgstr "渲染的 HTML"
#: blog/apps.py:8
msgid "blog"
msgstr "博客"
#: blog/models.py:16
msgid "post title"
msgstr "帖子标题"
#: blog/models.py:17
msgid "title"
msgstr "标题"
#: blog/models.py:64
msgid "post"
msgstr "职位"
#: blog/models.py:65
msgid "posts"
msgstr "职位"
#: blog/models.py:69
msgid ""
"a markdown file or markdown content must be provided - mutually exclusive"
msgstr "必须提供标记符文件或标记符内容 - 相互排斥"
#: blog/models.py:80
msgid "internal tag identifier for the post tag"
msgstr "职位标签的内部标签标识符"
#: blog/models.py:81
msgid "tag name"
msgstr "标签名称"
#: blog/models.py:85
msgid "user-friendly name for the post tag"
msgstr "方便用户使用的帖子标签名称"
#: blog/models.py:86
msgid "tag display name"
msgstr "标签显示名称"
#: blog/models.py:94
msgid "post tag"
msgstr "职位标签"
#: blog/models.py:95
msgid "post tags"
msgstr "帖子标签"

View file

@ -0,0 +1,54 @@
# Generated by Django 5.1.8 on 2025-04-28 11:56
import django.db.models.deletion
import django_extensions.db.fields
import markdown_field.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
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')),
],
options={
'verbose_name': 'post tag',
'verbose_name_plural': 'post tags',
},
),
migrations.CreateModel(
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')),
],
options={
'verbose_name': 'post',
'verbose_name_plural': 'posts',
},
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.1.8 on 2025-04-28 12:07
import django_extensions.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('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),
),
migrations.AlterField(
model_name='post',
name='title',
field=models.CharField(help_text='post title', max_length=128, unique=True, verbose_name='title'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.8 on 2025-04-28 12:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('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'),
),
]

View file

95
blog/models.py Normal file
View file

@ -0,0 +1,95 @@
from django.db.models import CASCADE, CharField, FileField, ForeignKey, ManyToManyField
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField
from markdown.extensions.toc import TocExtension
from markdown_field import MarkdownField
from core.abstract import NiceModel
class Post(NiceModel):
is_publicly_visible = True
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("content",
extensions=[
TocExtension(toc_depth=3),
"pymdownx.arithmatex",
"pymdownx.b64",
"pymdownx.betterem",
"pymdownx.blocks.admonition",
"pymdownx.blocks.caption",
"pymdownx.blocks.definition",
"pymdownx.blocks.details",
"pymdownx.blocks.html",
"pymdownx.blocks.tab",
"pymdownx.caret",
"pymdownx.critic",
"pymdownx.emoji",
"pymdownx.escapeall",
"pymdownx.extra",
"pymdownx.fancylists",
"pymdownx.highlight",
"pymdownx.inlinehilite",
"pymdownx.keys",
"pymdownx.magiclink",
"pymdownx.mark",
"pymdownx.pathconverter",
"pymdownx.progressbar",
"pymdownx.saneheaders",
"pymdownx.smartsymbols",
"pymdownx.snippets",
"pymdownx.striphtml",
"pymdownx.superfences",
"pymdownx.tasklist",
"pymdownx.tilde"
], blank=True, null=True)
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}"
class Meta:
verbose_name = _("post")
verbose_name_plural = _("posts")
def save(self, **kwargs):
if not any([self.file, self.content]) or all([self.file, self.content]):
raise ValueError(_("a markdown file or markdown content must be provided - mutually exclusive"))
super().save(**kwargs)
class PostTag(NiceModel):
is_publicly_visible = True
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(
max_length=255,
help_text=_("user-friendly name for the post tag"),
verbose_name=_("tag display name"),
unique=True,
)
def __str__(self):
return self.tag_name
class Meta:
verbose_name = _("post tag")
verbose_name_plural = _("post tags")

22
blog/serializers.py Normal file
View file

@ -0,0 +1,22 @@
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import ModelSerializer, Serializer
from blog.models import Post, PostTag
class PostTagSerializer(ModelSerializer):
class Meta:
model = PostTag
fields = "__all__"
class PostSerializer(Serializer):
tags = PostTagSerializer(many=True)
content = SerializerMethodField()
class Meta:
model = Post
fields = "__all__"
def get_content(self, obj: Post) -> str:
return obj.content.html.replace("\n", "<br/>")

0
blog/signals.py Normal file
View file

View file

0
blog/tests.py Normal file
View file

11
blog/urls.py Normal file
View file

@ -0,0 +1,11 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from blog.viewsets import PostViewSet
payment_router = DefaultRouter()
payment_router.register(prefix=r"posts", viewset=PostViewSet, basename="posts")
urlpatterns = [
path(r"", include(payment_router.urls)),
]

0
blog/utils/__init__.py Normal file
View file

3
blog/views.py Normal file
View file

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

15
blog/viewsets.py Normal file
View file

@ -0,0 +1,15 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.viewsets import ReadOnlyModelViewSet
from blog.filters import PostFilter
from blog.models import Post
from blog.serializers import PostSerializer
from core.permissions import EvibesPermission
class PostViewSet(ReadOnlyModelViewSet):
serializer_class = PostSerializer
permission_classes = (EvibesPermission,)
queryset = Post.objects.filter(is_active=True)
filter_backends = [DjangoFilterBackend]
filterset_class = PostFilter

40
blog/widgets.py Normal file
View file

@ -0,0 +1,40 @@
from django import forms
from django.utils.safestring import mark_safe
class MarkdownEditorWidget(forms.Textarea):
class Media:
css = {
'all': (
'https://cdnjs.cloudflare.com/ajax/libs/easymde/2.14.0/easymde.min.css',
)
}
js = (
'https://cdnjs.cloudflare.com/ajax/libs/easymde/2.14.0/easymde.min.js',
)
def render(self, name, value, attrs=None, renderer=None):
textarea_html = super().render(name, value, attrs, renderer)
textarea_id = attrs.get('id', f'id_{name}')
init_js = f"""
<script>
document.addEventListener('DOMContentLoaded', function() {{
var el = document.getElementById("{textarea_id}");
if (!el || !window.EasyMDE) return;
new EasyMDE({{
element: el,
spellChecker: false,
renderingConfig: {{ singleLineBreaks: false }},
autoDownloadFontAwesome: false,
toolbar: [
"bold","italic","heading","|",
"quote","unordered-list","ordered-list","|",
"link","image","|",
"preview","side-by-side","fullscreen","|",
"guide"
]
}});
}});
</script>
"""
return mark_safe(textarea_html + init_js)

0
core/__init__.py Normal file
View file

31
core/abstract.py Normal file
View file

@ -0,0 +1,31 @@
import uuid
from django.db.models import BooleanField, Model, UUIDField
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import CreationDateTimeField, ModificationDateTimeField
class NiceModel(Model):
id = 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 = 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 = 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))
super().save(**kwargs)
class Meta:
abstract = True
get_latest_by = "modified"

409
core/admin.py Normal file
View file

@ -0,0 +1,409 @@
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.urls import path
from django.utils.translation import gettext_lazy as _
from mptt.admin import DraggableMPTTAdmin
from evibes.settings import CONSTANCE_CONFIG, LANGUAGES
from .forms import OrderForm, OrderProductForm, VendorForm
from .models import (
Attribute,
AttributeGroup,
AttributeValue,
Brand,
Category,
Feedback,
Order,
OrderProduct,
Product,
ProductImage,
ProductTag,
PromoCode,
Promotion,
Stock,
Vendor,
Wishlist,
)
class BasicModelAdmin(ModelAdmin):
@admin.action(description=_("activate selected %(verbose_name_plural)s"))
def activate_selected(self, request, queryset):
queryset.update(is_active=True)
@admin.action(description=_("deactivate selected %(verbose_name_plural)s"))
def deactivate_selected(self, request, queryset):
queryset.update(is_active=False)
def get_actions(self, request):
actions = super().get_actions(request)
actions["activate_selected"] = (
self.activate_selected,
"activate_selected",
_("activate selected %(verbose_name_plural)s"),
)
actions["deactivate_selected"] = (
self.deactivate_selected,
"deactivate_selected",
_("deactivate selected %(verbose_name_plural)s"),
)
return actions
class AttributeValueInline(TabularInline):
model = AttributeValue
extra = 0
is_navtab = True
verbose_name = _("attribute value")
verbose_name_plural = _("attribute values")
autocomplete_fields = ['attribute']
@admin.register(AttributeGroup)
class AttributeGroupAdmin(BasicModelAdmin):
list_display = ("name", "modified")
search_fields = (
"uuid",
"name",
)
@admin.register(Attribute)
class AttributeAdmin(BasicModelAdmin):
list_display = ("name", "group", "value_type", "modified")
list_filter = ("value_type", "group", "is_active")
search_fields = ("uuid", "name", "group__name")
autocomplete_fields = ["categories", "group"]
@admin.register(AttributeValue)
class AttributeValueAdmin(BasicModelAdmin):
list_display = ("attribute", "value", "modified")
list_filter = ("attribute__group", "attribute", "is_active")
search_fields = ("uuid", "value", "attribute__name")
autocomplete_fields = ["attribute"]
class CategoryChildrenInline(admin.TabularInline):
model = Category
fk_name = "parent"
extra = 0
fields = ("name", "description", "is_active", "image", "markup_percent")
@admin.register(Category)
class CategoryAdmin(DraggableMPTTAdmin, BasicModelAdmin):
mptt_indent_field = "name"
list_display = ("indented_title", "parent", "is_active", "modified")
list_filter = ("is_active", "parent", "level")
list_display_links = ("indented_title",)
search_fields = (
"uuid",
"name",
)
inlines = [CategoryChildrenInline]
fieldsets = [
(
None,
{
"fields": (
"name",
"description",
"parent",
"is_active",
"image",
"markup_percent",
)
},
)
]
autocomplete_fields = ["parent"]
def get_prepopulated_fields(self, request, obj=None):
return {"name": ("description",)}
def indented_title(self, instance):
return instance.name
indented_title.short_description = _("name")
indented_title.admin_order_field = "name"
@admin.register(Brand)
class BrandAdmin(BasicModelAdmin):
list_display = ("name",)
list_filter = ("categories", "is_active")
search_fields = (
"uuid",
"name",
"categories__name",
)
class ProductImageInline(TabularInline):
model = ProductImage
extra = 0
is_navtab = True
verbose_name = _("image")
verbose_name_plural = _("images")
class StockInline(TabularInline):
model = Stock
extra = 0
is_navtab = True
verbose_name = _("stock")
verbose_name_plural = _("stocks")
@admin.register(Product)
class ProductAdmin(BasicModelAdmin):
list_display = ("name", "partnumber", "is_active", "category", "brand", "price", "rating", "modified")
list_filter = (
"is_active",
"category",
"attributes__attribute",
"brand",
"tags__tag_name",
"created",
"modified",
)
search_fields = (
"name",
"partnumber",
"brand__name",
"category__name",
"uuid",
)
readonly_fields = ("created", "modified", "uuid", "rating", "price")
autocomplete_fields = ("category", "brand", "tags")
translatable_fields = [f"name_{code.replace('-', '_')}" for code, _lang in LANGUAGES]
translatable_fields += [f"description_{code.replace('-', '_')}" for code, _lang in LANGUAGES]
def price(self, obj):
return obj.price
price.short_description = _("price")
def rating(self, obj):
return obj.rating
rating.short_description = _("rating")
fieldsets = (
(
_("basic info"),
{
"fields": (
"uuid",
"partnumber",
"is_active",
"name",
"category",
"brand",
"description",
"tags",
)
},
),
(_("important dates"), {"fields": ("created", "modified")}),
(_("translations"), {"fields": translatable_fields})
)
inlines = [AttributeValueInline, ProductImageInline, StockInline]
def get_changelist(self, request, **kwargs):
changelist = super().get_changelist(request, **kwargs)
changelist.filter_input_length = 64
return changelist
@admin.register(ProductTag)
class ProductTagAdmin(BasicModelAdmin):
list_display = ("name",)
search_fields = ("name",)
@admin.register(Vendor)
class VendorAdmin(BasicModelAdmin):
list_display = ("name", "markup_percent", "modified")
list_filter = ("markup_percent", "is_active")
search_fields = ("name",)
form = VendorForm
@admin.register(Feedback)
class FeedbackAdmin(BasicModelAdmin):
list_display = ("order_product", "rating", "comment", "modified")
list_filter = ("rating", "is_active")
search_fields = ("order_product__product__name", "comment")
class OrderProductInline(admin.TabularInline):
model = OrderProduct
extra = 0
readonly_fields = ("product", "quantity", "buy_price")
form = OrderProductForm
is_navtab = True
verbose_name = _("order product")
verbose_name_plural = _("order products")
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("product").only("product__name")
@admin.register(Order)
class OrderAdmin(BasicModelAdmin):
list_display = ("uuid", "user", "status", "total_price", "buy_time", "modified")
list_filter = ("status", "buy_time", "modified", "created")
search_fields = ("user__email", "status")
inlines = [OrderProductInline]
form = OrderForm
readonly_fields = ("total_price", "total_quantity", "buy_time")
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related(
"user",
"shipping_address",
"billing_address",
"order_products",
"promo_code",
)
def save_model(self, request, obj, form, change):
if form.cleaned_data.get("attributes") is None:
obj.attributes = None
if form.cleaned_data.get("notifications") is None:
obj.attributes = None
super().save_model(request, obj, form, change)
@admin.register(OrderProduct)
class OrderProductAdmin(BasicModelAdmin):
list_display = ("order", "product", "quantity", "buy_price", "status", "modified")
list_filter = ("status",)
search_fields = ("order__user__email", "product__name")
form = OrderProductForm
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("order", "product")
def save_model(self, request, obj, form, change):
if form.cleaned_data.get("attributes") is None:
obj.attributes = None
if form.cleaned_data.get("notifications") is None:
obj.attributes = None
super().save_model(request, obj, form, change)
@admin.register(PromoCode)
class PromoCodeAdmin(BasicModelAdmin):
list_display = (
"code",
"discount_percent",
"discount_amount",
"start_time",
"end_time",
"used_on",
)
list_filter = ("discount_percent", "discount_amount", "start_time", "end_time")
search_fields = ("code",)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("user")
@admin.register(Promotion)
class PromotionAdmin(BasicModelAdmin):
list_display = ("name", "discount_percent", "modified")
search_fields = ("name",)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("products")
@admin.register(Stock)
class StockAdmin(BasicModelAdmin):
list_display = ("product", "vendor", "sku", "quantity", "price", "modified")
list_filter = ("vendor", "quantity")
search_fields = ("product__name", "vendor__name", "sku")
autocomplete_fields = ("product", "vendor")
@admin.register(Wishlist)
class WishlistAdmin(BasicModelAdmin):
list_display = ("user", "modified")
search_fields = ("user__email",)
@admin.register(ProductImage)
class ProductImageAdmin(BasicModelAdmin):
list_display = ("alt", "product", "priority", "modified")
list_filter = ("priority",)
search_fields = ("alt", "product__name")
autocomplete_fields = ("product",)
class ConstanceAdmin(BaseConstanceAdmin):
def get_urls(self):
info = f"{self.model._meta.app_label}_{self.model._meta.model_name}"
return [
path(
"",
self.admin_site.admin_view(self.changelist_view),
name=f"{info}_changelist",
),
path("", self.admin_site.admin_view(self.changelist_view), name=f"{info}_add"),
]
class ConstanceConfig:
class Meta:
app_label = "core"
object_name = "Config"
concrete_model = None
model_name = module_name = "config"
verbose_name_plural = _("config")
abstract = False
swapped = False
def get_ordered_objects(self):
return False
def get_change_permission(self):
return f"change_{self.model_name}"
@property
def app_config(self):
return apps.get_app_config(self.app_label)
@property
def label(self):
return f"{self.app_label}.{self.object_name}"
@property
def label_lower(self):
return f"{self.app_label}.{self.model_name}"
_meta = Meta()
admin.site.unregister([Config])
admin.site.register([ConstanceConfig], ConstanceAdmin)
admin.site.site_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}"
admin.site.site_header = "eVibes"
admin.site.index_title = f"{CONSTANCE_CONFIG.get('PROJECT_NAME')[0]}"

12
core/apps.py Normal file
View file

@ -0,0 +1,12 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"
verbose_name = _("core")
def ready(self):
import core.elasticsearch.documents
import core.signals # noqa: F401

24
core/choices.py Normal file
View file

@ -0,0 +1,24 @@
from django.utils.translation import gettext_lazy as _
ORDER_PRODUCT_STATUS_CHOICES = (
("FINISHED", _("finished")),
("DELIVERING", _("delivering")),
("DELIVERED", _("delivered")),
("CANCELED", _("canceled")),
("FAILED", _("failed")),
("PENDING", _("pending")),
("ACCEPTED", _("accepted")),
("RETURNED", _("money returned")),
)
ORDER_STATUS_CHOICES = (
("PENDING", _("pending")),
("FAILED", _("failed")),
("PAYMENT", _("payment")),
("CREATED", _("created")),
("DELIVERING", _("delivering")),
("FINISHED", _("finished")),
("MOMENTAL", _("momental")),
)
TRANSACTION_STATUS_CHOICES = (("failed", _("failed")), ("successful", _("successful")))

0
core/data/.gitkeep Normal file
View file

View file

@ -0,0 +1,6 @@
{
"payment_methods": [
"CASH",
"CARD"
]
}

0
core/docs/__init__.py Normal file
View file

14
core/docs/drf/__init__.py Normal file
View file

@ -0,0 +1,14 @@
from drf_spectacular.utils import inline_serializer
from rest_framework import status
from rest_framework.fields import CharField
error = inline_serializer("error", fields={"detail": CharField()})
BASE_ERRORS = {
status.HTTP_400_BAD_REQUEST: error,
status.HTTP_401_UNAUTHORIZED: error,
status.HTTP_403_FORBIDDEN: error,
status.HTTP_404_NOT_FOUND: error,
status.HTTP_405_METHOD_NOT_ALLOWED: error,
status.HTTP_500_INTERNAL_SERVER_ERROR: error,
}

84
core/docs/drf/views.py Normal file
View file

@ -0,0 +1,84 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
from rest_framework import status
from rest_framework.fields import CharField, DictField, JSONField, ListField
from core.docs.drf import error
from core.serializers import (
CacheOperatorSerializer,
ContactUsSerializer,
LanguageSerializer,
)
CACHE_SCHEMA = {
"post": extend_schema(
summary=_("cache I/O"),
description=_(
"apply only a key to read permitted data from cache.\napply key, data and timeout with authentication to write data to cache." # noqa: E501
),
request=CacheOperatorSerializer,
responses={
status.HTTP_200_OK: inline_serializer("cache", fields={"data": JSONField()}),
status.HTTP_400_BAD_REQUEST: error,
},
),
}
LANGUAGE_SCHEMA = {
"get": extend_schema(
summary=_("get a list of supported languages"),
responses={
status.HTTP_200_OK: LanguageSerializer(many=True),
},
)
}
PARAMETERS_SCHEMA = {
"get": extend_schema(
summary=_("get application's exposable parameters"),
responses={status.HTTP_200_OK: inline_serializer("parameters", fields={"key": CharField(default="value")})},
)
}
CONTACT_US_SCHEMA = {
"post": extend_schema(
summary=_("send a message to the support team"),
request=ContactUsSerializer,
responses={
status.HTTP_200_OK: ContactUsSerializer,
status.HTTP_400_BAD_REQUEST: error,
},
)
}
REQUEST_CURSED_URL_SCHEMA = {
"post": extend_schema(
summary=_("request a CORSed URL"),
request=inline_serializer("url", fields={"url": CharField(default="https://example.org")}),
responses={
status.HTTP_200_OK: inline_serializer("data", fields={"data": JSONField()}),
status.HTTP_400_BAD_REQUEST: error,
},
)
}
SEARCH_SCHEMA = {
"get": extend_schema(
parameters=[
OpenApiParameter(
name="q",
description="The search query string.",
required=True,
type=str,
)
],
responses={
200: inline_serializer(
name="GlobalSearchResponse",
fields={"results": DictField(child=ListField(child=DictField(child=CharField())))},
),
400: inline_serializer(name="GlobalSearchErrorResponse", fields={"error": CharField()}),
},
description=(_("global search endpoint to query across project's tables")),
)
}

249
core/docs/drf/viewsets.py Normal file
View file

@ -0,0 +1,249 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework import status
from core.docs.drf import BASE_ERRORS
from core.serializers import (
AddOrderProductSerializer,
AddWishlistProductSerializer,
BulkAddWishlistProductSerializer,
BulkRemoveWishlistProductSerializer,
BuyOrderSerializer,
BuyUnregisteredOrderSerializer,
OrderDetailSerializer,
OrderSimpleSerializer,
ProductDetailSerializer,
ProductSimpleSerializer,
RemoveOrderProductSerializer,
RemoveWishlistProductSerializer,
WishlistDetailSerializer,
WishlistSimpleSerializer,
)
from payments.serializers import TransactionProcessSerializer
ATTRIBUTE_GROUP_SCHEMA = {
"list": extend_schema(
summary=_("list all attribute groups (simple view)"),
responses={status.HTTP_200_OK: ProductSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single attribute group (detailed view)"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create an attribute group"),
responses={status.HTTP_201_CREATED: ProductDetailSerializer, **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete an attribute group"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing attribute group saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing attribute group saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
}
ATTRIBUTE_SCHEMA = {
"list": extend_schema(
summary=_("list all attributes (simple view)"),
responses={status.HTTP_200_OK: ProductSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single attribute (detailed view)"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create an attribute"),
responses={status.HTTP_201_CREATED: ProductDetailSerializer, **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete an attribute"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing attribute saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing attribute saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
}
ATTRIBUTE_VALUE_SCHEMA = {
"list": extend_schema(
summary=_("list all attribute values (simple view)"),
responses={status.HTTP_200_OK: ProductSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single attribute value (detailed view)"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create an attribute value"),
responses={status.HTTP_201_CREATED: ProductDetailSerializer, **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete an attribute value"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing attribute value saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing attribute value saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
}
CATEGORY_SCHEMA = {
"list": extend_schema(
summary=_("list all categories (simple view)"),
responses={status.HTTP_200_OK: ProductSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single category (detailed view)"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a category"),
responses={status.HTTP_201_CREATED: ProductDetailSerializer, **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a category"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing category saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing category saving non-editables"),
responses={status.HTTP_200_OK: ProductDetailSerializer, **BASE_ERRORS},
),
}
ORDER_SCHEMA = {
"list": extend_schema(
summary=_("list all orders (simple view)"),
description=_("for non-staff users, only their own orders are returned."),
responses={status.HTTP_200_OK: OrderSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single order (detailed view)"),
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create an order"),
description=_("doesn't work for non-staff users."),
responses={status.HTTP_201_CREATED: OrderDetailSerializer, **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete an order"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing order saving non-editables"),
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing order saving non-editables"),
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
),
"buy": extend_schema(
summary=_("purchase an order"),
description=_(
"finalizes the order purchase. if `force_balance` is used,"
" the purchase is completed using the user's balance;"
" if `force_payment` is used, a transaction is initiated."
),
request=BuyOrderSerializer,
responses={
status.HTTP_200_OK: OrderDetailSerializer,
status.HTTP_202_ACCEPTED: TransactionProcessSerializer,
**BASE_ERRORS,
},
),
"buy_unregistered": extend_schema(
summary=_("purchase an order without account creation"),
description=_(
"finalizes the order purchase for a non-registered user."
),
request=BuyUnregisteredOrderSerializer,
responses={
status.HTTP_202_ACCEPTED: TransactionProcessSerializer,
**BASE_ERRORS,
},
),
"add_order_product": extend_schema(
summary=_("add product to order"),
description=_("adds a product to an order using the provided `product_uuid` and `attributes`."),
request=AddOrderProductSerializer,
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
),
"remove_order_product": extend_schema(
summary=_("remove product from order"),
description=_("removes a product from an order using the provided `product_uuid` and `attributes`."),
request=RemoveOrderProductSerializer,
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
),
}
WISHLIST_SCHEMA = {
"list": extend_schema(
summary=_("list all wishlists (simple view)"),
description=_("for non-staff users, only their own wishlists are returned."),
responses={status.HTTP_200_OK: WishlistSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single wishlist (detailed view)"),
responses={status.HTTP_200_OK: WishlistDetailSerializer, **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create an wishlist"),
description=_("Doesn't work for non-staff users."),
responses={status.HTTP_201_CREATED: WishlistDetailSerializer, **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete an wishlist"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing wishlist saving non-editables"),
responses={status.HTTP_200_OK: WishlistDetailSerializer, **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing wishlist saving non-editables"),
responses={status.HTTP_200_OK: WishlistDetailSerializer, **BASE_ERRORS},
),
"add_wishlist_product": extend_schema(
summary=_("add product to wishlist"),
description=_("adds a product to an wishlist using the provided `product_uuid`"),
request=AddWishlistProductSerializer,
responses={status.HTTP_200_OK: WishlistDetailSerializer, **BASE_ERRORS},
),
"remove_wishlist_product": extend_schema(
summary=_("remove product from wishlist"),
description=_("removes a product from an wishlist using the provided `product_uuid`"),
request=RemoveWishlistProductSerializer,
responses={status.HTTP_200_OK: WishlistDetailSerializer, **BASE_ERRORS},
),
"bulk_add_wishlist_products": extend_schema(
summary=_("add many products to wishlist"),
description=_("adds many products to an wishlist using the provided `product_uuids`"),
request=BulkAddWishlistProductSerializer,
responses={status.HTTP_200_OK: WishlistDetailSerializer, **BASE_ERRORS},
),
"bulk_remove_wishlist_products": extend_schema(
summary=_("remove many products from wishlist"),
description=_("removes many products from an wishlist using the provided `product_uuids`"),
request=BulkRemoveWishlistProductSerializer,
responses={status.HTTP_200_OK: WishlistDetailSerializer, **BASE_ERRORS},
),
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
core/docs/images/evibes.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
core/docs/images/evibes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

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

@ -0,0 +1,194 @@
from django.conf import settings
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from django_elasticsearch_dsl import fields
from elasticsearch import NotFoundError
from elasticsearch.dsl import Q, Search
SMART_FIELDS = [
"name^4",
"name.ngram^3",
"name.phonetic",
"description^2",
"description.ngram",
"description.phonetic",
"name.auto^4",
"description.auto^2",
"brand__name^2",
"brand__name.ngram",
"brand__name.auto",
"category__name^2",
"category__name.ngram",
"category__name.auto",
"title^4",
"title.ngram^3",
"title.phonetic",
"title.auto^4",
]
def process_query(query: str = ""):
"""
Perform a lenient, typotolerant, multiindex search.
* Fulltext with fuzziness for spelling mistakes
* `bool_prefix` for edgengram autocomplete / icontains
"""
if not query:
raise ValueError(_("no search term provided."))
query = query.strip()
try:
q = Q(
"bool",
should=[
Q(
"multi_match",
query=query,
fields=SMART_FIELDS,
fuzziness="AUTO",
operator="and",
),
Q(
"multi_match",
query=query,
fields=[f.replace(".auto", ".auto") for f in SMART_FIELDS if ".auto" in f],
type="bool_prefix",
),
],
minimum_should_match=1,
)
search = Search(index=["products", "categories", "brands", "posts"]).query(q).extra(size=100)
response = search.execute()
results = {"products": [], "categories": [], "brands": []}
for hit in response.hits:
obj_uuid = getattr(hit, "uuid", hit.meta.id)
obj_name = getattr(hit, "name", "N/A")
if hit.meta.index == "products":
results["products"].append({"uuid": obj_uuid, "name": obj_name})
elif hit.meta.index == "categories":
results["categories"].append({"uuid": obj_uuid, "name": obj_name})
elif hit.meta.index == "brands":
results["brands"].append({"uuid": obj_uuid, "name": obj_name})
return results
except NotFoundError:
raise Http404
LANGUAGE_ANALYZER_MAP = {
"ar": "arabic",
"cs": "czech",
"da": "danish",
"de": "german",
"en": "english",
"es": "spanish",
"fr": "french",
"hi": "hindi",
"it": "italian",
"ja": "standard", # Kuromoji plugin recommended for production
"kk": "standard", # No builtin Kazakh stemmer falls back to ICU/standard
"nl": "dutch",
"pl": "standard", # No builtin Polish stemmer falls back to ICU/standard
"pt": "portuguese",
"ro": "romanian",
"ru": "russian",
"zh": "standard", # smartcn / ICU plugin recommended for production
}
def _lang_analyzer(lang_code: str) -> str:
"""Return the bestguess ES analyzer for an ISO language code."""
base = lang_code.split("-")[0].lower()
return LANGUAGE_ANALYZER_MAP.get(base, "standard")
class ActiveOnlyMixin:
"""QuerySet & indexing helpers, so only *active* objects are indexed."""
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
def should_index_object(self, obj):
return getattr(obj, "is_active", False)
COMMON_ANALYSIS = {
"filter": {
"edge_ngram_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20,
},
"ngram_filter": {
"type": "ngram",
"min_gram": 2,
"max_gram": 20,
},
"double_metaphone": {
"type": "phonetic",
"encoder": "double_metaphone",
"replace": False,
},
},
"analyzer": {
"autocomplete": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "edge_ngram_filter"],
},
"autocomplete_search": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding"],
},
"name_ngram": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "ngram_filter"],
},
"name_phonetic": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "double_metaphone"],
},
"query_lc": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding"],
},
},
}
def _add_multilang_fields(cls):
for code, _lang in settings.LANGUAGES:
lc = code.replace("-", "_").lower()
analyzer = _lang_analyzer(code)
setattr(
cls,
f"name_{lc}",
fields.TextField(
attr=f"name_{lc}",
analyzer=analyzer,
copy_to="name",
fields={
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
},
),
)
setattr(
cls,
f"description_{lc}",
fields.TextField(
attr=f"description_{lc}",
analyzer=analyzer,
copy_to="description",
fields={
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(analyzer="name_ngram", search_analyzer="query_lc"),
"phonetic": fields.TextField(analyzer="name_phonetic"),
},
),
)

View file

@ -0,0 +1,105 @@
from django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registry
from core.elasticsearch import COMMON_ANALYSIS, ActiveOnlyMixin, _add_multilang_fields
from core.models import Brand, Category, Product
class _BaseDoc(ActiveOnlyMixin, Document):
name = fields.TextField(
analyzer="standard",
fields={
"raw": fields.KeywordField(ignore_above=256),
"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",
),
},
attr=None,
)
description = fields.TextField(
analyzer="standard",
fields={
"raw": fields.KeywordField(ignore_above=256),
"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",
),
},
attr=None,
)
class Index:
settings = {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": COMMON_ANALYSIS,
"index": {
"max_ngram_diff": 20,
},
}
class ProductDocument(_BaseDoc):
rating = fields.FloatField(attr="rating")
class Index(_BaseDoc.Index):
name = "products"
class Django:
model = Product
fields = ["uuid"]
_add_multilang_fields(ProductDocument)
registry.register_document(ProductDocument)
class CategoryDocument(_BaseDoc):
class Index(_BaseDoc.Index):
name = "categories"
class Django:
model = Category
fields = ["uuid"]
_add_multilang_fields(CategoryDocument)
registry.register_document(CategoryDocument)
class BrandDocument(ActiveOnlyMixin, Document):
name = fields.TextField(
attr="name",
analyzer="standard",
fields={
"raw": fields.KeywordField(ignore_above=256),
"ngram": fields.TextField(
analyzer="name_ngram", search_analyzer="query_lc"
),
"phonetic": fields.TextField(analyzer="name_phonetic"),
},
)
class Index:
name = "brands"
settings = {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": COMMON_ANALYSIS,
"index": {"max_ngram_diff": 18},
}
class Django:
model = Brand
fields = ["uuid"]
registry.register_document(BrandDocument)

5
core/errors.py Normal file
View file

@ -0,0 +1,5 @@
from django.core.exceptions import BadRequest
class NotEnoughMoneyError(BadRequest):
pass

295
core/filters.py Normal file
View file

@ -0,0 +1,295 @@
import json
import logging
from django.db.models import Avg, FloatField, OuterRef, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.utils.http import urlsafe_base64_decode
from django_filters import BaseInFilter, BooleanFilter, CharFilter, FilterSet, NumberFilter, OrderingFilter, UUIDFilter
from core.models import Brand, Category, Feedback, Order, Product, Wishlist
logger = logging.getLogger(__name__)
class CaseInsensitiveListFilter(BaseInFilter, CharFilter):
def filter(self, qs, value):
if value:
lookup = f"{self.field_name}__icontains"
q_objects = Q()
for v in value:
q_objects |= Q(**{lookup: v})
qs = qs.filter(q_objects)
return qs
class ProductFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label="UUID")
name = CharFilter(field_name="name", lookup_expr="icontains", label="Name")
categories = CaseInsensitiveListFilter(field_name="category__name", label="Categories")
category_uuid = CharFilter(field_name="category__uuid", lookup_expr="exact", label="Category")
tags = CaseInsensitiveListFilter(field_name="tags__tag_name", label="Tags")
min_price = NumberFilter(field_name="stocks__price", lookup_expr="gte", label="Min Price")
max_price = NumberFilter(field_name="stocks__price", lookup_expr="lte", label="Max Price")
is_active = BooleanFilter(field_name="is_active", label="Is Active")
brand = CharFilter(field_name="brand__name", lookup_expr="iexact", label="Brand")
attributes = CharFilter(method="filter_attributes", label="Attributes")
quantity = NumberFilter(field_name="stocks__quantity", lookup_expr="gt", label="Quantity")
order_by = OrderingFilter(
fields=(
("uuid", "uuid"),
("rating", "rating"),
("name", "name"),
("created", "created"),
("modified", "modified"),
("stocks__price", "price"),
("?", "random"),
),
initial="uuid",
)
class Meta:
model = Product
fields = [
"uuid",
"name",
"categories",
"category_uuid",
"attributes",
"created",
"modified",
"is_digital",
"is_active",
"tags",
"min_price",
"max_price",
"brand",
"quantity",
"order_by",
]
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
super().__init__(data=data, queryset=queryset, request=request, prefix=prefix)
ordering_param = self.data.get("order_by", "")
if ordering_param:
order_fields = [field.strip("-") for field in ordering_param.split(",")]
if "rating" in order_fields:
feedback_qs = (
Feedback.objects.filter(order_product__product_id=OuterRef("pk"))
.values("order_product__product_id")
.annotate(avg_rating=Avg("rating"))
.values("avg_rating")
)
self.queryset = self.queryset.annotate(
rating=Coalesce(
Subquery(feedback_qs, output_field=FloatField()), Value(0, output_field=FloatField())
)
)
def filter_attributes(self, queryset, _name, value):
if not value:
return queryset
if str(value).startswith("b64-"):
value = urlsafe_base64_decode(value[4:]).decode()
user = getattr(self.request, "user", None)
can_view_inactive_attrvals = user and user.has_perm("view_attributevalue")
pairs = [pair.strip() for pair in value.split(";") if "=" in pair]
q_list = []
for pair in pairs:
attr_name, filter_part = pair.split("=", 1)
attr_name = attr_name.strip()
filter_part = filter_part.strip()
if "-" in filter_part:
method, raw_value = filter_part.split("-", 1)
else:
method = "iexact"
raw_value = filter_part
method = method.lower().strip()
raw_value = self._infer_type(raw_value)
base_filter = Q(**{"attributes__attribute__name__iexact": attr_name})
if not can_view_inactive_attrvals:
base_filter &= Q(**{"attributes__is_active": True})
allowed_methods = {
"iexact",
"exact",
"icontains",
"contains",
"isnull",
"startswith",
"istartswith",
"endswith",
"iendswith",
"regex",
"iregex",
"lt",
"lte",
"gt",
"gte",
"in",
}
if method in allowed_methods:
field_lookup = f"attributes__value__{method}"
else:
field_lookup = "attributes__value__icontains"
base_filter &= Q(**{field_lookup: raw_value})
q_list.append(base_filter)
for q_obj in q_list:
queryset = queryset.filter(q_obj)
return queryset
@staticmethod
def _infer_type(value):
try:
parsed_value = json.loads(value)
if isinstance(parsed_value, (list, dict)):
return parsed_value
except (json.JSONDecodeError, TypeError):
pass
if value.lower() in ["true", "false"]:
return value.lower() == "true"
try:
return int(value)
except ValueError:
pass
try:
return float(value)
except ValueError:
pass
return value
@property
def qs(self):
"""
Override the queryset property to annotate a rating field
when the ordering parameters include rating. This makes ordering
by rating possible.
"""
qs = super().qs
# Check if ordering by rating is requested (could be "rating" or "-rating")
ordering_param = self.data.get("order_by", "")
if ordering_param:
order_fields = [field.strip() for field in ordering_param.split(",")]
if any(field.lstrip("-") == "rating" for field in order_fields):
# Annotate each product with its average rating.
# Here we use a Subquery to calculate the average rating from the Feedback model.
# Adjust the filter in Feedback.objects.filter(...) if your relationships differ.
feedback_qs = (
Feedback.objects.filter(order_product__product_id=OuterRef("pk"))
.values("order_product__product_id")
.annotate(avg_rating=Avg("rating"))
.values("avg_rating")
)
qs = qs.annotate(
rating=Coalesce(
Subquery(feedback_qs, output_field=FloatField()), Value(0, output_field=FloatField())
)
)
return qs
class OrderFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
user_email = CharFilter(field_name="user__email", lookup_expr="iexact")
user = UUIDFilter(field_name="user__uuid", lookup_expr="exact")
status = CharFilter(field_name="status", lookup_expr="icontains", label="Status")
order_by = OrderingFilter(
fields=(
("uuid", "uuid"),
("status", "status"),
("created", "created"),
("modified", "modified"),
("buy_time", "buy_time"),
("?", "random"),
)
)
class Meta:
model = Order
fields = ["uuid", "user_email", "user", "status", "order_by"]
class WishlistFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
user_email = CharFilter(field_name="user__email", lookup_expr="iexact")
user = UUIDFilter(field_name="user__uuid", lookup_expr="exact")
order_by = OrderingFilter(
fields=(("uuid", "uuid"), ("created", "created"), ("modified", "modified"), ("?", "random"))
)
class Meta:
model = Wishlist
fields = ["uuid", "user_email", "user", "order_by"]
class CategoryFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
name = CharFilter(field_name="name", lookup_expr="icontains")
parent_uuid = UUIDFilter(field_name="parent__uuid", lookup_expr="exact")
order_by = OrderingFilter(
fields=(
("uuid", "uuid"),
("name", "name"),
("?", "random"),
)
)
class Meta:
model = Category
fields = ["uuid", "name"]
class BrandFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
name = CharFilter(field_name="name", lookup_expr="icontains")
categories = CaseInsensitiveListFilter(field_name="categories__uuid", lookup_expr="exact")
order_by = OrderingFilter(
fields=(
("uuid", "uuid"),
("name", "name"),
("?", "random"),
)
)
class Meta:
model = Brand
fields = ["uuid", "name"]
class FeedbackFilter(FilterSet):
uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
product = UUIDFilter(field_name="order_product__product__uuid", lookup_expr="exact")
order_by = OrderingFilter(
fields=(
("uuid", "uuid"),
("product", "product"),
("rating", "rating"),
("created", "created"),
("modified", "modified"),
("?", "random"),
)
)
class Meta:
model = Feedback
fields = ["uuid", "product"]

42
core/forms.py Normal file
View file

@ -0,0 +1,42 @@
from django import forms
from .models import Order, OrderProduct, Product, Vendor
from .widgets import JSONTableWidget
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = "__all__"
widgets = {
"attributes": JSONTableWidget(),
}
class VendorForm(forms.ModelForm):
class Meta:
model = Vendor
fields = "__all__"
widgets = {
"authentication": JSONTableWidget(),
}
class OrderProductForm(forms.ModelForm):
class Meta:
model = OrderProduct
fields = "__all__"
widgets = {
"notifications": JSONTableWidget(),
"attributes": JSONTableWidget(),
}
class OrderForm(forms.ModelForm):
class Meta:
model = Order
fields = "__all__"
widgets = {
"notifications": JSONTableWidget(),
"attributes": JSONTableWidget(),
}

10
core/graphene/__init__.py Normal file
View file

@ -0,0 +1,10 @@
from graphene import Mutation
class BaseMutation(Mutation):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@staticmethod
def mutate(**kwargs):
pass

488
core/graphene/mutations.py Normal file
View file

@ -0,0 +1,488 @@
import logging
import requests
from django.core.cache import cache
from django.core.exceptions import BadRequest, PermissionDenied
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from graphene import UUID, Boolean, Field, Int, List, String
from graphene.types.generic import GenericScalar
from graphene_django.utils import camelize
from core.elasticsearch import process_query
from core.graphene import BaseMutation
from core.graphene.object_types import OrderType, ProductType, SearchResultsType, WishlistType
from core.models import Category, Order, Product, Wishlist
from core.utils import format_attributes, is_url_safe
from core.utils.caching import web_cache
from core.utils.emailing import contact_us_email
from core.utils.messages import permission_denied_message
from geo.graphene.object_types import UnregisteredCustomerAddressInput
from payments.graphene.object_types import TransactionType
logger = logging.getLogger(__name__)
class CacheOperator(BaseMutation):
class Meta:
description = _("cache I/O")
class Arguments:
key = String(required=True, description=_("key to look for in or set into the cache"))
data = GenericScalar(required=False, description=_("data to store in cache"))
timeout = Int(
required=False,
description=_("timeout in seconds to set the data for into the cache"),
)
data = GenericScalar(description=_("cached data"))
@staticmethod
def mutate(_parent, info, key, data=None, timeout=None):
return camelize(web_cache(info.context, key, data, timeout))
class RequestCursedURL(BaseMutation):
class Meta:
description = _("request a CORSed URL")
class Arguments:
url = String(required=True)
data = GenericScalar(description=_("camelized JSON data from the requested URL"))
@staticmethod
def mutate(_parent, info, url):
if not is_url_safe(url):
raise BadRequest(_("only URLs starting with http(s):// are allowed"))
try:
data = cache.get(url, None)
if not data:
response = requests.get(url, headers={"content-type": "application/json"})
response.raise_for_status()
data = camelize(response.json())
cache.set(url, data, 86400)
return {"data": data}
except Exception as e:
return {"data": {"error": str(e)}}
class AddOrderProduct(BaseMutation):
class Meta:
description = _("add a product to the order")
class Arguments:
product_uuid = UUID(required=True)
order_uuid = UUID(required=True)
attributes = String(required=False)
order = Field(OrderType)
@staticmethod
def mutate(_parent, info, product_uuid, order_uuid, attributes=None):
user = info.context.user
try:
order = Order.objects.get(uuid=order_uuid)
if not (user.has_perm("core.add_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message)
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"))
class RemoveOrderProduct(BaseMutation):
class Meta:
description = _("remove a product from the order")
class Arguments:
product_uuid = UUID(required=True)
order_uuid = UUID(required=True)
attributes = String(required=False)
order = Field(OrderType)
@staticmethod
def mutate(_parent, info, product_uuid, order_uuid, attributes=None):
user = info.context.user
try:
order = Order.objects.get(uuid=order_uuid)
if not (user.has_perm("core.change_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message)
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"))
class RemoveAllOrderProducts(BaseMutation):
class Meta:
description = _("remove all products from the order")
class Arguments:
order_uuid = UUID(required=True)
order = Field(OrderType)
@staticmethod
def mutate(_parent, info, order_uuid):
user = info.context.user
order = Order.objects.get(uuid=order_uuid)
if not (user.has_perm("core.delete_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message)
order = order.remove_all_products()
return RemoveAllOrderProducts(order=order)
class RemoveOrderProductsOfAKind(BaseMutation):
class Meta:
description = _("remove a product from the order")
class Arguments:
product_uuid = UUID(required=True)
order_uuid = UUID(required=True)
order = Field(OrderType)
@staticmethod
def mutate(_parent, info, product_uuid, order_uuid):
user = info.context.user
order = Order.objects.get(uuid=order_uuid)
if not (user.has_perm("core.delete_orderproduct") or user == order.user):
raise PermissionDenied(permission_denied_message)
order = order.remove_products_of_a_kind(product_uuid=product_uuid)
return RemoveOrderProductsOfAKind(order=order)
class BuyOrder(BaseMutation):
class Meta:
description = _("buy an order")
class Arguments:
order_uuid = UUID(required=True)
force_balance = Boolean(required=False)
force_payment = Boolean(required=False)
promocode_uuid = UUID(required=False)
order = Field(OrderType, required=False)
transaction = Field(TransactionType, required=False)
@staticmethod
def mutate(_parent, info, order_uuid, force_balance=False, force_payment=False, promocode_uuid=None):
user = info.context.user
try:
order = Order.objects.get(user=user, uuid=order_uuid)
instance = order.buy(
force_balance=force_balance, force_payment=force_payment, promocode_uuid=promocode_uuid
)
match str(type(instance)):
case "<class 'payments.models.Transaction'>":
return BuyOrder(transaction=instance)
case "<class 'core.models.Order'>":
return BuyOrder(order=instance)
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"))
class BuyUnregisteredOrder(BaseMutation):
class Meta:
description = _("buy an unregistered order")
class Arguments:
products = List(UUID, required=True)
promocode_uuid = UUID(required=False)
customer_name = String(required=True)
customer_email = String(required=True)
customer_phone = String(required=True)
customer_billing_address = UnregisteredCustomerAddressInput(required=True)
customer_shipping_address = UnregisteredCustomerAddressInput(required=False)
payment_method = String(required=True)
transaction = Field(TransactionType, required=False)
@staticmethod
def mutate(_parent, info, products, customer_name, customer_email, customer_phone, customer_billing_address,
payment_method, customer_shipping_address=None, promocode_uuid=None ):
order = Order.objects.create(status="MOMENTAL")
transaction = order.buy_without_registration(products=products,
promocode_uuid=promocode_uuid,
customer_name=customer_name,
customer_email=customer_email,
customer_phone=customer_phone,
customer_billing_address=customer_billing_address,
customer_shipping_address=customer_shipping_address,
payment_method=payment_method)
return BuyUnregisteredOrder(transaction=transaction)
class AddWishlistProduct(BaseMutation):
class Meta:
description = _("add a product to the wishlist")
class Arguments:
product_uuid = UUID(required=True)
wishlist_uuid = UUID(required=True)
wishlist = Field(WishlistType)
@staticmethod
def mutate(_parent, info, product_uuid, wishlist_uuid):
user = info.context.user
try:
wishlist = Wishlist.objects.get(uuid=wishlist_uuid)
if not (user.has_perm("core.change_wishlist") or user == wishlist.user):
raise PermissionDenied(permission_denied_message)
wishlist.add_product(product_uuid=product_uuid)
return AddWishlistProduct(wishlist=wishlist)
except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found"))
class RemoveWishlistProduct(BaseMutation):
class Meta:
description = _("remove a product from the wishlist")
class Arguments:
product_uuid = UUID(required=True)
wishlist_uuid = UUID(required=True)
wishlist = Field(WishlistType)
@staticmethod
def mutate(_parent, info, product_uuid, wishlist_uuid):
user = info.context.user
try:
wishlist = Wishlist.objects.get(uuid=wishlist_uuid)
if not (user.has_perm("core.change_wishlist") or user == wishlist.user):
raise PermissionDenied(permission_denied_message)
wishlist.remove_product(product_uuid=product_uuid)
return RemoveWishlistProduct(wishlist=wishlist)
except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found"))
class RemoveAllWishlistProducts(BaseMutation):
class Meta:
description = _("remove all products from the wishlist")
class Arguments:
wishlist_uuid = UUID(required=True)
wishlist = Field(WishlistType)
@staticmethod
def mutate(_parent, info, wishlist_uuid):
user = info.context.user
try:
wishlist = Wishlist.objects.get(uuid=wishlist_uuid)
if not (user.has_perm("core.change_wishlist") or user == wishlist.user):
raise PermissionDenied(permission_denied_message)
for product in wishlist.products.all():
wishlist.remove_product(product_uuid=product.pk)
return RemoveAllWishlistProducts(wishlist=wishlist)
except Wishlist.DoesNotExist:
raise Http404(_(f"wishlist {wishlist_uuid} not found"))
class BuyWishlist(BaseMutation):
class Meta:
description = _("buy all products from the wishlist")
class Arguments:
wishlist_uuid = UUID(required=True)
force_balance = Boolean(required=False)
force_payment = Boolean(required=False)
order = Field(OrderType, required=False)
transaction = Field(TransactionType, required=False)
@staticmethod
def mutate(_parent, info, wishlist_uuid, force_balance=False, force_payment=False):
user = info.context.user
try:
wishlist = Wishlist.objects.get(uuid=wishlist_uuid)
if not (user.has_perm("core.change_wishlist") or user == wishlist.user):
raise PermissionDenied(permission_denied_message)
order = Order.objects.create(user=user, status="MOMENTAL")
for product in (
wishlist.products.all()
if user.has_perm("core.change_wishlist")
else wishlist.products.filter(is_active=True)
):
order.add_product(product_uuid=product.pk)
instance = order.buy(force_balance=force_balance, force_payment=force_payment)
match str(type(instance)):
case "<class 'payments.models.Transaction'>":
return BuyWishlist(transaction=instance)
case "<class 'core.models.Order'>":
return BuyWishlist(order=instance)
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"))
class BuyProduct(BaseMutation):
class Meta:
description = _("buy a product")
class Arguments:
product_uuid = UUID(required=True)
attributes = String(
required=False,
description=_("please send the attributes as the string formatted like attr1=value1,attr2=value2"),
)
force_balance = Boolean(required=False)
force_payment = Boolean(required=False)
order = Field(OrderType, required=False)
transaction = Field(TransactionType, required=False)
@staticmethod
def mutate(_parent, info, product_uuid, attributes=None, force_balance=False, force_payment=False):
user = info.context.user
order = Order.objects.create(user=user, status="MOMENTAL")
order.add_product(product_uuid=product_uuid, attributes=format_attributes(attributes))
instance = order.buy(force_balance=force_balance, force_payment=force_payment)
match str(type(instance)):
case "<class 'payments.models.Transaction'>":
return BuyProduct(transaction=instance)
case "<class 'core.models.Order'>":
return BuyProduct(order=instance)
case _:
raise TypeError(_(f"wrong type came from order.buy() method: {type(instance)!s}"))
class CreateProduct(BaseMutation):
class Arguments:
name = String(required=True)
description = String()
category_uuid = UUID(required=True)
product = Field(ProductType)
@staticmethod
def mutate(_parent, info, name, category_uuid, description=None):
if not info.context.user.has_perm("core.add_product"):
raise PermissionDenied(permission_denied_message)
category = Category.objects.get(uuid=category_uuid)
product = Product.objects.create(name=name, description=description, category=category)
return CreateProduct(product=product)
class UpdateProduct(BaseMutation):
class Arguments:
uuid = UUID(required=True)
name = String()
description = String()
category_uuid = UUID()
product = Field(ProductType)
@staticmethod
def mutate(_parent, info, uuid, name=None, description=None, category_uuid=None):
user = info.context.user
if not user.has_perm("core.change_product"):
raise PermissionDenied(permission_denied_message)
product = Product.objects.get(uuid=uuid)
if name:
product.name = name
if description:
product.description = description
if category_uuid:
product.category = Category.objects.get(uuid=category_uuid)
product.save()
return UpdateProduct(product=product)
class DeleteProduct(BaseMutation):
class Arguments:
uuid = UUID(required=True)
ok = Boolean()
@staticmethod
def mutate(_parent, info, uuid):
user = info.context.user
if not user.has_perm("core.delete_product"):
raise PermissionDenied(permission_denied_message)
product = Product.objects.get(uuid=uuid)
product.delete()
return DeleteProduct(ok=True)
class ContactUs(BaseMutation):
class Arguments:
email = String(required=True)
name = String(required=True)
subject = String(required=True)
phone_number = String(required=False)
message = String(required=True)
received = Boolean(required=True)
error = String()
@staticmethod
def mutate(_parent, info, email, name, subject, message, phone_number=None):
try:
contact_us_email.delay(
{
"email": email,
"name": name,
"subject": subject,
"phone_number": phone_number,
"message": message,
}
)
return ContactUs(received=True)
except Exception as e:
return ContactUs(received=False, error=str(e))
class Search(BaseMutation):
class Arguments:
query = String(required=True)
results = Field(SearchResultsType)
class Meta:
description = _("elasticsearch - works like a charm")
@staticmethod
def mutate(_parent, info, query):
data = process_query(query)
return Search(
results=SearchResultsType(
products=data["products"],
categories=data["categories"],
brands=data["brands"],
)
)

View file

@ -0,0 +1,470 @@
from django.core.cache import cache
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 UUID, Field, Float, Int, List, NonNull, ObjectType, String, relay
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.utils import camelize
from mptt.querysets import TreeQuerySet
from core.models import (
Attribute,
AttributeGroup,
AttributeValue,
Brand,
Category,
Feedback,
Order,
OrderProduct,
Product,
ProductImage,
PromoCode,
Promotion,
Stock,
Vendor,
Wishlist,
)
from geo.graphene.object_types import AddressType
logger = __import__("logging").getLogger(__name__)
class AttributeType(DjangoObjectType):
values = List(lambda: AttributeValueType, description=_("attribute values"))
class Meta:
model = Attribute
interfaces = (relay.Node,)
fields = ("uuid", "value_type", "name")
filter_fields = ["uuid"]
description = _("attributes")
def resolve_values(self, info):
base_qs = AttributeValue.objects.filter(attribute=self)
product_uuid = getattr(info.context, "_product_uuid", None)
if product_uuid:
base_qs = base_qs.filter(product__uuid=product_uuid)
return base_qs
class AttributeGroupType(DjangoObjectType):
attributes = List(lambda: AttributeType, description=_("grouped attributes"))
class Meta:
model = AttributeGroup
interfaces = (relay.Node,)
fields = ("uuid", "parent", "name", "attributes")
filter_fields = ["uuid"]
description = _("groups of attributes")
def resolve_attributes(self, info):
product_uuid = getattr(info.context, "_product_uuid", None)
qs = self.attributes.all()
if product_uuid:
qs = qs.filter(values__product__uuid=product_uuid).distinct()
return qs
class BrandType(DjangoObjectType):
categories = List(lambda: CategoryType, description=_("categories"))
class Meta:
model = Brand
interfaces = (relay.Node,)
fields = ("uuid", "categories", "name")
filter_fields = ["uuid"]
description = _("brands")
def resolve_categories(self, info):
if info.context.user.has_perm("core.view_category"):
return self.categories.all()
return self.categories.filter(is_active=True)
class FilterableAttributeType(ObjectType):
attribute_name = String(required=True)
possible_values = List(String, required=True)
class MinMaxPriceType(ObjectType):
min_price = Float()
max_price = Float()
class CategoryType(DjangoObjectType):
children = List(
lambda: CategoryType,
depth=Int(default_value=None),
description=_("categories"),
)
image = String(description=_("category image url"))
markup_percent = Float(required=False, description=_("markup percentage"))
filterable_attributes = List(
NonNull(FilterableAttributeType),
description=_("which attributes and values can be used for filtering this category."),
)
min_max_prices = Field(
NonNull(MinMaxPriceType),
description=_("minimum and maximum prices for products in this category, if available."),
)
class Meta:
model = Category
interfaces = (relay.Node,)
fields = (
"uuid",
"markup_percent",
"attributes",
"children",
"name",
"description",
"image",
"min_max_prices",
)
filter_fields = ["uuid"]
description = _("categories")
def resolve_children(self, info, depth=None) -> TreeQuerySet:
max_depth = self.get_tree_depth()
if depth is None:
depth = max_depth
if depth <= 0:
return Category.objects.none()
categories = Category.objects.language(info.context.locale).filter(parent=self)
if info.context.user.has_perm("core.view_category"):
return categories
return categories.filter(is_active=True)
def resolve_image(self, info) -> str:
return info.context.build_absolute_uri(self.image.url) if self.image else ""
def resolve_markup_percent(self, info) -> float:
if info.context.user.has_perm("core.view_category"):
return float(self.markup_percent)
return 0.0
def resolve_filterable_attributes(self, info):
filterable_results = cache.get(f"{self.uuid}_filterable_results", [])
if len(filterable_results) > 0:
return filterable_results
for attr in (
self.attributes.all()
if info.context.user.has_perm("view_attribute")
else self.attributes.filter(is_active=True)
):
distinct_vals = (
AttributeValue.objects.annotate(value_length=Length("value"))
.filter(attribute=attr, product__category=self, value_length__lte=30)
.values_list("value", flat=True)
.distinct()
)
distinct_vals_list = list(distinct_vals)
if len(distinct_vals_list) <= 128:
filterable_results.append(
FilterableAttributeType(attribute_name=attr.name, possible_values=distinct_vals_list)
)
else:
pass
cache.set(f"{self.uuid}_filterable_results", filterable_results, 86400)
return filterable_results
def resolve_min_max_prices(self, info):
min_max_prices = cache.get(key=f"{self.name}_min_max_prices", default={})
if not min_max_prices:
price_aggregation = Product.objects.filter(category=self).aggregate(
min_price=Min("stocks__price"), max_price=Max("stocks__price")
)
min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0)
min_max_prices["max_price"] = price_aggregation.get("max_price", 0.0)
cache.set(key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400)
return MinMaxPriceType(min_price=min_max_prices["min_price"], max_price=min_max_prices["max_price"])
class VendorType(DjangoObjectType):
markup_percent = Float(description=_("markup percentage"))
class Meta:
model = Vendor
interfaces = (relay.Node,)
fields = ("uuid", "name", "markup_percent")
filter_fields = ["uuid"]
description = _("vendors")
class FeedbackType(DjangoObjectType):
comment = String(description=_("comment"))
rating = Int(description=_("rating value from 1 to 10, inclusive, or 0 if not set."))
class Meta:
model = Feedback
interfaces = (relay.Node,)
fields = ("uuid", "comment", "rating")
filter_fields = ["uuid"]
description = _("represents feedback from a user.")
class OrderProductType(DjangoObjectType):
attributes = GenericScalar(description=_("attributes"))
notifications = GenericScalar(description=_("notifications"))
download_url = String(description=_("download url for this order product if applicable"))
class Meta:
model = OrderProduct
interfaces = (relay.Node,)
fields = (
"uuid",
"product",
"quantity",
"status",
"comments",
"attributes",
"notifications",
)
filter_fields = ["uuid"]
description = _("order products")
def resolve_attributes(self, info):
return camelize(self.attributes)
def resolve_notifications(self, info):
return camelize(self.notifications)
def resolve_download_url(self, info) -> str | None:
return self.download_url
class OrderType(DjangoObjectType):
order_products = DjangoFilterConnectionField(
OrderProductType, description=_("a list of order products in this order")
)
billing_address = Field(AddressType, description=_("billing address"))
shipping_address = Field(
AddressType,
description=_("shipping address for this order, leave blank if same as billing address or if not applicable"),
)
total_price = Float(description=_("total price of this order"))
total_quantity = Int(description=_("total quantity of products in order"))
is_whole_digital = Float(description=_("are all products in the order digital"))
attributes = GenericScalar(description=_("attributes"))
notifications = GenericScalar(description=_("notifications"))
class Meta:
model = Order
interfaces = (relay.Node,)
fields = (
"uuid",
"billing_address",
"shipping_address",
"status",
"promo_code",
"buy_time",
"user",
"total_price",
"total_quantity",
"is_whole_digital",
)
description = _("orders")
def resolve_total_price(self, _info):
return self.total_price
def resolve_total_quantity(self, _info):
return self.total_quantity
def resolve_notifications(self, info):
return camelize(self.notifications)
def resolve_attributes(self, info):
return camelize(self.attributes)
class ProductImageType(DjangoObjectType):
image = String(description=_("image url"))
class Meta:
model = ProductImage
interfaces = (relay.Node,)
fields = ("uuid", "alt", "priority", "image")
filter_fields = ["uuid"]
description = _("product's images")
def resolve_image(self, info):
return info.context.build_absolute_uri(self.image.url) if self.image else ""
class ProductType(DjangoObjectType):
category = Field(CategoryType, description=_("category"))
images = DjangoFilterConnectionField(ProductImageType, description=_("images"))
feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks"))
brand = Field(BrandType, description=_("brand"))
attribute_groups = DjangoFilterConnectionField(AttributeGroupType, description=_("attribute groups"))
price = Float(description=_("price"))
quantity = Float(description=_("quantity"))
feedbacks_count = Int(description=_("number of feedbacks"))
class Meta:
model = Product
interfaces = (relay.Node,)
fields = (
"uuid",
"category",
"brand",
"tags",
"name",
"description",
"feedbacks",
"images",
"price",
)
filter_fields = ["uuid", "name"]
description = _("products")
def resolve_price(self, _info) -> float:
return self.price or 0.0
def resolve_feedbacks(self, _info) -> QuerySet[Feedback]:
if _info.context.user.has_perm("core.view_feedback"):
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:
return self.feedbacks_count or 0
def resolve_attribute_groups(self, info):
info.context._product_uuid = self.uuid
return AttributeGroup.objects.filter(attributes__values__product=self).distinct()
def resolve_quantity(self, _info) -> int:
return self.quantity or 0
class AttributeValueType(DjangoObjectType):
value = String(description=_("attribute value"))
class Meta:
model = AttributeValue
interfaces = (relay.Node,)
fields = ("uuid", "value")
filter_fields = ["uuid", "value"]
description = _("attribute value")
class PromoCodeType(DjangoObjectType):
discount = Float()
discount_type = String()
class Meta:
model = PromoCode
interfaces = (relay.Node,)
fields = (
"uuid",
"code",
"start_time",
"end_time",
"used_on",
)
filter_fields = ["uuid"]
description = _("promocodes")
def resolve_discount(self, info) -> float:
return self.discount_percent if self.discount_percent else self.discount_amount
def resolve_discount_type(self, info) -> str:
return "percent" if self.discount_percent else "amount"
class PromotionType(DjangoObjectType):
products = DjangoFilterConnectionField(ProductType, description=_("products on sale"))
class Meta:
model = Promotion
interfaces = (relay.Node,)
fields = ("uuid", "name", "discount_percent", "products")
filter_fields = ["uuid"]
description = _("promotions")
class StockType(DjangoObjectType):
vendor = Field(VendorType, description=_("vendor"))
product = Field(ProductType, description=_("product"))
class Meta:
model = Stock
interfaces = (relay.Node,)
fields = ("uuid", "vendor", "product", "price", "quantity", "sku")
filter_fields = ["uuid"]
description = _("stocks")
class WishlistType(DjangoObjectType):
products = DjangoFilterConnectionField(ProductType, description=_("wishlisted products"))
class Meta:
model = Wishlist
interfaces = (relay.Node,)
fields = ("uuid", "products", "user")
description = _("wishlists")
class ConfigType(ObjectType):
project_name = String(description=_("project name"))
base_domain = String(description=_("company email"))
company_name = String(description=_("company name"))
company_address = String(description=_("company address"))
company_phone_number = String(description=_("company phone number"))
email_from = String(description=_("email from, sometimes it must be used instead of host user value"))
email_host_user = String(description=_("email host user"))
payment_gateway_maximum = Float(description=_("maximum amount for payment"))
payment_gateway_minimum = Float(description=_("minimum amount for payment"))
class Meta:
description = _("company configuration")
class LanguageType(ObjectType):
code = String(description=_("language code"))
name = String(description=_("language name"))
flag = String(description=_("language flag, if exists :)"))
class Meta:
description = _("supported languages")
class SearchProductsResultsType(ObjectType):
uuid = UUID()
name = String()
class SearchCategoriesResultsType(ObjectType):
uuid = UUID()
name = String()
class SearchBrandsResultsType(ObjectType):
uuid = UUID()
name = String()
class SearchResultsType(ObjectType):
products = List(description=_("products search results"), of_type=SearchProductsResultsType)
categories = List(description=_("products search results"), of_type=SearchCategoriesResultsType)
brands = List(description=_("products search results"), of_type=SearchBrandsResultsType)

309
core/graphene/schema.py Normal file
View file

@ -0,0 +1,309 @@
import logging
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from graphene import Field, List, ObjectType, Schema
from graphene_django.filter import DjangoFilterConnectionField
from blog.filters import PostFilter
from blog.graphene.object_types import PostType
from core.filters import (
BrandFilter,
CategoryFilter,
FeedbackFilter,
OrderFilter,
ProductFilter,
WishlistFilter,
)
from core.graphene.mutations import (
AddOrderProduct,
AddWishlistProduct,
BuyOrder,
BuyProduct,
BuyWishlist,
CacheOperator,
ContactUs,
CreateProduct,
DeleteProduct,
RemoveAllOrderProducts,
RemoveAllWishlistProducts,
RemoveOrderProduct,
RemoveOrderProductsOfAKind,
RemoveWishlistProduct,
RequestCursedURL,
Search,
UpdateProduct,
)
from core.graphene.object_types import (
AttributeGroupType,
BrandType,
CategoryType,
ConfigType,
FeedbackType,
LanguageType,
OrderProductType,
OrderType,
ProductImageType,
ProductType,
PromoCodeType,
PromotionType,
StockType,
VendorType,
WishlistType,
)
from core.models import (
AttributeGroup,
Brand,
Category,
Feedback,
Order,
OrderProduct,
Product,
ProductImage,
PromoCode,
Promotion,
Stock,
Vendor,
Wishlist,
)
from core.utils import get_project_parameters
from core.utils.languages import get_flag_by_language
from core.utils.messages import permission_denied_message
from evibes.settings import LANGUAGES
from payments.graphene.mutations import Deposit
from vibes_auth.filters import UserFilter
from vibes_auth.graphene.mutations import (
ActivateUser,
ConfirmResetPassword,
CreateUser,
DeleteUser,
ObtainJSONWebToken,
RefreshJSONWebToken,
ResetPassword,
UpdateUser,
VerifyJSONWebToken,
)
from vibes_auth.graphene.object_types import UserType
from vibes_auth.models import User
logger = logging.getLogger(__name__)
class Query(ObjectType):
parameters = Field(ConfigType)
languages = List(LanguageType)
products = DjangoFilterConnectionField(ProductType, filterset_class=ProductFilter)
orders = DjangoFilterConnectionField(OrderType, filterset_class=OrderFilter)
users = DjangoFilterConnectionField(UserType, filterset_class=UserFilter)
attribute_groups = DjangoFilterConnectionField(AttributeGroupType)
categories = DjangoFilterConnectionField(CategoryType, filterset_class=CategoryFilter)
vendors = DjangoFilterConnectionField(VendorType)
feedbacks = DjangoFilterConnectionField(FeedbackType, filterset_class=FeedbackFilter)
order_products = DjangoFilterConnectionField(OrderProductType)
product_images = DjangoFilterConnectionField(ProductImageType)
stocks = DjangoFilterConnectionField(StockType)
wishlists = DjangoFilterConnectionField(WishlistType, filterset_class=WishlistFilter)
promotions = DjangoFilterConnectionField(PromotionType)
promocodes = DjangoFilterConnectionField(PromoCodeType)
brands = DjangoFilterConnectionField(BrandType, filterset_class=BrandFilter)
posts = DjangoFilterConnectionField(PostType, filterset_class=PostFilter)
@staticmethod
def resolve_parameters(_parent, _info):
return get_project_parameters()
@staticmethod
def resolve_languages(_parent, _info):
languages = cache.get("languages")
if not languages:
languages = [
{"code": lang[0], "name": lang[1], "flag": get_flag_by_language(lang[0])} for lang in LANGUAGES
]
cache.set("languages", languages, 60 * 60)
return languages
@staticmethod
def resolve_products(_parent, info, **kwargs):
if info.context.user.is_authenticated and kwargs.get("uuid"):
product = Product.objects.get(
uuid=kwargs["uuid"]
).select_related("brand", "category").prefetch_related("images", "stocks")
if product.is_active and product.brand.is_active and product.category.is_active:
info.context.user.add_to_recently_viewed(product.uuid)
return (
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
).select_related("brand", "category").prefetch_related("images", "stocks")
)
@staticmethod
def resolve_orders(_parent, info, **kwargs):
orders = Order.objects
user = info.context.user
if not user.is_authenticated:
raise PermissionDenied(permission_denied_message)
if user.has_perm("core.view_order"):
filters = {}
if kwargs.get("uuid"):
filters["uuid"] = kwargs["uuid"]
if kwargs.get("user"):
filters["user"] = kwargs["user"]
if kwargs.get("user_email"):
filters["user__email"] = kwargs["user_email"]
orders = orders.filter(**filters)
else:
filters = {"is_active": True, "user": user}
if kwargs.get("uuid"):
filters["uuid"] = kwargs["uuid"]
orders = orders.filter(**filters)
return orders
@staticmethod
def resolve_users(_parent, info, **kwargs):
if info.context.user.has_perm("vibes_auth.view_user"):
return User.objects.all()
users = User.objects.filter(uuid=info.context.user.pk)
return users if users.exists() else User.objects.none()
@staticmethod
def resolve_attribute_groups(_parent, info, **kwargs):
if info.context.user.has_perm("core.view_attributegroup"):
return AttributeGroup.objects.all()
return AttributeGroup.objects.filter(is_active=True)
@staticmethod
def resolve_categories(_parent, info, **kwargs):
categories = Category.objects.filter(parent=None)
if info.context.user.has_perm("core.view_category"):
return categories
return categories.filter(is_active=True)
@staticmethod
def resolve_vendors(_parent, info):
if not info.context.user.has_perm("core.view_vendor"):
raise PermissionDenied(permission_denied_message)
return Vendor.objects.all()
@staticmethod
def resolve_brands(_parent, info):
if not info.context.user.has_perm("core.view_brand"):
return Brand.objects.filter(is_active=True)
return Brand.objects.all()
@staticmethod
def resolve_feedbacks(_parent, info, **kwargs):
if info.context.user.has_perm("core.view_feedback"):
return Feedback.objects.all()
return Feedback.objects.filter(is_active=True)
@staticmethod
def resolve_order_products(_parent, info, **kwargs):
order_products = OrderProduct.objects
user = info.context.user
if user.has_perm("core.view_orderproduct"):
filters = {}
if kwargs.get("uuid"):
filters["uuid"] = kwargs["uuid"]
if kwargs.get("order"):
filters["order__uuid"] = kwargs["order"]
if kwargs.get("user"):
filters["user__uuid"] = kwargs["user"]
order_products = order_products.filter(**filters)
else:
filters = {"is_active": True, "user": user}
if kwargs.get("uuid"):
filters["uuid"] = kwargs["uuid"]
order_products = order_products.filter(**filters)
return order_products
@staticmethod
def resolve_product_images(_parent, info, **kwargs):
if info.context.user.has_perm("core.view_productimage"):
return ProductImage.objects.all()
return ProductImage.objects.filter(is_active=True)
@staticmethod
def resolve_stocks(_parent, info):
if not info.context.user.has_perm("core.view_stock"):
raise PermissionDenied(permission_denied_message)
return Stock.objects.all()
@staticmethod
def resolve_wishlists(_parent, info, **kwargs):
wishlists = Wishlist.objects
user = info.context.user
if not user.is_authenticated:
raise PermissionDenied(permission_denied_message)
if user.has_perm("core.view_wishlist"):
filters = {}
if kwargs.get("uuid"):
filters["uuid"] = kwargs["uuid"]
if kwargs.get("user_email"):
filters["user__email"] = kwargs["user_email"]
if kwargs.get("user"):
filters["user__uuid"] = kwargs["user"]
wishlists = wishlists.filter(**filters)
else:
filters = {"is_active": True, "user": user}
if kwargs.get("uuid"):
filters["uuid"] = kwargs["uuid"]
wishlists = wishlists.filter(**filters)
return wishlists
@staticmethod
def resolve_promotions(_parent, info, **kwargs):
promotions = Promotion.objects
if info.context.user.has_perm("core.view_promotion"):
return promotions.all()
return promotions.filter(is_active=True)
@staticmethod
def resolve_promocodes(_parent, info, **kwargs):
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)
class Mutation(ObjectType):
search = Search.Field()
cache = CacheOperator.Field()
request_cursed_URL = RequestCursedURL.Field() # noqa: N815
contact_us = ContactUs.Field()
add_wishlist_product = AddWishlistProduct.Field()
remove_wishlist_product = RemoveWishlistProduct.Field()
remove_all_wishlist_products = RemoveAllWishlistProducts.Field()
buy_wishlist = BuyWishlist.Field()
add_order_product = AddOrderProduct.Field()
remove_order_product = RemoveOrderProduct.Field()
remove_all_order_products = RemoveAllOrderProducts.Field()
remove_order_products_of_a_kind = RemoveOrderProductsOfAKind.Field()
buy_order = BuyOrder.Field()
deposit = Deposit.Field()
obtain_jwt_token = ObtainJSONWebToken.Field()
refresh_jwt_token = RefreshJSONWebToken.Field()
verify_jwt_token = VerifyJSONWebToken.Field()
create_user = CreateUser.Field()
update_user = UpdateUser.Field()
delete_user = DeleteUser.Field()
activate_user = ActivateUser.Field()
reset_password = ResetPassword.Field()
confirm_reset_password = ConfirmResetPassword.Field()
buy_product = BuyProduct.Field()
create_product = CreateProduct.Field()
update_product = UpdateProduct.Field()
delete_product = DeleteProduct.Field()
schema = Schema(query=Query, mutation=Mutation)

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

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