Compare commits

...

34 commits

Author SHA1 Message Date
1e1d0ef397 2026.1 2026-02-27 21:59:51 +03:00
0429b62ba1 Merge branch 'refs/heads/master' into storefront-nuxt 2026-02-27 21:52:34 +03:00
b1382b09c2 chore(gitignore): fix ignore paths for production configs
Adjusted paths in `.gitignore` to correctly handle production files, ensuring alignment with expected directory structure.
2026-02-27 21:51:51 +03:00
c2052d62fd Merge branch 'refs/heads/master' into storefront-nuxt 2026-02-27 21:47:30 +03:00
79be6ed4e4 feat(demo_data): use translation override to ensure consistent locale
wraps actions in a `with override("en")` block to enforce the use of the English locale during execution. This ensures consistent behavior and message formatting regardless of the server's default language settings.
2026-02-27 19:02:55 +03:00
a59c5f59dd feat(blog): enhance slug behavior and expand demo data attributes
update `slug` field for `Post` model using `TweakedAutoSlugField` to improve auto-slug behavior with Unicode and additional options. Add detailed `attribute_values` to demo products for enriched metadata. Update dependencies for stability and features.
2026-02-27 18:47:14 +03:00
a1cc0cfd30 feat(category): add min_price and max_price to serializer
enable retrieval of min and max product prices for categories to support price range filters. fixed a typo in queryset filter for brands.
2026-02-27 18:05:27 +03:00
f664b088a4 refactor(category): replace cache usage with model property for min/max price
remove redundant cache lookups for `min_price` and `max_price` in the category model by leveraging cached properties. minimizes complexity and improves maintainability while ensuring consistent behavior.
2026-02-25 12:19:39 +03:00
7efc19e081 feat(monitoring): automate Prometheus web config generation
Remove manual password hashing and web.yml setup in favor of automated generation. Add scripts for both Unix and Windows to create `monitoring/web.yml` using credentials from `.env`.

This improves maintainability and reduces manual intervention during setup and configuration.
2026-02-22 00:17:55 +03:00
069d416585 refactor(monitoring): remove django-prometheus integration
Replaced `django-prometheus` with the default Django components, including model mixins, database backends, and cache configuration. This change simplifies monitoring setup by removing unnecessary dependencies, reducing overhead, and improving compatibility.

**Details:**
- Removed Prometheus metrics endpoints and middleware.
- Updated database, cache, and model configurations to remove `django-prometheus`.
- Adjusted WSGI settings to integrate OpenTelemetry instrumentation instead of Prometheus.
- Updated dependency files and migration schemas accordingly.
2026-02-21 23:44:15 +03:00
1756c3f2b2 feat(debug): integrate django-debug-toolbar for enhanced debugging
Add `django-debug-toolbar` to assist with in-depth debugging during development. Updates were made to `settings`, `urls`, and dependency files to enable this feature.
2026-02-21 22:45:04 +03:00
b6d5409fa0 fix(settings): update health check link to correct URL path
Updated the `health_check` menu link to use the correct URL path, ensuring navigation works as expected.
2026-02-21 22:33:08 +03:00
236323b93b feat(urls): add name to health_check route
Add a name to the health_check URL pattern to improve readability and enable reverse lookups.
2026-02-21 22:26:04 +03:00
8883b9f43d feat(core): replace AutoSlugField with TweakedAutoSlugField for product slugs
Updated `product.slug` to use `TweakedAutoSlugField` for improved functionality, allowing unicode, overwrite capabilities, and enhanced population logic. Adjusted the corresponding migration script to ensure seamless database schema updates.

Also marked `brand.categories` as deprecated.
2026-02-21 22:13:36 +03:00
ec167d4e9c refactor(health-check): replace default views with custom configuration
migrated health check configuration to custom settings for more precise control. Removed unused `health_check` submodules to streamline dependencies. Updated URLs to use `HealthCheckView` with tailored checks. Streamlines health monitoring and reduces unnecessary bloat.
2026-02-21 20:24:33 +03:00
72834f01f6 feat(configuration): add support for configurable language code
allow setting `SCHON_LANGUAGE_CODE` via environment files for both Windows and Unix. Default remains `en-gb`. Updated `LANGUAGE_CODE` in settings to use the new environment variable for increased flexibility.
2026-02-21 20:06:41 +03:00
0962376252 refactor(engine): downgrade ty version and clean up unused code
- Downgrade `ty` dependency from 0.0.18 to 0.0.16 in `pyproject.toml` and related files to address compatibility issues.
- Refactor `filters.py` to use safer attribute handling for field errors.
- Remove unused `TestModelDocument` and `TestModel` references from `documents.py`, reducing unnecessary overhead.
- Minor cleanup in `serializers.py` for improved readability.
2026-02-21 20:01:28 +03:00
f8f051f4e9 feat(admin-docs): add base structure and templates for admin documentation
Introduce templates for admin documentation, including model details, views, template tags, filters, and bookmarklets. This enhances the admin interface by providing detailed documentation directly within the application.
2026-02-21 19:52:28 +03:00
c728204cb1 chore(deps): update dependencies in uv.lock
upgrade versions for `aiogram` (3.24.0 → 3.25.0), `async-lru` (2.1.0 → 2.2.0), `coverage` (7.13.3 → 7.13.4), and `cryptography` (46.0.4 → 46.0.5) to incorporate latest bug fixes and enhancements.
2026-02-21 19:49:47 +03:00
87ed875fe6 feat(category): add brands relationship and resolve method
enable brands association with categories and allow querying of active brands within a category. Updated GraphQL schema, models, and serializers to include this relationship while deprecating redundant category-to-brand ManyToManyField.
2026-02-21 18:27:10 +03:00
10f5c798d4 style(demo_data): fix line break for product image save method
Simplify readability by unifying the method call into a single line. No functional changes.
2026-02-21 18:08:22 +03:00
1c10d5ca53 feat(demo_data): enhance data with images, new categories, and multilingual updates
Expanded demo content with additional images for products and blog posts, improving user experience. Added new categories such as "Jewelry" and "Services," along with their subcategories. Supported richer multilingual descriptions and included new brands, ensuring broader and detailed offerings.
2026-02-21 18:07:07 +03:00
83a8ecfcee chore(gitignore): adjust entries for queries and nginx.conf paths
Modified `.gitignore` to include relative paths for `queries` and `nginx.conf`. This ensures consistency and prevents unintended exclusions.
2026-02-17 01:09:52 +03:00
4504019100 chore(gitignore): add nginx.conf to ignored files
Add `nginx.conf` to the `.gitignore` file to prevent accidental commits of server configuration files.
2026-02-17 00:48:30 +03:00
9d5b4fee90 fix(models, viewsets): filter inactive records in queries
ensure only active records are considered in `models.py` and `viewsets.py` by adding `is_active=True` filters. improves data integrity and prevents processing inactive entities.
2026-02-16 19:43:09 +03:00
1253496e30 feat(blog): add multilingual demo blog posts showcasing Schon's features and capabilities
Includes English and Russian versions for key topics such as platform overview, bilingual experience, gemstone certification guide, holiday gift guide, and spring 2026 collection. These posts demonstrate Schon's multilingual support and flexibility in presenting rich content.
2026-02-15 12:21:39 +03:00
6311993b14 refactor(core): improve type checking and fix password handling in demo data
Replaced `pyright:ignore` with `ty:ignore` for better compatibility and accuracy in type annotations. Removed inline passwords during user creation and updated logic to securely set and save passwords afterward.
2026-02-15 03:06:17 +03:00
72a54de707 fix(demo_data): set default passwords for staff and superuser in demo data
Ensure default passwords are assigned to demo users for consistent behavior during testing and development.
2026-02-15 02:17:34 +03:00
ad5e8dc335 chore(engine): add JavaScript source map for rapidoc-min.js 2026-02-15 02:16:10 +03:00
a71e1cc29e refactor(configurations): update domain references to wiseless.xyz
Update all configurations, fixtures, scripts, and documentation to replace occurrences of `schon.fureunoir.com` with the new `schon.wiseless.xyz` domain.

This ensures consistency across the project and reflects the updated domain structure.
2026-02-15 01:39:15 +03:00
dbc41e7c53 feat(demo_data): add blog post generation to demo data script
extend the demo data management command to include blog post and tag creation. enables easier setup for testing and showcasing blog-related features.
2026-02-15 01:36:35 +03:00
1f571f294a feat(demo_data): add blog post generation to demo data script
extend the demo data management command to include blog post and tag creation. enables easier setup for testing and showcasing blog-related features.
2026-02-15 01:35:36 +03:00
30171dfbc9 chore(makefile): remove unused targets for formatting and type checking, move to pre-commit
Simplifies the Makefile by removing redundant `format`, `check`, and `typecheck` targets. Streamlines build and maintenance process.
2026-02-12 11:55:48 +03:00
20473818a9 feat(emailing): add OpenAPI schemas for unsubscribe and tracking endpoints
Includes detailed OpenAPI schemas for unsubscribe (GET and POST) and tracking pixel (GET) endpoints, supporting email compatibility and event tracking. Added support for RFC 8058-compliant one-click unsubscribe functionality and transparent image-based email tracking.
2026-02-05 19:30:53 +03:00
507 changed files with 34280 additions and 22912 deletions

4
.gitignore vendored
View file

@ -192,7 +192,9 @@ engine/core/vendors/docs/*
# Production # Production
.initialized .initialized
queries/ /queries
/nginx.conf
/monitoring/web.yml
# AI assistants # AI assistants
.claude/ .claude/

View file

@ -106,17 +106,3 @@ migrate: clear
@$(call RUN_SCRIPT,migrate) @$(call RUN_SCRIPT,migrate)
migration: clear make-migrations migrate migration: clear make-migrations migrate
format: clear
@ruff format
check: clear
@ruff check
typecheck: clear
@ty check
precommit: clear
@ruff format
@ruff check
@ty check

View file

@ -0,0 +1,28 @@
# Generated by Django 5.2.11 on 2026-02-27 15:43
from django.db import migrations
import engine.core.utils.db
class Migration(migrations.Migration):
dependencies = [
("blog", "0008_alter_post_content_alter_post_content_ar_ar_and_more"),
]
operations = [
migrations.AlterField(
model_name="post",
name="slug",
field=engine.core.utils.db.TweakedAutoSlugField(
allow_unicode=True,
blank=True,
editable=False,
max_length=88,
null=True,
overwrite=True,
populate_from="title",
unique=True,
),
),
]

View file

@ -9,9 +9,9 @@ from django.db.models import (
TextField, TextField,
) )
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField
from engine.core.abstract import NiceModel from engine.core.abstract import NiceModel
from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function
class Post(NiceModel): class Post(NiceModel):
@ -47,8 +47,15 @@ class Post(NiceModel):
null=True, null=True,
) )
file = FileField(upload_to="posts/", blank=True, null=True) file = FileField(upload_to="posts/", blank=True, null=True)
slug = AutoSlugField( slug = TweakedAutoSlugField(
populate_from="title", allow_unicode=True, unique=True, editable=False populate_from="title",
slugify_function=unicode_slugify_function,
allow_unicode=True,
unique=True,
editable=False,
max_length=88,
overwrite=True,
null=True,
) )
tags = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts") tags = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts")
meta_description = CharField(max_length=150, blank=True, null=True) meta_description = CharField(max_length=150, blank=True, null=True)

View file

@ -421,10 +421,7 @@ class BrandAdmin(
"priority", "priority",
"is_active", "is_active",
) )
list_filter = ( list_filter = ("is_active",)
"categories",
"is_active",
)
search_fields = ( search_fields = (
"uuid", "uuid",
"name", "name",

View file

@ -1,9 +1,6 @@
from typing import Any
from django.db.models import Model, QuerySet from django.db.models import Model, QuerySet
from django_elasticsearch_dsl import Document, fields from django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registry from django_elasticsearch_dsl.registries import registry
from health_check.db.models import TestModel
from engine.core.elasticsearch import ( from engine.core.elasticsearch import (
COMMON_ANALYSIS, COMMON_ANALYSIS,
@ -195,18 +192,3 @@ class BrandDocument(ActiveOnlyMixin, BaseDocument):
add_multilang_fields(BrandDocument) add_multilang_fields(BrandDocument)
registry.register_document(BrandDocument) registry.register_document(BrandDocument)
class TestModelDocument(Document):
class Index:
name = "testmodels"
class Django:
model = TestModel
fields = ["title"]
ignore_signals = True
related_models: list[Any] = []
auto_refresh = False
registry.register_document(TestModelDocument)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
**Welcome to the Schon Demo Store**
> **You are viewing a live demonstration** of the [Schon e-commerce platform](https://schon.wiseless.xyz). Everything you see here — the products, the brands, the prices — is fictional, designed to showcase the platform's capabilities. No real transactions take place.
![Our showroom interior](/static/images/placeholder.png)
## What Is This Store?
This demo store is a fully functional showcase of **Schon** — a modern, production-ready e-commerce backend built for businesses of all sizes. We've set it up as a luxury gemstone and jewelry boutique to demonstrate how Schon handles rich product catalogs, multi-language support, advanced inventory management, and more.
Every gemstone you see, every brand description you read, and every price tag you encounter has been carefully crafted to demonstrate the platform's capabilities. From the 1.5-carat round brilliant diamond to the rare Paraiba tourmaline, each product showcases Schon's ability to handle detailed product data, high-resolution imagery, and complex category hierarchies.
## About the Schon Platform
**Schon** is a comprehensive e-commerce backend designed for businesses that demand reliability, flexibility, and performance.
### Key Features
- **Multi-language Support** — Full internationalization with 28 languages out of the box. Every product, category, and page can be translated, just like this demo store which runs in both English and Russian.
- **Advanced Product Management** — Rich product catalogs with attributes, variants, categories, brands, and tags. Support for digital and physical goods.
- **Inventory & Vendor Management** — Multi-vendor support with automated stock updates, markup management, and vendor-specific pricing.
- **Order Processing** — Complete order lifecycle management from cart to delivery, with support for multiple payment gateways.
- **Analytics & Reporting** — Built-in analytics with order tracking, revenue reports, and customer insights.
- **REST & GraphQL APIs** — Dual API support for maximum flexibility in building storefronts and integrations.
- **Admin Panel** — Powerful Django admin interface with custom dashboards, bulk operations, and real-time monitoring.
- **Security** — JWT authentication, role-based permissions, rate limiting, and industry-standard security practices.
### Technical Excellence
Schon is built with a modern technology stack:
- **Django & Django REST Framework** — Battle-tested Python web framework
- **PostgreSQL with PostGIS** — Geospatial-capable database
- **Elasticsearch** — Full-text search with faceted filtering
- **Redis** — Caching and session management
- **Celery** — Asynchronous task processing
- **Docker** — Containerized deployment
![Team at work](/static/images/placeholder.png)
## Explore the Demo
Feel free to browse the store, create an account, add items to your cart, and explore the full shopping experience. Remember, this is a demo — no real charges will be made and no real products will be shipped.
## Ready to Build Your Store?
If you like what you see, Schon can power your own e-commerce business. Visit [schon.wiseless.xyz](https://schon.wiseless.xyz) to learn about licensing options, get documentation, and start building your store today.
---
*Powered by [Schon](https://schon.wiseless.xyz) — E-commerce, done right.*

View file

@ -0,0 +1,51 @@
**Добро пожаловать в Демо-магазин Schon**
> **Вы просматриваете живую демонстрацию** [платформы электронной коммерции Schon](https://schon.wiseless.xyz). Всё, что вы видите здесь — товары, бренды, цены — является вымышленным и предназначено для демонстрации возможностей платформы. Реальные транзакции не осуществляются.
![Интерьер нашего шоурума](/static/images/placeholder.png)
## Что это за магазин?
Этот демонстрационный магазин — полнофункциональная витрина **Schon** — современного, готового к продуктивному использованию бэкенда электронной коммерции, созданного для бизнеса любого масштаба. Мы оформили его как бутик роскошных драгоценных камней и ювелирных изделий, чтобы продемонстрировать, как Schon работает с богатыми каталогами товаров, многоязычной поддержкой, расширенным управлением запасами и многим другим.
Каждый драгоценный камень, каждое описание бренда и каждый ценник были тщательно подготовлены для демонстрации возможностей платформы. От бриллианта круглой огранки 1,5 карата до редкого турмалина параиба — каждый товар показывает способность Schon работать с детализированными данными о продуктах, изображениями высокого разрешения и сложными иерархиями категорий.
## О платформе Schon
**Schon** — это комплексный бэкенд для электронной коммерции, разработанный для бизнеса, которому требуется надёжность, гибкость и производительность.
### Основные возможности
- **Многоязычная поддержка** — Полная интернационализация с поддержкой 28 языков. Каждый товар, категория и страница могут быть переведены, как в этом демо-магазине, который работает на английском и русском языках.
- **Расширенное управление товарами** — Богатые каталоги товаров с атрибутами, вариантами, категориями, брендами и тегами. Поддержка цифровых и физических товаров.
- **Управление запасами и поставщиками** — Мультивендорная поддержка с автоматическим обновлением остатков, управлением наценками и ценообразованием по поставщикам.
- **Обработка заказов** — Полный жизненный цикл заказа от корзины до доставки с поддержкой нескольких платёжных шлюзов.
- **Аналитика и отчёты** — Встроенная аналитика с отслеживанием заказов, отчётами о выручке и данными о клиентах.
- **REST и GraphQL API** — Двойная поддержка API для максимальной гибкости при создании витрин и интеграций.
- **Панель администратора** — Мощный интерфейс администратора Django с пользовательскими панелями мониторинга, массовыми операциями и мониторингом в реальном времени.
- **Безопасность** — JWT-аутентификация, ролевые разрешения, ограничение запросов и стандартные отраслевые практики безопасности.
### Техническое совершенство
Schon построен на современном технологическом стеке:
- **Django и Django REST Framework** — Проверенный временем веб-фреймворк на Python
- **PostgreSQL с PostGIS** — База данных с геопространственными возможностями
- **Elasticsearch** — Полнотекстовый поиск с фасетной фильтрацией
- **Redis** — Кэширование и управление сессиями
- **Celery** — Асинхронная обработка задач
- **Docker** — Контейнеризированное развёртывание
![Команда за работой](/static/images/placeholder.png)
## Исследуйте демо
Просматривайте магазин, создавайте учётную запись, добавляйте товары в корзину и изучайте полный покупательский опыт. Помните, что это демонстрация — реальные платежи не взимаются, реальные товары не отправляются.
## Готовы создать свой магазин?
Если вам понравилось увиденное, Schon может стать основой вашего собственного бизнеса в сфере электронной коммерции. Посетите [schon.wiseless.xyz](https://schon.wiseless.xyz), чтобы узнать о вариантах лицензирования, получить документацию и начать строить свой магазин уже сегодня.
---
*Работает на [Schon](https://schon.wiseless.xyz) — электронная коммерция, сделанная правильно.*

View file

@ -0,0 +1,77 @@
Ever wondered what runs behind the scenes of an online gemstone store? In this post, we pull back the curtain on the technology that powers the Schon Demo Store — and explain why we believe Schon is the right foundation for your next e-commerce project.
![Schon admin dashboard screenshot](/static/images/placeholder.png)
## The Challenge of Gemstone E-Commerce
Selling gemstones online presents unique challenges that generic e-commerce platforms struggle with:
- **Complex product data** — Each stone has dozens of attributes: carat weight, dimensions, cut, color grade, clarity, origin, treatment history, and certification details
- **High-value inventory** — Proper stock management with vendor integration and automated updates is critical
- **Global audience** — Buyers come from every continent and expect content in their language
- **Rich media** — High-resolution imagery is essential for customers to evaluate stones remotely
- **Trust and detail** — Detailed product information and professional presentation build the confidence needed for high-value purchases
## How Schon Solves This
### Flexible Product Attributes
Schon's attribute system lets you define custom attribute groups (Physical Properties, Grading, Origin) with typed values (float, string, boolean). These attributes are filterable, searchable, and automatically included in API responses. No rigid schemas — the system adapts to whatever you sell.
### Multi-Vendor Inventory
Our demo store uses a single vendor (Schon Demo), but the platform supports unlimited vendors, each with their own pricing, stock levels, and markup percentages. The stock updater service runs as a separate worker, syncing inventory from external supplier feeds in real-time.
![API documentation interface](/static/images/placeholder.png)
### API-First Architecture
The entire storefront is powered by Schon's REST and GraphQL APIs. This means you can build any frontend you want:
- A server-rendered storefront with Nuxt or Next.js
- A mobile app for iOS and Android
- A marketplace integration
- A B2B portal for wholesale buyers
The same API that serves product listings also handles authentication, cart management, order processing, and analytics — all through clean, well-documented endpoints.
### Built-In Analytics
Every order, every refund, every wishlist action is tracked. The admin dashboard provides real-time insights into sales trends, popular products, customer behavior, and inventory levels. For this demo, we generate realistic order data spanning 30 days to show these analytics in action.
### Production-Ready Infrastructure
Schon runs on:
- **Django** — Proven in production at companies like Instagram, Pinterest, and Mozilla
- **PostgreSQL** — Enterprise-grade database with PostGIS for location-based features
- **Redis** — Sub-millisecond caching for blazing-fast API responses
- **Elasticsearch** — Full-text search with autocomplete and faceted filtering
- **Celery** — Background tasks for email, inventory sync, and scheduled operations
- **Docker** — One-command deployment with compose
## See It in Action
This demo store is a real, running instance of Schon. Everything you experience — browsing products, filtering by category, switching languages, reading this blog post — is powered by the same platform available for your business.
We encourage you to explore:
- **The product catalog** — Filter, search, and browse across categories
- **The API** — Visit `/docs/swagger/` for interactive API documentation
- **The admin panel** — See how store operators manage their business
- **The blog** — This very post demonstrates Schon's built-in CMS capabilities
## Ready to Get Started?
Whether you're launching a gemstone boutique, a fashion brand, an electronics store, or any other e-commerce venture, Schon provides the foundation you need.
Visit [schon.wiseless.xyz](https://schon.wiseless.xyz) to:
- View the full feature list
- Access technical documentation
- Learn about licensing options
- Schedule a demo tailored to your business needs
---
*The Schon Demo Store demonstrates the [Schon e-commerce platform](https://schon.wiseless.xyz) in a realistic scenario. All products, brands, and transactions are fictional.*

View file

@ -0,0 +1,77 @@
Задумывались ли вы, что стоит за работой онлайн-магазина драгоценных камней? В этой статье мы приоткрываем завесу над технологией, обеспечивающей работу Демо-магазина Schon, и объясняем, почему Schon — правильная основа для вашего следующего проекта в электронной коммерции.
![Скриншот панели администратора Schon](/static/images/placeholder.png)
## Вызовы электронной коммерции драгоценных камней
Продажа драгоценных камней онлайн ставит уникальные задачи, с которыми универсальные платформы справляются с трудом:
- **Сложные данные о товарах** — Каждый камень имеет десятки атрибутов: вес в каратах, размеры, огранка, цветовая категория, чистота, происхождение, история обработки и данные сертификации
- **Дорогостоящие запасы** — Правильное управление складом с интеграцией поставщиков и автоматическими обновлениями критически важно
- **Глобальная аудитория** — Покупатели приходят со всех континентов и ожидают контент на своём языке
- **Богатый медиаконтент** — Изображения высокого разрешения необходимы для удалённой оценки камней покупателями
- **Доверие и детализация** — Подробная информация о товаре и профессиональная подача формируют уверенность, необходимую для крупных покупок
## Как Schon решает эти задачи
### Гибкая система атрибутов
Система атрибутов Schon позволяет определять пользовательские группы атрибутов (Физические свойства, Оценка качества, Происхождение) с типизированными значениями (число, строка, булево). Эти атрибуты фильтруемы, доступны для поиска и автоматически включаются в ответы API. Никаких жёстких схем — система адаптируется к любому товару.
### Мультивендорные запасы
Наш демо-магазин использует одного поставщика (Schon Demo), но платформа поддерживает неограниченное количество поставщиков, каждый со своим ценообразованием, остатками и процентами наценки. Сервис обновления остатков работает как отдельный воркер, синхронизируя запасы из внешних фидов поставщиков в реальном времени.
![Интерфейс документации API](/static/images/placeholder.png)
### API-ориентированная архитектура
Вся витрина работает через REST и GraphQL API Schon. Это значит, что вы можете создать любой фронтенд:
- Серверную витрину на Nuxt или Next.js
- Мобильное приложение для iOS и Android
- Интеграцию с маркетплейсом
- B2B-портал для оптовых покупателей
Тот же API, который обслуживает каталог товаров, обрабатывает аутентификацию, управление корзиной, оформление заказов и аналитику — всё через чистые, хорошо документированные эндпоинты.
### Встроенная аналитика
Каждый заказ, каждый возврат, каждое действие со списком желаний отслеживается. Панель администратора предоставляет аналитику в реальном времени: тренды продаж, популярные товары, поведение клиентов и уровни запасов. Для этого демо мы генерируем реалистичные данные заказов за 30 дней, чтобы показать аналитику в действии.
### Инфраструктура, готовая к продуктиву
Schon работает на:
- **Django** — Проверен в продуктиве такими компаниями как Instagram, Pinterest и Mozilla
- **PostgreSQL** — База данных корпоративного уровня с PostGIS для геолокационных функций
- **Redis** — Кэширование за доли миллисекунды для молниеносных ответов API
- **Elasticsearch** — Полнотекстовый поиск с автодополнением и фасетной фильтрацией
- **Celery** — Фоновые задачи для рассылок, синхронизации запасов и планируемых операций
- **Docker** — Развёртывание одной командой через compose
## Посмотрите в действии
Этот демо-магазин — реальный, работающий экземпляр Schon. Всё, что вы видите — просмотр товаров, фильтрация по категориям, переключение языков, чтение этого блога — работает на той же платформе, доступной для вашего бизнеса.
Мы приглашаем вас исследовать:
- **Каталог товаров** — Фильтруйте, ищите и просматривайте по категориям
- **API** — Посетите `/docs/swagger/` для интерактивной документации API
- **Панель администратора** — Узнайте, как операторы магазинов управляют бизнесом
- **Блог** — Эта самая статья демонстрирует встроенные CMS-возможности Schon
## Готовы начать?
Запускаете ли вы бутик драгоценных камней, модный бренд, магазин электроники или любой другой проект электронной коммерции — Schon предоставит необходимую основу.
Посетите [schon.wiseless.xyz](https://schon.wiseless.xyz), чтобы:
- Ознакомиться с полным списком возможностей
- Получить техническую документацию
- Узнать о вариантах лицензирования
- Запланировать демонстрацию, адаптированную под ваш бизнес
---
*Демо-магазин Schon демонстрирует [платформу электронной коммерции Schon](https://schon.wiseless.xyz) в реалистичном сценарии. Все товары, бренды и транзакции являются вымышленными.*

View file

@ -0,0 +1,34 @@
We're pleased to announce that the Schon Demo Store now offers a **complete bilingual experience** in English and Russian. Every product description, category name, brand story, and informational page has been professionally translated to provide a seamless shopping experience for Russian-speaking customers.
![Side-by-side language comparison](/static/images/placeholder.png)
## What's Translated
- **Full product catalog** — All 50+ gemstone listings with detailed descriptions, specifications, and grading information
- **Category navigation** — Browse gemstone categories in your preferred language
- **Brand pages** — Read about each of our partner brands in Russian
- **Informational pages** — Privacy Policy, Terms & Conditions, FAQ, Shipping Info, and Return Policy are all available in both languages
- **Blog and news** — Stay up to date with store announcements in your language
## Powered by Schon's i18n Engine
This bilingual capability is built into the core of the **Schon e-commerce platform**. Schon supports **28 languages** out of the box, with every translatable field — from product names to meta descriptions — managed through an integrated translation system.
For store operators, this means:
- **No duplicate content** — A single product entry holds all language variants
- **Automatic language detection** — The API serves content in the user's preferred language
- **SEO-friendly** — Each language version gets proper meta tags and descriptions
- **Easy management** — Translations are managed through the admin panel alongside the original content
![Multilingual product page example](/static/images/placeholder.png)
## Try It Yourself
Switch your browser's language preference to Russian (or use the language selector in your storefront) to see the full translated experience. Every detail has been localized, from the product specifications to the checkout flow.
This is just one example of how Schon makes it straightforward to serve a global customer base without maintaining separate storefronts for each market.
---
*The Schon Demo Store showcases the [Schon e-commerce platform](https://schon.wiseless.xyz). All products and transactions are fictional. Interested in multilingual e-commerce? [Learn more](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,34 @@
Мы рады сообщить, что Демо-магазин Schon теперь предлагает **полноценный двуязычный опыт** на английском и русском языках. Каждое описание товара, название категории, история бренда и информационная страница профессионально переведены для обеспечения удобного шоппинга для русскоязычных покупателей.
![Сравнение языковых версий](/static/images/placeholder.png)
## Что переведено
- **Весь каталог товаров**Все 50+ листингов драгоценных камней с подробными описаниями, характеристиками и информацией о качестве
- **Навигация по категориям** — Просматривайте категории драгоценных камней на предпочтительном языке
- **Страницы брендов** — Читайте о каждом из наших брендов-партнёров на русском языке
- **Информационные страницы** — Политика конфиденциальности, Условия использования, FAQ, Информация о доставке и Политика возврата доступны на обоих языках
- **Блог и новости** — Следите за объявлениями магазина на вашем языке
## На базе движка интернационализации Schon
Эта двуязычная возможность встроена в ядро **платформы электронной коммерции Schon**. Schon поддерживает **28 языков**, при этом каждое переводимое поле — от названий товаров до мета-описаний — управляется через интегрированную систему переводов.
Для операторов магазинов это означает:
- **Без дублирования контента** — Одна запись товара содержит все языковые варианты
- **Автоматическое определение языка** — API выдаёт контент на предпочтительном языке пользователя
- **SEO-оптимизация** — Каждая языковая версия получает корректные мета-теги и описания
- **Удобное управление** — Переводы управляются через панель администратора наряду с оригинальным контентом
![Пример многоязычной страницы товара](/static/images/placeholder.png)
## Попробуйте сами
Переключите языковые настройки браузера на русский (или используйте переключатель языка в витрине) для просмотра полностью переведённого интерфейса. Каждая деталь локализована — от характеристик товаров до процесса оформления заказа.
Это лишь один пример того, как Schon упрощает обслуживание глобальной клиентской базы без необходимости поддерживать отдельные витрины для каждого рынка.
---
*Демо-магазин Schon демонстрирует [платформу электронной коммерции Schon](https://schon.wiseless.xyz). Все товары и транзакции являются вымышленными. Интересует многоязычная электронная коммерция? [Узнайте больше](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,95 @@
**Schon Demo Store**
> **Demo Notice:** This FAQ pertains to the Schon Demo Store, a demonstration environment for the [Schon e-commerce platform](https://schon.wiseless.xyz). All products and transactions are fictional.
![Customer browsing our online store](/static/images/placeholder.png)
## General Questions
### What is this website?
This is a demonstration store powered by the Schon e-commerce platform. It showcases the platform's capabilities using a fictional luxury gemstone and jewelry catalog. No real products are sold, and no real transactions are processed.
### Can I actually buy the gemstones shown here?
No. All products displayed in this store are fictional. The names, descriptions, prices, and images are for demonstration purposes only. No real goods are available for purchase.
### Is this store connected to a real payment system?
No. The demo store does not process real payments. Any checkout or payment flows you encounter are simulated to demonstrate the platform's e-commerce capabilities.
### Can I create an account?
Yes, you can create an account to explore the full range of features, including adding products to your cart, managing wishlists, and simulating the checkout process. Demo accounts may be periodically reset.
## About the Products
### Are the gemstone descriptions accurate?
The product descriptions are written to be realistic and informative for demonstration purposes. While they reference real gemstone types, grades, and origins, the specific products listed do not exist. No gemological certificates mentioned in the descriptions are real.
### Why are some products very expensive?
The pricing is designed to simulate a realistic luxury gemstone market. Prices range from affordable semi-precious stones to rare investment-grade gems to demonstrate how the platform handles different price tiers and product categories.
### How are the product images generated?
Product images are used for demonstration purposes and may not accurately represent real gemstones matching the given descriptions.
## About the Schon Platform
### What is Schon?
Schon is a production-ready e-commerce backend built with Django. It provides a complete solution for online stores, including product management, order processing, inventory control, multi-language support, and powerful APIs.
### What technologies does Schon use?
Schon is built on Django and Django REST Framework, with PostgreSQL (including PostGIS for geospatial features), Redis for caching, Elasticsearch for search, and Celery for background tasks. It supports both REST and GraphQL APIs.
### Does Schon support multiple languages?
Yes. Schon supports 28 languages out of the box. This demo store demonstrates bilingual support (English and Russian), but the platform can handle any combination of supported languages.
### Can I use Schon for my own store?
Absolutely. Schon is available for licensing. Visit [schon.wiseless.xyz](https://schon.wiseless.xyz) to learn more about pricing, features, and how to get started.
### Does Schon include a storefront?
Schon is a backend platform that provides REST and GraphQL APIs. It can power any frontend — whether it's a custom-built storefront, a mobile app, or a third-party integration. Reference storefront implementations are available.
### What payment gateways does Schon support?
Schon has an extensible payment gateway architecture. Integration with specific payment providers can be configured based on your business requirements.
## Technical Questions
### Can I access the API?
Yes. The demo store exposes both REST and GraphQL APIs:
- **REST API:** Available at the store's base URL
- **GraphQL:** Available at `/graphql/`
- **Swagger Documentation:** Available at `/docs/swagger/`
### What about mobile apps?
Schon's API-first design makes it ideal for powering mobile applications. The same APIs that serve the web storefront can be used by iOS and Android apps.
### How is the demo data generated?
The demo store uses a built-in management command that creates fictional products, brands, categories, users, and orders. This tool can be used to quickly populate a new Schon installation for testing and evaluation.
## Contact
### How can I learn more about Schon?
Visit [schon.wiseless.xyz](https://schon.wiseless.xyz) for comprehensive documentation, pricing information, and contact details.
### I found a bug in the demo. How can I report it?
We appreciate your feedback. Please contact us at support@wiseless.xyz with details about the issue you encountered.
---
*This document is part of the Schon Demo Store — a demonstration environment showcasing the [Schon e-commerce platform](https://schon.wiseless.xyz). All store data, including products, brands, and prices, is fictional. Interested in launching your own store? [Learn more about Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,95 @@
**Демо-магазин Schon**
> **Уведомление:** Данный раздел FAQ относится к Демо-магазину Schon — демонстрационной среде для [платформы электронной коммерции Schon](https://schon.wiseless.xyz). Все товары и транзакции являются вымышленными.
![Покупатель просматривает наш онлайн-магазин](/static/images/placeholder.png)
## Общие вопросы
### Что представляет собой этот сайт?
Это демонстрационный магазин, работающий на платформе электронной коммерции Schon. Он демонстрирует возможности платформы на примере вымышленного каталога роскошных драгоценных камней и ювелирных изделий. Реальные товары не продаются, реальные транзакции не обрабатываются.
### Можно ли купить представленные здесь драгоценные камни?
Нет. Все товары, представленные в этом магазине, являются вымышленными. Названия, описания, цены и изображения предназначены исключительно для демонстрации. Реальные товары не продаются.
### Подключён ли этот магазин к реальной платёжной системе?
Нет. Демо-магазин не обрабатывает реальные платежи. Все процессы оформления заказа и оплаты, которые вы встретите, являются симуляцией, демонстрирующей возможности электронной коммерции платформы.
### Можно ли создать учётную запись?
Да, вы можете создать учётную запись для изучения полного набора функций, включая добавление товаров в корзину, управление списками желаний и симуляцию процесса оформления заказа. Демонстрационные учётные записи могут периодически сбрасываться.
## О товарах
### Соответствуют ли описания драгоценных камней действительности?
Описания товаров написаны реалистично и информативно в демонстрационных целях. Хотя в них упоминаются реальные типы, классы и происхождение драгоценных камней, конкретные перечисленные товары не существуют. Геммологические сертификаты, упомянутые в описаниях, не являются реальными.
### Почему некоторые товары очень дорогие?
Ценообразование разработано для имитации реального рынка роскошных драгоценных камней. Цены варьируются от доступных полудрагоценных камней до редких камней инвестиционного качества, чтобы продемонстрировать, как платформа работает с различными ценовыми категориями и типами товаров.
### Как созданы изображения товаров?
Изображения товаров используются в демонстрационных целях и могут неточно соответствовать реальным драгоценным камням указанных в описаниях характеристик.
## О платформе Schon
### Что такое Schon?
Schon — это готовый к продуктивному использованию бэкенд электронной коммерции, построенный на Django. Он предоставляет комплексное решение для интернет-магазинов, включая управление товарами, обработку заказов, контроль запасов, многоязычную поддержку и мощные API.
### Какие технологии использует Schon?
Schon построен на Django и Django REST Framework с использованием PostgreSQL (включая PostGIS для геопространственных функций), Redis для кэширования, Elasticsearch для поиска и Celery для фоновых задач. Поддерживаются как REST, так и GraphQL API.
### Поддерживает ли Schon несколько языков?
Да. Schon поддерживает 28 языков. Этот демо-магазин демонстрирует двуязычную поддержку (английский и русский), но платформа может работать с любой комбинацией поддерживаемых языков.
### Могу ли я использовать Schon для своего магазина?
Безусловно. Schon доступен для лицензирования. Посетите [schon.wiseless.xyz](https://schon.wiseless.xyz), чтобы узнать больше о ценах, возможностях и о том, как начать работу.
### Включает ли Schon витрину магазина?
Schon является бэкенд-платформой, предоставляющей REST и GraphQL API. Он может обеспечивать работу любого фронтенда — будь то витрина собственной разработки, мобильное приложение или сторонняя интеграция. Доступны референсные реализации витрин.
### Какие платёжные шлюзы поддерживает Schon?
Schon имеет расширяемую архитектуру платёжных шлюзов. Интеграция с конкретными платёжными провайдерами настраивается в соответствии с требованиями вашего бизнеса.
## Технические вопросы
### Можно ли получить доступ к API?
Да. Демо-магазин предоставляет как REST, так и GraphQL API:
- **REST API:** Доступен по базовому URL магазина
- **GraphQL:** Доступен по адресу `/graphql/`
- **Документация Swagger:** Доступна по адресу `/docs/swagger/`
### Как насчёт мобильных приложений?
API-ориентированный дизайн Schon делает его идеальным для мобильных приложений. Те же API, которые обслуживают веб-витрину, могут использоваться приложениями iOS и Android.
### Как генерируются демонстрационные данные?
Демо-магазин использует встроенную команду управления, которая создаёт вымышленные товары, бренды, категории, пользователей и заказы. Этот инструмент позволяет быстро наполнить новую установку Schon для тестирования и ознакомления.
## Контакты
### Как узнать больше о Schon?
Посетите [schon.wiseless.xyz](https://schon.wiseless.xyz) для получения подробной документации, информации о ценах и контактных данных.
### Я обнаружил ошибку в демо. Как я могу сообщить о ней?
Мы ценим вашу обратную связь. Пожалуйста, свяжитесь с нами по адресу support@wiseless.xyz с описанием обнаруженной проблемы.
---
*Этот документ является частью Демо-магазина Schon — демонстрационной среды, представляющей [платформу электронной коммерции Schon](https://schon.wiseless.xyz). Все данные магазина, включая товары, бренды и цены, являются вымышленными. Хотите запустить собственный магазин? [Узнайте больше о Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,78 @@
Whether you're purchasing your first gemstone or adding to an established collection, understanding certification is essential. This guide explains the major gemological laboratories, what their reports cover, and why certification matters.
![GIA certificate example](/static/images/placeholder.png)
## Why Certification Matters
A gemstone certificate (also called a grading report) is an independent, expert assessment of a stone's characteristics. It provides:
- **Objective verification** of color, clarity, cut, and carat weight
- **Treatment disclosure** — whether the stone has been heated, oiled, or otherwise enhanced
- **Origin determination** — geographic source, which significantly affects value
- **Confidence** for both buyer and seller in the transaction
## Major Gemological Laboratories
### GIA (Gemological Institute of America)
The most widely recognized laboratory worldwide, particularly for diamonds. GIA reports are considered the gold standard for:
- Diamond grading (the "4Cs" system was developed by GIA)
- Colored gemstone identification
- Pearl grading
### GRS (Gem Research Swisslab)
Based in Switzerland, GRS specializes in colored gemstones and is particularly respected for:
- Origin determination for rubies, sapphires, and emeralds
- Color grading with trade names (e.g., "Pigeon Blood" for rubies, "Royal Blue" for sapphires)
- Treatment analysis
### Gubelin Gem Lab
Another Swiss laboratory with over 100 years of history, known for:
- Detailed origin reports
- Inclusion analysis
- Research-grade documentation
### SSEF (Swiss Gemmological Institute)
Part of the Swiss Foundation for the Research of Gemstones, SSEF is recognized for:
- Advanced testing methods
- Pearl testing (natural vs. cultured)
- High-value gemstone verification
![Gemological testing equipment](/static/images/placeholder.png)
## What a Certificate Includes
A typical gemstone certificate will document:
1. **Identification** — The gemstone species and variety
2. **Weight** — Precise carat weight
3. **Dimensions** — Length, width, and depth in millimeters
4. **Shape and Cut** — The cut style and shape
5. **Color** — Hue, saturation, and tone description
6. **Clarity** — Inclusion type and visibility
7. **Treatment** — Any enhancements detected (or "no indication of treatment")
8. **Origin** — Geographic source when determinable
9. **Photographs** — Images of the stone as examined
## Tips for Buyers
- **Always request a certificate** for significant purchases (generally above $1,000)
- **Verify the certificate** directly with the issuing laboratory using the report number
- **Understand treatment codes** — "H" for heated, "N" for no treatment, etc.
- **Compare like with like** — Certificates from different labs may use different grading scales
- **Keep certificates safe** — They are essential for insurance, resale, and estate purposes
## Our Commitment
Every gemstone in our collection over $5,000 comes with certification from a recognized laboratory. Lower-priced items include our own detailed quality assessment. You can find certification details in each product's specifications section.
---
*This guide is published by the Schon Demo Store, powered by the [Schon e-commerce platform](https://schon.wiseless.xyz). All products mentioned are fictional. Visit [schon.wiseless.xyz](https://schon.wiseless.xyz) to learn about building your own gemstone or jewelry store.*

View file

@ -0,0 +1,78 @@
Приобретаете ли вы свой первый драгоценный камень или пополняете существующую коллекцию — понимание сертификации необходимо. В этом руководстве мы объясним, какие существуют основные геммологические лаборатории, что включают их отчёты и почему сертификация важна.
![Пример сертификата GIA](/static/images/placeholder.png)
## Зачем нужна сертификация
Сертификат драгоценного камня (также называемый экспертным заключением) — это независимая экспертная оценка характеристик камня. Он обеспечивает:
- **Объективную верификацию** цвета, чистоты, огранки и веса в каратах
- **Раскрытие обработки** — подвергался ли камень нагреву, промасливанию или иному облагораживанию
- **Определение происхождения** — географический источник, существенно влияющий на стоимость
- **Уверенность** как для покупателя, так и для продавца в сделке
## Основные геммологические лаборатории
### GIA (Геммологический институт Америки)
Наиболее признанная лаборатория в мире, особенно в области бриллиантов. Отчёты GIA считаются золотым стандартом для:
- Оценки бриллиантов (система «4C» была разработана GIA)
- Идентификации цветных драгоценных камней
- Оценки жемчуга
### GRS (Gem Research Swisslab)
Швейцарская лаборатория, специализирующаяся на цветных драгоценных камнях, особенно уважаемая за:
- Определение происхождения рубинов, сапфиров и изумрудов
- Градацию цвета с торговыми наименованиями (например, «Голубиная кровь» для рубинов, «Королевский синий» для сапфиров)
- Анализ обработки
### Gubelin Gem Lab
Ещё одна швейцарская лаборатория с более чем 100-летней историей, известная:
- Детальными отчётами о происхождении
- Анализом включений
- Документацией исследовательского уровня
### SSEF (Швейцарский геммологический институт)
Часть Швейцарского фонда исследований драгоценных камней. SSEF признан за:
- Передовые методы тестирования
- Экспертизу жемчуга (натуральный или культивированный)
- Верификацию драгоценных камней высокой стоимости
![Геммологическое оборудование для тестирования](/static/images/placeholder.png)
## Что включает сертификат
Типичный сертификат драгоценного камня содержит:
1. **Идентификация** — Вид и разновидность камня
2. **Вес** — Точный вес в каратах
3. **Размеры** — Длина, ширина и глубина в миллиметрах
4. **Форма и огранка** — Стиль огранки и форма
5. **Цвет** — Описание тона, насыщенности и оттенка
6. **Чистота** — Тип и видимость включений
7. **Обработка** — Выявленные улучшения (или «без признаков обработки»)
8. **Происхождение** — Географический источник, когда его можно определить
9. **Фотографии** — Снимки камня в момент экспертизы
## Советы покупателям
- **Всегда запрашивайте сертификат** при значительных покупках (как правило, свыше $1 000)
- **Проверяйте сертификат** напрямую в выдавшей его лаборатории по номеру отчёта
- **Изучите коды обработки** — «H» означает нагрев, «N» — без обработки и т.д.
- **Сравнивайте сопоставимое** — Сертификаты разных лабораторий могут использовать различные шкалы оценки
- **Храните сертификаты в безопасности** — Они необходимы для страхования, перепродажи и оценки наследства
## Наши гарантии
Каждый драгоценный камень в нашей коллекции стоимостью свыше $5 000 сопровождается сертификатом признанной лаборатории. Для товаров с более низкой ценой мы предоставляем собственную детальную оценку качества. Информацию о сертификации вы найдёте в разделе характеристик каждого товара.
---
*Это руководство опубликовано Демо-магазином Schon на платформе [Schon](https://schon.wiseless.xyz). Все упомянутые товары являются вымышленными. Посетите [schon.wiseless.xyz](https://schon.wiseless.xyz), чтобы узнать о создании собственного магазина драгоценных камней или ювелирных изделий.*

View file

@ -0,0 +1,58 @@
Looking for the perfect gift that combines beauty, rarity, and lasting value? Our curators have selected the most impressive gemstones under $5,000 from this season's collection.
![Curated gift selection display](/static/images/placeholder.png)
## Our Top Picks
### 1. Ethiopian Welo Opal 3.8ct — $3,500
A mesmerizing play-of-color gem with a unique honeycomb pattern. Opals are perfect for someone who appreciates the unusual and extraordinary. This piece from Terra Rara's collection captures light like a miniature galaxy.
### 2. Tahitian Pearl 12mm Peacock — $3,200
Nothing says elegance quite like a Tahitian pearl. This 12mm pearl from Oceanic Pearls features stunning peacock overtones — dark body color with iridescent green and purple flashes. A timeless gift.
### 3. Rubellite Tourmaline 4.3ct — $3,500
A vivid raspberry-red tourmaline with exceptional saturation. Crimson Vault has selected a cushion-cut piece that rivals much more expensive rubies in visual impact.
### 4. Indicolite Tourmaline 3.6ct — $2,800
For those who love blue, this teal-colored tourmaline from Azure Dreams offers a unique alternative to sapphires. The oval cut from Afghanistan shows beautiful color depth.
### 5. Madagascar Aquamarine 7.5ct — $2,200
A generous 7.5-carat aquamarine with excellent clarity and a soothing light blue color. Azure Dreams sourced this octagon-cut beauty from Madagascar.
### 6. Watermelon Tourmaline 8.5ct — $1,800
A conversation piece like no other — this slice-cut tourmaline displays a pink center surrounded by a green rim, like a tiny watermelon. Crystal Kingdom's collection includes several of these natural wonders.
### 7. Siberian Amethyst 8.5ct — $1,200
The legendary deep purple of Siberian amethyst, with red flashes that make it one of the most sought-after quartz varieties. A cushion-cut gem from Crystal Kingdom.
### 8. Fire Opal 2.1ct Mexican Orange — $1,200
A brilliant transparent opal with intense orange color from Mexico. Lumina Treasures offers this unique piece that glows with inner fire.
![Gift-wrapped gemstone box](/static/images/placeholder.png)
## Gift-Worthy Presentation
Every gemstone purchase from our store includes:
- A detailed quality certificate or assessment
- Premium packaging with branded presentation box
- Care instructions specific to the gemstone type
- Free standard shipping on orders over $500
## Need Help Choosing?
Not sure which gemstone suits your recipient? Consider their birth month, favorite color, or personal style. Our product pages include detailed information about each stone's properties, symbolism, and care requirements.
Browse our full collection using the category filters, price range selector, or simply search by gemstone type. Our platform makes finding the perfect gem effortless.
---
*The Schon Demo Store is powered by [Schon](https://schon.wiseless.xyz). All products and prices are fictional and for demonstration purposes only.*

View file

@ -0,0 +1,58 @@
Ищете идеальный подарок, сочетающий красоту, редкость и непреходящую ценность? Наши кураторы отобрали самые впечатляющие драгоценные камни стоимостью до $5 000 из коллекции этого сезона.
![Витрина с подарочной подборкой](/static/images/placeholder.png)
## Наш выбор
### 1. Эфиопский опал Вело 3.8 карата — $3 500
Завораживающий камень с игрой цвета и уникальным сотовым рисунком. Опалы идеальны для тех, кто ценит необычное и экстраординарное. Этот экземпляр из коллекции Terra Rara улавливает свет, словно миниатюрная галактика.
### 2. Таитянский жемчуг 12мм павлиний — $3 200
Ничто не говорит об элегантности так, как таитянский жемчуг. Эта 12-мм жемчужина от Oceanic Pearls отличается потрясающими павлиньими переливами — тёмное тело с иризирующими зелёными и фиолетовыми вспышками. Вневременной подарок.
### 3. Рубеллит турмалин 4.3 карата — $3 500
Яркий малиново-красный турмалин с исключительной насыщенностью. Crimson Vault отобрал экземпляр огранки «Кушон», который по визуальному воздействию соперничает с гораздо более дорогими рубинами.
### 4. Индиголит турмалин 3.6 карата — $2 800
Для любителей синего — этот турмалин сине-зелёного цвета от Azure Dreams предлагает уникальную альтернативу сапфирам. Овальная огранка из Афганистана демонстрирует красивую глубину цвета.
### 5. Мадагаскарский аквамарин 7.5 карата — $2 200
Великодушные 7,5 карата аквамарина с отличной чистотой и успокаивающим светло-голубым цветом. Azure Dreams добыл этот прекрасный камень восьмиугольной огранки на Мадагаскаре.
### 6. Арбузный турмалин 8.5 карата — $1 800
Камень, привлекающий внимание как никакой другой — турмалин огранки «слайс» демонстрирует розовый центр, окружённый зелёным ободком, словно миниатюрный арбуз. В коллекции Crystal Kingdom есть несколько таких природных чудес.
### 7. Сибирский аметист 8.5 карата — $1 200
Легендарный глубокий фиолетовый цвет сибирского аметиста с красными вспышками, делающими его одной из самых желанных разновидностей кварца. Камень огранки «Кушон» от Crystal Kingdom.
### 8. Огненный опал 2.1 карата мексиканский оранжевый — $1 200
Блестящий прозрачный опал интенсивного оранжевого цвета из Мексики. Lumina Treasures предлагает этот уникальный камень, светящийся внутренним огнём.
![Подарочная коробка с драгоценным камнем](/static/images/placeholder.png)
## Подарочное оформление
Каждая покупка драгоценного камня в нашем магазине включает:
- Подробный сертификат качества или экспертную оценку
- Премиальную упаковку с фирменной подарочной коробкой
- Инструкции по уходу, специфичные для типа камня
- Бесплатную стандартную доставку при заказе от $500
## Нужна помощь в выборе?
Не уверены, какой камень подойдёт вашему получателю? Учтите месяц рождения, любимый цвет или личный стиль. Страницы товаров содержат подробную информацию о свойствах каждого камня, его символике и требованиях к уходу.
Просматривайте всю коллекцию с помощью фильтров по категориям, ценовому диапазону или просто ищите по типу камня. Наша платформа делает поиск идеального камня лёгким.
---
*Демо-магазин Schon работает на платформе [Schon](https://schon.wiseless.xyz). Все товары и цены являются вымышленными и предназначены исключительно для демонстрации.*

View file

@ -0,0 +1,113 @@
**Schon Demo Store**
> **Demo Notice:** This is a demonstration store powered by the [Schon](https://schon.wiseless.xyz) e-commerce platform. No real transactions are processed, and no actual personal data is collected through purchases. This privacy policy is provided as an example of a production-ready document. If you are interested in deploying Schon for your own store, please visit [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Professional gemstone display case](/static/images/placeholder.png)
## 1. Introduction
Welcome to Schon Demo Store ("we," "our," or "us"). We are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website and use our services.
By accessing or using our website, you agree to the terms of this Privacy Policy. If you do not agree with the practices described herein, please do not use our services.
## 2. Information We Collect
### 2.1 Information You Provide Directly
- **Account Information:** When you create an account, we collect your name, email address, and password.
- **Order Information:** When placing an order, we collect your shipping address, billing address, and payment details.
- **Communication Data:** When you contact us, we collect the content of your messages, your email address, and any other information you provide.
### 2.2 Information Collected Automatically
- **Usage Data:** We collect information about how you interact with our website, including pages visited, time spent, and navigation patterns.
- **Device Information:** We collect information about the device you use to access our website, including device type, operating system, browser type, and screen resolution.
- **Log Data:** Our servers automatically record information such as your IP address, access times, and referring URLs.
### 2.3 Cookies and Similar Technologies
We use cookies and similar tracking technologies to enhance your browsing experience and analyze website traffic. For more details, see Section 7 below.
## 3. How We Use Your Information
We use the collected information for the following purposes:
- **Order Processing:** To process and fulfill your orders, including shipping and payment processing.
- **Account Management:** To create and manage your user account.
- **Customer Support:** To respond to your inquiries and provide assistance.
- **Personalization:** To personalize your shopping experience and recommend products.
- **Analytics:** To analyze website usage and improve our services.
- **Marketing:** To send promotional communications, subject to your consent and applicable laws.
- **Legal Compliance:** To comply with legal obligations and enforce our terms of service.
## 4. Information Sharing and Disclosure
We do not sell your personal information. We may share your information in the following circumstances:
- **Service Providers:** We share information with third-party service providers who assist us in operating our website, processing payments, shipping orders, and providing customer support.
- **Legal Requirements:** We may disclose information when required by law, regulation, or legal process.
- **Business Transfers:** In the event of a merger, acquisition, or sale of assets, your information may be transferred as part of the transaction.
- **Consent:** We may share information with your explicit consent.
## 5. Data Security
We implement industry-standard security measures to protect your personal information, including:
- Encryption of data in transit using TLS/SSL protocols
- Secure storage of sensitive data with encryption at rest
- Regular security audits and vulnerability assessments
- Access controls limiting data access to authorized personnel only
While we strive to protect your information, no method of transmission or storage is completely secure. We cannot guarantee absolute security.
## 6. Data Retention
We retain your personal information for as long as your account is active or as needed to provide you with our services. We may retain certain information for longer periods as required by law or for legitimate business purposes.
## 7. Cookies
### 7.1 Types of Cookies We Use
- **Essential Cookies:** Required for the website to function properly, including session management and security.
- **Analytics Cookies:** Help us understand how visitors interact with our website.
- **Preference Cookies:** Remember your settings and preferences for a better experience.
### 7.2 Managing Cookies
You can control cookies through your browser settings. Disabling certain cookies may affect the functionality of our website.
## 8. Your Rights
Depending on your location, you may have the following rights regarding your personal data:
- **Access:** Request a copy of the personal data we hold about you.
- **Correction:** Request correction of inaccurate or incomplete data.
- **Deletion:** Request deletion of your personal data, subject to legal obligations.
- **Portability:** Request a copy of your data in a portable format.
- **Objection:** Object to the processing of your data for certain purposes.
- **Withdrawal of Consent:** Withdraw your consent at any time where processing is based on consent.
To exercise these rights, please contact us using the information provided in Section 11.
## 9. International Data Transfers
Your information may be transferred to and processed in countries other than your country of residence. We ensure appropriate safeguards are in place to protect your data in accordance with applicable laws.
## 10. Children's Privacy
Our services are not directed to individuals under the age of 16. We do not knowingly collect personal information from children. If we become aware that we have collected data from a child, we will take steps to delete it promptly.
## 11. Contact Information
If you have questions about this Privacy Policy or wish to exercise your data rights, please contact us:
- **Email:** privacy@wiseless.xyz
- **Address:** Schon Demo Store, Demo District, Internet City
## 12. Changes to This Policy
We may update this Privacy Policy from time to time. We will notify you of any material changes by posting the updated policy on our website and updating the "Last updated" date. Your continued use of our services after such changes constitutes your acceptance of the updated policy.
---
*This document is part of the Schon Demo Store — a demonstration environment showcasing the [Schon e-commerce platform](https://schon.wiseless.xyz). All store data, including products, brands, and prices, is fictional. Interested in launching your own store? [Learn more about Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,113 @@
**Демо-магазин Schon**
> **Уведомление:** Это демонстрационный магазин, работающий на платформе электронной коммерции [Schon](https://schon.wiseless.xyz). Реальные транзакции не обрабатываются, персональные данные в рамках покупок не собираются. Настоящая политика конфиденциальности представлена в качестве примера документа, готового к использованию в рабочей среде. Если вы заинтересованы в развёртывании Schon для вашего магазина, посетите [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Профессиональная витрина с драгоценными камнями](/static/images/placeholder.png)
## 1. Введение
Добро пожаловать в Демо-магазин Schon («мы», «наш» или «нас»). Мы стремимся защищать вашу конфиденциальность и обеспечивать безопасность ваших персональных данных. Настоящая Политика конфиденциальности разъясняет, каким образом мы собираем, используем, раскрываем и защищаем вашу информацию при посещении нашего веб-сайта и использовании наших услуг.
Используя наш веб-сайт, вы соглашаетесь с условиями настоящей Политики конфиденциальности. Если вы не согласны с описанными здесь практиками, пожалуйста, воздержитесь от использования наших услуг.
## 2. Собираемая информация
### 2.1 Информация, предоставляемая вами напрямую
- **Данные учётной записи:** При создании учётной записи мы собираем ваше имя, адрес электронной почты и пароль.
- **Данные заказа:** При оформлении заказа мы собираем адрес доставки, адрес для выставления счёта и платёжные реквизиты.
- **Данные переписки:** При обращении к нам мы собираем содержание ваших сообщений, адрес электронной почты и иную предоставленную вами информацию.
### 2.2 Автоматически собираемая информация
- **Данные об использовании:** Мы собираем информацию о вашем взаимодействии с веб-сайтом, включая посещённые страницы, время нахождения на сайте и шаблоны навигации.
- **Данные об устройстве:** Мы собираем информацию об устройстве, с которого вы заходите на сайт, включая тип устройства, операционную систему, тип браузера и разрешение экрана.
- **Данные журналов:** Наши серверы автоматически фиксируют информацию, такую как ваш IP-адрес, время доступа и URL-адреса переходов.
### 2.3 Файлы cookie и аналогичные технологии
Мы используем файлы cookie и аналогичные технологии отслеживания для улучшения работы с сайтом и анализа трафика. Подробнее см. раздел 7.
## 3. Использование информации
Собранная информация используется в следующих целях:
- **Обработка заказов:** Для оформления и выполнения заказов, включая доставку и обработку платежей.
- **Управление учётной записью:** Для создания и ведения вашей учётной записи.
- **Поддержка клиентов:** Для ответа на ваши обращения и оказания помощи.
- **Персонализация:** Для персонализации покупательского опыта и рекомендации товаров.
- **Аналитика:** Для анализа использования сайта и улучшения наших услуг.
- **Маркетинг:** Для отправки рекламных сообщений при наличии вашего согласия и в соответствии с применимым законодательством.
- **Соблюдение законодательства:** Для выполнения правовых обязательств и обеспечения соблюдения условий обслуживания.
## 4. Передача и раскрытие информации
Мы не продаём ваши персональные данные. Мы можем передавать вашу информацию в следующих случаях:
- **Поставщики услуг:** Мы передаём информацию сторонним поставщикам услуг, которые содействуют нам в работе сайта, обработке платежей, доставке заказов и поддержке клиентов.
- **Требования законодательства:** Мы можем раскрывать информацию в случаях, предусмотренных законом, нормативными актами или в рамках судебного процесса.
- **Реорганизация бизнеса:** В случае слияния, поглощения или продажи активов ваша информация может быть передана в рамках сделки.
- **Согласие:** Мы можем передавать информацию при наличии вашего явного согласия.
## 5. Безопасность данных
Мы применяем стандартные отраслевые меры безопасности для защиты ваших персональных данных, включая:
- Шифрование данных при передаче с использованием протоколов TLS/SSL
- Безопасное хранение конфиденциальных данных с шифрованием
- Регулярные аудиты безопасности и оценки уязвимостей
- Контроль доступа, ограничивающий доступ к данным уполномоченным сотрудникам
Несмотря на наши усилия по защите вашей информации, ни один метод передачи или хранения данных не является абсолютно безопасным. Мы не можем гарантировать полную безопасность.
## 6. Сроки хранения данных
Мы храним ваши персональные данные в течение срока действия вашей учётной записи или столько, сколько необходимо для предоставления услуг. Отдельные данные могут храниться дольше в соответствии с требованиями законодательства или для обоснованных деловых целей.
## 7. Файлы cookie
### 7.1 Типы используемых файлов cookie
- **Необходимые:** Обязательны для корректной работы сайта, включая управление сессиями и безопасность.
- **Аналитические:** Помогают понять, как посетители взаимодействуют с сайтом.
- **Функциональные:** Сохраняют ваши настройки и предпочтения для улучшения работы.
### 7.2 Управление файлами cookie
Вы можете управлять файлами cookie через настройки браузера. Отключение некоторых файлов cookie может повлиять на функциональность сайта.
## 8. Ваши права
В зависимости от вашего местонахождения вы можете обладать следующими правами в отношении персональных данных:
- **Доступ:** Запросить копию хранящихся у нас персональных данных.
- **Исправление:** Запросить исправление неточных или неполных данных.
- **Удаление:** Запросить удаление ваших персональных данных с учётом правовых обязательств.
- **Переносимость:** Запросить копию данных в переносимом формате.
- **Возражение:** Возразить против обработки данных в определённых целях.
- **Отзыв согласия:** Отозвать согласие в любое время, если обработка основана на согласии.
Для реализации указанных прав обратитесь к нам, используя контактные данные из раздела 11.
## 9. Международная передача данных
Ваша информация может передаваться и обрабатываться в странах, отличных от страны вашего проживания. Мы обеспечиваем надлежащие гарантии защиты ваших данных в соответствии с применимым законодательством.
## 10. Конфиденциальность детей
Наши услуги не предназначены для лиц младше 16 лет. Мы сознательно не собираем персональные данные детей. Если нам станет известно о сборе данных ребёнка, мы незамедлительно примем меры по их удалению.
## 11. Контактная информация
Если у вас есть вопросы о настоящей Политике конфиденциальности или вы хотите реализовать свои права в отношении данных, свяжитесь с нами:
- **Электронная почта:** privacy@wiseless.xyz
- **Адрес:** Демо-магазин Schon, Демонстрационный район, Интернет-сити
## 12. Изменения настоящей Политики
Мы можем обновлять настоящую Политику конфиденциальности. О любых существенных изменениях мы уведомим вас путём размещения обновлённой политики на нашем сайте и обновления даты «Последнее обновление». Продолжение использования наших услуг после внесения таких изменений означает ваше согласие с обновлённой политикой.
---
*Этот документ является частью Демо-магазина Schon — демонстрационной среды, представляющей [платформу электронной коммерции Schon](https://schon.wiseless.xyz). Все данные магазина, включая товары, бренды и цены, являются вымышленными. Хотите запустить собственный магазин? [Узнайте больше о Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,112 @@
**Schon Demo Store**
> **Demo Notice:** This is a demonstration store powered by the [Schon](https://schon.wiseless.xyz) e-commerce platform. No real products are sold or shipped. This Return Policy is provided as an example of a production-ready document. To deploy Schon for your own store, visit [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Secure packaging for returns](/static/images/placeholder.png)
## 1. Overview
At Schon Demo Store, we want you to be completely satisfied with your purchase. If you are not satisfied for any reason, we offer a straightforward return process as outlined below.
## 2. Return Eligibility
### 2.1 General Conditions
Items may be returned within **30 days** of delivery, provided they meet the following conditions:
- The item is in its original, unworn, and unaltered condition
- The item is accompanied by all original packaging, certificates, and documentation
- The item shows no signs of damage, wear, or modification
- The item's security seal or tag (if applicable) is intact
### 2.2 Non-Returnable Items
The following items cannot be returned:
- Custom or specially ordered items
- Items that have been resized, engraved, or otherwise modified
- Items purchased during final clearance sales (marked as "Final Sale")
- Gift cards and store credits
### 2.3 Gemstone-Specific Conditions
Due to the nature of gemstones and precious jewelry:
- All returned gemstones will be inspected and verified against their original certification
- Items must be returned in the same condition as received, including any GIA, GRS, or other certificates
- Loose stones must be returned in their original gem container or packaging
## 3. Return Process
### 3.1 Initiating a Return
To initiate a return:
1. Log in to your account and navigate to your order history
2. Select the order containing the item(s) you wish to return
3. Click "Request Return" and follow the instructions
4. You will receive a Return Merchandise Authorization (RMA) number via email
### 3.2 Shipping the Return
- Use the prepaid shipping label provided with your RMA confirmation
- Pack the item securely in its original packaging
- Include all certificates, documentation, and accessories
- Ship the package within **7 days** of receiving your RMA number
### 3.3 Return Shipping
- **Domestic returns:** Free return shipping via insured courier
- **International returns:** Return shipping costs are the responsibility of the customer; we recommend using an insured and trackable shipping method
## 4. Refunds
### 4.1 Processing Time
Refunds are processed within **5-10 business days** after we receive and inspect the returned item.
### 4.2 Refund Method
Refunds are issued to the original payment method:
- **Credit/Debit Card:** 5-10 business days to appear on your statement
- **Store Credit:** Applied immediately upon approval
- **Bank Transfer:** 5-7 business days after processing
### 4.3 Partial Refunds
We reserve the right to issue a partial refund if the returned item shows signs of use, damage, or is missing original packaging or documentation.
## 5. Exchanges
### 5.1 Exchange Process
If you would like to exchange an item for a different product:
1. Follow the return process outlined in Section 3
2. Place a new order for the desired item
3. Your refund will be processed as described in Section 4
### 5.2 Price Differences
If the exchange item is a different price, the difference will be charged to or refunded to your original payment method.
## 6. Damaged or Defective Items
If you receive a damaged or defective item:
- Contact us within **48 hours** of delivery
- Provide photos of the damage or defect
- We will arrange for a free return and full refund or replacement
- Original packaging should be retained for inspection
## 7. Contact Us
For questions about returns or to request assistance:
- **Email:** returns@wiseless.xyz
- **Response time:** Within 24 hours on business days
---
*This document is part of the Schon Demo Store — a demonstration environment showcasing the [Schon e-commerce platform](https://schon.wiseless.xyz). All store data, including products, brands, and prices, is fictional. Interested in launching your own store? [Learn more about Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,112 @@
**Демо-магазин Schon**
> **Уведомление:** Это демонстрационный магазин, работающий на платформе электронной коммерции [Schon](https://schon.wiseless.xyz). Реальные товары не продаются и не отправляются. Настоящая Политика возврата представлена в качестве примера документа, готового к использованию в рабочей среде. Для развёртывания Schon для вашего магазина посетите [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Надёжная упаковка для возвратов](/static/images/placeholder.png)
## 1. Обзор
В Демо-магазине Schon мы хотим, чтобы вы были полностью удовлетворены покупкой. Если по какой-либо причине вы не удовлетворены, мы предлагаем простой процесс возврата, описанный ниже.
## 2. Условия возврата
### 2.1 Общие условия
Товары могут быть возвращены в течение **30 дней** с момента доставки при соблюдении следующих условий:
- Товар находится в оригинальном, неношеном и неизменённом состоянии
- Товар сопровождается всей оригинальной упаковкой, сертификатами и документацией
- На товаре отсутствуют следы повреждений, износа или модификации
- Защитная пломба или бирка товара (при наличии) не нарушена
### 2.2 Товары, не подлежащие возврату
Возврату не подлежат следующие товары:
- Товары, изготовленные или заказанные по индивидуальному заказу
- Товары, которые были изменены в размере, гравированы или иным образом модифицированы
- Товары, приобретённые в рамках финальных распродаж (с пометкой «Финальная продажа»)
- Подарочные карты и кредиты магазина
### 2.3 Особые условия для драгоценных камней
В связи со спецификой драгоценных камней и ювелирных изделий:
- Все возвращённые драгоценные камни проходят проверку и сверку с оригинальной сертификацией
- Товары должны быть возвращены в том же состоянии, в котором были получены, включая сертификаты GIA, GRS и прочие
- Камни без оправы должны быть возвращены в оригинальной упаковке или контейнере для камней
## 3. Процесс возврата
### 3.1 Оформление возврата
Для оформления возврата:
1. Войдите в свою учётную запись и перейдите к истории заказов
2. Выберите заказ, содержащий товар(ы), который(ые) вы хотите вернуть
3. Нажмите «Запросить возврат» и следуйте инструкциям
4. Вы получите номер авторизации возврата товара (RMA) по электронной почте
### 3.2 Отправка возврата
- Используйте предоплаченную транспортную этикетку, предоставленную с подтверждением RMA
- Надёжно упакуйте товар в оригинальную упаковку
- Приложите все сертификаты, документацию и аксессуары
- Отправьте посылку в течение **7 дней** после получения номера RMA
### 3.3 Доставка возврата
- **Внутренние возвраты:** Бесплатная обратная доставка застрахованной курьерской службой
- **Международные возвраты:** Стоимость обратной доставки оплачивается покупателем; рекомендуем использовать застрахованный и отслеживаемый способ доставки
## 4. Возврат средств
### 4.1 Сроки обработки
Возврат средств обрабатывается в течение **5-10 рабочих дней** после получения и проверки возвращённого товара.
### 4.2 Способ возврата средств
Средства возвращаются на исходный способ оплаты:
- **Кредитная/дебетовая карта:** 5-10 рабочих дней для отражения в выписке
- **Кредит магазина:** Начисляется немедленно после одобрения
- **Банковский перевод:** 5-7 рабочих дней после обработки
### 4.3 Частичный возврат средств
Мы оставляем за собой право произвести частичный возврат средств, если возвращённый товар имеет следы использования, повреждения или отсутствует оригинальная упаковка или документация.
## 5. Обмен
### 5.1 Процесс обмена
Если вы хотите обменять товар на другой:
1. Выполните процедуру возврата, описанную в разделе 3
2. Оформите новый заказ на желаемый товар
3. Возврат средств будет обработан в соответствии с разделом 4
### 5.2 Разница в цене
Если обменный товар имеет другую стоимость, разница будет списана с вашего исходного способа оплаты или возвращена на него.
## 6. Повреждённые или дефектные товары
Если вы получили повреждённый или дефектный товар:
- Свяжитесь с нами в течение **48 часов** с момента доставки
- Предоставьте фотографии повреждения или дефекта
- Мы организуем бесплатный возврат и полное возмещение стоимости или замену
- Оригинальную упаковку следует сохранить для проверки
## 7. Связь с нами
По вопросам возврата или для получения помощи:
- **Электронная почта:** returns@wiseless.xyz
- **Время ответа:** В течение 24 часов в рабочие дни
---
*Этот документ является частью Демо-магазина Schon — демонстрационной среды, представляющей [платформу электронной коммерции Schon](https://schon.wiseless.xyz). Все данные магазина, включая товары, бренды и цены, являются вымышленными. Хотите запустить собственный магазин? [Узнайте больше о Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,103 @@
**Schon Demo Store**
> **Demo Notice:** This is a demonstration store powered by the [Schon](https://schon.wiseless.xyz) e-commerce platform. No real products are shipped. This Shipping Information page is provided as an example of a production-ready document. To deploy Schon for your own store, visit [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Premium insured shipping packaging](/static/images/placeholder.png)
## 1. Shipping Methods
We offer the following shipping options for all orders:
| Method | Estimated Delivery | Cost |
|---|---|---|
| Standard Shipping | 5-7 business days | Free on orders over $500 |
| Express Shipping | 2-3 business days | $25 |
| Priority Overnight | Next business day | $50 |
| International Standard | 7-14 business days | Calculated at checkout |
| International Express | 3-5 business days | Calculated at checkout |
## 2. Processing Time
- Orders are processed within **1-2 business days** after payment confirmation.
- Orders placed before 2:00 PM (EST) on business days are typically processed the same day.
- Custom or specially ordered items may require additional processing time of **5-10 business days**.
## 3. Shipping Costs
### 3.1 Domestic Shipping
- **Free standard shipping** on all orders over $500
- Orders under $500 incur a flat rate of $15 for standard shipping
- Express and overnight options are available at the rates listed above
### 3.2 International Shipping
- International shipping rates are calculated at checkout based on destination, weight, and selected service
- All international orders are shipped with full insurance coverage
- Customs duties and import taxes are the responsibility of the recipient and are not included in the shipping cost
## 4. Insurance and Security
All shipments are:
- **Fully insured** for the total value of the order
- **Shipped via secure courier** with signature confirmation required
- **Tracked** with real-time tracking updates sent to your email
- **Discreetly packaged** with no external indication of contents or value
For orders exceeding $10,000, additional security measures may apply, including:
- Armored courier delivery
- Required government-issued ID verification upon delivery
- Scheduled delivery appointment
## 5. Order Tracking
Once your order ships, you will receive:
1. A shipping confirmation email with your tracking number
2. Real-time tracking updates via email and SMS (if opted in)
3. Delivery notification upon successful delivery
You can also track your order at any time by logging into your account and visiting your order history.
## 6. Delivery Details
### 6.1 Signature Required
All deliveries require a signature from an adult (18+) at the delivery address. Packages will not be left unattended.
### 6.2 Delivery Attempts
- The courier will make up to **3 delivery attempts**
- After 3 failed attempts, the package will be held at the nearest courier facility for **7 days**
- If unclaimed, the package will be returned to us and a full refund will be issued minus return shipping costs
### 6.3 Address Accuracy
Please ensure your shipping address is complete and accurate. We are not responsible for delays or losses resulting from incorrect address information.
## 7. Shipping Restrictions
We currently ship to most countries worldwide. However, we cannot ship to:
- P.O. Boxes (for insured items)
- Military/diplomatic addresses (APO/FPO)
- Countries under trade sanctions
Please contact us if you are unsure whether we ship to your location.
## 8. Holiday and Peak Seasons
During holiday seasons and peak shopping periods, please allow for additional processing and delivery time. We recommend placing orders early to ensure timely delivery.
## 9. Contact Us
For shipping inquiries:
- **Email:** shipping@wiseless.xyz
- **Response time:** Within 24 hours on business days
---
*This document is part of the Schon Demo Store — a demonstration environment showcasing the [Schon e-commerce platform](https://schon.wiseless.xyz). All store data, including products, brands, and prices, is fictional. Interested in launching your own store? [Learn more about Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,103 @@
**Демо-магазин Schon**
> **Уведомление:** Это демонстрационный магазин, работающий на платформе электронной коммерции [Schon](https://schon.wiseless.xyz). Реальные товары не отправляются. Настоящая страница с информацией о доставке представлена в качестве примера документа, готового к использованию в рабочей среде. Для развёртывания Schon для вашего магазина посетите [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Премиальная застрахованная упаковка для доставки](/static/images/placeholder.png)
## 1. Способы доставки
Мы предлагаем следующие варианты доставки для всех заказов:
| Способ | Ориентировочные сроки | Стоимость |
|---|---|---|
| Стандартная доставка | 5-7 рабочих дней | Бесплатно при заказе от $500 |
| Экспресс-доставка | 2-3 рабочих дня | $25 |
| Приоритетная доставка | Следующий рабочий день | $50 |
| Международная стандартная | 7-14 рабочих дней | Рассчитывается при оформлении |
| Международная экспресс | 3-5 рабочих дней | Рассчитывается при оформлении |
## 2. Сроки обработки
- Заказы обрабатываются в течение **1-2 рабочих дней** после подтверждения оплаты.
- Заказы, оформленные до 14:00 (EST) в рабочие дни, как правило, обрабатываются в тот же день.
- Изготовление товаров по индивидуальному заказу может потребовать дополнительного времени обработки — **5-10 рабочих дней**.
## 3. Стоимость доставки
### 3.1 Доставка по стране
- **Бесплатная стандартная доставка** для всех заказов на сумму свыше $500
- Для заказов менее $500 стоимость стандартной доставки составляет фиксированные $15
- Экспресс и приоритетная доставка доступны по тарифам, указанным выше
### 3.2 Международная доставка
- Стоимость международной доставки рассчитывается при оформлении заказа на основании пункта назначения, веса и выбранной услуги
- Все международные заказы отправляются с полным страховым покрытием
- Таможенные пошлины и импортные налоги оплачиваются получателем и не включены в стоимость доставки
## 4. Страхование и безопасность
Все отправления:
- **Полностью застрахованы** на полную стоимость заказа
- **Отправляются защищённой курьерской службой** с обязательным подтверждением подписью
- **Отслеживаются** с отправкой обновлений в реальном времени на вашу электронную почту
- **Упакованы неприметно** без внешних указаний на содержимое или стоимость
Для заказов на сумму свыше $10 000 могут применяться дополнительные меры безопасности, включая:
- Доставка бронированной курьерской службой
- Обязательная верификация документа, удостоверяющего личность, при доставке
- Доставка по предварительной записи
## 5. Отслеживание заказа
После отправки заказа вы получите:
1. Электронное письмо с подтверждением отправки и номером отслеживания
2. Обновления отслеживания в реальном времени по электронной почте и SMS (при подписке)
3. Уведомление о доставке при успешном вручении
Вы также можете отследить заказ в любое время, войдя в свою учётную запись и перейдя в историю заказов.
## 6. Условия доставки
### 6.1 Обязательная подпись
Все доставки требуют подписи совершеннолетнего лица (18+) по адресу доставки. Посылки не оставляются без присмотра.
### 6.2 Попытки доставки
- Курьер предпримет до **3 попыток доставки**
- После 3 неудачных попыток посылка будет храниться в ближайшем отделении курьерской службы в течение **7 дней**
- Если посылка не будет востребована, она будет возвращена нам, и будет произведён полный возврат средств за вычетом стоимости обратной доставки
### 6.3 Точность адреса
Пожалуйста, убедитесь, что ваш адрес доставки указан полностью и точно. Мы не несём ответственности за задержки или утрату посылок вследствие неверно указанного адреса.
## 7. Ограничения доставки
В настоящее время мы осуществляем доставку в большинство стран мира. Однако доставка невозможна:
- На абонентские ящики (для застрахованных отправлений)
- На военные и дипломатические адреса (APO/FPO)
- В страны, находящиеся под торговыми санкциями
Пожалуйста, свяжитесь с нами, если вы не уверены, осуществляем ли мы доставку в ваш регион.
## 8. Праздничные и пиковые периоды
В праздничные сезоны и периоды повышенного спроса просим учитывать дополнительное время на обработку и доставку. Рекомендуем оформлять заказы заблаговременно для своевременного получения.
## 9. Связь с нами
По вопросам доставки:
- **Электронная почта:** shipping@wiseless.xyz
- **Время ответа:** В течение 24 часов в рабочие дни
---
*Этот документ является частью Демо-магазина Schon — демонстрационной среды, представляющей [платформу электронной коммерции Schon](https://schon.wiseless.xyz). Все данные магазина, включая товары, бренды и цены, являются вымышленными. Хотите запустить собственный магазин? [Узнайте больше о Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,35 @@
We are excited to announce the arrival of our **Spring 2026 Collection** — a curated selection of exceptional gemstones sourced from the world's most renowned origins.
![New emerald arrivals](/static/images/placeholder.png)
## What's New
This season's collection brings a fresh wave of color and brilliance to our catalog:
### Vivid Emeralds from New Sources
We've expanded our emerald selection with stunning pieces from **Afghanistan's Panjshir Valley** and **Ethiopia's emerging deposits**. These new arrivals complement our existing Colombian and Zambian offerings, giving collectors and designers more options across different price points and color profiles.
![Asscher cut diamond close-up](/static/images/placeholder.png)
### Expanded Diamond Cuts
Our diamond selection now includes the elegant **Marquise** and sophisticated **Asscher** cuts alongside our existing round brilliant, princess, oval, cushion, and emerald cuts. These additions reflect growing demand for distinctive silhouettes in engagement rings and fine jewelry.
### Crystal Kingdom Quartz Expansion
Our partners at Crystal Kingdom have delivered an outstanding selection of quartz varieties, including the coveted **Madeira Citrine**, the delicate **Rose de France Amethyst**, and the rare **Prasiolite** (green amethyst). These pieces showcase the incredible diversity within the quartz family.
### Rare Ruby Additions
Crimson Vault has sourced exceptional new rubies, including a pristine **unheated Burmese ruby** and vivid **Thai rubies** — expanding our ruby collection with both investment-grade and accessible pieces.
## Browse the Collection
All new arrivals are available now in our catalog. Use our advanced filters to browse by gemstone type, origin, price range, or brand. Every piece comes with detailed specifications and high-resolution imagery.
Our multi-language catalog ensures you can explore every product in your preferred language — currently available in English and Russian, with 28 languages supported by the platform.
---
*The Schon Demo Store is powered by [Schon](https://schon.wiseless.xyz) — a modern e-commerce platform built for businesses that demand flexibility and performance. All products shown are fictional and for demonstration purposes only.*

View file

@ -0,0 +1,35 @@
Мы рады сообщить о поступлении нашей **Весенней коллекции 2026** — тщательно отобранной подборки исключительных драгоценных камней из самых известных месторождений мира.
![Новые поступления изумрудов](/static/images/placeholder.png)
## Что нового
Коллекция этого сезона привносит новую волну цвета и блеска в наш каталог:
### Яркие изумруды из новых источников
Мы расширили ассортимент изумрудов потрясающими экземплярами из **Панджшерской долины Афганистана** и **новых месторождений Эфиопии**. Эти новинки дополняют наши существующие колумбийские и замбийские предложения, предоставляя коллекционерам и дизайнерам больше вариантов в различных ценовых категориях и цветовых профилях.
![Бриллиант огранки «Ашер» крупным планом](/static/images/placeholder.png)
### Расширенный выбор огранок бриллиантов
Наш ассортимент бриллиантов теперь включает элегантную огранку **«Маркиз»** и утончённую **«Ашер»** наряду с существующими круглой, «Принцессой», овальной, «Кушон» и изумрудной огранками. Эти дополнения отражают растущий спрос на выразительные силуэты в обручальных кольцах и ювелирных изделиях.
### Расширение кварцевой коллекции Crystal Kingdom
Наши партнёры из Crystal Kingdom доставили выдающуюся подборку разновидностей кварца, включая желанный **цитрин «Мадейра»**, нежный **аметист «Роз де Франс»** и редкий **празиолит** (зелёный аметист). Эти экземпляры демонстрируют невероятное разнообразие семейства кварцев.
### Редкие рубины
Crimson Vault получил исключительные новые рубины, включая безупречный **необработанный бирманский рубин** и яркие **тайские рубины** — расширяя нашу коллекцию рубинов как инвестиционными, так и доступными экземплярами.
## Смотрите коллекцию
Все новинки уже доступны в нашем каталоге. Используйте расширенные фильтры для просмотра по типу камня, происхождению, ценовому диапазону или бренду. Каждый экземпляр сопровождается подробными характеристиками и изображениями высокого разрешения.
Наш многоязычный каталог позволяет исследовать каждый товар на предпочтительном языке — в настоящее время доступны английский и русский, платформа поддерживает 28 языков.
---
*Демо-магазин Schon работает на платформе [Schon](https://schon.wiseless.xyz) — современной платформе электронной коммерции для бизнеса, требующего гибкости и производительности. Все представленные товары являются вымышленными и предназначены исключительно для демонстрации.*

View file

@ -0,0 +1,135 @@
**Schon Demo Store**
> **Demo Notice:** This is a demonstration store powered by the [Schon](https://schon.wiseless.xyz) e-commerce platform. No real transactions are processed, no real products are sold, and no real money is charged. These Terms & Conditions are provided as an example of a production-ready legal document. To deploy Schon for your own store, visit [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Professional gemstone display case](/static/images/placeholder.png)
## 1. Acceptance of Terms
By accessing and using the Schon Demo Store website ("Website"), you accept and agree to be bound by these Terms & Conditions ("Terms"). If you do not agree to these Terms, you must not use the Website.
## 2. Definitions
- **"Store"** refers to the Schon Demo Store and its associated services.
- **"User," "you," or "your"** refers to any individual or entity accessing the Website.
- **"Products"** refers to all items listed for sale on the Website.
- **"We," "us," or "our"** refers to the Schon Demo Store operator.
## 3. Demo Environment Disclaimer
This Website is a **demonstration environment** for the Schon e-commerce platform. Please note:
- All products, brands, descriptions, and prices displayed are **entirely fictional**.
- No real orders will be shipped, and no real payment will be processed.
- User accounts created on this demo may be periodically reset or removed.
- This demo showcases the capabilities of the Schon platform for evaluation purposes.
## 4. User Accounts
### 4.1 Registration
To access certain features, you may need to create an account. You agree to provide accurate and complete information during registration and to keep your account credentials secure.
### 4.2 Account Responsibility
You are responsible for all activities that occur under your account. You must notify us immediately of any unauthorized use of your account.
### 4.3 Account Termination
We reserve the right to suspend or terminate accounts at our discretion, particularly in the demo environment where periodic resets may occur.
## 5. Products and Pricing
### 5.1 Product Information
We strive to display accurate product information, including descriptions, images, and specifications. However, as this is a demo environment, all product data is fictional and for illustrative purposes only.
### 5.2 Pricing
All prices displayed are in the store's configured currency and are fictional. Prices may be changed without notice as part of demo updates.
### 5.3 Availability
Product availability shown in the demo is simulated and does not reflect real inventory.
## 6. Orders and Payments
### 6.1 Order Process
The order process on this demo site simulates a real e-commerce transaction flow. No actual goods are shipped and no actual payments are collected.
### 6.2 Order Confirmation
Order confirmations generated by the demo are simulated and do not constitute a binding contract for the sale of goods.
### 6.3 Payment Processing
No real payment processing occurs on the demo site. Any payment forms displayed are for demonstration purposes only.
## 7. Intellectual Property
### 7.1 Store Content
All content on this Website, including text, graphics, logos, and software, is the property of Schon or its licensors and is protected by applicable intellectual property laws.
### 7.2 Limited License
You are granted a limited, non-exclusive, non-transferable license to access and use the Website for personal, non-commercial evaluation purposes.
### 7.3 Restrictions
You may not:
- Reproduce, distribute, or modify the Website content without our written consent
- Use the Website for any illegal or unauthorized purpose
- Attempt to gain unauthorized access to the Website's systems or networks
- Use automated tools to scrape or extract data from the Website
## 8. Limitation of Liability
### 8.1 Disclaimer of Warranties
The Website and its content are provided "as is" and "as available" without warranties of any kind, express or implied. We disclaim all warranties, including but not limited to merchantability, fitness for a particular purpose, and non-infringement.
### 8.2 Limitation
To the fullest extent permitted by law, we shall not be liable for any indirect, incidental, special, consequential, or punitive damages arising from your use of the Website or inability to use the Website.
### 8.3 Maximum Liability
Our total liability for any claims arising from your use of the Website shall not exceed the amount you have paid to us in the twelve (12) months preceding the claim. In the case of this demo environment, that amount is zero.
## 9. Indemnification
You agree to indemnify, defend, and hold harmless the Store, its operators, and affiliates from any claims, damages, losses, and expenses arising from your use of the Website or violation of these Terms.
## 10. Third-Party Links
The Website may contain links to third-party websites. We are not responsible for the content or practices of these external sites and recommend reviewing their respective terms and privacy policies.
## 11. Governing Law
These Terms shall be governed by and construed in accordance with the laws of the jurisdiction in which the Store operator is established, without regard to conflict of law principles.
## 12. Dispute Resolution
Any disputes arising from these Terms or use of the Website shall be resolved through good-faith negotiation. If negotiation fails, disputes shall be submitted to binding arbitration in accordance with the applicable rules of the jurisdiction.
## 13. Modifications
We reserve the right to modify these Terms at any time. Changes will be effective upon posting to the Website. Your continued use of the Website after modifications constitutes acceptance of the updated Terms.
## 14. Severability
If any provision of these Terms is found to be unenforceable, the remaining provisions shall continue in full force and effect.
## 15. Contact Information
For questions about these Terms, please contact us:
- **Email:** legal@wiseless.xyz
- **Address:** Schon Demo Store, Demo District, Internet City
---
*This document is part of the Schon Demo Store — a demonstration environment showcasing the [Schon e-commerce platform](https://schon.wiseless.xyz). All store data, including products, brands, and prices, is fictional. Interested in launching your own store? [Learn more about Schon](https://schon.wiseless.xyz).*

View file

@ -0,0 +1,135 @@
**Демо-магазин Schon**
> **Уведомление:** Это демонстрационный магазин, работающий на платформе электронной коммерции [Schon](https://schon.wiseless.xyz). Реальные транзакции не обрабатываются, реальные товары не продаются, реальные платежи не взимаются. Настоящие Условия использования представлены в качестве примера юридического документа, готового к использованию в рабочей среде. Для развёртывания Schon для вашего магазина посетите [schon.wiseless.xyz](https://schon.wiseless.xyz).
![Профессиональная витрина с драгоценными камнями](/static/images/placeholder.png)
## 1. Принятие условий
Получая доступ к веб-сайту Демо-магазина Schon («Веб-сайт») и используя его, вы принимаете и соглашаетесь соблюдать настоящие Условия использования («Условия»). Если вы не согласны с настоящими Условиями, вы не должны использовать Веб-сайт.
## 2. Определения
- **«Магазин»** означает Демо-магазин Schon и связанные с ним услуги.
- **«Пользователь», «вы» или «ваш»** означает любое физическое или юридическое лицо, получающее доступ к Веб-сайту.
- **«Товары»** означает все позиции, представленные к продаже на Веб-сайте.
- **«Мы», «нас» или «наш»** означает оператора Демо-магазина Schon.
## 3. Оговорка о демонстрационной среде
Данный Веб-сайт является **демонстрационной средой** платформы электронной коммерции Schon. Обратите внимание:
- Все товары, бренды, описания и цены, отображаемые на сайте, являются **полностью вымышленными**.
- Реальные заказы не отправляются, реальные платежи не обрабатываются.
- Учётные записи, созданные в демонстрационной среде, могут периодически сбрасываться или удаляться.
- Данная демонстрация представляет возможности платформы Schon для ознакомительных целей.
## 4. Учётные записи пользователей
### 4.1 Регистрация
Для доступа к определённым функциям может потребоваться создание учётной записи. Вы обязуетесь предоставлять точную и полную информацию при регистрации и обеспечивать безопасность учётных данных.
### 4.2 Ответственность за учётную запись
Вы несёте ответственность за все действия, совершённые с использованием вашей учётной записи. Вы обязаны незамедлительно уведомить нас о любом несанкционированном использовании вашей учётной записи.
### 4.3 Прекращение действия учётной записи
Мы оставляем за собой право приостановить или прекратить действие учётных записей по своему усмотрению, в особенности в демонстрационной среде, где могут проводиться периодические сбросы.
## 5. Товары и цены
### 5.1 Информация о товарах
Мы стремимся отображать точную информацию о товарах, включая описания, изображения и характеристики. Однако, поскольку это демонстрационная среда, все данные о товарах являются вымышленными и служат исключительно для иллюстрации.
### 5.2 Цены
Все отображаемые цены указаны в настроенной валюте магазина и являются вымышленными. Цены могут изменяться без уведомления в рамках обновлений демонстрации.
### 5.3 Наличие
Информация о наличии товаров в демонстрационной среде является симулированной и не отражает реальных запасов.
## 6. Заказы и платежи
### 6.1 Процесс оформления заказа
Процесс оформления заказа на данном демонстрационном сайте имитирует реальный процесс электронной коммерции. Реальные товары не отправляются, реальные платежи не взимаются.
### 6.2 Подтверждение заказа
Подтверждения заказов, формируемые демонстрационной средой, являются симулированными и не представляют собой обязывающий договор купли-продажи товаров.
### 6.3 Обработка платежей
На демонстрационном сайте не осуществляется реальная обработка платежей. Все отображаемые платёжные формы предназначены исключительно для демонстрации.
## 7. Интеллектуальная собственность
### 7.1 Контент магазина
Весь контент на данном Веб-сайте, включая тексты, графику, логотипы и программное обеспечение, является собственностью Schon или его лицензиаров и защищён применимым законодательством об интеллектуальной собственности.
### 7.2 Ограниченная лицензия
Вам предоставляется ограниченная, неисключительная, непередаваемая лицензия на доступ и использование Веб-сайта в личных, некоммерческих ознакомительных целях.
### 7.3 Ограничения
Запрещается:
- Воспроизводить, распространять или модифицировать контент Веб-сайта без нашего письменного согласия
- Использовать Веб-сайт в незаконных или несанкционированных целях
- Предпринимать попытки несанкционированного доступа к системам или сетям Веб-сайта
- Использовать автоматизированные инструменты для извлечения данных с Веб-сайта
## 8. Ограничение ответственности
### 8.1 Отказ от гарантий
Веб-сайт и его контент предоставляются «как есть» и «по мере доступности» без каких-либо гарантий, явных или подразумеваемых. Мы отказываемся от всех гарантий, включая, помимо прочего, гарантии товарной пригодности, пригодности для определённой цели и ненарушения прав.
### 8.2 Ограничение
В максимальной степени, допускаемой законом, мы не несём ответственности за косвенные, случайные, специальные, последующие или штрафные убытки, возникающие в связи с использованием Веб-сайта или невозможностью его использования.
### 8.3 Максимальная ответственность
Наша совокупная ответственность по любым требованиям, возникающим из использования вами Веб-сайта, не превышает суммы, уплаченной вами нам в течение двенадцати (12) месяцев, предшествующих предъявлению требования. В случае данной демонстрационной среды эта сумма равна нулю.
## 9. Возмещение убытков
Вы обязуетесь возместить убытки, защитить и оградить Магазин, его операторов и аффилированных лиц от любых претензий, убытков, потерь и расходов, возникающих в связи с использованием вами Веб-сайта или нарушением настоящих Условий.
## 10. Ссылки на сторонние ресурсы
Веб-сайт может содержать ссылки на сторонние веб-сайты. Мы не несём ответственности за контент или практики данных внешних сайтов и рекомендуем ознакомиться с их условиями и политиками конфиденциальности.
## 11. Применимое право
Настоящие Условия регулируются и толкуются в соответствии с законодательством юрисдикции, в которой зарегистрирован оператор Магазина, без учёта коллизионных норм.
## 12. Разрешение споров
Любые споры, возникающие из настоящих Условий или использования Веб-сайта, разрешаются путём добросовестных переговоров. В случае неудачи переговоров споры передаются на обязательное арбитражное разбирательство в соответствии с применимыми правилами юрисдикции.
## 13. Изменения
Мы оставляем за собой право изменять настоящие Условия в любое время. Изменения вступают в силу с момента их публикации на Веб-сайте. Продолжение использования Веб-сайта после внесения изменений означает принятие обновлённых Условий.
## 14. Делимость
Если какое-либо положение настоящих Условий будет признано недействительным, остальные положения сохраняют полную юридическую силу.
## 15. Контактная информация
По вопросам, связанным с настоящими Условиями, обращайтесь:
- **Электронная почта:** legal@wiseless.xyz
- **Адрес:** Демо-магазин Schon, Демонстрационный район, Интернет-сити
---
*Этот документ является частью Демо-магазина Schon — демонстрационной среды, представляющей [платформу электронной коммерции Schon](https://schon.wiseless.xyz). Все данные магазина, включая товары, бренды и цены, являются вымышленными. Хотите запустить собственный магазин? [Узнайте больше о Schon](https://schon.wiseless.xyz).*

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -3,8 +3,7 @@ from contextlib import suppress
from typing import Any from typing import Any
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.db.models import QuerySet
from django.db.models import Max, Min, QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphene import ( from graphene import (
UUID, UUID,
@ -230,6 +229,7 @@ class CategoryType(DjangoObjectType):
"minimum and maximum prices for products in this category, if available." "minimum and maximum prices for products in this category, if available."
), ),
) )
brands = List(lambda: BrandType, description=_("brands in this category"))
tags = DjangoFilterConnectionField( tags = DjangoFilterConnectionField(
lambda: CategoryTagType, description=_("tags for this category") lambda: CategoryTagType, description=_("tags for this category")
) )
@ -249,6 +249,7 @@ class CategoryType(DjangoObjectType):
"slug", "slug",
"description", "description",
"image", "image",
"brands",
"min_max_prices", "min_max_prices",
) )
filter_fields = ["uuid"] filter_fields = ["uuid"]
@ -277,23 +278,15 @@ class CategoryType(DjangoObjectType):
return self.filterable_attributes return self.filterable_attributes
def resolve_min_max_prices(self: Category, _info): def resolve_min_max_prices(self: Category, _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 { return {
"min_price": min_max_prices["min_price"], "min_price": self.min_price,
"max_price": min_max_prices["max_price"], "max_price": self.max_price,
} }
def resolve_brands(self: Category, info) -> QuerySet[Brand]:
return self.brands
def resolve_seo_meta(self: Category, info): def resolve_seo_meta(self: Category, info):
lang = graphene_current_lang() lang = graphene_current_lang()
base = f"https://{settings.BASE_DOMAIN}" base = f"https://{settings.BASE_DOMAIN}"

View file

@ -14,7 +14,7 @@ class Command(BaseCommand):
# Group stocks by (product, vendor) # Group stocks by (product, vendor)
stocks_by_group = defaultdict(list) stocks_by_group = defaultdict(list)
for stock in Stock.objects.all().order_by("modified"): for stock in Stock.objects.all().order_by("modified"):
stocks_by_group[stock.product_pk].append(stock) stocks_by_group[stock.product_pk].append(stock) # ty: ignore[possibly-missing-attribute]
stock_deletions: list[str] = [] stock_deletions: list[str] = []
for group in stocks_by_group.values(): for group in stocks_by_group.values():

View file

@ -10,11 +10,14 @@ from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from django.utils.translation import override
from engine.blog.models import Post, PostTag
from engine.core.models import ( from engine.core.models import (
Address, Address,
Attribute, Attribute,
AttributeGroup, AttributeGroup,
AttributeValue,
Brand, Brand,
Category, Category,
CategoryTag, CategoryTag,
@ -30,9 +33,9 @@ from engine.core.models import (
from engine.payments.models import Balance from engine.payments.models import Balance
from engine.vibes_auth.models import Group, User from engine.vibes_auth.models import Group, User
DEMO_EMAIL_DOMAIN = "demo.schon.store" DEMO_EMAIL_DOMAIN = "wiseless.xyz"
DEMO_VENDOR_NAME = "GemSource Global"
DEMO_IMAGES_DIR = Path(settings.BASE_DIR) / "engine/core/fixtures/demo_products_images" DEMO_IMAGES_DIR = Path(settings.BASE_DIR) / "engine/core/fixtures/demo_products_images"
DEMO_BLOG_DIR = Path(settings.BASE_DIR) / "engine/core/fixtures/demo_blog_posts"
class Command(BaseCommand): class Command(BaseCommand):
@ -84,6 +87,7 @@ class Command(BaseCommand):
self._load_demo_data() self._load_demo_data()
with override("en"):
if action == "install": if action == "install":
self._install(options) self._install(options)
elif action == "remove": elif action == "remove":
@ -95,13 +99,15 @@ class Command(BaseCommand):
def staff_user(self): def staff_user(self):
user, _ = User.objects.get_or_create( user, _ = User.objects.get_or_create(
email=f"staff@{DEMO_EMAIL_DOMAIN}", email=f"staff@{DEMO_EMAIL_DOMAIN}",
password="Staff!Demo888",
first_name="Alice", first_name="Alice",
last_name="Schon", last_name="Schon",
is_staff=True, is_staff=True,
is_active=True, is_active=True,
is_verified=True, is_verified=True,
) )
if _:
user.set_password("Staff!Demo888")
user.save()
if not user.groups.filter(name="E-Commerce Admin").exists(): if not user.groups.filter(name="E-Commerce Admin").exists():
user.groups.add(Group.objects.get(name="E-Commerce Admin")) user.groups.add(Group.objects.get(name="E-Commerce Admin"))
return user return user
@ -110,7 +116,6 @@ class Command(BaseCommand):
def super_user(self): def super_user(self):
user, _ = User.objects.get_or_create( user, _ = User.objects.get_or_create(
email=f"super@{DEMO_EMAIL_DOMAIN}", email=f"super@{DEMO_EMAIL_DOMAIN}",
password="Super!Demo888",
first_name="Bob", first_name="Bob",
last_name="Schon", last_name="Schon",
is_superuser=True, is_superuser=True,
@ -118,6 +123,9 @@ class Command(BaseCommand):
is_active=True, is_active=True,
is_verified=True, is_verified=True,
) )
if _:
user.set_password("Super!Demo888")
user.save()
return user return user
def _load_demo_data(self) -> None: def _load_demo_data(self) -> None:
@ -161,6 +169,10 @@ class Command(BaseCommand):
wishlist_count = self._create_demo_wishlists(users, products) wishlist_count = self._create_demo_wishlists(users, products)
self.stdout.write(self.style.SUCCESS(f"Created {wishlist_count} wishlists")) self.stdout.write(self.style.SUCCESS(f"Created {wishlist_count} wishlists"))
self.stdout.write("Creating blog posts...")
blog_count = self._create_blog_posts()
self.stdout.write(self.style.SUCCESS(f"Created {blog_count} blog posts"))
self.stdout.write( self.stdout.write(
self.style.SUCCESS(f"Created staff {self.staff_user.email} user") self.style.SUCCESS(f"Created staff {self.staff_user.email} user")
) )
@ -190,13 +202,23 @@ class Command(BaseCommand):
Wishlist.objects.filter(user__in=demo_users).delete() Wishlist.objects.filter(user__in=demo_users).delete()
post_titles = [p["title"] for p in self.demo_data.get("blog_posts", [])]
blog_count = Post.objects.filter(title__in=post_titles).delete()[0]
self.stdout.write(f" Removed blog posts: {blog_count}")
post_tag_names = [
t["tag_name"] for t in self.demo_data.get("post_tags", [])
]
PostTag.objects.filter(tag_name__in=post_tag_names).delete()
demo_users.delete() demo_users.delete()
demo_vendor_name = self.demo_data["vendor"]["name"]
try: try:
vendor = Vendor.objects.get(name=DEMO_VENDOR_NAME) vendor = Vendor.objects.get(name=demo_vendor_name)
Stock.objects.filter(vendor=vendor).delete() Stock.objects.filter(vendor=vendor).delete()
vendor.delete() vendor.delete()
self.stdout.write(f" Removed vendor: {DEMO_VENDOR_NAME}") self.stdout.write(f" Removed vendor: {demo_vendor_name}")
except Vendor.DoesNotExist: except Vendor.DoesNotExist:
pass pass
@ -213,6 +235,10 @@ class Command(BaseCommand):
if product_dir.exists() and not any(product_dir.iterdir()): if product_dir.exists() and not any(product_dir.iterdir()):
shutil.rmtree(product_dir, ignore_errors=True) shutil.rmtree(product_dir, ignore_errors=True)
# Delete OrderProducts referencing demo products (from non-demo users)
# to avoid ProtectedError since OrderProduct.product uses PROTECT
OrderProduct.objects.filter(product__in=products).delete()
products.delete() products.delete()
brand_names = [b["name"] for b in self.demo_data["brands"]] brand_names = [b["name"] for b in self.demo_data["brands"]]
@ -254,6 +280,7 @@ class Command(BaseCommand):
self.stdout.write(f" Products: {Product.objects.count()}") self.stdout.write(f" Products: {Product.objects.count()}")
self.stdout.write(f" Categories: {Category.objects.count()}") self.stdout.write(f" Categories: {Category.objects.count()}")
self.stdout.write(f" Brands: {Brand.objects.count()}") self.stdout.write(f" Brands: {Brand.objects.count()}")
self.stdout.write(f" Blog posts: {Post.objects.count()}")
self.stdout.write(f" Users created: {len(users)}") self.stdout.write(f" Users created: {len(users)}")
self.stdout.write(f" Orders created: {len(orders)}") self.stdout.write(f" Orders created: {len(orders)}")
self.stdout.write(f" Refunded orders: {refunded_count}") self.stdout.write(f" Refunded orders: {refunded_count}")
@ -316,6 +343,17 @@ class Command(BaseCommand):
attr.name_ru_ru = attr_data["name_ru"] attr.name_ru_ru = attr_data["name_ru"]
attr.save() attr.save()
attr_lookup = {}
for attr_data in data["attributes"]:
group = attr_groups.get(attr_data["group"])
if group:
try:
attr_lookup[attr_data["name"]] = Attribute.objects.get(
group=group, name=attr_data["name"]
)
except Attribute.DoesNotExist:
pass
brands = {} brands = {}
for brand_data in data["brands"]: for brand_data in data["brands"]:
brand, created = Brand.objects.get_or_create( brand, created = Brand.objects.get_or_create(
@ -360,15 +398,15 @@ class Command(BaseCommand):
"description": prod_data["description"], "description": prod_data["description"],
"category": category, "category": category,
"brand": brand, "brand": brand,
"is_digital": False, "is_digital": prod_data.get("is_digital", False),
}, },
) )
if created: if created:
if "name_ru" in prod_data: if "name_ru" in prod_data:
product.name_ru_ru = prod_data["name_ru"] product.name_ru_ru = prod_data["name_ru"] # ty: ignore[invalid-assignment]
if "description_ru" in prod_data: if "description_ru" in prod_data:
product.description_ru_ru = prod_data["description_ru"] product.description_ru_ru = prod_data["description_ru"] # ty: ignore[invalid-assignment]
product.save() product.save()
Stock.objects.create( Stock.objects.create(
@ -383,27 +421,66 @@ class Command(BaseCommand):
# Add product image # Add product image
self._add_product_image(product, prod_data["partnumber"]) self._add_product_image(product, prod_data["partnumber"])
def _add_product_image(self, product: Product, partnumber: str) -> None: # Add attribute values
image_path = DEMO_IMAGES_DIR / f"{partnumber}.jpg" for attr_name, av_data in prod_data.get("attribute_values", {}).items():
if not image_path.exists(): attr = attr_lookup.get(attr_name)
image_path = DEMO_IMAGES_DIR / "placeholder.png" if attr:
if isinstance(av_data, dict):
value = str(av_data["en"])
value_ru = av_data.get("ru")
else:
value = str(av_data)
value_ru = None
av, created = AttributeValue.objects.get_or_create(
product=product,
attribute=attr,
defaults={"value": value},
)
if created and value_ru:
av.value_ru_ru = value_ru # ty:ignore[invalid-assignment]
av.save()
if not image_path.exists(): def _find_image(self, partnumber: str, suffix: str = "") -> Path | None:
extensions = (".jpg", ".jpeg", ".png", ".webp")
for ext in extensions:
candidate = DEMO_IMAGES_DIR / f"{partnumber}{suffix}{ext}"
if candidate.exists():
return candidate
return None
def _add_product_image(self, product: Product, partnumber: str) -> None:
primary = self._find_image(partnumber)
if not primary:
primary = DEMO_IMAGES_DIR / "placeholder.png"
if not primary.exists():
self.stdout.write( self.stdout.write(
self.style.WARNING(f" No image found for {partnumber}, skipping...") self.style.WARNING(f" No image found for {partnumber}, skipping...")
) )
return return
self._save_product_image(product, primary, priority=1)
n = 2
while True:
variant = self._find_image(partnumber, f" ({n})")
if not variant:
break
self._save_product_image(product, variant, priority=n)
n += 1
def _save_product_image(
self, product: Product, image_path: Path, priority: int
) -> None:
with open(image_path, "rb") as f: with open(image_path, "rb") as f:
image_content = f.read() image_content = f.read()
filename = image_path.name
product_image = ProductImage( product_image = ProductImage(
product=product, product=product,
alt=product.name, alt=product.name,
priority=1, priority=priority,
) )
product_image.image.save(filename, ContentFile(image_content), save=True) product_image.image.save(image_path.name, ContentFile(image_content), save=True)
@transaction.atomic @transaction.atomic
def _create_demo_users(self, count: int) -> list: def _create_demo_users(self, count: int) -> list:
@ -585,3 +662,66 @@ class Command(BaseCommand):
users_with_wishlists.add(user.id) users_with_wishlists.add(user.id)
return len(users_with_wishlists) return len(users_with_wishlists)
@transaction.atomic
def _create_blog_posts(self) -> int:
data = self.demo_data
author = self.staff_user
count = 0
for tag_data in data.get("post_tags", []):
tag, created = PostTag.objects.get_or_create(
tag_name=tag_data["tag_name"],
defaults={"name": tag_data["name"]},
)
if created and "name_ru" in tag_data:
tag.name_ru_ru = tag_data["name_ru"]
tag.save()
for post_data in data.get("blog_posts", []):
if Post.objects.filter(title=post_data["title"]).exists():
continue
content_en = self._load_blog_content(post_data["content_file"], "en")
content_ru = self._load_blog_content(post_data["content_file"], "ru")
if not content_en:
self.stdout.write(
self.style.WARNING(
f" No content found for {post_data['content_file']}, skipping..."
)
)
continue
post = Post(
author=author,
title=post_data["title"],
content=content_en,
meta_description=post_data.get("meta_description", ""),
is_static_page=post_data.get("is_static_page", False),
)
if "title_ru" in post_data:
post.title_ru_ru = post_data["title_ru"] # ty:ignore[unresolved-attribute]
if content_ru:
post.content_ru_ru = content_ru # ty:ignore[unresolved-attribute]
if "meta_description_ru" in post_data:
post.meta_description_ru_ru = post_data["meta_description_ru"] # ty:ignore[unresolved-attribute]
post.save()
for tag_name in post_data.get("tags", []):
try:
tag = PostTag.objects.get(tag_name=tag_name)
post.tags.add(tag)
except PostTag.DoesNotExist:
pass
count += 1
return count
def _load_blog_content(self, content_file: str, lang: str) -> str | None:
file_path = DEMO_BLOG_DIR / f"{content_file}.{lang}.md"
if not file_path.exists():
return None
with open(file_path, encoding="utf-8") as f:
return f.read()

View file

@ -1,7 +1,6 @@
import uuid import uuid
import django_extensions.db.fields import django_extensions.db.fields
import django_prometheus.models
from django.db import migrations, models from django.db import migrations, models
@ -251,10 +250,7 @@ class Migration(migrations.Migration):
"verbose_name": "category tag", "verbose_name": "category tag",
"verbose_name_plural": "category tags", "verbose_name_plural": "category tags",
}, },
bases=( bases=(models.Model,),
django_prometheus.models.ExportModelOperationsMixin("category_tag"),
models.Model,
),
), ),
migrations.AddField( migrations.AddField(
model_name="category", model_name="category",

View file

@ -2,7 +2,6 @@ import uuid
import django.db.models.deletion import django.db.models.deletion
import django_extensions.db.fields import django_extensions.db.fields
import django_prometheus.models
from django.db import migrations, models from django.db import migrations, models
@ -80,10 +79,7 @@ class Migration(migrations.Migration):
"verbose_name": "order CRM link", "verbose_name": "order CRM link",
"verbose_name_plural": "orders CRM links", "verbose_name_plural": "orders CRM links",
}, },
bases=( bases=(models.Model,),
django_prometheus.models.ExportModelOperationsMixin("crm_provider"),
models.Model,
),
), ),
migrations.CreateModel( migrations.CreateModel(
name="OrderCrmLink", name="OrderCrmLink",
@ -148,9 +144,6 @@ class Migration(migrations.Migration):
"verbose_name": "order CRM link", "verbose_name": "order CRM link",
"verbose_name_plural": "orders CRM links", "verbose_name_plural": "orders CRM links",
}, },
bases=( bases=(models.Model,),
django_prometheus.models.ExportModelOperationsMixin("order_crm_link"),
models.Model,
),
), ),
] ]

View file

@ -0,0 +1,39 @@
# Generated by Django 5.2.11 on 2026-02-21 19:12
from django.db import migrations, models
import engine.core.utils.db
class Migration(migrations.Migration):
dependencies = [
("core", "0054_product_export_to_marketplaces"),
]
operations = [
migrations.AlterField(
model_name="brand",
name="categories",
field=models.ManyToManyField(
blank=True,
help_text="DEPRECATED",
to="core.category",
verbose_name="DEPRECATED",
),
),
migrations.AlterField(
model_name="product",
name="slug",
field=engine.core.utils.db.TweakedAutoSlugField(
allow_unicode=True,
blank=True,
editable=False,
max_length=88,
null=True,
overwrite=True,
populate_from=("name", "brand__slug", "category__slug", "uuid"),
unique=True,
verbose_name="Slug",
),
),
]

View file

@ -2,7 +2,7 @@ import datetime
import json import json
import logging import logging
from contextlib import suppress from contextlib import suppress
from typing import Any, Iterable, Self from typing import TYPE_CHECKING, Any, Iterable, Self
from constance import config from constance import config
from django.conf import settings from django.conf import settings
@ -29,6 +29,7 @@ from django.db.models import (
JSONField, JSONField,
ManyToManyField, ManyToManyField,
Max, Max,
Min,
OneToOneField, OneToOneField,
PositiveIntegerField, PositiveIntegerField,
QuerySet, QuerySet,
@ -46,7 +47,6 @@ from django.utils.functional import cached_property
from django.utils.http import urlsafe_base64_encode from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField from django_extensions.db.fields import AutoSlugField
from django_prometheus.models import ExportModelOperationsMixin
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel from mptt.models import MPTTModel
@ -73,10 +73,13 @@ from engine.core.validators import validate_category_image_dimensions
from engine.payments.models import Transaction from engine.payments.models import Transaction
from schon.utils.misc import create_object from schon.utils.misc import create_object
if TYPE_CHECKING:
from django.db.models import Manager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel): class AttributeGroup(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a group of attributes, which can be hierarchical." "Represents a group of attributes, which can be hierarchical."
" This class is used to manage and organize attribute groups." " This class is used to manage and organize attribute groups."
@ -84,6 +87,9 @@ class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel):
" This can be useful for categorizing and managing attributes more effectively in acomplex system." " This can be useful for categorizing and managing attributes more effectively in acomplex system."
) )
if TYPE_CHECKING:
attributes: Manager["Attribute"]
is_publicly_visible = True is_publicly_visible = True
parent = ForeignKey( parent = ForeignKey(
"self", "self",
@ -109,7 +115,7 @@ class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel):
verbose_name_plural = _("attribute groups") verbose_name_plural = _("attribute groups")
class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): class Vendor(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a vendor entity capable of storing information about external vendors and their interaction requirements." "Represents a vendor entity capable of storing information about external vendors and their interaction requirements."
" The Vendor class is used to define and manage information related to an external vendor." " The Vendor class is used to define and manage information related to an external vendor."
@ -193,7 +199,7 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel):
] ]
class ProductTag(ExportModelOperationsMixin("product_tag"), NiceModel): class ProductTag(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a product tag used for classifying or identifying products." "Represents a product tag used for classifying or identifying products."
" The ProductTag class is designed to uniquely identify and classify products through a combination" " The ProductTag class is designed to uniquely identify and classify products through a combination"
@ -225,7 +231,7 @@ class ProductTag(ExportModelOperationsMixin("product_tag"), NiceModel):
verbose_name_plural = _("product tags") verbose_name_plural = _("product tags")
class CategoryTag(ExportModelOperationsMixin("category_tag"), NiceModel): class CategoryTag(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a category tag used for products." "Represents a category tag used for products."
" This class models a category tag that can be used to associate and classify products." " This class models a category tag that can be used to associate and classify products."
@ -256,7 +262,7 @@ class CategoryTag(ExportModelOperationsMixin("category_tag"), NiceModel):
verbose_name_plural = _("category tags") verbose_name_plural = _("category tags")
class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): class Category(NiceModel, MPTTModel):
__doc__ = _( __doc__ = _(
"Represents a category entity to organize and group related items in a hierarchical structure." "Represents a category entity to organize and group related items in a hierarchical structure."
" Categories may have hierarchical relationships with other categories, supporting parent-child relationships." " Categories may have hierarchical relationships with other categories, supporting parent-child relationships."
@ -267,6 +273,10 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
" as well as assign attributes like images, tags, or priority." " as well as assign attributes like images, tags, or priority."
) )
if TYPE_CHECKING:
products: Manager["Product"]
children: Manager["Category"]
is_publicly_visible = True is_publicly_visible = True
image = ImageField( image = ImageField(
@ -402,7 +412,8 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
@cached_property @cached_property
def filterable_attributes(self) -> list[FilterableAttribute]: def filterable_attributes(self) -> list[FilterableAttribute]:
rows = ( rows = (
AttributeValue.objects.annotate(value_length=Length("value")) AttributeValue.objects.filter(is_active=True)
.annotate(value_length=Length("value"))
.filter( .filter(
product__is_active=True, product__is_active=True,
product__category=self, product__category=self,
@ -442,13 +453,35 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel):
# Fallback to favicon.png from static files # Fallback to favicon.png from static files
return static("favicon.png") return static("favicon.png")
@cached_property
def brands(self) -> QuerySet["Brand"]:
return Brand.objects.filter(
product__category=self,
product__is_active=True,
is_active=True,
).distinct()
@cached_property
def min_price(self) -> float:
return (
self.products.filter(is_active=True).aggregate(Min("price"))["price__min"]
or 0.0
)
@cached_property
def max_price(self) -> float:
return (
self.products.filter(is_active=True).aggregate(Max("price"))["price__max"]
or 0.0
)
class Meta: class Meta:
verbose_name = _("category") verbose_name = _("category")
verbose_name_plural = _("categories") verbose_name_plural = _("categories")
ordering = ["tree_id", "lft"] ordering = ["tree_id", "lft"]
class Brand(ExportModelOperationsMixin("brand"), NiceModel): class Brand(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a Brand object in the system. " "Represents a Brand object in the system. "
"This class handles information and attributes related to a brand, including its name, logos, " "This class handles information and attributes related to a brand, including its name, logos, "
@ -489,8 +522,8 @@ class Brand(ExportModelOperationsMixin("brand"), NiceModel):
categories = ManyToManyField( categories = ManyToManyField(
"core.Category", "core.Category",
blank=True, blank=True,
help_text=_("optional categories that this brand is associated with"), help_text=_("DEPRECATED"),
verbose_name=_("associated categories"), verbose_name=_("DEPRECATED"),
) )
slug = AutoSlugField( slug = AutoSlugField(
populate_from=("name",), populate_from=("name",),
@ -518,7 +551,7 @@ class Brand(ExportModelOperationsMixin("brand"), NiceModel):
verbose_name_plural = _("brands") verbose_name_plural = _("brands")
class Stock(ExportModelOperationsMixin("stock"), NiceModel): class Stock(NiceModel):
__doc__ = _( __doc__ = _(
"Represents the stock of a product managed in the system." "Represents the stock of a product managed in the system."
" This class provides details about the relationship between vendors, products, and their stock information, " " This class provides details about the relationship between vendors, products, and their stock information, "
@ -586,7 +619,7 @@ class Stock(ExportModelOperationsMixin("stock"), NiceModel):
verbose_name_plural = _("stock entries") verbose_name_plural = _("stock entries")
class Product(ExportModelOperationsMixin("product"), NiceModel): class Product(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a product with attributes such as category, brand, tags, digital status, name, description, part number, and slug." "Represents a product with attributes such as category, brand, tags, digital status, name, description, part number, and slug."
" Provides related utility properties to retrieve ratings, feedback counts, price, quantity, and total orders." " Provides related utility properties to retrieve ratings, feedback counts, price, quantity, and total orders."
@ -596,6 +629,12 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
" its associated information within an application." " its associated information within an application."
) )
if TYPE_CHECKING:
images: Manager["ProductImage"]
stocks: Manager["Stock"]
attributes: Manager["AttributeValue"]
category_id: Any
is_publicly_visible = True is_publicly_visible = True
category = ForeignKey( category = ForeignKey(
@ -655,7 +694,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
help_text=_("part number for this product"), help_text=_("part number for this product"),
verbose_name=_("part number"), verbose_name=_("part number"),
) )
slug = AutoSlugField( slug = TweakedAutoSlugField(
populate_from=( populate_from=(
"name", "name",
"brand__slug", "brand__slug",
@ -726,12 +765,17 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
@property @property
def price(self: Self) -> float: def price(self: Self) -> float:
stock = self.stocks.only("price").order_by("-price").first() stock = (
self.stocks.filter(is_active=True).only("price").order_by("-price").first()
)
return round(stock.price, 2) if stock else 0.0 return round(stock.price, 2) if stock else 0.0
@cached_property @cached_property
def quantity(self) -> int: def quantity(self) -> int:
return self.stocks.aggregate(total=Sum("quantity"))["total"] or 0 return (
self.stocks.filter(is_active=True).aggregate(total=Sum("quantity"))["total"]
or 0
)
@property @property
def total_orders(self): def total_orders(self):
@ -753,7 +797,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel):
return self.images.exists() return self.images.exists()
class Attribute(ExportModelOperationsMixin("attribute"), NiceModel): class Attribute(NiceModel):
__doc__ = _( __doc__ = _(
"Represents an attribute in the system." "Represents an attribute in the system."
" This class is used to define and manage attributes," " This class is used to define and manage attributes,"
@ -812,7 +856,7 @@ class Attribute(ExportModelOperationsMixin("attribute"), NiceModel):
verbose_name_plural = _("attributes") verbose_name_plural = _("attributes")
class AttributeValue(ExportModelOperationsMixin("attribute_value"), NiceModel): class AttributeValue(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a specific value for an attribute that is linked to a product. " "Represents a specific value for an attribute that is linked to a product. "
"It links the 'attribute' to a unique 'value', allowing " "It links the 'attribute' to a unique 'value', allowing "
@ -852,7 +896,7 @@ class AttributeValue(ExportModelOperationsMixin("attribute_value"), NiceModel):
verbose_name_plural = _("attribute values") verbose_name_plural = _("attribute values")
class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel): class ProductImage(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a product image associated with a product in the system. " "Represents a product image associated with a product in the system. "
"This class is designed to manage images for products, including functionality " "This class is designed to manage images for products, including functionality "
@ -906,7 +950,7 @@ class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel):
verbose_name_plural = _("product images") verbose_name_plural = _("product images")
class Promotion(ExportModelOperationsMixin("promotion"), NiceModel): class Promotion(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a promotional campaign for products with a discount. " "Represents a promotional campaign for products with a discount. "
"This class is used to define and manage promotional campaigns that offer a " "This class is used to define and manage promotional campaigns that offer a "
@ -952,7 +996,7 @@ class Promotion(ExportModelOperationsMixin("promotion"), NiceModel):
return str(self.id) return str(self.id)
class Wishlist(ExportModelOperationsMixin("wishlist"), NiceModel): class Wishlist(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a user's wishlist for storing and managing desired products. " "Represents a user's wishlist for storing and managing desired products. "
"The class provides functionality to manage a collection of products, " "The class provides functionality to manage a collection of products, "
@ -1023,7 +1067,7 @@ class Wishlist(ExportModelOperationsMixin("wishlist"), NiceModel):
return self return self
class Documentary(ExportModelOperationsMixin("attribute_group"), NiceModel): class Documentary(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a documentary record tied to a product. " "Represents a documentary record tied to a product. "
"This class is used to store information about documentaries related to specific " "This class is used to store information about documentaries related to specific "
@ -1054,7 +1098,7 @@ class Documentary(ExportModelOperationsMixin("attribute_group"), NiceModel):
return self.document.name.split(".")[-1] or _("unresolved") return self.document.name.split(".")[-1] or _("unresolved")
class Address(ExportModelOperationsMixin("address"), NiceModel): class Address(NiceModel):
__doc__ = _( __doc__ = _(
"Represents an address entity that includes location details and associations with a user. " "Represents an address entity that includes location details and associations with a user. "
"Provides functionality for geographic and address data storage, as well " "Provides functionality for geographic and address data storage, as well "
@ -1119,7 +1163,7 @@ class Address(ExportModelOperationsMixin("address"), NiceModel):
return f"{base} for {self.user.email}" if self.user else base return f"{base} for {self.user.email}" if self.user else base
class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel): class PromoCode(NiceModel):
__doc__ = _( __doc__ = _(
"Represents a promotional code that can be used for discounts, managing its validity, " "Represents a promotional code that can be used for discounts, managing its validity, "
"type of discount, and application. " "type of discount, and application. "
@ -1250,7 +1294,7 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel):
return promo_amount return promo_amount
class Order(ExportModelOperationsMixin("order"), NiceModel): class Order(NiceModel):
__doc__ = _( __doc__ = _(
"Represents an order placed by a user." "Represents an order placed by a user."
" This class models an order within the application," " This class models an order within the application,"
@ -1261,6 +1305,10 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
" Equally, functionality supports managing the products in the order lifecycle." " Equally, functionality supports managing the products in the order lifecycle."
) )
if TYPE_CHECKING:
order_products: Manager["OrderProduct"]
payments_transactions: Manager[Transaction]
is_publicly_visible = False is_publicly_visible = False
billing_address = ForeignKey( billing_address = ForeignKey(
@ -1429,7 +1477,8 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
if promotions.exists(): if promotions.exists():
buy_price -= round( buy_price -= round(
product.price * (promotions.first().discount_percent / 100), 2 product.price * (promotions.first().discount_percent / 100), # ty: ignore[possibly-missing-attribute]
2,
) )
order_product, is_created = OrderProduct.objects.get_or_create( order_product, is_created = OrderProduct.objects.get_or_create(
@ -1474,7 +1523,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
order_product.delete() order_product.delete()
return self return self
if order_product.quantity == 1: if order_product.quantity == 1:
self.order_products.remove(order_product) self.order_products.remove(order_product) # ty: ignore[unresolved-attribute]
order_product.delete() order_product.delete()
else: else:
order_product.quantity -= 1 order_product.quantity -= 1
@ -1497,7 +1546,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
_("you cannot remove products from an order that is not a pending one") _("you cannot remove products from an order that is not a pending one")
) )
for order_product in self.order_products.all(): for order_product in self.order_products.all():
self.order_products.remove(order_product) self.order_products.remove(order_product) # ty: ignore[unresolved-attribute]
order_product.delete() order_product.delete()
return self return self
@ -1509,7 +1558,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
try: try:
product = Product.objects.get(uuid=product_uuid) product = Product.objects.get(uuid=product_uuid)
order_product = self.order_products.get(product=product, order=self) order_product = self.order_products.get(product=product, order=self)
self.order_products.remove(order_product) self.order_products.remove(order_product) # ty: ignore[unresolved-attribute]
order_product.delete() order_product.delete()
except Product.DoesNotExist as dne: except Product.DoesNotExist as dne:
name = "Product" name = "Product"
@ -1775,6 +1824,8 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
crm_links = OrderCrmLink.objects.filter(order=self) crm_links = OrderCrmLink.objects.filter(order=self)
if crm_links.exists(): if crm_links.exists():
crm_link = crm_links.first() crm_link = crm_links.first()
if not crm_link:
return False
crm_integration = create_object( crm_integration = create_object(
crm_link.crm.integration_location, crm_link.crm.name crm_link.crm.integration_location, crm_link.crm.name
) )
@ -1820,7 +1871,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel):
return None return None
class Feedback(ExportModelOperationsMixin("feedback"), NiceModel): class Feedback(NiceModel):
__doc__ = _( __doc__ = _(
"Manages user feedback for products. " "Manages user feedback for products. "
"This class is designed to capture and store user feedback for specific products " "This class is designed to capture and store user feedback for specific products "
@ -1869,7 +1920,7 @@ class Feedback(ExportModelOperationsMixin("feedback"), NiceModel):
verbose_name_plural = _("feedbacks") verbose_name_plural = _("feedbacks")
class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): class OrderProduct(NiceModel):
__doc__ = _( __doc__ = _(
"Represents products associated with orders and their attributes. " "Represents products associated with orders and their attributes. "
"The OrderProduct model maintains information about a product that is part of an order, " "The OrderProduct model maintains information about a product that is part of an order, "
@ -1881,6 +1932,9 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
"and stores a reference to them." "and stores a reference to them."
) )
if TYPE_CHECKING:
download: "DigitalAssetDownload"
is_publicly_visible = False is_publicly_visible = False
buy_price = FloatField( buy_price = FloatField(
@ -2032,9 +2086,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel):
return None return None
class CustomerRelationshipManagementProvider( class CustomerRelationshipManagementProvider(NiceModel):
ExportModelOperationsMixin("crm_provider"), NiceModel
):
name = CharField(max_length=128, unique=True, verbose_name=_("name")) name = CharField(max_length=128, unique=True, verbose_name=_("name"))
integration_url = URLField( integration_url = URLField(
blank=True, null=True, help_text=_("URL of the integration") blank=True, null=True, help_text=_("URL of the integration")
@ -2077,7 +2129,7 @@ class CustomerRelationshipManagementProvider(
verbose_name_plural = _("CRMs") verbose_name_plural = _("CRMs")
class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel): class OrderCrmLink(NiceModel):
order = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links") order = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links")
crm = ForeignKey( crm = ForeignKey(
to=CustomerRelationshipManagementProvider, to=CustomerRelationshipManagementProvider,
@ -2094,7 +2146,7 @@ class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel):
verbose_name_plural = _("orders CRM links") verbose_name_plural = _("orders CRM links")
class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceModel): class DigitalAssetDownload(NiceModel):
__doc__ = _( __doc__ = _(
"Represents the downloading functionality for digital assets associated with orders. " "Represents the downloading functionality for digital assets associated with orders. "
"The DigitalAssetDownload class provides the ability to manage and access " "The DigitalAssetDownload class provides the ability to manage and access "

View file

@ -26,6 +26,7 @@ from engine.core.models import (
Wishlist, Wishlist,
) )
from engine.core.serializers.simple import ( from engine.core.serializers.simple import (
BrandSimpleSerializer,
CategorySimpleSerializer, CategorySimpleSerializer,
ProductSimpleSerializer, ProductSimpleSerializer,
) )
@ -60,6 +61,9 @@ class CategoryDetailListSerializer(ListSerializer):
class CategoryDetailSerializer(ModelSerializer): class CategoryDetailSerializer(ModelSerializer):
children = SerializerMethodField() children = SerializerMethodField()
filterable_attributes = SerializerMethodField() filterable_attributes = SerializerMethodField()
brands = BrandSimpleSerializer(many=True, read_only=True)
min_price = SerializerMethodField()
max_price = SerializerMethodField()
class Meta: class Meta:
model = Category model = Category
@ -71,6 +75,7 @@ class CategoryDetailSerializer(ModelSerializer):
"image", "image",
"markup_percent", "markup_percent",
"filterable_attributes", "filterable_attributes",
"brands",
"children", "children",
"slug", "slug",
"created", "created",
@ -95,6 +100,12 @@ class CategoryDetailSerializer(ModelSerializer):
return list(serializer.data) return list(serializer.data)
return [] return []
def get_min_price(self, obj: Category):
return obj.min_price
def get_max_price(self, obj: Category):
return obj.max_price
class BrandDetailSerializer(ModelSerializer): class BrandDetailSerializer(ModelSerializer):
categories = CategorySimpleSerializer(many=True) categories = CategorySimpleSerializer(many=True)

3915
engine/core/static/js/rapidoc-min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

View file

@ -0,0 +1,34 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% translate "Documentation bookmarklets" %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Bookmarklets' %}</span>
</div>
{% endblock %}
{% block content %}
<h1 class="font-semibold mb-4 text-xl text-base-900 dark:text-base-100">{% translate "Documentation bookmarklets" %}</h1>
<p class="text-sm text-base-500 dark:text-base-400 mb-6">{% blocktranslate trimmed %}
To install bookmarklets, drag the link to your bookmarks toolbar, or right-click
the link and add it to your bookmarks. Now you can select the bookmarklet
from any page in the site.
{% endblocktranslate %}</p>
<div class="border border-base-200 rounded-default bg-white shadow-xs dark:bg-base-900 dark:border-base-800">
<div class="px-4 py-4">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-primary-600 dark:text-primary-500">bookmark</span>
<a href="javascript:(function(){if(typeof XMLHttpRequest!='undefined'){x=new XMLHttpRequest()}else{return;}x.open('HEAD',location.href,false);x.send(null);try{view=x.getResponseHeader('x-view');}catch(e){alert('No view found for this page');return;}if(view=='undefined'){alert('No view found for this page');}document.location='{% url 'django-admindocs-views-index' %}'+view+'/';})()" class="font-semibold text-primary-600 dark:text-primary-500 hover:underline">{% translate "Documentation for this page" %}</a>
</div>
<p class="text-sm text-base-500 dark:text-base-400 ml-9">{% translate "Jumps you from any page to the documentation for the view that generates that page." %}</p>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,58 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% translate 'Documentation' %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Documentation' %}</span>
</div>
{% endblock %}
{% block content %}
<h1 class="font-semibold mb-6 text-xl text-base-900 dark:text-base-100">{% translate 'Documentation' %}</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<a href="models/" class="group border border-base-200 rounded-default bg-white p-5 shadow-xs hover:border-primary-300 hover:shadow-md transition dark:bg-base-900 dark:border-base-800 dark:hover:border-primary-700">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-primary-600 dark:text-primary-500">database</span>
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% translate 'Models' %}</h2>
</div>
<p class="text-sm text-base-500 dark:text-base-400">{% translate 'Models are descriptions of all the objects in the system and their associated fields. Each model has a list of fields which can be accessed as template variables' %}.</p>
</a>
<a href="views/" class="group border border-base-200 rounded-default bg-white p-5 shadow-xs hover:border-primary-300 hover:shadow-md transition dark:bg-base-900 dark:border-base-800 dark:hover:border-primary-700">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-primary-600 dark:text-primary-500">visibility</span>
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% translate 'Views' %}</h2>
</div>
<p class="text-sm text-base-500 dark:text-base-400">{% translate 'Each page on the public site is generated by a view. The view defines which template is used to generate the page and which objects are available to that template.' %}</p>
</a>
<a href="tags/" class="group border border-base-200 rounded-default bg-white p-5 shadow-xs hover:border-primary-300 hover:shadow-md transition dark:bg-base-900 dark:border-base-800 dark:hover:border-primary-700">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-primary-600 dark:text-primary-500">code</span>
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% translate 'Tags' %}</h2>
</div>
<p class="text-sm text-base-500 dark:text-base-400">{% translate 'List of all the template tags and their functions.' %}</p>
</a>
<a href="filters/" class="group border border-base-200 rounded-default bg-white p-5 shadow-xs hover:border-primary-300 hover:shadow-md transition dark:bg-base-900 dark:border-base-800 dark:hover:border-primary-700">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-primary-600 dark:text-primary-500">filter_alt</span>
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% translate 'Filters' %}</h2>
</div>
<p class="text-sm text-base-500 dark:text-base-400">{% translate 'Filters are actions which can be applied to variables in a template to alter the output.' %}</p>
</a>
<a href="bookmarklets/" class="group border border-base-200 rounded-default bg-white p-5 shadow-xs hover:border-primary-300 hover:shadow-md transition dark:bg-base-900 dark:border-base-800 dark:hover:border-primary-700">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-primary-600 dark:text-primary-500">bookmark</span>
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% translate 'Bookmarklets' %}</h2>
</div>
<p class="text-sm text-base-500 dark:text-base-400">{% translate 'Tools for your browser to quickly access admin functionality.' %}</p>
</a>
</div>
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% translate 'Please install docutils' %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Documentation' %}</span>
</div>
{% endblock %}
{% block content %}
<div class="border border-amber-200 rounded-default bg-amber-50 shadow-xs p-6 dark:bg-amber-900/20 dark:border-amber-800">
<div class="flex items-start gap-3">
<span class="material-symbols-outlined text-amber-600 dark:text-amber-400 mt-0.5">warning</span>
<div>
<h1 class="font-semibold text-lg text-amber-800 dark:text-amber-200 mb-2">{% translate 'Documentation' %}</h1>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-2">{% blocktranslate with "https://docutils.sourceforge.io/" as link %}The admin documentation system requires Python's <a href="{{ link }}" class="underline font-medium">docutils</a> library.{% endblocktranslate %}</p>
<p class="text-sm text-amber-700 dark:text-amber-300">{% blocktranslate with "https://pypi.org/project/docutils/" as link %}Please ask your administrators to install <a href="{{ link }}" class="underline font-medium">docutils</a>.{% endblocktranslate %}</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,91 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% blocktranslate %}Model: {{ name }}{% endblocktranslate %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-models-index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Models' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{{ name }}</span>
</div>
{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="font-semibold text-xl text-base-900 dark:text-base-100">{{ name }}</h1>
{% if summary %}<p class="mt-1 text-sm text-base-500 dark:text-base-400">{{ summary }}</p>{% endif %}
{% if description %}<div class="mt-2 text-sm text-base-600 dark:text-base-400 prose dark:prose-invert max-w-none">{{ description }}</div>{% endif %}
</div>
<div class="border border-base-200 rounded-default bg-white shadow-xs mb-6 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h3 class="font-semibold text-base text-base-900 dark:text-base-100 flex items-center gap-2">
<span class="material-symbols-outlined md-18 text-primary-600 dark:text-primary-500">list</span>
{% translate 'Fields' %}
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-base-200 dark:border-base-800 bg-base-50 dark:bg-base-800/50">
<th class="px-4 py-2.5 text-left font-semibold text-base-700 dark:text-base-300">{% translate 'Field' %}</th>
<th class="px-4 py-2.5 text-left font-semibold text-base-700 dark:text-base-300">{% translate 'Type' %}</th>
<th class="px-4 py-2.5 text-left font-semibold text-base-700 dark:text-base-300">{% translate 'Description' %}</th>
</tr>
</thead>
<tbody class="divide-y divide-base-200 dark:divide-base-800">
{% for field in fields|dictsort:"name" %}
<tr class="hover:bg-base-50 dark:hover:bg-base-800/30 transition">
<td class="px-4 py-2.5 font-mono text-sm text-base-900 dark:text-base-100 whitespace-nowrap">{{ field.name }}</td>
<td class="px-4 py-2.5 text-base-500 dark:text-base-400 whitespace-nowrap">
<span class="bg-base-100 dark:bg-base-800 px-2 py-0.5 rounded text-xs font-medium">{{ field.data_type }}</span>
</td>
<td class="px-4 py-2.5 text-base-600 dark:text-base-400">{{ field.verbose }}{% if field.help_text %} &mdash; {{ field.help_text|safe }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if methods %}
<div class="border border-base-200 rounded-default bg-white shadow-xs mb-6 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h3 class="font-semibold text-base text-base-900 dark:text-base-100 flex items-center gap-2">
<span class="material-symbols-outlined md-18 text-primary-600 dark:text-primary-500">function</span>
{% translate 'Methods with arguments' %}
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-base-200 dark:border-base-800 bg-base-50 dark:bg-base-800/50">
<th class="px-4 py-2.5 text-left font-semibold text-base-700 dark:text-base-300">{% translate 'Method' %}</th>
<th class="px-4 py-2.5 text-left font-semibold text-base-700 dark:text-base-300">{% translate 'Arguments' %}</th>
<th class="px-4 py-2.5 text-left font-semibold text-base-700 dark:text-base-300">{% translate 'Description' %}</th>
</tr>
</thead>
<tbody class="divide-y divide-base-200 dark:divide-base-800">
{% for method in methods|dictsort:"name" %}
<tr class="hover:bg-base-50 dark:hover:bg-base-800/30 transition">
<td class="px-4 py-2.5 font-mono text-sm text-base-900 dark:text-base-100 whitespace-nowrap">{{ method.name }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-base-500 dark:text-base-400">{{ method.arguments }}</td>
<td class="px-4 py-2.5 text-base-600 dark:text-base-400">{{ method.verbose }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<a href="{% url 'django-admindocs-models-index' %}" class="inline-flex items-center gap-1 text-sm text-primary-600 dark:text-primary-500 hover:underline">
<span class="material-symbols-outlined md-18">arrow_back</span>
{% translate 'Back to Model documentation' %}
</a>
{% endblock %}

View file

@ -0,0 +1,55 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% translate 'Models' %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Models' %}</span>
</div>
{% endblock %}
{% block content %}
<h1 class="font-semibold mb-6 text-xl text-base-900 dark:text-base-100">{% translate 'Model documentation' %}</h1>
{% regroup models by app_config as grouped_models %}
<div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
<div class="grow flex flex-col gap-4">
{% for group in grouped_models %}
<div id="app-{{ group.grouper.label }}" class="border border-base-200 rounded-default bg-white shadow-xs dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{{ group.grouper.verbose_name }}</h2>
<span class="text-xs text-base-400 dark:text-base-500">{{ group.grouper.name }}</span>
</div>
<div class="divide-y divide-base-200 dark:divide-base-800">
{% for model in group.list %}
<a href="{% url 'django-admindocs-models-detail' app_label=model.app_label model_name=model.model_name %}" class="flex items-center gap-2 px-4 py-2.5 text-sm hover:bg-base-50 dark:hover:bg-base-800 transition">
<span class="material-symbols-outlined md-18 text-base-400 dark:text-base-500">table_chart</span>
<span class="text-primary-600 dark:text-primary-500 font-medium">{{ model.object_name }}</span>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="shrink-0 lg:w-56">
<div class="border border-base-200 rounded-default bg-white shadow-xs sticky top-4 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-sm text-base-900 dark:text-base-100">{% translate 'Model groups' %}</h2>
</div>
<nav class="p-2">
{% regroup models by app_config as grouped_models %}
{% for group in grouped_models %}
<a href="#app-{{ group.grouper.label }}" class="block px-3 py-1.5 rounded-default text-sm text-base-600 hover:bg-base-50 hover:text-primary-600 dark:text-base-400 dark:hover:bg-base-800 dark:hover:text-primary-500">{{ group.grouper.verbose_name }}</a>
{% endfor %}
</nav>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,42 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% blocktranslate %}Template: {{ name }}{% endblocktranslate %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Templates' %}</span>
</div>
{% endblock %}
{% block content %}
<h1 class="font-semibold mb-2 text-xl text-base-900 dark:text-base-100">{% blocktranslate %}Template: <code class="font-mono text-primary-600 dark:text-primary-500">{{ name }}</code>{% endblocktranslate %}</h1>
<div class="border border-base-200 rounded-default bg-white shadow-xs mb-6 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% blocktranslate %}Search path for template <code class="font-mono">{{ name }}</code>:{% endblocktranslate %}</h2>
</div>
<div class="divide-y divide-base-200 dark:divide-base-800">
{% for template in templates|dictsort:"order" %}
<div class="flex items-center gap-2 px-4 py-2.5 text-sm">
<span class="text-base-400 dark:text-base-500 font-mono text-xs w-6 text-right shrink-0">{{ template.order }}</span>
<code class="font-mono text-sm {% if template.exists %}text-base-900 dark:text-base-100{% else %}text-base-400 dark:text-base-600{% endif %}">{{ template.file }}</code>
{% if not template.exists %}
<span class="bg-base-100 dark:bg-base-800 px-2 py-0.5 rounded text-xs text-base-400 dark:text-base-500">{% translate '(does not exist)' %}</span>
{% else %}
<span class="material-symbols-outlined md-18 text-green-500">check_circle</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<a href="{% url 'django-admindocs-docroot' %}" class="inline-flex items-center gap-1 text-sm text-primary-600 dark:text-primary-500 hover:underline">
<span class="material-symbols-outlined md-18">arrow_back</span>
{% translate 'Back to Documentation' %}
</a>
{% endblock %}

View file

@ -0,0 +1,63 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% translate 'Template filters' %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Filters' %}</span>
</div>
{% endblock %}
{% block content %}
<h1 class="font-semibold mb-6 text-xl text-base-900 dark:text-base-100">{% translate 'Template filter documentation' %}</h1>
{% regroup filters|dictsort:"library" by library as filter_libraries %}
<div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
<div class="grow flex flex-col gap-4">
{% for library in filter_libraries %}
<div class="border border-base-200 rounded-default bg-white shadow-xs dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% firstof library.grouper _("Built-in filters") %}</h2>
{% if library.grouper %}
<p class="mt-1 text-xs text-base-400 dark:text-base-500">
{% blocktranslate with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these filters, put <code class="font-mono bg-base-100 dark:bg-base-800 px-1 py-0.5 rounded">{{ code }}</code> in your template before using the filter.{% endblocktranslate %}
</p>
{% endif %}
</div>
<div class="divide-y divide-base-200 dark:divide-base-800">
{% for filter in library.list|dictsort:"name" %}
<div id="{{ library.grouper|default:"built_in" }}-{{ filter.name }}" class="px-4 py-3">
<h3 class="font-mono font-semibold text-sm text-base-900 dark:text-base-100">{{ filter.name }}</h3>
<div class="mt-1 text-sm text-base-600 dark:text-base-400 prose dark:prose-invert max-w-none prose-sm">
{{ filter.title }}
{{ filter.body }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="shrink-0 lg:w-56">
{% for library in filter_libraries %}
<div class="border border-base-200 rounded-default bg-white shadow-xs sticky top-4 mb-4 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-sm text-base-900 dark:text-base-100">{% firstof library.grouper _("Built-in filters") %}</h2>
</div>
<nav class="p-2 max-h-96 overflow-y-auto">
{% for filter in library.list|dictsort:"name" %}
<a href="#{{ library.grouper|default:"built_in" }}-{{ filter.name }}" class="block px-3 py-1 rounded-default text-xs font-mono text-base-600 hover:bg-base-50 hover:text-primary-600 dark:text-base-400 dark:hover:bg-base-800 dark:hover:text-primary-500">{{ filter.name }}</a>
{% endfor %}
</nav>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,63 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% translate 'Template tags' %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Tags' %}</span>
</div>
{% endblock %}
{% block content %}
<h1 class="font-semibold mb-6 text-xl text-base-900 dark:text-base-100">{% translate 'Template tag documentation' %}</h1>
{% regroup tags|dictsort:"library" by library as tag_libraries %}
<div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
<div class="grow flex flex-col gap-4">
{% for library in tag_libraries %}
<div class="border border-base-200 rounded-default bg-white shadow-xs dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">{% firstof library.grouper _("Built-in tags") %}</h2>
{% if library.grouper %}
<p class="mt-1 text-xs text-base-400 dark:text-base-500">
{% blocktranslate with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these tags, put <code class="font-mono bg-base-100 dark:bg-base-800 px-1 py-0.5 rounded">{{ code }}</code> in your template before using the tag.{% endblocktranslate %}
</p>
{% endif %}
</div>
<div class="divide-y divide-base-200 dark:divide-base-800">
{% for tag in library.list|dictsort:"name" %}
<div id="{{ library.grouper|default:"built_in" }}-{{ tag.name }}" class="px-4 py-3">
<h3 class="font-mono font-semibold text-sm text-base-900 dark:text-base-100">{{ tag.name }}</h3>
<div class="mt-1 text-sm text-base-600 dark:text-base-400 prose dark:prose-invert max-w-none prose-sm">
<p class="font-medium">{{ tag.title|striptags }}</p>
{{ tag.body }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="shrink-0 lg:w-56">
{% for library in tag_libraries %}
<div class="border border-base-200 rounded-default bg-white shadow-xs sticky top-4 mb-4 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-sm text-base-900 dark:text-base-100">{% firstof library.grouper _("Built-in tags") %}</h2>
</div>
<nav class="p-2 max-h-96 overflow-y-auto">
{% for tag in library.list|dictsort:"name" %}
<a href="#{{ library.grouper|default:"built_in" }}-{{ tag.name }}" class="block px-3 py-1 rounded-default text-xs font-mono text-base-600 hover:bg-base-50 hover:text-primary-600 dark:text-base-400 dark:hover:bg-base-800 dark:hover:text-primary-500">{{ tag.name }}</a>
{% endfor %}
</nav>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% blocktranslate %}View: {{ name }}{% endblocktranslate %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-views-index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Views' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{{ name }}</span>
</div>
{% endblock %}
{% block content %}
<div class="border border-base-200 rounded-default bg-white shadow-xs mb-6 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h1 class="font-semibold text-lg text-base-900 dark:text-base-100">{{ name }}</h1>
{% if summary %}<p class="mt-1 text-sm text-base-500 dark:text-base-400">{{ summary }}</p>{% endif %}
</div>
<div class="px-4 py-4 text-sm text-base-600 dark:text-base-400 prose dark:prose-invert max-w-none">
{{ body }}
{% if meta.Context %}
<h3 class="font-semibold text-base-900 dark:text-base-100 mt-4 mb-1">{% translate 'Context:' %}</h3>
<p>{{ meta.Context }}</p>
{% endif %}
{% if meta.Templates %}
<h3 class="font-semibold text-base-900 dark:text-base-100 mt-4 mb-1">{% translate 'Templates:' %}</h3>
<p>{{ meta.Templates }}</p>
{% endif %}
</div>
</div>
<a href="{% url 'django-admindocs-views-index' %}" class="inline-flex items-center gap-1 text-sm text-primary-600 dark:text-primary-500 hover:underline">
<span class="material-symbols-outlined md-18">arrow_back</span>
{% translate 'Back to View documentation' %}
</a>
{% endblock %}

View file

@ -0,0 +1,66 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %}{% translate 'Views' %}{% endblock %}
{% block breadcrumbs %}
<div class="flex gap-2 items-center px-4 py-3 text-sm text-base-400 dark:text-base-500">
<a href="{% url 'admin:index' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Home' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<a href="{% url 'django-admindocs-docroot' %}" class="hover:text-primary-600 dark:hover:text-primary-500">{% translate 'Documentation' %}</a>
<span class="material-symbols-outlined md-18">chevron_right</span>
<span class="text-base-700 dark:text-base-300">{% translate 'Views' %}</span>
</div>
{% endblock %}
{% block content %}
<h1 class="font-semibold mb-6 text-xl text-base-900 dark:text-base-100">{% translate 'View documentation' %}</h1>
{% regroup views|dictsort:'namespace' by namespace as views_by_ns %}
<div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
<div class="grow flex flex-col gap-4">
{% for ns_views in views_by_ns %}
<div id="ns-{{ ns_views.grouper|default:'empty' }}" class="border border-base-200 rounded-default bg-white shadow-xs dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-base text-base-900 dark:text-base-100">
{% if ns_views.grouper %}
{% blocktranslate with ns_views.grouper as name %}Views by namespace {{ name }}{% endblocktranslate %}
{% else %}
{% blocktranslate %}Views by empty namespace{% endblocktranslate %}
{% endif %}
</h2>
</div>
<div class="divide-y divide-base-200 dark:divide-base-800">
{% for view in ns_views.list|dictsort:"url" %}
{% ifchanged %}
<div class="px-4 py-3 hover:bg-base-50 dark:hover:bg-base-800/30 transition">
<a href="{% url 'django-admindocs-views-detail' view=view.full_name %}" class="font-mono text-sm text-primary-600 dark:text-primary-500 hover:underline">{{ view.url }}</a>
<p class="mt-1 text-xs text-base-400 dark:text-base-500">
{% blocktranslate with view.full_name as full_name and view.url_name as url_name %}View function: <code>{{ full_name }}</code>. Name: <code>{{ url_name }}</code>.{% endblocktranslate %}
</p>
{% if view.title %}<p class="mt-1 text-sm text-base-600 dark:text-base-400">{{ view.title }}</p>{% endif %}
</div>
{% endifchanged %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="shrink-0 lg:w-56">
<div class="border border-base-200 rounded-default bg-white shadow-xs sticky top-4 dark:bg-base-900 dark:border-base-800">
<div class="border-b border-base-200 dark:border-base-800 px-4 py-3">
<h2 class="font-semibold text-sm text-base-900 dark:text-base-100">{% translate 'Jump to namespace' %}</h2>
</div>
<nav class="p-2">
{% for ns_views in views_by_ns %}
<a href="#ns-{{ ns_views.grouper|default:'empty' }}" class="block px-3 py-1.5 rounded-default text-sm text-base-600 hover:bg-base-50 hover:text-primary-600 dark:text-base-400 dark:hover:bg-base-800 dark:hover:text-primary-500">
{% if ns_views.grouper %}{{ ns_views.grouper }}{% else %}{% translate "Empty namespace" %}{% endif %}
</a>
{% endfor %}
</nav>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% load tz static %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} - API Documentation</title>
<link rel="icon" type="image/png" href="{% static 'favicon.ico' %}">
<script type="module" src="{% static 'js/rapidoc-min.js' %}"></script>
<style>
body {
margin: 0;
padding: 0;
}
rapi-doc {
--primary-color: #5c7182;
--bg-color: #fafafa;
--text-color: #22282d;
--nav-bg-color: #ffffff;
--nav-text-color: #22282d;
--nav-hover-bg-color: #e2e7ea;
--nav-accent-color: #5c7182;
}
@media (prefers-color-scheme: dark) {
rapi-doc {
--bg-color: #22282d;
--text-color: #e2e7ea;
--nav-bg-color: #353d44;
--nav-text-color: #e2e7ea;
--nav-hover-bg-color: #44515d;
}
}
</style>
</head>
<body>
<rapi-doc
spec-url="{{ schema_url }}"
heading-text="{{ title }}"
theme="dark"
render-style="read"
schema-style="table"
show-header="false"
show-info="true"
allow-authentication="true"
allow-server-selection="true"
allow-try="true"
allow-spec-url-load="false"
allow-spec-file-load="false"
show-method-in-nav-bar="as-colored-block"
nav-bg-color="#ffffff"
nav-text-color="#22282d"
nav-hover-bg-color="#e2e7ea"
nav-accent-color="#5c7182"
primary-color="#5c7182"
regular-font="system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
mono-font="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace"
load-fonts="false"
sort-endpoints-by="path"
>
<img slot="logo" src="{% static 'favicon.png' %}" alt="{{ title }}" style="max-height: 40px; max-width: 150px;">
</rapi-doc>
</body>
</html>

View file

@ -0,0 +1,35 @@
{% load unfold %}
{% load filters %}
{% with tabs=adminform|tabs %}
{% if tabs %}
{% with active_tab=tabs|tabs_active_unicode %}
<div class="{% fieldset_rows_classes %}" x-data="{ activeFieldsetTab: '{{ active_tab }}'}" x-show="activeTab == 'general'">
<div class="{% if adminform.model_admin.compressed_fields %}border-b border-base-200 border-dashed dark:border-base-800{% endif %}">
<nav class="bg-base-100 cursor-pointer flex flex-col font-medium gap-1 m-2 p-1 rounded-default text-important dark:border-base-700 md:inline-flex md:flex-row md:w-auto dark:bg-white/[.06] *:flex *:flex-row *:gap-1 *:items-center *:px-2.5 *:py-[5px] *:rounded-default *:hover:bg-base-700/[.06] *:dark:hover:bg-white/[.06] [&>.active]:bg-white [&>.active]:shadow-xs [&>.active]:dark:bg-base-700 [&>.active]:hover:bg-white [&>.active]:dark:hover:bg-base-700">
{% for fieldset in tabs %}
<a x-on:click="activeFieldsetTab = '{{ fieldset.name|unicode_slugify }}'" x-bind:class="activeFieldsetTab == '{{ fieldset.name|unicode_slugify }}' && 'active'">
{{ fieldset.name }}
{% with error_count=fieldset|tabs_errors_count %}
{% if error_count > 0 %}
<span data-testid="error-count" class="bg-red-500 inline-flex font-semibold items-center justify-center leading-none rounded-full w-4 h-4 text-white relative text-center text-[11px] whitespace-nowrap uppercase">
{{ error_count }}
</span>
{% endif %}
{% endwith %}
</a>
{% endfor %}
</nav>
</div>
{% for fieldset in tabs %}
<div class="tab-wrapper{% if fieldset.name %} fieldset-{{ fieldset.name|unicode_slugify }}{% endif %}"
x-show="activeFieldsetTab == '{{ fieldset.name|unicode_slugify }}'">
{% include 'admin/includes/fieldset.html' with stacked=1 %}
</div>
{% endfor %}
</div>
{% endwith %}
{% endif %}
{% endwith %}

View file

@ -1,4 +1,6 @@
from django import template from django import template
from django.contrib.admin.helpers import Fieldset
from django.utils.text import slugify
register = template.Library() register = template.Library()
@ -7,3 +9,30 @@ register = template.Library()
def endswith(value, arg): def endswith(value, arg):
"""Returns True if the value ends with the argument.""" """Returns True if the value ends with the argument."""
return str(value).endswith(arg) return str(value).endswith(arg)
@register.filter
def unicode_slugify(value):
"""Slugify that preserves non-ASCII characters (Russian, Chinese, etc.)."""
return slugify(str(value), allow_unicode=True)
@register.filter
def tabs_active_unicode(fieldsets: list[Fieldset]) -> str:
"""Unicode-safe version of Unfold's tabs_active filter."""
active = ""
if len(fieldsets) > 0 and hasattr(fieldsets[0], "name"):
active = slugify(str(fieldsets[0].name), allow_unicode=True)
for fieldset in fieldsets:
for field_line in fieldset:
for field in field_line:
if (
not field.is_readonly
and getattr(field, "errors", [])
and hasattr(fieldset, "name")
):
active = slugify(str(fieldset.name), allow_unicode=True)
return active

View file

@ -174,7 +174,7 @@ def get_top_returned_products(
p = product_by_id[pid] p = product_by_id[pid]
img = "" img = ""
with suppress(Exception): with suppress(Exception):
img = p.images.first().image_url if p.images.exists() else "" img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute]
result.append( result.append(
{ {
"name": p.name, "name": p.name,

View file

@ -8,7 +8,7 @@ from datetime import datetime
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
from math import ceil, log10 from math import ceil, log10
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, TypeVar
from constance import config from constance import config
from django.conf import settings from django.conf import settings
@ -36,6 +36,8 @@ from engine.payments.errors import RatesError
from engine.payments.utils import get_rates from engine.payments.utils import get_rates
from schon.utils.misc import LoggingError, LogLevel from schon.utils.misc import LoggingError, LogLevel
_BrandOrCategory = TypeVar("_BrandOrCategory", Brand, Category)
if TYPE_CHECKING: if TYPE_CHECKING:
from engine.core.models import OrderProduct from engine.core.models import OrderProduct
@ -320,8 +322,8 @@ class AbstractVendor(ABC):
@staticmethod @staticmethod
def _auto_resolver_helper( def _auto_resolver_helper(
model: type[Brand] | type[Category], resolving_name: str model: type[_BrandOrCategory], resolving_name: str
) -> Brand | Category | None: ) -> _BrandOrCategory | None:
"""Internal helper for resolving Brand/Category by name with deduplication.""" """Internal helper for resolving Brand/Category by name with deduplication."""
queryset = model.objects.filter(name=resolving_name) queryset = model.objects.filter(name=resolving_name)
if not queryset.exists(): if not queryset.exists():
@ -453,7 +455,7 @@ class AbstractVendor(ABC):
# step back 1 to land on a “9” ending # step back 1 to land on a “9” ending
psychological = next_threshold - 1 psychological = next_threshold - 1
return float(psychological) return float(psychological) if psychological > price else float(ceil(price))
def get_vendor_instance(self, safe: bool = False) -> Vendor | None: def get_vendor_instance(self, safe: bool = False) -> Vendor | None:
""" """
@ -672,6 +674,8 @@ class AbstractVendor(ABC):
.order_by("uuid") .order_by("uuid")
.first() .first()
) )
if not attribute:
return None
fields_to_update: list[str] = [] fields_to_update: list[str] = []
if not attribute.is_active: if not attribute.is_active:
attribute.is_active = True attribute.is_active = True

View file

@ -30,13 +30,10 @@ from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.vary import vary_on_headers from django.views.decorators.vary import vary_on_headers
from django.views.generic import TemplateView
from django_ratelimit.decorators import ratelimit from django_ratelimit.decorators import ratelimit
from drf_spectacular.utils import extend_schema_view from drf_spectacular.utils import extend_schema_view
from drf_spectacular.views import ( from drf_spectacular.views import SpectacularAPIView
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
from graphene_file_upload.django import FileUploadGraphQLView from graphene_file_upload.django import FileUploadGraphQLView
from rest_framework import status from rest_framework import status
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
@ -100,7 +97,7 @@ def sitemap_index(request, *args, **kwargs):
# noinspection PyTypeChecker # noinspection PyTypeChecker
sitemap_index.__doc__ = _( # pyright: ignore[reportUnknownVariableType] sitemap_index.__doc__ = _( # ty:ignore[invalid-assignment]
"Handles the request for the sitemap index and returns an XML response. " "Handles the request for the sitemap index and returns an XML response. "
"It ensures the response includes the appropriate content type header for XML." "It ensures the response includes the appropriate content type header for XML."
) )
@ -115,7 +112,7 @@ def sitemap_detail(request, *args, **kwargs):
# noinspection PyTypeChecker # noinspection PyTypeChecker
sitemap_detail.__doc__ = _( # pyright: ignore[reportUnknownVariableType] sitemap_detail.__doc__ = _( # ty:ignore[invalid-assignment]
"Handles the detailed view response for a sitemap. " "Handles the detailed view response for a sitemap. "
"This function processes the request, fetches the appropriate " "This function processes the request, fetches the appropriate "
"sitemap detail response, and sets the Content-Type header for XML." "sitemap detail response, and sets the Content-Type header for XML."
@ -133,19 +130,13 @@ class CustomSpectacularAPIView(SpectacularAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class CustomSwaggerView(SpectacularSwaggerView): class RapiDocView(TemplateView):
def get_context_data(self, **kwargs): template_name = "rapidoc.html"
# noinspection PyUnresolvedReferences
context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute]
context["script_url"] = self.request.build_absolute_uri()
return context
class CustomRedocView(SpectacularRedocView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# noinspection PyUnresolvedReferences context = super().get_context_data(**kwargs)
context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute] context["title"] = settings.SPECTACULAR_SETTINGS.get("TITLE", "API")
context["script_url"] = self.request.build_absolute_uri() context["schema_url"] = self.request.build_absolute_uri("/docs/schema/")
return context return context
@ -616,7 +607,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
product = Product.objects.filter(pk=wished_first["products"]).first() product = Product.objects.filter(pk=wished_first["products"]).first()
if product: if product:
img = ( img = (
product.images.first().image_url if product.images.exists() else "" product.images.first().image_url if product.images.exists() else "" # ty: ignore[possibly-missing-attribute]
) )
most_wished = { most_wished = {
"name": product.name, "name": product.name,
@ -640,7 +631,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
if not pid or pid not in product_by_id: if not pid or pid not in product_by_id:
continue continue
p = product_by_id[pid] p = product_by_id[pid]
img = p.images.first().image_url if p.images.exists() else "" img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute]
most_wished_list.append( most_wished_list.append(
{ {
"name": p.name, "name": p.name,
@ -696,10 +687,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
.order_by("total_qty")[:5] .order_by("total_qty")[:5]
) )
for p in products: for p in products:
qty = int(p.total_qty or 0) qty = int(p.total_qty or 0) # ty: ignore[possibly-missing-attribute]
img = "" img = ""
with suppress(Exception): with suppress(Exception):
img = p.images.first().image_url if p.images.exists() else "" img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute]
low_stock_list.append( low_stock_list.append(
{ {
"name": p.name, "name": p.name,
@ -743,7 +734,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
product = Product.objects.filter(pk=popular_first["product"]).first() product = Product.objects.filter(pk=popular_first["product"]).first()
if product: if product:
img = ( img = (
product.images.first().image_url if product.images.exists() else "" product.images.first().image_url if product.images.exists() else "" # ty: ignore[possibly-missing-attribute]
) )
most_popular = { most_popular = {
"name": product.name, "name": product.name,
@ -767,7 +758,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
if not pid or pid not in product_by_id: if not pid or pid not in product_by_id:
continue continue
p = product_by_id[pid] p = product_by_id[pid]
img = p.images.first().image_url if p.images.exists() else "" img = p.images.first().image_url if p.images.exists() else "" # ty: ignore[possibly-missing-attribute]
most_popular_list.append( most_popular_list.append(
{ {
"name": p.name, "name": p.name,

View file

@ -383,16 +383,18 @@ class BrandViewSet(SchonViewSet):
return obj return obj
def get_queryset(self): def get_queryset(self):
queryset = Brand.objects.all() qs = super().get_queryset()
if self.request.user.has_perm("core.view_brand"): # ty:ignore[possibly-missing-attribute]
if self.request.user.has_perm("view_category"): # ty:ignore[possibly-missing-attribute] if self.request.user.has_perm("core.view_brand"): # ty:ignore[possibly-missing-attribute]
queryset = queryset.prefetch_related("categories") return qs.prefetch_related("categories")
else: return qs.prefetch_related(
queryset = queryset.prefetch_related( Prefetch("categories", queryset=Category.objects.filter(is_active=True))
)
if self.request.user.has_perm("core.view_category"): # ty:ignore[possibly-missing-attribute]
return qs.filter(is_active=True).prefetch_related("categories")
return qs.filter(is_active=True).prefetch_related(
Prefetch("categories", queryset=Category.objects.filter(is_active=True)) Prefetch("categories", queryset=Category.objects.filter(is_active=True))
) )
return queryset
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@action( @action(

View file

@ -0,0 +1,253 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiExample,
OpenApiParameter,
OpenApiResponse,
extend_schema,
inline_serializer,
)
from rest_framework import serializers, status
from engine.core.docs.drf import error
_unsubscribe_token_param = OpenApiParameter(
name="token",
location=OpenApiParameter.QUERY,
description=_(
"UUID token for unsubscribing. This token is unique per user and is included "
"in the unsubscribe link of every campaign email. The token remains constant "
"for each user unless regenerated."
),
required=True,
type=str,
examples=[
OpenApiExample(
name="Valid token",
value="550e8400-e29b-41d4-a716-446655440000",
description="A valid UUID v4 unsubscribe token",
),
],
)
_unsubscribe_success_response = inline_serializer(
name="UnsubscribeSuccessResponse",
fields={
"detail": serializers.CharField(
default=_("You have been successfully unsubscribed from our emails.")
),
},
)
_unsubscribe_already_response = inline_serializer(
name="UnsubscribeAlreadyResponse",
fields={
"detail": serializers.CharField(default=_("You are already unsubscribed.")),
},
)
UNSUBSCRIBE_GET_SCHEMA = extend_schema(
tags=["emailing"],
operation_id="emailing_unsubscribe_get",
summary=_("Unsubscribe from email campaigns"),
description=_(
"Unsubscribe a user from all marketing email campaigns using their unique "
"unsubscribe token.\n\n"
"This endpoint is designed for email client compatibility where clicking a link "
"triggers a GET request. The user will no longer receive promotional emails "
"after successful unsubscription.\n\n"
"**Note:** Transactional emails (order confirmations, password resets, etc.) "
"are not affected by this setting."
),
parameters=[_unsubscribe_token_param],
responses={
status.HTTP_200_OK: OpenApiResponse(
response=_unsubscribe_success_response,
description=_("Successfully unsubscribed from email campaigns."),
examples=[
OpenApiExample(
name="Unsubscribed",
value={
"detail": "You have been successfully unsubscribed from our emails."
},
),
OpenApiExample(
name="Already unsubscribed",
value={"detail": "You are already unsubscribed."},
),
],
),
status.HTTP_400_BAD_REQUEST: OpenApiResponse(
response=error,
description=_("Invalid or missing unsubscribe token."),
examples=[
OpenApiExample(
name="Missing token",
value={"detail": "Unsubscribe token is required."},
),
OpenApiExample(
name="Invalid format",
value={"detail": "Invalid unsubscribe token format."},
),
],
),
status.HTTP_404_NOT_FOUND: OpenApiResponse(
response=error,
description=_("User associated with the token was not found."),
examples=[
OpenApiExample(
name="User not found",
value={"detail": "User not found."},
),
],
),
},
examples=[
OpenApiExample(
name="Unsubscribe request",
description="Example unsubscribe request with token",
value=None,
request_only=True,
),
],
)
UNSUBSCRIBE_POST_SCHEMA = extend_schema(
tags=["emailing"],
operation_id="emailing_unsubscribe_post",
summary=_("One-Click Unsubscribe (RFC 8058)"),
description=_(
"RFC 8058 compliant one-click unsubscribe endpoint for email campaigns.\n\n"
"This endpoint supports the List-Unsubscribe-Post header mechanism defined in "
"RFC 8058, which allows email clients to unsubscribe users with a single click "
"without leaving the email application.\n\n"
"The token can be provided either as a query parameter or in the request body.\n\n"
"**Standards Compliance:**\n"
"- RFC 8058: Signaling One-Click Functionality for List Email Headers\n"
"- RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands\n\n"
"**Note:** Transactional emails are not affected by this setting."
),
parameters=[_unsubscribe_token_param],
request=inline_serializer(
name="UnsubscribeRequest",
fields={
"token": serializers.UUIDField(
required=False,
help_text=_(
"Unsubscribe token (alternative to query parameter). "
"Can be omitted if token is provided in URL."
),
),
},
),
responses={
status.HTTP_200_OK: OpenApiResponse(
response=_unsubscribe_success_response,
description=_("Successfully unsubscribed from email campaigns."),
examples=[
OpenApiExample(
name="Unsubscribed",
value={
"detail": "You have been successfully unsubscribed from our emails."
},
),
OpenApiExample(
name="Already unsubscribed",
value={"detail": "You are already unsubscribed."},
),
],
),
status.HTTP_400_BAD_REQUEST: OpenApiResponse(
response=error,
description=_("Invalid or missing unsubscribe token."),
examples=[
OpenApiExample(
name="Missing token",
value={"detail": "Unsubscribe token is required."},
),
OpenApiExample(
name="Invalid format",
value={"detail": "Invalid unsubscribe token format."},
),
],
),
status.HTTP_404_NOT_FOUND: OpenApiResponse(
response=error,
description=_("User associated with the token was not found."),
examples=[
OpenApiExample(
name="User not found",
value={"detail": "User not found."},
),
],
),
},
external_docs={
"description": "RFC 8058 - Signaling One-Click Functionality",
"url": "https://datatracker.ietf.org/doc/html/rfc8058",
},
)
UNSUBSCRIBE_SCHEMA = {
"get": UNSUBSCRIBE_GET_SCHEMA,
"post": UNSUBSCRIBE_POST_SCHEMA,
}
TRACKING_SCHEMA = {
"get": extend_schema(
tags=["emailing"],
operation_id="emailing_tracking_pixel",
summary=_("Track email open event"),
description=_(
"Records when a campaign email is opened by the recipient.\n\n"
"This endpoint is called automatically when the tracking pixel (1x1 transparent GIF) "
"embedded in the email is loaded by the recipient's email client.\n\n"
"**How it works:**\n"
"1. Each campaign email contains a unique tracking pixel URL with a `tid` parameter\n"
"2. When the email is opened and images are loaded, this endpoint is called\n"
"3. The recipient's status is updated to 'opened' and the timestamp is recorded\n"
"4. The campaign's aggregate opened count is updated\n\n"
"**Privacy considerations:**\n"
"- Only the first open is recorded (subsequent opens are ignored)\n"
"- No personal information beyond the tracking ID is logged\n"
"- Users who disable image loading will not trigger this event\n\n"
"**Response:**\n"
"Returns a 1x1 transparent GIF image regardless of whether tracking succeeded, "
"to ensure consistent behavior and prevent information leakage."
),
parameters=[
OpenApiParameter(
name="tid",
location=OpenApiParameter.QUERY,
description=_(
"Tracking ID (UUID) unique to each campaign-recipient combination. "
"This ID links the open event to a specific recipient and campaign."
),
required=True,
type=str,
examples=[
OpenApiExample(
name="Valid tracking ID",
value="123e4567-e89b-12d3-a456-426614174000",
description="A valid UUID v4 tracking identifier",
),
],
),
],
responses={
status.HTTP_200_OK: OpenApiResponse(
response=OpenApiTypes.BINARY,
description=_(
"1x1 transparent GIF image. Always returned regardless of tracking status "
"to maintain consistent behavior."
),
),
status.HTTP_404_NOT_FOUND: OpenApiResponse(
description=_(
"Returned when no tracking ID is provided. Note: Invalid tracking IDs "
"still return 200 with the GIF to prevent enumeration attacks."
),
),
},
),
}

View file

@ -13,7 +13,7 @@ from engine.vibes_auth.serializers import (
TOKEN_OBTAIN_SCHEMA = { TOKEN_OBTAIN_SCHEMA = {
"post": extend_schema( "post": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("obtain a token pair"), summary=_("obtain a token pair"),
description=_("obtain a token pair (refresh and access) for authentication."), description=_("obtain a token pair (refresh and access) for authentication."),
@ -36,7 +36,7 @@ TOKEN_OBTAIN_SCHEMA = {
TOKEN_REFRESH_SCHEMA = { TOKEN_REFRESH_SCHEMA = {
"post": extend_schema( "post": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("refresh a token pair"), summary=_("refresh a token pair"),
description=_("refresh a token pair (refresh and access)."), description=_("refresh a token pair (refresh and access)."),
@ -59,7 +59,7 @@ TOKEN_REFRESH_SCHEMA = {
TOKEN_VERIFY_SCHEMA = { TOKEN_VERIFY_SCHEMA = {
"post": extend_schema( "post": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("verify a token"), summary=_("verify a token"),
description=_("Verify a token (refresh or access)."), description=_("Verify a token (refresh or access)."),

View file

@ -14,7 +14,7 @@ from engine.vibes_auth.serializers import (
USER_SCHEMA = { USER_SCHEMA = {
"create": extend_schema( "create": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("create a new user"), summary=_("create a new user"),
request=UserSerializer, request=UserSerializer,
@ -22,14 +22,14 @@ USER_SCHEMA = {
), ),
"retrieve": extend_schema( "retrieve": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("retrieve a user's details"), summary=_("retrieve a user's details"),
responses={status.HTTP_200_OK: UserSerializer, **BASE_ERRORS}, responses={status.HTTP_200_OK: UserSerializer, **BASE_ERRORS},
), ),
"update": extend_schema( "update": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("update a user's details"), summary=_("update a user's details"),
request=UserSerializer, request=UserSerializer,
@ -37,7 +37,7 @@ USER_SCHEMA = {
), ),
"partial_update": extend_schema( "partial_update": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("partially update a user's details"), summary=_("partially update a user's details"),
request=UserSerializer, request=UserSerializer,
@ -45,14 +45,14 @@ USER_SCHEMA = {
), ),
"destroy": extend_schema( "destroy": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("delete a user"), summary=_("delete a user"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
), ),
"reset_password": extend_schema( "reset_password": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("reset a user's password by sending a reset password email"), summary=_("reset a user's password by sending a reset password email"),
request=ResetPasswordSerializer, request=ResetPasswordSerializer,
@ -60,7 +60,7 @@ USER_SCHEMA = {
), ),
"upload_avatar": extend_schema( "upload_avatar": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("handle avatar upload for a user"), summary=_("handle avatar upload for a user"),
request={ request={
@ -78,7 +78,7 @@ USER_SCHEMA = {
), ),
"confirm_password_reset": extend_schema( "confirm_password_reset": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("confirm a user's password reset"), summary=_("confirm a user's password reset"),
request=ConfirmPasswordResetSerializer, request=ConfirmPasswordResetSerializer,
@ -90,7 +90,7 @@ USER_SCHEMA = {
), ),
"activate": extend_schema( "activate": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("activate a user's account"), summary=_("activate a user's account"),
request=ActivateEmailSerializer, request=ActivateEmailSerializer,

View file

@ -1,15 +1,17 @@
from uuid import UUID from uuid import UUID
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import OpenApiParameter, extend_schema from drf_spectacular.utils import extend_schema_view
from rest_framework import status from rest_framework import status
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from engine.vibes_auth.docs.drf.emailing import TRACKING_SCHEMA, UNSUBSCRIBE_SCHEMA
from engine.vibes_auth.models import User from engine.vibes_auth.models import User
@extend_schema_view(**UNSUBSCRIBE_SCHEMA)
class UnsubscribeView(APIView): class UnsubscribeView(APIView):
""" """
Public endpoint for one-click unsubscribe from email campaigns. Public endpoint for one-click unsubscribe from email campaigns.
@ -20,44 +22,10 @@ class UnsubscribeView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
authentication_classes = [] authentication_classes = []
@extend_schema(
summary="Unsubscribe from email campaigns",
description="Unsubscribe a user from email campaigns using their unsubscribe token.",
parameters=[
OpenApiParameter(
name="token",
description="Unsubscribe token from the email",
required=True,
type=str,
),
],
responses={
200: {"description": "Successfully unsubscribed"},
400: {"description": "Invalid or missing token"},
404: {"description": "User not found"},
},
)
def get(self, request): def get(self, request):
"""Handle GET request for unsubscribe (email link click).""" """Handle GET request for unsubscribe (email link click)."""
return self._process_unsubscribe(request) return self._process_unsubscribe(request)
@extend_schema(
summary="Unsubscribe from email campaigns (One-Click)",
description="RFC 8058 compliant one-click unsubscribe endpoint.",
parameters=[
OpenApiParameter(
name="token",
description="Unsubscribe token from the email",
required=True,
type=str,
),
],
responses={
200: {"description": "Successfully unsubscribed"},
400: {"description": "Invalid or missing token"},
404: {"description": "User not found"},
},
)
def post(self, request): def post(self, request):
"""Handle POST request for one-click unsubscribe (RFC 8058).""" """Handle POST request for one-click unsubscribe (RFC 8058)."""
return self._process_unsubscribe(request) return self._process_unsubscribe(request)
@ -103,6 +71,7 @@ class UnsubscribeView(APIView):
) )
@extend_schema_view(**TRACKING_SCHEMA)
class TrackingView(APIView): class TrackingView(APIView):
""" """
Endpoint for tracking email opens and clicks. Endpoint for tracking email opens and clicks.
@ -113,22 +82,6 @@ class TrackingView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
authentication_classes = [] authentication_classes = []
@extend_schema(
summary="Track email open",
description="Track when a campaign email is opened.",
parameters=[
OpenApiParameter(
name="tid",
description="Tracking ID from the email",
required=True,
type=str,
),
],
responses={
200: {"description": "Tracking recorded"},
404: {"description": "Invalid tracking ID"},
},
)
def get(self, request): def get(self, request):
"""Track email open via tracking pixel.""" """Track email open via tracking pixel."""
from django.utils import timezone from django.utils import timezone

View file

@ -180,7 +180,7 @@ class TokenObtainSerializer(Serializer):
@classmethod @classmethod
def get_token(cls, user: AuthUser) -> Token: def get_token(cls, user: AuthUser) -> Token:
if cls.token_class is not None: if cls.token_class is not None:
return cls.token_class.for_user(user) # ty: ignore[invalid-argument-type] return cls.token_class.for_user(user)
else: else:
raise RuntimeError(_("must set token_class attribute on class.")) raise RuntimeError(_("must set token_class attribute on class."))

View file

@ -1,9 +0,0 @@
import getpass
import bcrypt
print(
bcrypt.hashpw(
getpass.getpass("Password: ").encode("utf-8"), bcrypt.gensalt()
).decode()
)

View file

@ -3,16 +3,6 @@ global:
evaluation_interval: 15s evaluation_interval: 15s
scrape_configs: scrape_configs:
- job_name: 'app'
metrics_path: /prometheus/metrics
scheme: http
static_configs:
- targets: [ 'app:8000' ]
- job_name: 'worker'
static_configs:
- targets: [ 'worker:8888' ]
- job_name: 'database' - job_name: 'database'
static_configs: static_configs:
- targets: [ 'database_exporter:9187' ] - targets: [ 'database_exporter:9187' ]

View file

@ -1,2 +0,0 @@
basic_auth_users:
schon: $2b$12$0HraDYmrZnJ089LcH9Vsn.Wv5V5a8oDlucTNm0.5obhULjPyLiYoy

View file

@ -12,10 +12,10 @@ upstream storefront_frontend {
server { server {
listen 443 ssl http2; listen 443 ssl http2;
server_name api.schon.fureunoir.com; server_name api.schon.wiseless.xyz;
ssl_certificate /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/schon.wiseless.xyz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/schon.wiseless.xyz/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -72,10 +72,10 @@ server {
server { server {
listen 443 ssl http2; listen 443 ssl http2;
server_name schon.fureunoir.com www.schon.fureunoir.com; server_name schon.wiseless.xyz www.schon.wiseless.xyz;
ssl_certificate /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/schon.wiseless.xyz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/schon.wiseless.xyz/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -128,10 +128,10 @@ server {
server { server {
listen 443 ssl http2; listen 443 ssl http2;
server_name prometheus.schon.fureunoir.com; server_name prometheus.schon.wiseless.xyz;
ssl_certificate /etc/letsencrypt/live/schon.fureunoir.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/schon.wiseless.xyz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/schon.wiseless.xyz/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -173,6 +173,6 @@ server {
server { server {
listen 80; listen 80;
server_name api.schon.fureunoir.com www.schon.fureunoir.com schon.fureunoir.com prometheus.schon.fureunoir.com; server_name api.schon.wiseless.xyz www.schon.wiseless.xyz schon.wiseless.xyz prometheus.schon.wiseless.xyz;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }

View file

@ -6,70 +6,71 @@ authors = [{ name = "fureunoir", email = "contact@fureunoir.com" }]
readme = "README.md" readme = "README.md"
requires-python = ">=3.12,<=3.13" requires-python = ">=3.12,<=3.13"
dependencies = [ dependencies = [
"aiogram==3.24.0", "aiogram==3.25.0",
"aiosmtpd==1.4.6", "aiosmtpd==1.4.6",
"channels==4.3.2", "channels==4.3.2",
"channels-redis==4.3.0", "channels-redis==4.3.0",
"colorlog==6.10.1", "colorlog==6.10.1",
"coverage==7.13.2", "coverage==7.13.4",
"click==8.3.1", "click==8.3.1",
"cryptography==46.0.3", "cryptography==46.0.5",
"django==5.2.9", "django==5.2.11",
"django-cacheops==7.2", "django-cacheops==7.2",
"django-constance==4.3.4", "django-constance==4.3.4",
"django-cors-headers==4.9.0", "django-cors-headers==4.9.0",
"django-dbbackup==5.1.2", "django-dbbackup==5.2.0",
"django-elasticsearch-dsl==8.2", "django-elasticsearch-dsl==8.2",
"django-extensions==4.1", "django-extensions==4.1",
"django-filter==25.2", "django-filter==25.2",
"django-health-check==3.20.8", "django-health-check==4.0.6",
"django-import-export[all]==4.4.0", "django-import-export[all]==4.4.0",
"django-json-widget==2.1.1", "django-json-widget==2.1.1",
"django-model-utils==5.0.0", "django-model-utils==5.0.0",
"django-md-field==0.1.0", "django-md-field==0.1.0",
"django-modeltranslation==0.19.19", "django-modeltranslation==0.19.19",
"django-mptt==0.18.0", "django-mptt==0.18.0",
"django-prometheus==2.4.1",
"django-redis==6.0.0", "django-redis==6.0.0",
"django-ratelimit==4.1.0", "django-ratelimit==4.1.0",
"django-storages==1.14.6", "django-storages==1.14.6",
"django-unfold==0.76.0", "django-unfold==0.81.0",
"django-debug-toolbar==6.2.0",
"django-widget-tweaks==1.5.1", "django-widget-tweaks==1.5.1",
"djangorestframework==3.16.1", "djangorestframework==3.16.1",
"djangorestframework-recursive==0.1.2", "djangorestframework-recursive==0.1.2",
"djangorestframework-simplejwt[crypto]==5.5.1", "djangorestframework-simplejwt[crypto]==5.5.1",
"djangorestframework-xml==2.0.0", "djangorestframework-xml==2.0.0",
"djangorestframework-yaml==2.0.0", "djangorestframework-yaml==2.0.0",
"djangoql==0.18.1", "djangoql==0.19.1",
"docutils==0.22.4", "docutils==0.22.4",
"drf-spectacular[sidecar]==0.29.0", "drf-spectacular==0.29.0",
"drf-spectacular-websocket==1.3.1", "drf-spectacular-websocket==1.3.1",
"drf-orjson-renderer==1.8.0", "drf-orjson-renderer==1.8.0",
"elasticsearch-dsl==8.18.0", "elasticsearch-dsl==8.18.0",
"filelock==3.20.3", "filelock==3.24.3",
"filetype==1.2.0", "filetype==1.2.0",
"graphene-django==3.2.3", "graphene-django==3.2.3",
"graphene-file-upload==1.3.0", "graphene-file-upload==1.3.0",
"httpx==0.28.1", "httpx==0.28.1",
"opentelemetry-instrumentation-django==0.60b1",
"paramiko==4.0.0", "paramiko==4.0.0",
"pillow==12.1.0", "pillow==12.1.1",
"pip==25.3", "pip==26.0.1",
"polib==1.2.0", "polib==1.2.0",
"PyJWT==2.10.1", "PyJWT==2.11.0",
"pytest==9.0.2", "pytest==9.0.2",
"pytest-django==4.11.1", "pytest-django==4.12.0",
"python-slugify==8.0.4", "python-slugify==8.0.4",
"psutil==7.2.1", "psutil==7.2.2",
"psycopg[binary]==3.2.9", "psycopg[binary]==3.3.3",
"redis==7.1.0", "redis==7.2.1",
"requests==2.32.5", "requests==2.32.5",
"sentry-sdk[django,celery,opentelemetry]==2.50.0", "sentry-sdk[django,celery,opentelemetry]==2.53.0",
"six==1.17.0", "six==1.17.0",
"swapper==1.4.0", "swapper==1.4.0",
"uvicorn==0.40.0", "uvicorn==0.41.0",
"zeep==4.3.2", "zeep==4.3.2",
"websockets==16.0", "websockets==16.0",
"whitenoise==6.11.0", "whitenoise==6.12.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@ -79,20 +80,20 @@ worker = [
"django-celery-results==2.6.0", "django-celery-results==2.6.0",
] ]
linting = [ linting = [
"ty==0.0.13", "ty==0.0.16",
"ruff==0.14.14", "ruff==0.15.4",
"celery-types==0.24.0", "celery-types==0.24.0",
"django-stubs==5.2.9", "django-stubs==5.2.9",
"djangorestframework-stubs==3.16.7", "djangorestframework-stubs==3.16.8",
"types-requests==2.32.4.20260107", "types-requests==2.32.4.20260107",
"types-redis==4.6.0.20241004", "types-redis==4.6.0.20241004",
"types-paramiko==4.0.0.20250822", "types-paramiko==4.0.0.20250822",
"types-psutil==7.2.1.20260116", "types-psutil==7.2.2.20260130",
"types-pillow==10.2.0.20240822", "types-pillow==10.2.0.20240822",
"types-docutils==0.22.3.20251115", "types-docutils==0.22.3.20260223",
"types-six==1.17.0.20251009", "types-six==1.17.0.20251009",
] ]
openai = ["openai==2.15.0"] openai = ["openai==2.24.0"]
jupyter = ["jupyter==1.1.1"] jupyter = ["jupyter==1.1.1"]
[tool.uv] [tool.uv]

View file

@ -108,7 +108,6 @@ UNSAFE_CACHE_KEYS: list[str] = []
SITE_ID: int = 1 SITE_ID: int = 1
INSTALLED_APPS: list[str] = [ INSTALLED_APPS: list[str] = [
"django_prometheus",
"unfold", "unfold",
"unfold.contrib.filters", "unfold.contrib.filters",
"unfold.contrib.forms", "unfold.contrib.forms",
@ -128,15 +127,6 @@ INSTALLED_APPS: list[str] = [
"django.contrib.gis", "django.contrib.gis",
"django.contrib.humanize", "django.contrib.humanize",
"health_check", "health_check",
"health_check.db",
"health_check.cache",
"health_check.storage",
"health_check.contrib.migrations",
"health_check.contrib.celery_ping",
"health_check.contrib.psutil",
"health_check.contrib.redis",
"health_check.contrib.db_heartbeat",
"health_check.contrib.mail",
"cacheops", "cacheops",
"django_celery_beat", "django_celery_beat",
"django_celery_results", "django_celery_results",
@ -168,11 +158,11 @@ INSTALLED_APPS: list[str] = [
if DEBUG: if DEBUG:
wn_app_index = INSTALLED_APPS.index("django.contrib.staticfiles") - 1 wn_app_index = INSTALLED_APPS.index("django.contrib.staticfiles") - 1
INSTALLED_APPS.insert(wn_app_index, "whitenoise.runserver_nostatic") INSTALLED_APPS.insert(wn_app_index, "whitenoise.runserver_nostatic")
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE: list[str] = [ MIDDLEWARE: list[str] = [
"schon.middleware.BlockInvalidHostMiddleware", "schon.middleware.BlockInvalidHostMiddleware",
"schon.middleware.RateLimitMiddleware", "schon.middleware.RateLimitMiddleware",
"django_prometheus.middleware.PrometheusBeforeMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@ -185,9 +175,14 @@ MIDDLEWARE: list[str] = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"schon.middleware.CustomLocaleMiddleware", "schon.middleware.CustomLocaleMiddleware",
"schon.middleware.CamelCaseMiddleWare", "schon.middleware.CamelCaseMiddleWare",
"django_prometheus.middleware.PrometheusAfterMiddleware",
] ]
if DEBUG:
MIDDLEWARE.insert(
MIDDLEWARE.index("django.contrib.sessions.middleware.SessionMiddleware"),
"debug_toolbar.middleware.DebugToolbarMiddleware",
)
TEMPLATES: list[ TEMPLATES: list[
dict[str, str | list[str | Path] | dict[str, str | list[str]] | Path | bool] dict[str, str | list[str | Path] | dict[str, str | list[str]] | Path | bool]
] = [ ] = [
@ -251,7 +246,7 @@ LANGUAGES: tuple[tuple[str, str], ...] = (
("zh-hans", "简体中文"), ("zh-hans", "简体中文"),
) )
LANGUAGE_CODE: str = "en-gb" LANGUAGE_CODE: str = getenv("SCHON_LANGUAGE_CODE", "en-gb")
LANGUAGES_FLAGS: dict[str, str] = { LANGUAGES_FLAGS: dict[str, str] = {
"ar-ar": "🇸🇦", "ar-ar": "🇸🇦",
@ -402,6 +397,16 @@ INTERNAL_IPS: list[str] = [
"127.0.0.1", "127.0.0.1",
] ]
if DEBUG:
import socket
# Docker: resolve container's gateway IP so debug toolbar works
try:
_, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
except socket.gaierror:
pass
if getenv("SENTRY_DSN"): if getenv("SENTRY_DSN"):
import sentry_sdk import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.celery import CeleryIntegration

View file

@ -5,7 +5,7 @@ from schon.settings.base import REDIS_PASSWORD
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django_prometheus.cache.backends.redis.RedisCache", "BACKEND": "django_redis.cache.RedisCache",
"LOCATION": getenv( "LOCATION": getenv(
"CELERY_BROKER_URL", f"redis://:{REDIS_PASSWORD}@redis:6379/0" "CELERY_BROKER_URL", f"redis://:{REDIS_PASSWORD}@redis:6379/0"
), ),

View file

@ -2,7 +2,7 @@ from os import getenv
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django_prometheus.db.backends.postgis", "ENGINE": "django.contrib.gis.db.backends.postgis",
"NAME": getenv("POSTGRES_DB"), "NAME": getenv("POSTGRES_DB"),
"USER": getenv("POSTGRES_USER"), "USER": getenv("POSTGRES_USER"),
"PASSWORD": getenv("POSTGRES_PASSWORD"), "PASSWORD": getenv("POSTGRES_PASSWORD"),

View file

@ -88,7 +88,6 @@ The API supports multiple response formats:
## Health & Monitoring ## Health & Monitoring
- Health checks: `/health/` - Health checks: `/health/`
- Prometheus metrics: `/prometheus/metrics/`
## Version ## Version
Current API version: {version} Current API version: {version}
@ -118,20 +117,12 @@ SPECTACULAR_SETTINGS = {
"DESCRIPTION": SPECTACULAR_DESCRIPTION, "DESCRIPTION": SPECTACULAR_DESCRIPTION,
"VERSION": SCHON_VERSION, # noqa: F405 "VERSION": SCHON_VERSION, # noqa: F405
"TOS": "https://schon.wiseless.xyz/terms-of-service", "TOS": "https://schon.wiseless.xyz/terms-of-service",
"SWAGGER_UI_DIST": "SIDECAR",
"CAMELIZE_NAMES": True, "CAMELIZE_NAMES": True,
"POSTPROCESSING_HOOKS": [ "POSTPROCESSING_HOOKS": [
"schon.utils.renderers.camelize_serializer_fields", "schon.utils.renderers.camelize_serializer_fields",
"drf_spectacular.hooks.postprocess_schema_enums", "drf_spectacular.hooks.postprocess_schema_enums",
], ],
"REDOC_DIST": "SIDECAR",
"ENABLE_DJANGO_DEPLOY_CHECK": not DEBUG, # noqa: F405 "ENABLE_DJANGO_DEPLOY_CHECK": not DEBUG, # noqa: F405
"SWAGGER_UI_FAVICON_HREF": r"/static/favicon.png",
"SWAGGER_UI_SETTINGS": {
"connectSocket": False,
"socketMaxMessages": 8,
"socketMessagesInitialOpened": False,
},
"SERVERS": [ "SERVERS": [
{ {
"url": f"https://api.{BASE_DOMAIN}/", "url": f"https://api.{BASE_DOMAIN}/",

View file

@ -49,7 +49,7 @@ UNFOLD: dict[str, Any] = {
{ {
"icon": "health_metrics", "icon": "health_metrics",
"title": _("Health"), "title": _("Health"),
"link": reverse_lazy("health_check:health_check_home"), "link": reverse_lazy("health_check"),
}, },
{ {
"title": _("Support"), "title": _("Support"),
@ -116,14 +116,9 @@ UNFOLD: dict[str, Any] = {
"link": reverse_lazy("core:sitemap-index"), "link": reverse_lazy("core:sitemap-index"),
}, },
{ {
"title": "Swagger", "title": "API Docs",
"icon": "integration_instructions", "icon": "api",
"link": reverse_lazy("swagger-ui-platform"), "link": reverse_lazy("rapidoc-platform"),
},
{
"title": "Redoc",
"icon": "integration_instructions",
"link": reverse_lazy("redoc-ui-platform"),
}, },
{ {
"title": "GraphQL", "title": "GraphQL",

View file

@ -3,13 +3,14 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from health_check.views import HealthCheckView
from redis.asyncio import Redis as RedisClient
from engine.core.graphene.schema import schema from engine.core.graphene.schema import schema
from engine.core.views import ( from engine.core.views import (
CustomGraphQLView, CustomGraphQLView,
CustomRedocView,
CustomSpectacularAPIView, CustomSpectacularAPIView,
CustomSwaggerView, RapiDocView,
favicon_view, favicon_view,
index, index,
) )
@ -22,16 +23,28 @@ urlpatterns = [
index, index,
), ),
path( path(
r"health/", "health/",
include( HealthCheckView.as_view(
"health_check.urls", checks=[
"health_check.Cache",
"health_check.DNS",
"health_check.Database",
"health_check.Mail",
"health_check.Storage",
"health_check.contrib.psutil.Disk",
"health_check.contrib.psutil.Memory",
"health_check.contrib.celery.Ping",
(
"health_check.contrib.redis.Redis",
{
"client_factory": lambda: RedisClient.from_url(
settings.CELERY_BROKER_URL
)
},
), ),
],
), ),
path( name="health_check",
r"prometheus/",
include(
"django_prometheus.urls",
),
), ),
path( path(
r"i18n/setlang/", r"i18n/setlang/",
@ -55,19 +68,14 @@ urlpatterns = [
### DOCUMENTATION URLS ### ### DOCUMENTATION URLS ###
path( path(
r"docs/", r"docs/",
RapiDocView.as_view(),
name="rapidoc-platform",
),
path(
r"docs/schema/",
CustomSpectacularAPIView.as_view(urlconf="schon.urls"), CustomSpectacularAPIView.as_view(urlconf="schon.urls"),
name="schema-platform", name="schema-platform",
), ),
path(
r"docs/swagger/",
CustomSwaggerView.as_view(url_name="schema-platform"),
name="swagger-ui-platform",
),
path(
r"docs/redoc/",
CustomRedocView.as_view(url_name="schema-platform"),
name="redoc-ui-platform",
),
### ENGINE APPS URLS ### ### ENGINE APPS URLS ###
path( path(
r"b2b/", r"b2b/",
@ -101,4 +109,7 @@ urlpatterns = [
] ]
if settings.DEBUG: if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns += debug_toolbar_urls()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -10,7 +10,10 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
import os import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
from opentelemetry.instrumentation.django import DjangoInstrumentor
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "schon.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "schon.settings")
DjangoInstrumentor().instrument()
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -44,17 +44,18 @@ if [ -f .env ]; then
fi fi
SCHON_PROJECT_NAME=$(prompt_default SCHON_PROJECT_NAME Schon) SCHON_PROJECT_NAME=$(prompt_default SCHON_PROJECT_NAME Schon)
SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.fureunoir.com) SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.wiseless.xyz)
SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.fureunoir.com) SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.wiseless.xyz)
SENTRY_DSN=$(prompt_default SENTRY_DSN "") SENTRY_DSN=$(prompt_default SENTRY_DSN "")
DEBUG=$(prompt_default DEBUG 1) DEBUG=$(prompt_default DEBUG 1)
TIME_ZONE=$(prompt_default TIME_ZONE "Europe/London") TIME_ZONE=$(prompt_default TIME_ZONE "Europe/London")
SCHON_LANGUAGE_CODE=$(prompt_default SCHON_LANGUAGE_CODE "en-gb")
SECRET_KEY=$(prompt_autogen SECRET_KEY 32) SECRET_KEY=$(prompt_autogen SECRET_KEY 32)
JWT_SIGNING_KEY=$(prompt_autogen JWT_SIGNING_KEY 64) JWT_SIGNING_KEY=$(prompt_autogen JWT_SIGNING_KEY 64)
ALLOWED_HOSTS=$(prompt_default ALLOWED_HOSTS "schon.fureunoir.com api.schon.fureunoir.com") ALLOWED_HOSTS=$(prompt_default ALLOWED_HOSTS "schon.wiseless.xyz api.schon.wiseless.xyz")
CSRF_TRUSTED_ORIGINS=$(prompt_default CSRF_TRUSTED_ORIGINS "https://schon.fureunoir.com https://api.schon.fureunoir.com https://www.schon.fureunoir.com") CSRF_TRUSTED_ORIGINS=$(prompt_default CSRF_TRUSTED_ORIGINS "https://schon.wiseless.xyz https://api.schon.wiseless.xyz https://www.schon.wiseless.xyz")
CORS_ALLOWED_ORIGINS=$(prompt_default CORS_ALLOWED_ORIGINS "$CSRF_TRUSTED_ORIGINS") CORS_ALLOWED_ORIGINS=$(prompt_default CORS_ALLOWED_ORIGINS "$CSRF_TRUSTED_ORIGINS")
POSTGRES_DB=$(prompt_default POSTGRES_DB schon) POSTGRES_DB=$(prompt_default POSTGRES_DB schon)
@ -73,11 +74,11 @@ PROMETHEUS_USER=$(prompt_default PROMETHEUS_USER schon)
PROMETHEUS_PASSWORD=$(prompt_autogen PROMETHEUS_PASSWORD 16) PROMETHEUS_PASSWORD=$(prompt_autogen PROMETHEUS_PASSWORD 16)
EMAIL_BACKEND=$(prompt_default EMAIL_BACKEND django.core.mail.backends.smtp.EmailBackend) EMAIL_BACKEND=$(prompt_default EMAIL_BACKEND django.core.mail.backends.smtp.EmailBackend)
EMAIL_HOST=$(prompt_default EMAIL_HOST smtp.whatever.schon.fureunoir.com) EMAIL_HOST=$(prompt_default EMAIL_HOST smtp.whatever.schon.wiseless.xyz)
EMAIL_PORT=$(prompt_default EMAIL_PORT 465) EMAIL_PORT=$(prompt_default EMAIL_PORT 465)
EMAIL_USE_TLS=$(prompt_default EMAIL_USE_TLS 0) EMAIL_USE_TLS=$(prompt_default EMAIL_USE_TLS 0)
EMAIL_USE_SSL=$(prompt_default EMAIL_USE_SSL 1) EMAIL_USE_SSL=$(prompt_default EMAIL_USE_SSL 1)
EMAIL_HOST_USER=$(prompt_default EMAIL_HOST_USER your-email-user@whatever.schon.fureunoir.com) EMAIL_HOST_USER=$(prompt_default EMAIL_HOST_USER your-email-user@whatever.schon.wiseless.xyz)
EMAIL_FROM=$EMAIL_HOST_USER EMAIL_FROM=$EMAIL_HOST_USER
EMAIL_HOST_PASSWORD=$(prompt_default EMAIL_HOST_PASSWORD SUPERSECRETEMAILHOSTPASSWORD) EMAIL_HOST_PASSWORD=$(prompt_default EMAIL_HOST_PASSWORD SUPERSECRETEMAILHOSTPASSWORD)
@ -95,6 +96,8 @@ SCHON_STOREFRONT_DOMAIN="${SCHON_STOREFRONT_DOMAIN}"
SCHON_BASE_DOMAIN="${SCHON_BASE_DOMAIN}" SCHON_BASE_DOMAIN="${SCHON_BASE_DOMAIN}"
SENTRY_DSN="${SENTRY_DSN}" SENTRY_DSN="${SENTRY_DSN}"
DEBUG=${DEBUG} DEBUG=${DEBUG}
TIME_ZONE="${TIME_ZONE}"
SCHON_LANGUAGE_CODE="${SCHON_LANGUAGE_CODE}"
SECRET_KEY="${SECRET_KEY}" SECRET_KEY="${SECRET_KEY}"
JWT_SIGNING_KEY="${JWT_SIGNING_KEY}" JWT_SIGNING_KEY="${JWT_SIGNING_KEY}"

View file

@ -81,6 +81,10 @@ case "$install_choice" in
fi fi
log_success "Images built successfully" log_success "Images built successfully"
# Generate Prometheus web config from .env
log_step "Generating Prometheus web config..."
generate_prometheus_web_config
echo echo
log_result "Docker installation complete!" log_result "Docker installation complete!"
log_info "You can now use: make run" log_info "You can now use: make run"

View file

@ -3,6 +3,10 @@ set -euo pipefail
source ./scripts/Unix/starter.sh source ./scripts/Unix/starter.sh
# Generate Prometheus web config from .env
log_step "Generating Prometheus web config..."
generate_prometheus_web_config
# Shutdown services # Shutdown services
log_step "Shutting down..." log_step "Shutting down..."
if ! output=$(docker compose down 2>&1); then if ! output=$(docker compose down 2>&1); then

View file

@ -20,6 +20,10 @@ else
log_warning "jq is not installed; skipping image verification step." log_warning "jq is not installed; skipping image verification step."
fi fi
# Generate Prometheus web config from .env
log_step "Generating Prometheus web config..."
generate_prometheus_web_config
# Start services # Start services
log_step "Spinning services up..." log_step "Spinning services up..."
if ! output=$(docker compose up --no-build --detach --wait 2>&1); then if ! output=$(docker compose up --no-build --detach --wait 2>&1); then

View file

@ -50,17 +50,18 @@ if (Test-Path '.env')
} }
$SCHON_PROJECT_NAME = Prompt-Default 'SCHON_PROJECT_NAME' 'Schon' $SCHON_PROJECT_NAME = Prompt-Default 'SCHON_PROJECT_NAME' 'Schon'
$SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.fureunoir.com' $SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.wiseless.xyz'
$SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.fureunoir.com' $SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.wiseless.xyz'
$SENTRY_DSN = Prompt-Default 'SENTRY_DSN' '' $SENTRY_DSN = Prompt-Default 'SENTRY_DSN' ''
$DEBUG = Prompt-Default 'DEBUG' '1' $DEBUG = Prompt-Default 'DEBUG' '1'
$TIME_ZONE = Prompt-Default 'TIME_ZONE' 'Europe/London' $TIME_ZONE = Prompt-Default 'TIME_ZONE' 'Europe/London'
$SCHON_LANGUAGE_CODE = Prompt-Default 'SCHON_LANGUAGE_CODE' 'en-gb'
$SECRET_KEY = Prompt-AutoGen 'SECRET_KEY' 32 $SECRET_KEY = Prompt-AutoGen 'SECRET_KEY' 32
$JWT_SIGNING_KEY = Prompt-AutoGen 'JWT_SIGNING_KEY' 64 $JWT_SIGNING_KEY = Prompt-AutoGen 'JWT_SIGNING_KEY' 64
$ALLOWED_HOSTS = Prompt-Default 'ALLOWED_HOSTS' 'schon.fureunoir.com api.schon.fureunoir.com' $ALLOWED_HOSTS = Prompt-Default 'ALLOWED_HOSTS' 'schon.wiseless.xyz api.schon.wiseless.xyz'
$CSRF_TRUSTED_ORIGINS = Prompt-Default 'CSRF_TRUSTED_ORIGINS' 'https://schon.fureunoir.com https://api.schon.fureunoir.com https://www.schon.fureunoir.com' $CSRF_TRUSTED_ORIGINS = Prompt-Default 'CSRF_TRUSTED_ORIGINS' 'https://schon.wiseless.xyz https://api.schon.wiseless.xyz https://www.schon.wiseless.xyz'
$CORS_ALLOWED_ORIGINS = Prompt-Default 'CORS_ALLOWED_ORIGINS' $CSRF_TRUSTED_ORIGINS $CORS_ALLOWED_ORIGINS = Prompt-Default 'CORS_ALLOWED_ORIGINS' $CSRF_TRUSTED_ORIGINS
$POSTGRES_DB = Prompt-Default 'POSTGRES_DB' 'schon' $POSTGRES_DB = Prompt-Default 'POSTGRES_DB' 'schon'
@ -80,11 +81,11 @@ $PROMETHEUS_USER = Prompt-Default 'PROMETHEUS_USER' 'schon'
$PROMETHEUS_PASSWORD = Prompt-AutoGen 'PROMETHEUS_PASSWORD' 16 $PROMETHEUS_PASSWORD = Prompt-AutoGen 'PROMETHEUS_PASSWORD' 16
$EMAIL_BACKEND = Prompt-Default 'EMAIL_BACKEND' 'django.core.mail.backends.smtp.EmailBackend' $EMAIL_BACKEND = Prompt-Default 'EMAIL_BACKEND' 'django.core.mail.backends.smtp.EmailBackend'
$EMAIL_HOST = Prompt-Default 'EMAIL_HOST' 'smtp.whatever.schon.fureunoir.com' $EMAIL_HOST = Prompt-Default 'EMAIL_HOST' 'smtp.whatever.schon.wiseless.xyz'
$EMAIL_PORT = Prompt-Default 'EMAIL_PORT' '465' $EMAIL_PORT = Prompt-Default 'EMAIL_PORT' '465'
$EMAIL_USE_TLS = Prompt-Default 'EMAIL_USE_TLS' '0' $EMAIL_USE_TLS = Prompt-Default 'EMAIL_USE_TLS' '0'
$EMAIL_USE_SSL = Prompt-Default 'EMAIL_USE_SSL' '1' $EMAIL_USE_SSL = Prompt-Default 'EMAIL_USE_SSL' '1'
$EMAIL_HOST_USER = Prompt-Default 'EMAIL_HOST_USER' 'your-email-user@whatever.schon.fureunoir.com' $EMAIL_HOST_USER = Prompt-Default 'EMAIL_HOST_USER' 'your-email-user@whatever.schon.wiseless.xyz'
$EMAIL_FROM = Prompt-Default 'EMAIL_FROM' $EMAIL_HOST_USER $EMAIL_FROM = Prompt-Default 'EMAIL_FROM' $EMAIL_HOST_USER
$EMAIL_HOST_PASSWORD = Prompt-Default 'EMAIL_HOST_PASSWORD' 'SUPERSECRETEMAILHOSTPASSWORD' $EMAIL_HOST_PASSWORD = Prompt-Default 'EMAIL_HOST_PASSWORD' 'SUPERSECRETEMAILHOSTPASSWORD'
@ -102,6 +103,8 @@ $lines = @(
"SCHON_BASE_DOMAIN=""$SCHON_BASE_DOMAIN""" "SCHON_BASE_DOMAIN=""$SCHON_BASE_DOMAIN"""
"SENTRY_DSN=""$SENTRY_DSN""" "SENTRY_DSN=""$SENTRY_DSN"""
"DEBUG=$DEBUG" "DEBUG=$DEBUG"
"TIME_ZONE=""$TIME_ZONE"""
"SCHON_LANGUAGE_CODE=""$SCHON_LANGUAGE_CODE"""
"" ""
"SECRET_KEY=""$SECRET_KEY""" "SECRET_KEY=""$SECRET_KEY"""
"JWT_SIGNING_KEY=""$JWT_SIGNING_KEY""" "JWT_SIGNING_KEY=""$JWT_SIGNING_KEY"""

View file

@ -42,5 +42,9 @@ if ($LASTEXITCODE -ne 0) {
} }
Write-Success "Images built successfully" Write-Success "Images built successfully"
# Generate Prometheus web config from .env
Write-Step "Generating Prometheus web config..."
New-PrometheusWebConfig
Write-Result "" Write-Result ""
Write-Result "You can now use run.ps1 script or run: make run" Write-Result "You can now use run.ps1 script or run: make run"

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