Initial Commit
This commit is contained in:
commit
68febcdb08
460 changed files with 117847 additions and 0 deletions
35
.dockerignore
Normal file
35
.dockerignore
Normal 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
89
.gitignore
vendored
Normal 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
36
Dockerfile
Normal 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
58
LICENSE
Normal 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 Software’s 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 author’s 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 author’s prior consent or without paying the required royalties is prohibited. Using the Software in a commercial setting without honoring these terms violates the author’s 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 author’s 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 author’s 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
134
README.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# eVibes
|
||||||
|

|
||||||
|
|
||||||
|
eVibes is an eCommerce backend service built with Django. It’s 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)
|
||||||
|
|
||||||
|

|
||||||
0
blog/__init__.py
Normal file
0
blog/__init__.py
Normal file
43
blog/admin.py
Normal file
43
blog/admin.py
Normal 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
12
blog/apps.py
Normal 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
0
blog/docs/__init__.py
Normal file
0
blog/docs/drf/__init__.py
Normal file
0
blog/docs/drf/__init__.py
Normal file
0
blog/docs/drf/views.py
Normal file
0
blog/docs/drf/views.py
Normal file
0
blog/elasticsearch/__init__.py
Normal file
0
blog/elasticsearch/__init__.py
Normal file
35
blog/elasticsearch/documents.py
Normal file
35
blog/elasticsearch/documents.py
Normal 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
26
blog/filters.py
Normal 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
13
blog/forms.py
Normal 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;"}),
|
||||||
|
}
|
||||||
0
blog/graphene/__init__.py
Normal file
0
blog/graphene/__init__.py
Normal file
27
blog/graphene/object_types.py
Normal file
27
blog/graphene/object_types.py
Normal 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"]
|
||||||
BIN
blog/locale/ar_AR/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/ar_AR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
71
blog/locale/ar_AR/LC_MESSAGES/django.po
Normal file
71
blog/locale/ar_AR/LC_MESSAGES/django.po
Normal 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 "علامات المشاركة"
|
||||||
BIN
blog/locale/cs_CZ/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/cs_CZ/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/cs_CZ/LC_MESSAGES/django.po
Normal file
73
blog/locale/cs_CZ/LC_MESSAGES/django.po
Normal 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ů"
|
||||||
BIN
blog/locale/da_DK/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/da_DK/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
72
blog/locale/da_DK/LC_MESSAGES/django.po
Normal file
72
blog/locale/da_DK/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/de_DE/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/de_DE/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/de_DE/LC_MESSAGES/django.po
Normal file
73
blog/locale/de_DE/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/en_GB/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/en_GB/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
77
blog/locale/en_GB/LC_MESSAGES/django.po
Normal file
77
blog/locale/en_GB/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/en_US/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/en_US/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
72
blog/locale/en_US/LC_MESSAGES/django.po
Normal file
72
blog/locale/en_US/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/es_ES/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/es_ES/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/es_ES/LC_MESSAGES/django.po
Normal file
73
blog/locale/es_ES/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/fr_FR/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/fr_FR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/fr_FR/LC_MESSAGES/django.po
Normal file
73
blog/locale/fr_FR/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/hi_IN/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/hi_IN/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
75
blog/locale/hi_IN/LC_MESSAGES/django.po
Normal file
75
blog/locale/hi_IN/LC_MESSAGES/django.po
Normal 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 ""
|
||||||
BIN
blog/locale/it_IT/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/it_IT/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/it_IT/LC_MESSAGES/django.po
Normal file
73
blog/locale/it_IT/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/ja_JP/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/ja_JP/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/ja_JP/LC_MESSAGES/django.po
Normal file
73
blog/locale/ja_JP/LC_MESSAGES/django.po
Normal 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 "投稿タグ"
|
||||||
BIN
blog/locale/kk_KZ/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/kk_KZ/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
75
blog/locale/kk_KZ/LC_MESSAGES/django.po
Normal file
75
blog/locale/kk_KZ/LC_MESSAGES/django.po
Normal 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 ""
|
||||||
BIN
blog/locale/nl_NL/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/nl_NL/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/nl_NL/LC_MESSAGES/django.po
Normal file
73
blog/locale/nl_NL/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/pl_PL/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/pl_PL/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/pl_PL/LC_MESSAGES/django.po
Normal file
73
blog/locale/pl_PL/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
72
blog/locale/pt_BR/LC_MESSAGES/django.po
Normal file
72
blog/locale/pt_BR/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/ro_RO/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/ro_RO/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/ro_RO/LC_MESSAGES/django.po
Normal file
73
blog/locale/ro_RO/LC_MESSAGES/django.po
Normal 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"
|
||||||
BIN
blog/locale/ru_RU/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/ru_RU/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
73
blog/locale/ru_RU/LC_MESSAGES/django.po
Normal file
73
blog/locale/ru_RU/LC_MESSAGES/django.po
Normal 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 "Тэги постов"
|
||||||
BIN
blog/locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
BIN
blog/locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
71
blog/locale/zh_Hans/LC_MESSAGES/django.po
Normal file
71
blog/locale/zh_Hans/LC_MESSAGES/django.po
Normal 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 "帖子标签"
|
||||||
54
blog/migrations/0001_initial.py
Normal file
54
blog/migrations/0001_initial.py
Normal 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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
24
blog/migrations/0002_alter_post_slug_alter_post_title.py
Normal file
24
blog/migrations/0002_alter_post_slug_alter_post_title.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
blog/migrations/0003_alter_post_tags.py
Normal file
18
blog/migrations/0003_alter_post_tags.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
blog/migrations/__init__.py
Normal file
0
blog/migrations/__init__.py
Normal file
95
blog/models.py
Normal file
95
blog/models.py
Normal 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
22
blog/serializers.py
Normal 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
0
blog/signals.py
Normal file
0
blog/templates/__init__.py
Normal file
0
blog/templates/__init__.py
Normal file
0
blog/tests.py
Normal file
0
blog/tests.py
Normal file
11
blog/urls.py
Normal file
11
blog/urls.py
Normal 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
0
blog/utils/__init__.py
Normal file
3
blog/views.py
Normal file
3
blog/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
15
blog/viewsets.py
Normal file
15
blog/viewsets.py
Normal 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
40
blog/widgets.py
Normal 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
0
core/__init__.py
Normal file
31
core/abstract.py
Normal file
31
core/abstract.py
Normal 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
409
core/admin.py
Normal 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
12
core/apps.py
Normal 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
24
core/choices.py
Normal 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
0
core/data/.gitkeep
Normal file
6
core/data/payment_methods.json
Normal file
6
core/data/payment_methods.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"payment_methods": [
|
||||||
|
"CASH",
|
||||||
|
"CARD"
|
||||||
|
]
|
||||||
|
}
|
||||||
0
core/docs/__init__.py
Normal file
0
core/docs/__init__.py
Normal file
14
core/docs/drf/__init__.py
Normal file
14
core/docs/drf/__init__.py
Normal 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
84
core/docs/drf/views.py
Normal 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
249
core/docs/drf/viewsets.py
Normal 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},
|
||||||
|
),
|
||||||
|
}
|
||||||
BIN
core/docs/images/evibes-big.png
Normal file
BIN
core/docs/images/evibes-big.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
core/docs/images/evibes.ico
Normal file
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
BIN
core/docs/images/evibes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
core/docs/images/favicon.svg
Normal file
25
core/docs/images/favicon.svg
Normal 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 |
194
core/elasticsearch/__init__.py
Normal file
194
core/elasticsearch/__init__.py
Normal 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, typo‑tolerant, multi‑index search.
|
||||||
|
|
||||||
|
* Full‑text with fuzziness for spelling mistakes
|
||||||
|
* `bool_prefix` for edge‑ngram 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 built‑in Kazakh stemmer ‑ falls back to ICU/standard
|
||||||
|
"nl": "dutch",
|
||||||
|
"pl": "standard", # No built‑in 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 best‑guess 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"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
105
core/elasticsearch/documents.py
Normal file
105
core/elasticsearch/documents.py
Normal 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
5
core/errors.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.core.exceptions import BadRequest
|
||||||
|
|
||||||
|
|
||||||
|
class NotEnoughMoneyError(BadRequest):
|
||||||
|
pass
|
||||||
295
core/filters.py
Normal file
295
core/filters.py
Normal 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
42
core/forms.py
Normal 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
10
core/graphene/__init__.py
Normal 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
488
core/graphene/mutations.py
Normal 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"],
|
||||||
|
)
|
||||||
|
)
|
||||||
470
core/graphene/object_types.py
Normal file
470
core/graphene/object_types.py
Normal 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
309
core/graphene/schema.py
Normal 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)
|
||||||
BIN
core/locale/ar_AR/LC_MESSAGES/django.mo
Normal file
BIN
core/locale/ar_AR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1699
core/locale/ar_AR/LC_MESSAGES/django.po
Normal file
1699
core/locale/ar_AR/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
core/locale/cs_CZ/LC_MESSAGES/django.mo
Normal file
BIN
core/locale/cs_CZ/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1728
core/locale/cs_CZ/LC_MESSAGES/django.po
Normal file
1728
core/locale/cs_CZ/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
core/locale/da_DK/LC_MESSAGES/django.mo
Normal file
BIN
core/locale/da_DK/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1743
core/locale/da_DK/LC_MESSAGES/django.po
Normal file
1743
core/locale/da_DK/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
core/locale/de_DE/LC_MESSAGES/django.mo
Normal file
BIN
core/locale/de_DE/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1776
core/locale/de_DE/LC_MESSAGES/django.po
Normal file
1776
core/locale/de_DE/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
core/locale/en_GB/LC_MESSAGES/django.mo
Normal file
BIN
core/locale/en_GB/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue