diff --git a/.gitignore b/.gitignore index c9d46f23..66ea8e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -192,7 +192,9 @@ engine/core/vendors/docs/* # Production .initialized -queries/ +./queries +./nginx.conf +monitoring/web.yml # AI assistants .claude/ diff --git a/Makefile b/Makefile index 9845e4e2..12e60654 100644 --- a/Makefile +++ b/Makefile @@ -106,17 +106,3 @@ migrate: clear @$(call RUN_SCRIPT,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 diff --git a/engine/blog/migrations/0009_alter_post_slug.py b/engine/blog/migrations/0009_alter_post_slug.py new file mode 100644 index 00000000..1b886f71 --- /dev/null +++ b/engine/blog/migrations/0009_alter_post_slug.py @@ -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, + ), + ), + ] diff --git a/engine/blog/models.py b/engine/blog/models.py index 23e9cdd7..5706d3a8 100644 --- a/engine/blog/models.py +++ b/engine/blog/models.py @@ -9,9 +9,9 @@ from django.db.models import ( TextField, ) from django.utils.translation import gettext_lazy as _ -from django_extensions.db.fields import AutoSlugField from engine.core.abstract import NiceModel +from engine.core.utils.db import TweakedAutoSlugField, unicode_slugify_function class Post(NiceModel): @@ -47,8 +47,15 @@ class Post(NiceModel): null=True, ) file = FileField(upload_to="posts/", blank=True, null=True) - slug = AutoSlugField( - populate_from="title", allow_unicode=True, unique=True, editable=False + slug = TweakedAutoSlugField( + 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") meta_description = CharField(max_length=150, blank=True, null=True) diff --git a/engine/core/admin.py b/engine/core/admin.py index 94df948e..d6f06de4 100644 --- a/engine/core/admin.py +++ b/engine/core/admin.py @@ -421,10 +421,7 @@ class BrandAdmin( "priority", "is_active", ) - list_filter = ( - "categories", - "is_active", - ) + list_filter = ("is_active",) search_fields = ( "uuid", "name", diff --git a/engine/core/elasticsearch/documents.py b/engine/core/elasticsearch/documents.py index cf7f05b5..1c80e753 100644 --- a/engine/core/elasticsearch/documents.py +++ b/engine/core/elasticsearch/documents.py @@ -1,9 +1,6 @@ -from typing import Any - from django.db.models import Model, QuerySet from django_elasticsearch_dsl import Document, fields from django_elasticsearch_dsl.registries import registry -from health_check.db.models import TestModel from engine.core.elasticsearch import ( COMMON_ANALYSIS, @@ -195,18 +192,3 @@ class BrandDocument(ActiveOnlyMixin, BaseDocument): add_multilang_fields(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) diff --git a/engine/core/fixtures/demo.json b/engine/core/fixtures/demo.json index e2669881..0624eb96 100644 --- a/engine/core/fixtures/demo.json +++ b/engine/core/fixtures/demo.json @@ -2,7 +2,10 @@ "category_tags": [ {"tag_name": "precious", "name": "Precious Stones", "name_ru": "Драгоценные камни"}, {"tag_name": "semi-precious", "name": "Semi-Precious Stones", "name_ru": "Полудрагоценные камни"}, - {"tag_name": "organic", "name": "Organic Gems", "name_ru": "Органические камни"} + {"tag_name": "organic", "name": "Organic Gems", "name_ru": "Органические камни"}, + {"tag_name": "jewelry", "name": "Jewelry", "name_ru": "Ювелирные изделия"}, + {"tag_name": "services", "name": "Services", "name_ru": "Услуги"}, + {"tag_name": "metals", "name": "Precious Metals", "name_ru": "Драгоценные металлы"} ], "product_tags": [ {"tag_name": "certified", "name": "GIA Certified", "name_ru": "Сертификат GIA"}, @@ -73,6 +76,21 @@ "name_ru": "Хрустальное Королевство", "description": "Quartz varieties and crystal formations of museum quality.", "description_ru": "Разновидности кварца и кристаллические образования музейного качества." + }, + { + "name": "Maison Royale", + "description": "Exquisite handcrafted jewelry blending timeless design with contemporary elegance.", + "description_ru": "Изысканные ювелирные изделия ручной работы, сочетающие вечный дизайн с современной элегантностью." + }, + { + "name": "Artisan's Forge", + "description": "Contemporary jewelry atelier specializing in unique, bespoke pieces.", + "description_ru": "Современное ювелирное ателье, специализирующееся на уникальных изделиях по индивидуальному заказу." + }, + { + "name": "Noble Metals Trading", + "description": "Premium precious metals for investment, jewelry making, and industrial applications.", + "description_ru": "Премиальные драгоценные металлы для инвестиций, ювелирного дела и промышленного применения." } ], "categories": [ @@ -163,6 +181,150 @@ "description_ru": "Многоцветные драгоценные камни с электрическими свойствами", "parent": "Gemstones", "markup_percent": 7 + }, + { + "name": "Jewelry", + "name_ru": "Ювелирные изделия", + "description": "Handcrafted fine jewelry featuring precious gemstones and metals", + "description_ru": "Ювелирные изделия ручной работы с драгоценными камнями и металлами", + "parent": null, + "markup_percent": 0 + }, + { + "name": "Rings", + "name_ru": "Кольца", + "description": "Engagement rings, cocktail rings, and statement pieces", + "description_ru": "Обручальные кольца, коктейльные кольца и эффектные украшения", + "parent": "Jewelry", + "markup_percent": 20 + }, + { + "name": "Necklaces & Pendants", + "name_ru": "Колье и подвески", + "description": "Elegant necklaces and pendants for every occasion", + "description_ru": "Элегантные колье и подвески для любого случая", + "parent": "Jewelry", + "markup_percent": 18 + }, + { + "name": "Earrings", + "name_ru": "Серьги", + "description": "Studs, drops, and chandelier earrings", + "description_ru": "Серьги-гвоздики, серьги-капли и каскадные серьги", + "parent": "Jewelry", + "markup_percent": 15 + }, + { + "name": "Bracelets & Bangles", + "name_ru": "Браслеты и обручи", + "description": "Tennis bracelets, bangles, and chain bracelets", + "description_ru": "Теннисные браслеты, обручи и цепочные браслеты", + "parent": "Jewelry", + "markup_percent": 15 + }, + { + "name": "Brooches & Pins", + "name_ru": "Броши и булавки", + "description": "Decorative brooches and lapel pins", + "description_ru": "Декоративные броши и булавки для лацканов", + "parent": "Jewelry", + "markup_percent": 12 + }, + { + "name": "Services", + "name_ru": "Услуги", + "description": "Professional gemological and jewelry services", + "description_ru": "Профессиональные геммологические и ювелирные услуги", + "parent": null, + "markup_percent": 0 + }, + { + "name": "Gemstone Appraisal", + "name_ru": "Оценка камней", + "description": "Professional gemstone evaluation and certification services", + "description_ru": "Профессиональная оценка и сертификация драгоценных камней", + "parent": "Services", + "markup_percent": 0 + }, + { + "name": "Custom Jewelry Design", + "name_ru": "Индивидуальный дизайн", + "description": "Bespoke jewelry design and 3D rendering services", + "description_ru": "Дизайн ювелирных изделий по индивидуальному заказу и 3D-моделирование", + "parent": "Services", + "markup_percent": 0 + }, + { + "name": "Stone Setting", + "name_ru": "Закрепка камней", + "description": "Professional stone setting in various mount styles", + "description_ru": "Профессиональная закрепка камней в различных типах оправ", + "parent": "Services", + "markup_percent": 0 + }, + { + "name": "Jewelry Repair", + "name_ru": "Ремонт украшений", + "description": "Restoration and repair services for fine jewelry", + "description_ru": "Реставрация и ремонт ювелирных изделий", + "parent": "Services", + "markup_percent": 0 + }, + { + "name": "Stone Cutting", + "name_ru": "Огранка камней", + "description": "Custom gemstone cutting, re-cutting, and polishing", + "description_ru": "Индивидуальная огранка, переогранка и полировка камней", + "parent": "Services", + "markup_percent": 0 + }, + { + "name": "Metals", + "name_ru": "Металлы", + "description": "Precious metals for investment and jewelry making", + "description_ru": "Драгоценные металлы для инвестиций и ювелирного дела", + "parent": null, + "markup_percent": 0 + }, + { + "name": "Gold", + "name_ru": "Золото", + "description": "Fine gold in bars, coins, and raw forms", + "description_ru": "Золото в слитках, монетах и необработанном виде", + "parent": "Metals", + "markup_percent": 3 + }, + { + "name": "Silver", + "name_ru": "Серебро", + "description": "Sterling and fine silver for crafting and investment", + "description_ru": "Стерлинговое и чистое серебро для мастерства и инвестиций", + "parent": "Metals", + "markup_percent": 5 + }, + { + "name": "Platinum", + "name_ru": "Платина", + "description": "Premium platinum for jewelry and investment", + "description_ru": "Премиальная платина для ювелирных изделий и инвестиций", + "parent": "Metals", + "markup_percent": 4 + }, + { + "name": "Palladium", + "name_ru": "Палладий", + "description": "Palladium products for jewelry and industrial use", + "description_ru": "Изделия из палладия для ювелирного дела и промышленности", + "parent": "Metals", + "markup_percent": 4 + }, + { + "name": "Rhodium", + "name_ru": "Родий", + "description": "Rhodium plating materials and services", + "description_ru": "Материалы и услуги для родирования", + "parent": "Metals", + "markup_percent": 6 } ], "products": [ @@ -176,7 +338,13 @@ "partnumber": "DIA-RB-150-D-VVS1", "price": 18500, "purchase_price": 15000, - "quantity": 3 + "quantity": 3, + "attribute_values": { + "Carat Weight": 1.5, + "Cut": {"en": "Round Brilliant", "ru": "Круглая бриллиантовая"}, + "Color Grade": "D", + "Clarity Grade": "VVS1" + } }, { "name": "Princess Cut Diamond 2.0ct E VS2", @@ -188,7 +356,13 @@ "partnumber": "DIA-PC-200-E-VS2", "price": 24000, "purchase_price": 19500, - "quantity": 2 + "quantity": 2, + "attribute_values": { + "Carat Weight": 2.0, + "Cut": {"en": "Princess", "ru": "Принцесса"}, + "Color Grade": "E", + "Clarity Grade": "VS2" + } }, { "name": "Oval Diamond 1.2ct F IF", @@ -200,7 +374,13 @@ "partnumber": "DIA-OV-120-F-IF", "price": 28500, "purchase_price": 23000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 1.2, + "Cut": {"en": "Oval", "ru": "Овальная"}, + "Color Grade": "F", + "Clarity Grade": "IF" + } }, { "name": "Cushion Cut Diamond 3.0ct G VS1", @@ -212,7 +392,13 @@ "partnumber": "DIA-CU-300-G-VS1", "price": 42000, "purchase_price": 35000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 3.0, + "Cut": {"en": "Cushion", "ru": "Кушон"}, + "Color Grade": "G", + "Clarity Grade": "VS1" + } }, { "name": "Emerald Cut Diamond 1.8ct D VVS2", @@ -224,7 +410,13 @@ "partnumber": "DIA-EM-180-D-VVS2", "price": 32000, "purchase_price": 26000, - "quantity": 2 + "quantity": 2, + "attribute_values": { + "Carat Weight": 1.8, + "Cut": {"en": "Emerald", "ru": "Изумрудная"}, + "Color Grade": "D", + "Clarity Grade": "VVS2" + } }, { "name": "Fancy Yellow Diamond 2.5ct", @@ -236,7 +428,12 @@ "partnumber": "DIA-FY-250", "price": 65000, "purchase_price": 52000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 2.5, + "Cut": {"en": "Radiant", "ru": "Радиант"}, + "Color Grade": {"en": "Fancy Intense Yellow", "ru": "Фантазийный интенсивно-жёлтый"} + } }, { "name": "Pink Diamond 0.5ct Fancy Light", @@ -248,7 +445,14 @@ "partnumber": "DIA-PNK-050-FL", "price": 125000, "purchase_price": 100000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 0.5, + "Cut": {"en": "Pear", "ru": "Грушевидная"}, + "Color Grade": {"en": "Fancy Light Pink", "ru": "Фантазийный светло-розовый"}, + "Country of Origin": {"en": "Australia", "ru": "Австралия"}, + "Mine": {"en": "Argyle", "ru": "Аргайл"} + } }, { "name": "Burmese Ruby 2.5ct Pigeon Blood", @@ -260,7 +464,13 @@ "partnumber": "RUB-BUR-250-PB", "price": 125000, "purchase_price": 100000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 2.5, + "Cut": {"en": "Oval", "ru": "Овальная"}, + "Color Grade": {"en": "Pigeon Blood", "ru": "Голубиная кровь"}, + "Country of Origin": {"en": "Myanmar", "ru": "Мьянма"} + } }, { "name": "Mozambique Ruby 1.8ct Vivid Red", @@ -272,7 +482,12 @@ "partnumber": "RUB-MOZ-180-VR", "price": 8500, "purchase_price": 6800, - "quantity": 4 + "quantity": 4, + "attribute_values": { + "Carat Weight": 1.8, + "Color Grade": {"en": "Vivid Red", "ru": "Насыщенно-красный"}, + "Country of Origin": {"en": "Mozambique", "ru": "Мозамбик"} + } }, { "name": "Star Ruby 3.2ct Six-Ray", @@ -284,7 +499,13 @@ "partnumber": "RUB-STAR-320-SR", "price": 15000, "purchase_price": 12000, - "quantity": 2 + "quantity": 2, + "attribute_values": { + "Carat Weight": 3.2, + "Cut": {"en": "Cabochon", "ru": "Кабошон"}, + "Color Grade": {"en": "Red", "ru": "Красный"}, + "Country of Origin": {"en": "Sri Lanka", "ru": "Шри-Ланка"} + } }, { "name": "Kashmir Sapphire 3.0ct Cornflower Blue", @@ -296,7 +517,13 @@ "partnumber": "SAP-KAS-300-CB", "price": 185000, "purchase_price": 150000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 3.0, + "Color Grade": {"en": "Cornflower Blue", "ru": "Васильково-голубой"}, + "Country of Origin": {"en": "India", "ru": "Индия"}, + "Mine": {"en": "Kashmir", "ru": "Кашмир"} + } }, { "name": "Ceylon Sapphire 2.2ct Royal Blue", @@ -308,7 +535,12 @@ "partnumber": "SAP-CEY-220-RB", "price": 12500, "purchase_price": 10000, - "quantity": 3 + "quantity": 3, + "attribute_values": { + "Carat Weight": 2.2, + "Color Grade": {"en": "Royal Blue", "ru": "Королевский синий"}, + "Country of Origin": {"en": "Sri Lanka", "ru": "Шри-Ланка"} + } }, { "name": "Padparadscha Sapphire 1.5ct", @@ -320,7 +552,12 @@ "partnumber": "SAP-PAD-150", "price": 45000, "purchase_price": 36000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 1.5, + "Color Grade": {"en": "Pink-Orange", "ru": "Розово-оранжевый"}, + "Country of Origin": {"en": "Sri Lanka", "ru": "Шри-Ланка"} + } }, { "name": "Yellow Sapphire 4.0ct Golden", @@ -332,7 +569,12 @@ "partnumber": "SAP-YEL-400-GD", "price": 6500, "purchase_price": 5200, - "quantity": 5 + "quantity": 5, + "attribute_values": { + "Carat Weight": 4.0, + "Color Grade": {"en": "Golden Yellow", "ru": "Золотисто-жёлтый"}, + "Country of Origin": {"en": "Sri Lanka", "ru": "Шри-Ланка"} + } }, { "name": "Pink Sapphire 1.8ct Hot Pink", @@ -344,7 +586,12 @@ "partnumber": "SAP-PNK-180-HP", "price": 4200, "purchase_price": 3400, - "quantity": 6 + "quantity": 6, + "attribute_values": { + "Carat Weight": 1.8, + "Color Grade": {"en": "Hot Pink", "ru": "Ярко-розовый"}, + "Country of Origin": {"en": "Madagascar", "ru": "Мадагаскар"} + } }, { "name": "Colombian Emerald 2.8ct Muzo Green", @@ -356,7 +603,13 @@ "partnumber": "EME-COL-280-MZ", "price": 35000, "purchase_price": 28000, - "quantity": 2 + "quantity": 2, + "attribute_values": { + "Carat Weight": 2.8, + "Color Grade": {"en": "Deep Green", "ru": "Глубокий зелёный"}, + "Country of Origin": {"en": "Colombia", "ru": "Колумбия"}, + "Mine": {"en": "Muzo", "ru": "Музо"} + } }, { "name": "Zambian Emerald 3.5ct Vivid Green", @@ -368,7 +621,12 @@ "partnumber": "EME-ZAM-350-VG", "price": 18500, "purchase_price": 15000, - "quantity": 3 + "quantity": 3, + "attribute_values": { + "Carat Weight": 3.5, + "Color Grade": {"en": "Vivid Bluish-Green", "ru": "Насыщенный сине-зелёный"}, + "Country of Origin": {"en": "Zambia", "ru": "Замбия"} + } }, { "name": "Brazilian Emerald 1.2ct Medium Green", @@ -380,7 +638,12 @@ "partnumber": "EME-BRA-120-MG", "price": 2800, "purchase_price": 2200, - "quantity": 8 + "quantity": 8, + "attribute_values": { + "Carat Weight": 1.2, + "Color Grade": {"en": "Medium Green", "ru": "Средне-зелёный"}, + "Country of Origin": {"en": "Brazil", "ru": "Бразилия"} + } }, { "name": "Australian Black Opal 5.2ct", @@ -392,7 +655,13 @@ "partnumber": "OPL-BLK-520-LR", "price": 28000, "purchase_price": 22500, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 5.2, + "Cut": {"en": "Cabochon", "ru": "Кабошон"}, + "Country of Origin": {"en": "Australia", "ru": "Австралия"}, + "Mine": {"en": "Lightning Ridge", "ru": "Лайтнинг Ридж"} + } }, { "name": "Ethiopian Welo Opal 3.8ct", @@ -404,7 +673,12 @@ "partnumber": "OPL-ETH-380-WL", "price": 3500, "purchase_price": 2800, - "quantity": 5 + "quantity": 5, + "attribute_values": { + "Carat Weight": 3.8, + "Cut": {"en": "Cabochon", "ru": "Кабошон"}, + "Country of Origin": {"en": "Ethiopia", "ru": "Эфиопия"} + } }, { "name": "Boulder Opal 8.5ct", @@ -416,7 +690,12 @@ "partnumber": "OPL-BLD-850", "price": 4800, "purchase_price": 3800, - "quantity": 3 + "quantity": 3, + "attribute_values": { + "Carat Weight": 8.5, + "Cut": {"en": "Freeform", "ru": "Свободная форма"}, + "Country of Origin": {"en": "Australia", "ru": "Австралия"} + } }, { "name": "Fire Opal 2.1ct Mexican Orange", @@ -428,7 +707,12 @@ "partnumber": "OPL-FIRE-210-MX", "price": 1200, "purchase_price": 950, - "quantity": 7 + "quantity": 7, + "attribute_values": { + "Carat Weight": 2.1, + "Color Grade": {"en": "Intense Orange", "ru": "Интенсивный оранжевый"}, + "Country of Origin": {"en": "Mexico", "ru": "Мексика"} + } }, { "name": "South Sea Pearl 14mm Golden", @@ -440,7 +724,12 @@ "partnumber": "PRL-SSG-14MM", "price": 8500, "purchase_price": 6800, - "quantity": 4 + "quantity": 4, + "attribute_values": { + "Dimensions (mm)": "14", + "Color Grade": {"en": "Golden", "ru": "Золотистый"}, + "Clarity Grade": "AAA" + } }, { "name": "Tahitian Pearl 12mm Peacock", @@ -452,7 +741,12 @@ "partnumber": "PRL-TAH-12MM-PC", "price": 3200, "purchase_price": 2500, - "quantity": 6 + "quantity": 6, + "attribute_values": { + "Dimensions (mm)": "12", + "Color Grade": {"en": "Peacock", "ru": "Павлиний"}, + "Country of Origin": {"en": "Tahiti", "ru": "Таити"} + } }, { "name": "Akoya Pearl Strand 7mm", @@ -464,7 +758,12 @@ "partnumber": "PRL-AKO-7MM-STR", "price": 4500, "purchase_price": 3600, - "quantity": 3 + "quantity": 3, + "attribute_values": { + "Dimensions (mm)": "7", + "Color Grade": {"en": "White with Rose Overtone", "ru": "Белый с розовым перламутром"}, + "Country of Origin": {"en": "Japan", "ru": "Япония"} + } }, { "name": "Freshwater Pearl Set", @@ -476,7 +775,11 @@ "partnumber": "PRL-FW-SET-10", "price": 850, "purchase_price": 680, - "quantity": 10 + "quantity": 10, + "attribute_values": { + "Dimensions (mm)": "9-10", + "Color Grade": {"en": "Multi-Pastel", "ru": "Мульти-пастельный"} + } }, { "name": "Siberian Amethyst 8.5ct Deep Purple", @@ -488,7 +791,13 @@ "partnumber": "AME-SIB-850-DP", "price": 1200, "purchase_price": 950, - "quantity": 5 + "quantity": 5, + "attribute_values": { + "Carat Weight": 8.5, + "Cut": {"en": "Cushion", "ru": "Кушон"}, + "Color Grade": {"en": "Deep Purple", "ru": "Тёмно-фиолетовый"}, + "Country of Origin": {"en": "Russia", "ru": "Россия"} + } }, { "name": "Uruguayan Amethyst 12.3ct", @@ -500,7 +809,12 @@ "partnumber": "AME-URU-1230", "price": 650, "purchase_price": 520, - "quantity": 8 + "quantity": 8, + "attribute_values": { + "Carat Weight": 12.3, + "Cut": {"en": "Oval", "ru": "Овальная"}, + "Country of Origin": {"en": "Uruguay", "ru": "Уругвай"} + } }, { "name": "Ametrine 6.8ct Bi-Color", @@ -512,7 +826,13 @@ "partnumber": "AME-TRI-680-BC", "price": 450, "purchase_price": 360, - "quantity": 6 + "quantity": 6, + "attribute_values": { + "Carat Weight": 6.8, + "Cut": {"en": "Emerald", "ru": "Изумрудная"}, + "Color Grade": {"en": "Bi-Color Purple/Gold", "ru": "Двухцветный фиолетовый/золотистый"}, + "Country of Origin": {"en": "Bolivia", "ru": "Боливия"} + } }, { "name": "Santa Maria Aquamarine 4.2ct", @@ -524,7 +844,12 @@ "partnumber": "AQU-SM-420", "price": 5500, "purchase_price": 4400, - "quantity": 2 + "quantity": 2, + "attribute_values": { + "Carat Weight": 4.2, + "Color Grade": {"en": "Intense Blue", "ru": "Интенсивный голубой"}, + "Country of Origin": {"en": "Brazil", "ru": "Бразилия"} + } }, { "name": "Madagascar Aquamarine 7.5ct", @@ -536,7 +861,13 @@ "partnumber": "AQU-MAD-750", "price": 2200, "purchase_price": 1750, - "quantity": 4 + "quantity": 4, + "attribute_values": { + "Carat Weight": 7.5, + "Cut": {"en": "Octagon", "ru": "Восьмиугольная"}, + "Color Grade": {"en": "Light Blue", "ru": "Светло-голубой"}, + "Country of Origin": {"en": "Madagascar", "ru": "Мадагаскар"} + } }, { "name": "Pakistani Aquamarine 15.2ct", @@ -548,7 +879,13 @@ "partnumber": "AQU-PAK-1520", "price": 4800, "purchase_price": 3850, - "quantity": 2 + "quantity": 2, + "attribute_values": { + "Carat Weight": 15.2, + "Color Grade": {"en": "Medium Blue", "ru": "Средне-голубой"}, + "Country of Origin": {"en": "Pakistan", "ru": "Пакистан"}, + "Mine": {"en": "Shigar Valley", "ru": "Долина Шигар"} + } }, { "name": "AAA Tanzanite 5.8ct Vivid Blue", @@ -560,7 +897,13 @@ "partnumber": "TAN-AAA-580-VB", "price": 8500, "purchase_price": 6800, - "quantity": 2 + "quantity": 2, + "attribute_values": { + "Carat Weight": 5.8, + "Cut": {"en": "Trillion", "ru": "Триллион"}, + "Color Grade": {"en": "Vivid Blue-Violet", "ru": "Насыщенный сине-фиолетовый"}, + "Country of Origin": {"en": "Tanzania", "ru": "Танзания"} + } }, { "name": "Tanzanite 3.2ct Blue-Violet", @@ -572,7 +915,13 @@ "partnumber": "TAN-320-BV", "price": 3200, "purchase_price": 2560, - "quantity": 4 + "quantity": 4, + "attribute_values": { + "Carat Weight": 3.2, + "Cut": {"en": "Oval", "ru": "Овальная"}, + "Color Grade": {"en": "Blue-Violet", "ru": "Сине-фиолетовый"}, + "Country of Origin": {"en": "Tanzania", "ru": "Танзания"} + } }, { "name": "Tanzanite Pair 2.0ct Each", @@ -584,7 +933,12 @@ "partnumber": "TAN-PAIR-200", "price": 5800, "purchase_price": 4650, - "quantity": 3 + "quantity": 3, + "attribute_values": { + "Carat Weight": 2.0, + "Color Grade": {"en": "Blue-Violet", "ru": "Сине-фиолетовый"}, + "Country of Origin": {"en": "Tanzania", "ru": "Танзания"} + } }, { "name": "Paraiba Tourmaline 1.2ct Neon Blue", @@ -596,7 +950,12 @@ "partnumber": "TOU-PAR-120-NB", "price": 85000, "purchase_price": 68000, - "quantity": 1 + "quantity": 1, + "attribute_values": { + "Carat Weight": 1.2, + "Color Grade": {"en": "Neon Blue", "ru": "Неоново-голубой"}, + "Country of Origin": {"en": "Brazil", "ru": "Бразилия"} + } }, { "name": "Watermelon Tourmaline 8.5ct", @@ -608,7 +967,12 @@ "partnumber": "TOU-WM-850", "price": 1800, "purchase_price": 1450, - "quantity": 4 + "quantity": 4, + "attribute_values": { + "Carat Weight": 8.5, + "Cut": {"en": "Slice", "ru": "Слайс"}, + "Color Grade": {"en": "Bi-Color Pink/Green", "ru": "Двухцветный розовый/зелёный"} + } }, { "name": "Rubellite Tourmaline 4.3ct", @@ -620,7 +984,12 @@ "partnumber": "TOU-RUB-430", "price": 3500, "purchase_price": 2800, - "quantity": 3 + "quantity": 3, + "attribute_values": { + "Carat Weight": 4.3, + "Cut": {"en": "Cushion", "ru": "Кушон"}, + "Color Grade": {"en": "Raspberry Red", "ru": "Малиново-красный"} + } }, { "name": "Chrome Tourmaline 2.8ct", @@ -632,7 +1001,12 @@ "partnumber": "TOU-CHR-280", "price": 2200, "purchase_price": 1750, - "quantity": 5 + "quantity": 5, + "attribute_values": { + "Carat Weight": 2.8, + "Color Grade": {"en": "Intense Green", "ru": "Интенсивный зелёный"}, + "Country of Origin": {"en": "East Africa", "ru": "Восточная Африка"} + } }, { "name": "Indicolite Tourmaline 3.6ct", @@ -644,7 +1018,721 @@ "partnumber": "TOU-IND-360", "price": 2800, "purchase_price": 2250, + "quantity": 4, + "attribute_values": { + "Carat Weight": 3.6, + "Cut": {"en": "Oval", "ru": "Овальная"}, + "Color Grade": {"en": "Teal Blue", "ru": "Сине-зелёный"}, + "Country of Origin": {"en": "Afghanistan", "ru": "Афганистан"} + } + }, + { + "name": "Marquise Diamond 1.0ct D VVS2", + "name_ru": "Бриллиант огранки «Маркиз» 1.0 карата D VVS2", + "description": "Elegant 1.0 carat marquise cut diamond with D color and VVS2 clarity. The elongated shape creates an illusion of greater size. GIA certified with excellent polish.", + "description_ru": "Элегантный бриллиант огранки «Маркиз» 1.0 карата с цветом D и чистотой VVS2. Удлинённая форма создаёт иллюзию большего размера. Сертификат GIA с превосходной полировкой.", + "category": "Diamonds", + "brand": "Sparkle & Stone", + "partnumber": "DIA-MQ-100-D-VVS2", + "price": 12500, + "purchase_price": 10000, + "quantity": 2, + "attribute_values": { + "Carat Weight": 1.0, + "Cut": {"en": "Marquise", "ru": "Маркиз"}, + "Color Grade": "D", + "Clarity Grade": "VVS2" + } + }, + { + "name": "Asscher Cut Diamond 2.2ct E VS1", + "name_ru": "Бриллиант огранки «Ашер» 2.2 карата E VS1", + "description": "Sophisticated 2.2 carat Asscher cut diamond with E color and VS1 clarity. Art Deco inspired step-cut with mesmerizing windmill pattern. GIA certified.", + "description_ru": "Утончённый бриллиант огранки «Ашер» 2.2 карата с цветом E и чистотой VS1. Ступенчатая огранка в стиле ар-деко с завораживающим рисунком «ветряная мельница». Сертификат GIA.", + "category": "Diamonds", + "brand": "Sparkle & Stone", + "partnumber": "DIA-AS-220-E-VS1", + "price": 28000, + "purchase_price": 22500, + "quantity": 1, + "attribute_values": { + "Carat Weight": 2.2, + "Cut": {"en": "Asscher", "ru": "Ашер"}, + "Color Grade": "E", + "Clarity Grade": "VS1" + } + }, + { + "name": "Thai Ruby 1.5ct Vivid Red", + "name_ru": "Тайский рубин 1.5 карата насыщенно-красный", + "description": "Exceptional 1.5 carat Thai ruby with vivid red color and excellent transparency. Classic Siamese origin known for deep saturation. Minor heat treatment.", + "description_ru": "Исключительный тайский рубин 1.5 карата с насыщенным красным цветом и отличной прозрачностью. Классическое сиамское происхождение, известное глубокой насыщенностью. Незначительная термообработка.", + "category": "Rubies", + "brand": "Crimson Vault", + "partnumber": "RUB-THA-150-VR", + "price": 6200, + "purchase_price": 5000, + "quantity": 3, + "attribute_values": { + "Carat Weight": 1.5, + "Color Grade": {"en": "Vivid Red", "ru": "Насыщенно-красный"}, + "Country of Origin": {"en": "Thailand", "ru": "Таиланд"} + } + }, + { + "name": "Burmese Ruby 1.0ct Unheated", + "name_ru": "Бирманский рубин 1.0 карата без нагрева", + "description": "Rare 1.0 carat unheated Burmese ruby with natural vivid red color. Mogok origin with silk inclusions creating a soft glow. GRS certified no-heat.", + "description_ru": "Редкий бирманский рубин 1.0 карата без нагрева с природным насыщенным красным цветом. Происхождение Могок с шёлковыми включениями, создающими мягкое свечение. Сертификат GRS «без нагрева».", + "category": "Rubies", + "brand": "Crimson Vault", + "partnumber": "RUB-BUR-100-UH", + "price": 18000, + "purchase_price": 14500, + "quantity": 1, + "attribute_values": { + "Carat Weight": 1.0, + "Color Grade": {"en": "Vivid Red", "ru": "Насыщенно-красный"}, + "Country of Origin": {"en": "Myanmar", "ru": "Мьянма"}, + "Mine": {"en": "Mogok", "ru": "Могок"} + } + }, + { + "name": "Pink Sapphire Pair 1.2ct Each", + "name_ru": "Пара розовых сапфиров по 1.2 карата", + "description": "Perfectly matched pair of 1.2 carat pink sapphires ideal for earrings. Identical color saturation and oval cut. Sri Lankan origin.", + "description_ru": "Идеально подобранная пара розовых сапфиров по 1.2 карата, идеальна для серёг. Идентичная насыщенность цвета и овальная огранка. Происхождение Шри-Ланка.", + "category": "Sapphires", + "brand": "Crimson Vault", + "partnumber": "SAP-PNK-PAIR-120", + "price": 5800, + "purchase_price": 4650, + "quantity": 2, + "attribute_values": { + "Carat Weight": 1.2, + "Cut": {"en": "Oval", "ru": "Овальная"}, + "Color Grade": {"en": "Pink", "ru": "Розовый"}, + "Country of Origin": {"en": "Sri Lanka", "ru": "Шри-Ланка"} + } + }, + { + "name": "Afghan Emerald 1.8ct Panjshir", + "name_ru": "Афганский изумруд 1.8 карата Панджшер", + "description": "Exceptional 1.8 carat emerald from Afghanistan's legendary Panjshir Valley. Vivid green with remarkable clarity for an emerald. Minor oil treatment.", + "description_ru": "Исключительный изумруд 1.8 карата из легендарной Панджшерской долины Афганистана. Яркий зелёный с выдающейся для изумруда чистотой. Незначительная масляная обработка.", + "category": "Emeralds", + "brand": "Evergreen Gems", + "partnumber": "EME-AFG-180-PJ", + "price": 12000, + "purchase_price": 9600, + "quantity": 2, + "attribute_values": { + "Carat Weight": 1.8, + "Color Grade": {"en": "Vivid Green", "ru": "Яркий зелёный"}, + "Country of Origin": {"en": "Afghanistan", "ru": "Афганистан"}, + "Mine": {"en": "Panjshir", "ru": "Панджшер"} + } + }, + { + "name": "Ethiopian Emerald 2.5ct Vivid", + "name_ru": "Эфиопский изумруд 2.5 карата насыщенный", + "description": "Striking 2.5 carat Ethiopian emerald from the newly discovered deposits. Excellent clarity with vivid green color rivaling Colombian stones. Untreated.", + "description_ru": "Впечатляющий эфиопский изумруд 2.5 карата из недавно открытых месторождений. Превосходная чистота с насыщенным зелёным цветом, соперничающим с колумбийскими камнями. Без обработки.", + "category": "Emeralds", + "brand": "Evergreen Gems", + "partnumber": "EME-ETH-250", + "price": 9500, + "purchase_price": 7600, + "quantity": 3, + "attribute_values": { + "Carat Weight": 2.5, + "Color Grade": {"en": "Vivid Green", "ru": "Яркий зелёный"}, + "Country of Origin": {"en": "Ethiopia", "ru": "Эфиопия"} + } + }, + { + "name": "Colombian Emerald Pair 1.0ct Each", + "name_ru": "Пара колумбийских изумрудов по 1.0 карата", + "description": "Matched pair of 1.0 carat Colombian emeralds from the Muzo district. Identical deep green color with characteristic jardín. Perfect for earrings.", + "description_ru": "Подобранная пара колумбийских изумрудов по 1.0 карата из района Музо. Идентичный глубокий зелёный цвет с характерным «жардин». Идеальна для серёг.", + "category": "Emeralds", + "brand": "Evergreen Gems", + "partnumber": "EME-COL-PAIR-100", + "price": 14000, + "purchase_price": 11200, + "quantity": 2, + "attribute_values": { + "Carat Weight": 1.0, + "Color Grade": {"en": "Deep Green", "ru": "Глубокий зелёный"}, + "Country of Origin": {"en": "Colombia", "ru": "Колумбия"}, + "Mine": {"en": "Muzo", "ru": "Музо"} + } + }, + { + "name": "Ceylon Sapphire 5.0ct Unheated", + "name_ru": "Цейлонский сапфир 5.0 карата без нагрева", + "description": "Magnificent 5.0 carat unheated Ceylon sapphire with medium-deep blue color. Exceptional size for an untreated stone. Cushion cut with GRS certificate.", + "description_ru": "Великолепный цейлонский сапфир 5.0 карата без нагрева средне-глубокого синего цвета. Исключительный размер для необработанного камня. Огранка «Кушон» с сертификатом GRS.", + "category": "Sapphires", + "brand": "Lumina Treasures", + "partnumber": "SAP-CEY-500-UH", + "price": 45000, + "purchase_price": 36000, + "quantity": 1, + "attribute_values": { + "Carat Weight": 5.0, + "Cut": {"en": "Cushion", "ru": "Кушон"}, + "Color Grade": {"en": "Medium-Deep Blue", "ru": "Средне-глубокий синий"}, + "Country of Origin": {"en": "Sri Lanka", "ru": "Шри-Ланка"} + } + }, + { + "name": "Baroque South Sea Pearl 16mm White", + "name_ru": "Барочный жемчуг Южных морей 16мм белый", + "description": "Extraordinary 16mm baroque South Sea pearl with lustrous white body and orient overtones. Unique organic shape makes each piece one-of-a-kind. AAA grade.", + "description_ru": "Выдающийся барочный жемчуг Южных морей 16мм с блестящим белым телом и перламутровыми переливами. Уникальная органическая форма делает каждый экземпляр единственным в своём роде. Класс AAA.", + "category": "Pearls", + "brand": "Oceanic Pearls", + "partnumber": "PRL-BAR-16MM-WH", + "price": 6500, + "purchase_price": 5200, + "quantity": 3, + "attribute_values": { + "Dimensions (mm)": "16", + "Cut": {"en": "Baroque", "ru": "Барочная"}, + "Color Grade": {"en": "White", "ru": "Белый"}, + "Clarity Grade": "AAA" + } + }, + { + "name": "Madeira Citrine 8.5ct", + "name_ru": "Цитрин «Мадейра» 8.5 карата", + "description": "Premium 8.5 carat Madeira citrine with coveted deep reddish-orange color. The finest citrine variety, named after the famous wine. Emerald cut with exceptional clarity.", + "description_ru": "Премиальный цитрин «Мадейра» 8.5 карата с желанным глубоким красновато-оранжевым цветом. Лучшая разновидность цитрина, названная в честь знаменитого вина. Изумрудная огранка с исключительной чистотой.", + "category": "Amethyst", + "brand": "Crystal Kingdom", + "partnumber": "AME-CIT-850-MD", + "price": 950, + "purchase_price": 760, + "quantity": 4, + "attribute_values": { + "Carat Weight": 8.5, + "Cut": {"en": "Emerald", "ru": "Изумрудная"}, + "Color Grade": {"en": "Reddish-Orange", "ru": "Красновато-оранжевый"} + } + }, + { + "name": "Rose de France Amethyst 14.0ct", + "name_ru": "Аметист «Роз де Франс» 14.0 карата", + "description": "Delicate 14.0 carat Rose de France amethyst with soft lavender-pink color. Oval cut with excellent transparency. Named after the French tradition of pale amethyst jewelry.", + "description_ru": "Нежный аметист «Роз де Франс» 14.0 карата мягкого лавандово-розового цвета. Овальная огранка с отличной прозрачностью. Назван в честь французской традиции ювелирных изделий с бледным аметистом.", + "category": "Amethyst", + "brand": "Crystal Kingdom", + "partnumber": "AME-RDF-1400", + "price": 420, + "purchase_price": 340, + "quantity": 6, + "attribute_values": { + "Carat Weight": 14.0, + "Cut": {"en": "Oval", "ru": "Овальная"}, + "Color Grade": {"en": "Lavender-Pink", "ru": "Лавандово-розовый"} + } + }, + { + "name": "Prasiolite 11.2ct Green Amethyst", + "name_ru": "Празиолит 11.2 карата зелёный аметист", + "description": "Rare 11.2 carat natural prasiolite (green amethyst) with subtle mint-green color. Cushion cut from a limited Brazilian deposit. Increasingly sought after by collectors.", + "description_ru": "Редкий природный празиолит 11.2 карата (зелёный аметист) с нежным мятно-зелёным цветом. Огранка «Кушон» из ограниченного бразильского месторождения. Всё более востребован коллекционерами.", + "category": "Amethyst", + "brand": "Crystal Kingdom", + "partnumber": "AME-PRS-1120", + "price": 380, + "purchase_price": 300, + "quantity": 5, + "attribute_values": { + "Carat Weight": 11.2, + "Cut": {"en": "Cushion", "ru": "Кушон"}, + "Color Grade": {"en": "Mint Green", "ru": "Мятно-зелёный"}, + "Country of Origin": {"en": "Brazil", "ru": "Бразилия"} + } + }, + { + "name": "Diamond Solitaire Engagement Ring", + "name_ru": "Помолвочное кольцо с бриллиантом-солитером", + "description": "Classic 18K white gold engagement ring featuring a 1.0ct round brilliant diamond. Six-prong Tiffany-style setting with a comfort-fit band. GIA certified stone included.", + "description_ru": "Классическое помолвочное кольцо из белого золота 18K с бриллиантом круглой огранки 1.0 карата. Закрепка в стиле Тиффани с шестью крапанами и ободком комфортной посадки. В комплекте камень с сертификатом GIA.", + "category": "Rings", + "brand": "Maison Royale", + "partnumber": "JWL-RNG-DIA-SOL", + "price": 8500, + "purchase_price": 6800, + "quantity": 5 + }, + { + "name": "Ruby & Diamond Halo Ring", + "name_ru": "Кольцо с рубином и бриллиантовым гало", + "description": "Stunning 14K rose gold ring with a 1.2ct oval ruby center stone surrounded by a halo of 0.5ctw round diamonds. Vintage-inspired design with milgrain detailing.", + "description_ru": "Потрясающее кольцо из розового золота 14K с овальным рубином 1.2 карата в центре, обрамлённым ореолом бриллиантов общим весом 0.5 карата. Дизайн в винтажном стиле с миллигрейной отделкой.", + "category": "Rings", + "brand": "Maison Royale", + "partnumber": "JWL-RNG-RBY-HLO", + "price": 4200, + "purchase_price": 3400, "quantity": 4 + }, + { + "name": "Emerald Pendant Necklace", + "name_ru": "Колье с изумрудной подвеской", + "description": "Elegant 18K yellow gold pendant featuring a 2.0ct pear-shaped Colombian emerald. Suspended on a delicate 18-inch cable chain. Includes quality assessment certificate.", + "description_ru": "Элегантная подвеска из жёлтого золота 18K с колумбийским изумрудом грушевидной формы 2.0 карата. Подвешена на изящной якорной цепочке длиной 45 см. В комплекте сертификат оценки качества.", + "category": "Necklaces & Pendants", + "brand": "Maison Royale", + "partnumber": "JWL-NCK-EME-PND", + "price": 12000, + "purchase_price": 9600, + "quantity": 3 + }, + { + "name": "Diamond Tennis Necklace", + "name_ru": "Бриллиантовое теннисное колье", + "description": "Luxurious 14K white gold tennis necklace with 5.0ctw round brilliant diamonds. 16-inch length with secure box clasp and safety catch. Each diamond is VS clarity, G-H color.", + "description_ru": "Роскошное теннисное колье из белого золота 14K с бриллиантами круглой огранки общим весом 5.0 карата. Длина 40 см с надёжным замком-коробочкой и предохранителем. Каждый бриллиант чистоты VS, цвета G-H.", + "category": "Necklaces & Pendants", + "brand": "Maison Royale", + "partnumber": "JWL-NCK-DIA-TNS", + "price": 15500, + "purchase_price": 12400, + "quantity": 2 + }, + { + "name": "Diamond Stud Earrings", + "name_ru": "Серьги-гвоздики с бриллиантами", + "description": "Classic 14K white gold diamond stud earrings with 1.0ctw total weight. Matched pair of round brilliant diamonds, E-F color, VS1-VS2 clarity. Four-prong basket setting with friction backs.", + "description_ru": "Классические серьги-гвоздики из белого золота 14K с бриллиантами общим весом 1.0 карата. Подобранная пара бриллиантов круглой огранки, цвет E-F, чистота VS1-VS2. Четырёхкрапанная закрепка с фрикционными застёжками.", + "category": "Earrings", + "brand": "Artisan's Forge", + "partnumber": "JWL-EAR-DIA-STD", + "price": 5200, + "purchase_price": 4200, + "quantity": 6 + }, + { + "name": "Sapphire Drop Earrings", + "name_ru": "Серьги-капли с сапфирами", + "description": "Elegant 18K gold drop earrings featuring 2.0ctw pear-shaped Ceylon sapphires accented by 0.3ctw diamond halos. Lever-back closure for secure, comfortable wear.", + "description_ru": "Элегантные серьги-капли из золота 18K с грушевидными цейлонскими сапфирами общим весом 2.0 карата, обрамлёнными бриллиантовым гало общим весом 0.3 карата. Застёжка с английским замком для надёжного и комфортного ношения.", + "category": "Earrings", + "brand": "Maison Royale", + "partnumber": "JWL-EAR-SAP-DRP", + "price": 3800, + "purchase_price": 3050, + "quantity": 4 + }, + { + "name": "Diamond Tennis Bracelet", + "name_ru": "Бриллиантовый теннисный браслет", + "description": "Timeless 14K white gold tennis bracelet with 3.0ctw round brilliant diamonds. 7-inch length with secure double-lock clasp. Diamonds are G-H color, VS clarity.", + "description_ru": "Вечный теннисный браслет из белого золота 14K с бриллиантами круглой огранки общим весом 3.0 карата. Длина 18 см с надёжным двойным замком. Бриллианты цвета G-H, чистоты VS.", + "category": "Bracelets & Bangles", + "brand": "Maison Royale", + "partnumber": "JWL-BRC-DIA-TNS", + "price": 8800, + "purchase_price": 7050, + "quantity": 3 + }, + { + "name": "Emerald & Gold Bangle", + "name_ru": "Обруч с изумрудами и золотом", + "description": "Sophisticated 18K yellow gold bangle set with 1.5ctw emerald-cut emeralds and 0.5ctw accent diamonds. Hinged design with safety chain. Inner circumference 6.5 inches.", + "description_ru": "Утончённый обруч из жёлтого золота 18K с изумрудами изумрудной огранки общим весом 1.5 карата и бриллиантовыми акцентами общим весом 0.5 карата. Шарнирный дизайн с предохранительной цепочкой. Внутренний обхват 16.5 см.", + "category": "Bracelets & Bangles", + "brand": "Artisan's Forge", + "partnumber": "JWL-BRC-EME-BNG", + "price": 6500, + "purchase_price": 5200, + "quantity": 2 + }, + { + "name": "Diamond Floral Brooch", + "name_ru": "Бриллиантовая цветочная брошь", + "description": "Art Nouveau inspired platinum brooch with 2.0ctw diamonds arranged in a floral motif. Features round brilliant and marquise-cut diamonds with exceptional sparkle.", + "description_ru": "Брошь из платины в стиле ар-нуво с бриллиантами общим весом 2.0 карата, выложенными в цветочный мотив. Включает бриллианты круглой и маркизной огранки с исключительным блеском.", + "category": "Brooches & Pins", + "brand": "Maison Royale", + "partnumber": "JWL-BRH-DIA-FLR", + "price": 4500, + "purchase_price": 3600, + "quantity": 3 + }, + { + "name": "Multi-Gem Lapel Pin", + "name_ru": "Булавка для лацкана с мульти-камнями", + "description": "Contemporary 18K gold lapel pin featuring a curated mix of sapphire, ruby, and emerald cabochons totaling 1.2ct. Modern geometric design by Artisan's Forge.", + "description_ru": "Современная булавка для лацкана из золота 18K с подобранной комбинацией кабошонов сапфира, рубина и изумруда общим весом 1.2 карата. Современный геометрический дизайн от Кузницы Мастера.", + "category": "Brooches & Pins", + "brand": "Artisan's Forge", + "partnumber": "JWL-BRH-GEM-LPL", + "price": 2800, + "purchase_price": 2250, + "quantity": 5 + }, + { + "name": "Standard Gemstone Appraisal", + "name_ru": "Стандартная оценка камня", + "description": "Professional appraisal of a single gemstone by our certified gemologist. Includes identification, weight verification, color and clarity grading, treatment detection, and a detailed written report.", + "description_ru": "Профессиональная оценка одного драгоценного камня нашим сертифицированным геммологом. Включает идентификацию, проверку веса, определение цвета и чистоты, обнаружение обработки и подробный письменный отчёт.", + "category": "Gemstone Appraisal", + "brand": null, + "partnumber": "SVC-APR-STD", + "price": 150, + "purchase_price": 80, + "quantity": 999, + "is_digital": true + }, + { + "name": "Collection Appraisal Package", + "name_ru": "Оценка коллекции", + "description": "Comprehensive appraisal service for up to 10 gemstones. Ideal for estate evaluation, insurance documentation, or collection inventory. Each stone receives individual assessment and photography.", + "description_ru": "Комплексная услуга оценки до 10 драгоценных камней. Идеальна для оценки наследства, страховой документации или инвентаризации коллекции. Каждый камень получает индивидуальную оценку и фотографирование.", + "category": "Gemstone Appraisal", + "brand": null, + "partnumber": "SVC-APR-COL", + "price": 500, + "purchase_price": 250, + "quantity": 999, + "is_digital": true + }, + { + "name": "Design Consultation Session", + "name_ru": "Консультация по дизайну", + "description": "One-on-one consultation with our master jeweler to discuss your custom piece. Includes initial sketches, material recommendations, stone selection guidance, and a detailed project estimate.", + "description_ru": "Индивидуальная консультация с нашим мастером-ювелиром по созданию изделия на заказ. Включает начальные эскизы, рекомендации по материалам, помощь в выборе камней и детальную смету проекта.", + "category": "Custom Jewelry Design", + "brand": null, + "partnumber": "SVC-DSN-CSL", + "price": 250, + "purchase_price": 100, + "quantity": 999, + "is_digital": true + }, + { + "name": "Full Custom Design & 3D Rendering", + "name_ru": "Полный дизайн и 3D-моделирование", + "description": "Complete custom jewelry design service including CAD modeling, photorealistic 3D rendering, and wax prototype. Two rounds of revisions included. Final design ready for production.", + "description_ru": "Полная услуга индивидуального дизайна ювелирного изделия, включая CAD-моделирование, фотореалистичный 3D-рендеринг и восковой прототип. Включены два раунда правок. Финальный дизайн готов к производству.", + "category": "Custom Jewelry Design", + "brand": null, + "partnumber": "SVC-DSN-3DR", + "price": 800, + "purchase_price": 350, + "quantity": 999, + "is_digital": true + }, + { + "name": "Standard Prong Setting", + "name_ru": "Стандартная крапановая закрепка", + "description": "Professional prong setting service for a single gemstone. Includes four or six-prong setting in your choice of gold, platinum, or silver mount. Stone security guaranteed.", + "description_ru": "Профессиональная услуга крапановой закрепки одного драгоценного камня. Включает четырёх- или шестикрапанную закрепку в выбранной вами оправе из золота, платины или серебра. Гарантия надёжности крепления камня.", + "category": "Stone Setting", + "brand": null, + "partnumber": "SVC-SET-PRG", + "price": 200, + "purchase_price": 100, + "quantity": 999, + "is_digital": true + }, + { + "name": "Pavé Setting Service", + "name_ru": "Закрепка паве", + "description": "Expert pavé or channel setting of small accent stones. Price per stone for melee diamonds or colored gemstones. Minimum 10 stones per order. Includes stone alignment and leveling.", + "description_ru": "Экспертная закрепка паве или канальная закрепка мелких акцентных камней. Цена за камень для бриллиантов мелле или цветных камней. Минимум 10 камней в заказе. Включает выравнивание и нивелировку камней.", + "category": "Stone Setting", + "brand": null, + "partnumber": "SVC-SET-PVE", + "price": 45, + "purchase_price": 20, + "quantity": 999, + "is_digital": true + }, + { + "name": "Basic Jewelry Repair", + "name_ru": "Базовый ремонт украшений", + "description": "Standard repair services including ring sizing (up or down 2 sizes), prong retipping, clasp replacement, and chain soldering. Turnaround time 3-5 business days.", + "description_ru": "Стандартные ремонтные услуги, включая изменение размера кольца (на 2 размера вверх или вниз), восстановление крапанов, замену застёжек и пайку цепочек. Срок выполнения 3-5 рабочих дней.", + "category": "Jewelry Repair", + "brand": null, + "partnumber": "SVC-RPR-BSC", + "price": 120, + "purchase_price": 60, + "quantity": 999, + "is_digital": true + }, + { + "name": "Full Restoration Service", + "name_ru": "Полная реставрация", + "description": "Comprehensive jewelry restoration including cleaning, polishing, rhodium plating, stone tightening, and structural repair. Ideal for heirloom and vintage pieces. Includes before/after documentation.", + "description_ru": "Комплексная реставрация ювелирного изделия, включая чистку, полировку, родирование, подтяжку камней и структурный ремонт. Идеальна для фамильных и винтажных украшений. Включает документацию «до и после».", + "category": "Jewelry Repair", + "brand": null, + "partnumber": "SVC-RPR-FUL", + "price": 650, + "purchase_price": 300, + "quantity": 999, + "is_digital": true + }, + { + "name": "Standard Gemstone Re-cutting", + "name_ru": "Стандартная переогранка камня", + "description": "Professional re-cutting of an existing gemstone to improve brilliance, repair damage, or change shape. Includes precision faceting on state-of-the-art equipment. Weight loss assessment provided before work begins.", + "description_ru": "Профессиональная переогранка существующего драгоценного камня для улучшения блеска, устранения повреждений или изменения формы. Включает прецизионную огранку на современном оборудовании. Оценка потери веса предоставляется до начала работ.", + "category": "Stone Cutting", + "brand": null, + "partnumber": "SVC-CUT-STD", + "price": 300, + "purchase_price": 150, + "quantity": 999, + "is_digital": true + }, + { + "name": "Custom Fantasy Cut", + "name_ru": "Индивидуальная фантазийная огранка", + "description": "Bespoke fantasy or designer cut for rough or pre-cut gemstones. Our master cutter creates unique facet patterns that maximize color, brilliance, and visual impact. Includes design consultation.", + "description_ru": "Индивидуальная фантазийная или дизайнерская огранка необработанных или предварительно огранённых камней. Наш мастер-огранщик создаёт уникальные рисунки граней, максимизирующие цвет, блеск и визуальный эффект. Включает консультацию по дизайну.", + "category": "Stone Cutting", + "brand": null, + "partnumber": "SVC-CUT-FNT", + "price": 1200, + "purchase_price": 550, + "quantity": 999, + "is_digital": true + }, + { + "name": "24K Gold Bar 1oz LBMA", + "name_ru": "Слиток золота 24K 1 унция LBMA", + "description": "Investment-grade 1 troy ounce gold bar, 999.9 fine (24K). LBMA certified with unique serial number and assay certificate. Sealed in tamper-evident packaging.", + "description_ru": "Инвестиционный слиток золота 1 тройская унция, проба 999.9 (24K). Сертификат LBMA с уникальным серийным номером и пробирным сертификатом. В запечатанной защитной упаковке.", + "category": "Gold", + "brand": "Noble Metals Trading", + "partnumber": "MTL-GLD-BAR-1OZ", + "price": 2100, + "purchase_price": 1950, + "quantity": 20 + }, + { + "name": "18K Gold Wire 1mm (per gram)", + "name_ru": "Золотая проволока 18K 1мм (за грамм)", + "description": "Premium 18K (750) gold wire, 1mm diameter. Ideal for jewelry making, custom settings, and repair work. Sold per gram. Available in yellow, white, and rose gold alloys.", + "description_ru": "Премиальная золотая проволока 18K (750), диаметр 1 мм. Идеальна для ювелирного дела, индивидуальных оправ и ремонтных работ. Продаётся за грамм. Доступна в сплавах жёлтого, белого и розового золота.", + "category": "Gold", + "brand": "Noble Metals Trading", + "partnumber": "MTL-GLD-WIR-18K", + "price": 58, + "purchase_price": 52, + "quantity": 500 + }, + { + "name": "Sterling Silver Sheet 20ga (per oz)", + "name_ru": "Лист стерлингового серебра 20ga (за унцию)", + "description": "High-quality sterling silver (925) sheet metal, 20 gauge thickness. Dead soft temper for easy forming and fabrication. Sold per troy ounce. Ideal for bezels, backplates, and decorative elements.", + "description_ru": "Высококачественный лист стерлингового серебра (925), толщина 20 ga. Мягкий отжиг для лёгкой формовки и обработки. Продаётся за тройскую унцию. Идеален для оправ, подложек и декоративных элементов.", + "category": "Silver", + "brand": "Noble Metals Trading", + "partnumber": "MTL-SLV-SHT-925", + "price": 32, + "purchase_price": 26, + "quantity": 500 + }, + { + "name": "Fine Silver Wire 999 1mm (per oz)", + "name_ru": "Проволока чистого серебра 999 1мм (за унцию)", + "description": "Fine silver (999) wire, 1mm diameter. Exceptionally pure and malleable, perfect for wire wrapping, chain making, and granulation. Sold per troy ounce.", + "description_ru": "Проволока чистого серебра (999), диаметр 1 мм. Исключительно чистое и пластичное, идеальное для проволочной обмотки, изготовления цепочек и зерни. Продаётся за тройскую унцию.", + "category": "Silver", + "brand": "Noble Metals Trading", + "partnumber": "MTL-SLV-WIR-999", + "price": 28, + "purchase_price": 22, + "quantity": 500 + }, + { + "name": "Platinum Bar 1oz", + "name_ru": "Слиток платины 1 унция", + "description": "Investment-grade 1 troy ounce platinum bar, 999.5 fine. LPPM certified with serial number and assay certificate. Ideal for precious metals portfolio diversification.", + "description_ru": "Инвестиционный слиток платины 1 тройская унция, проба 999.5. Сертификат LPPM с серийным номером и пробирным сертификатом. Идеален для диверсификации портфеля драгоценных металлов.", + "category": "Platinum", + "brand": "Noble Metals Trading", + "partnumber": "MTL-PLT-BAR-1OZ", + "price": 1050, + "purchase_price": 950, + "quantity": 15 + }, + { + "name": "Platinum Wire 0.8mm (per gram)", + "name_ru": "Платиновая проволока 0.8мм (за грамм)", + "description": "950 platinum wire, 0.8mm diameter. Ideal for high-end jewelry fabrication, especially prong and bezel settings. Excellent durability and hypoallergenic properties. Sold per gram.", + "description_ru": "Платиновая проволока 950, диаметр 0.8 мм. Идеальна для изготовления ювелирных изделий премиум-класса, особенно крапановых и ободковых оправ. Отличная долговечность и гипоаллергенные свойства. Продаётся за грамм.", + "category": "Platinum", + "brand": "Noble Metals Trading", + "partnumber": "MTL-PLT-WIR-950", + "price": 35, + "purchase_price": 30, + "quantity": 300 + }, + { + "name": "Palladium Bar 1oz", + "name_ru": "Слиток палладия 1 унция", + "description": "Investment-grade 1 troy ounce palladium bar, 999.5 fine. Sealed with certificate of authenticity. A lightweight alternative to platinum with excellent investment potential.", + "description_ru": "Инвестиционный слиток палладия 1 тройская унция, проба 999.5. Запечатан с сертификатом подлинности. Лёгкая альтернатива платине с отличным инвестиционным потенциалом.", + "category": "Palladium", + "brand": "Noble Metals Trading", + "partnumber": "MTL-PLD-BAR-1OZ", + "price": 1200, + "purchase_price": 1080, + "quantity": 10 + }, + { + "name": "Palladium Sheet 24ga (per gram)", + "name_ru": "Лист палладия 24ga (за грамм)", + "description": "950 palladium sheet, 24 gauge. Increasingly popular in modern jewelry design for its light weight and bright white color that won't tarnish. Sold per gram.", + "description_ru": "Лист палладия 950, толщина 24 ga. Всё более популярен в современном ювелирном дизайне благодаря лёгкости и яркому белому цвету, не подверженному потемнению. Продаётся за грамм.", + "category": "Palladium", + "brand": "Noble Metals Trading", + "partnumber": "MTL-PLD-SHT-950", + "price": 42, + "purchase_price": 36, + "quantity": 200 + }, + { + "name": "Rhodium Plating Solution 100ml", + "name_ru": "Раствор для родирования 100мл", + "description": "Professional-grade rhodium plating solution (2g Rh/100ml). Ready-to-use bath for electroplating white gold, silver, and other jewelry metals. Produces a bright, reflective, tarnish-resistant finish.", + "description_ru": "Профессиональный раствор для родирования (2г Rh/100мл). Готовая к использованию ванна для гальванического покрытия белого золота, серебра и других ювелирных металлов. Создаёт яркое, отражающее, устойчивое к потемнению покрытие.", + "category": "Rhodium", + "brand": "Noble Metals Trading", + "partnumber": "MTL-RHD-SOL-100", + "price": 180, + "purchase_price": 140, + "quantity": 50 + }, + { + "name": "Rhodium Plating Service", + "name_ru": "Услуга родирования", + "description": "Professional rhodium plating for a single jewelry piece. Restores the bright white finish on white gold jewelry or provides a protective coating on silver. Turnaround 1-2 business days.", + "description_ru": "Профессиональное родирование одного ювелирного изделия. Восстанавливает яркий белый блеск украшений из белого золота или создаёт защитное покрытие на серебре. Срок выполнения 1-2 рабочих дня.", + "category": "Rhodium", + "brand": null, + "partnumber": "MTL-RHD-SVC-PLT", + "price": 85, + "purchase_price": 40, + "quantity": 999, + "is_digital": true + } + ], + "post_tags": [ + {"tag_name": "legal", "name": "Legal", "name_ru": "Правовые документы"}, + {"tag_name": "info", "name": "Information", "name_ru": "Информация"}, + {"tag_name": "company", "name": "Company", "name_ru": "О компании"}, + {"tag_name": "news", "name": "News", "name_ru": "Новости"} + ], + "blog_posts": [ + { + "title": "Privacy Policy", + "title_ru": "Политика конфиденциальности", + "meta_description": "Privacy policy for Schon Demo Store — how we handle your data", + "meta_description_ru": "Политика конфиденциальности Демо-магазина Schon — как мы обрабатываем ваши данные", + "is_static_page": true, + "tags": ["legal"], + "content_file": "privacy-policy" + }, + { + "title": "Terms & Conditions", + "title_ru": "Условия использования", + "meta_description": "Terms and conditions for using the Schon Demo Store", + "meta_description_ru": "Условия использования Демо-магазина Schon", + "is_static_page": true, + "tags": ["legal"], + "content_file": "terms-and-conditions" + }, + { + "title": "About Us", + "title_ru": "О нас", + "meta_description": "Learn about the Schon Demo Store and the Schon e-commerce platform", + "meta_description_ru": "Узнайте о Демо-магазине Schon и платформе электронной коммерции Schon", + "is_static_page": true, + "tags": ["company"], + "content_file": "about-us" + }, + { + "title": "FAQ", + "title_ru": "Часто задаваемые вопросы", + "meta_description": "Frequently asked questions about the Schon Demo Store", + "meta_description_ru": "Часто задаваемые вопросы о Демо-магазине Schon", + "is_static_page": true, + "tags": ["info"], + "content_file": "faq" + }, + { + "title": "Return Policy", + "title_ru": "Политика возврата", + "meta_description": "Return policy and refund information for the Schon Demo Store", + "meta_description_ru": "Политика возврата и информация о возмещении в Демо-магазине Schon", + "is_static_page": true, + "tags": ["legal", "info"], + "content_file": "return-policy" + }, + { + "title": "Shipping Information", + "title_ru": "Информация о доставке", + "meta_description": "Shipping methods, costs, and delivery details for the Schon Demo Store", + "meta_description_ru": "Способы доставки, стоимость и условия доставки в Демо-магазине Schon", + "is_static_page": true, + "tags": ["info"], + "content_file": "shipping-info" + }, + { + "title": "New Collection: Spring 2026 Gemstone Arrivals", + "title_ru": "Новая коллекция: поступление драгоценных камней весна 2026", + "meta_description": "Discover our Spring 2026 collection featuring new emeralds, diamond cuts, and rare quartz varieties", + "meta_description_ru": "Откройте нашу весеннюю коллекцию 2026 с новыми изумрудами, огранками бриллиантов и редкими кварцами", + "is_static_page": false, + "tags": ["news", "company"], + "content_file": "spring-2026-collection" + }, + { + "title": "Now Available in Russian: Full Bilingual Shopping Experience", + "title_ru": "Теперь на русском: полноценный двуязычный шоппинг", + "meta_description": "Our store now offers a complete bilingual experience in English and Russian", + "meta_description_ru": "Наш магазин теперь предлагает полноценный двуязычный опыт на английском и русском", + "is_static_page": false, + "tags": ["news", "company"], + "content_file": "bilingual-experience" + }, + { + "title": "Understanding Gemstone Certification: A Buyer's Guide", + "title_ru": "Сертификация драгоценных камней: руководство покупателя", + "meta_description": "Learn about GIA, GRS, and other gemological laboratories and why certification matters", + "meta_description_ru": "Узнайте о GIA, GRS и других геммологических лабораториях и важности сертификации", + "is_static_page": false, + "tags": ["info"], + "content_file": "gemstone-certification-guide" + }, + { + "title": "Holiday Gift Guide: Top Gemstones Under $5,000", + "title_ru": "Подарочный гид: лучшие драгоценные камни до $5 000", + "meta_description": "Our curated selection of the most impressive gemstones under $5,000", + "meta_description_ru": "Наша подборка самых впечатляющих драгоценных камней стоимостью до $5 000", + "is_static_page": false, + "tags": ["news"], + "content_file": "holiday-gift-guide" + }, + { + "title": "Behind the Platform: How Schon Powers This Store", + "title_ru": "За кулисами платформы: как Schon обеспечивает работу этого магазина", + "meta_description": "A technical look at the Schon e-commerce platform and why it's the right choice for your business", + "meta_description_ru": "Техническое знакомство с платформой Schon и почему она подходит для вашего бизнеса", + "is_static_page": false, + "tags": ["company"], + "content_file": "behind-the-platform" } ], "vendor": { @@ -653,7 +1741,7 @@ }, "demo_users": { "password": "Schon!Demo888", - "email_domain": "demo.schon.store", + "email_domain": "wiseless.xyz", "first_names": [ "Emma", "Liam", "Olivia", "Noah", "Ava", "Ethan", "Sophia", "Mason", "Isabella", "William", "Mia", "James", "Charlotte", "Benjamin", "Amelia", diff --git a/engine/core/fixtures/demo_blog_posts/about-us.en.md b/engine/core/fixtures/demo_blog_posts/about-us.en.md new file mode 100644 index 00000000..d844ee6a --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/about-us.en.md @@ -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.* diff --git a/engine/core/fixtures/demo_blog_posts/about-us.ru.md b/engine/core/fixtures/demo_blog_posts/about-us.ru.md new file mode 100644 index 00000000..30588bd5 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/about-us.ru.md @@ -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) — электронная коммерция, сделанная правильно.* diff --git a/engine/core/fixtures/demo_blog_posts/behind-the-platform.en.md b/engine/core/fixtures/demo_blog_posts/behind-the-platform.en.md new file mode 100644 index 00000000..adc5b04f --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/behind-the-platform.en.md @@ -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.* diff --git a/engine/core/fixtures/demo_blog_posts/behind-the-platform.ru.md b/engine/core/fixtures/demo_blog_posts/behind-the-platform.ru.md new file mode 100644 index 00000000..dab9e736 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/behind-the-platform.ru.md @@ -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) в реалистичном сценарии. Все товары, бренды и транзакции являются вымышленными.* diff --git a/engine/core/fixtures/demo_blog_posts/bilingual-experience.en.md b/engine/core/fixtures/demo_blog_posts/bilingual-experience.en.md new file mode 100644 index 00000000..f1f9fcfa --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/bilingual-experience.en.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/bilingual-experience.ru.md b/engine/core/fixtures/demo_blog_posts/bilingual-experience.ru.md new file mode 100644 index 00000000..6ad437e7 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/bilingual-experience.ru.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/faq.en.md b/engine/core/fixtures/demo_blog_posts/faq.en.md new file mode 100644 index 00000000..5b933955 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/faq.en.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/faq.ru.md b/engine/core/fixtures/demo_blog_posts/faq.ru.md new file mode 100644 index 00000000..e1f7a488 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/faq.ru.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/gemstone-certification-guide.en.md b/engine/core/fixtures/demo_blog_posts/gemstone-certification-guide.en.md new file mode 100644 index 00000000..157358e9 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/gemstone-certification-guide.en.md @@ -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.* diff --git a/engine/core/fixtures/demo_blog_posts/gemstone-certification-guide.ru.md b/engine/core/fixtures/demo_blog_posts/gemstone-certification-guide.ru.md new file mode 100644 index 00000000..171827e8 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/gemstone-certification-guide.ru.md @@ -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), чтобы узнать о создании собственного магазина драгоценных камней или ювелирных изделий.* diff --git a/engine/core/fixtures/demo_blog_posts/holiday-gift-guide.en.md b/engine/core/fixtures/demo_blog_posts/holiday-gift-guide.en.md new file mode 100644 index 00000000..871d91eb --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/holiday-gift-guide.en.md @@ -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.* diff --git a/engine/core/fixtures/demo_blog_posts/holiday-gift-guide.ru.md b/engine/core/fixtures/demo_blog_posts/holiday-gift-guide.ru.md new file mode 100644 index 00000000..5ac01e19 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/holiday-gift-guide.ru.md @@ -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). Все товары и цены являются вымышленными и предназначены исключительно для демонстрации.* diff --git a/engine/core/fixtures/demo_blog_posts/privacy-policy.en.md b/engine/core/fixtures/demo_blog_posts/privacy-policy.en.md new file mode 100644 index 00000000..f1dcc9f5 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/privacy-policy.en.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/privacy-policy.ru.md b/engine/core/fixtures/demo_blog_posts/privacy-policy.ru.md new file mode 100644 index 00000000..cde6fda4 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/privacy-policy.ru.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/return-policy.en.md b/engine/core/fixtures/demo_blog_posts/return-policy.en.md new file mode 100644 index 00000000..2a714c08 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/return-policy.en.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/return-policy.ru.md b/engine/core/fixtures/demo_blog_posts/return-policy.ru.md new file mode 100644 index 00000000..c4323e44 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/return-policy.ru.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/shipping-info.en.md b/engine/core/fixtures/demo_blog_posts/shipping-info.en.md new file mode 100644 index 00000000..df852f11 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/shipping-info.en.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/shipping-info.ru.md b/engine/core/fixtures/demo_blog_posts/shipping-info.ru.md new file mode 100644 index 00000000..d3ce53a0 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/shipping-info.ru.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/spring-2026-collection.en.md b/engine/core/fixtures/demo_blog_posts/spring-2026-collection.en.md new file mode 100644 index 00000000..0e225281 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/spring-2026-collection.en.md @@ -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.* diff --git a/engine/core/fixtures/demo_blog_posts/spring-2026-collection.ru.md b/engine/core/fixtures/demo_blog_posts/spring-2026-collection.ru.md new file mode 100644 index 00000000..aaec775e --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/spring-2026-collection.ru.md @@ -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) — современной платформе электронной коммерции для бизнеса, требующего гибкости и производительности. Все представленные товары являются вымышленными и предназначены исключительно для демонстрации.* diff --git a/engine/core/fixtures/demo_blog_posts/terms-and-conditions.en.md b/engine/core/fixtures/demo_blog_posts/terms-and-conditions.en.md new file mode 100644 index 00000000..df075760 --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/terms-and-conditions.en.md @@ -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).* diff --git a/engine/core/fixtures/demo_blog_posts/terms-and-conditions.ru.md b/engine/core/fixtures/demo_blog_posts/terms-and-conditions.ru.md new file mode 100644 index 00000000..3b2d4f9d --- /dev/null +++ b/engine/core/fixtures/demo_blog_posts/terms-and-conditions.ru.md @@ -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).* diff --git a/engine/core/fixtures/demo_products_images/AME-CIT-850-MD.jpeg b/engine/core/fixtures/demo_products_images/AME-CIT-850-MD.jpeg new file mode 100644 index 00000000..0bdb8fdb Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AME-CIT-850-MD.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/AME-PRS-1120.jpg b/engine/core/fixtures/demo_products_images/AME-PRS-1120.jpg new file mode 100644 index 00000000..429df4f5 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AME-PRS-1120.jpg differ diff --git a/engine/core/fixtures/demo_products_images/AME-RDF-1400.jpeg b/engine/core/fixtures/demo_products_images/AME-RDF-1400.jpeg new file mode 100644 index 00000000..7280d903 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AME-RDF-1400.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/AQU-MAD-750.jpg b/engine/core/fixtures/demo_products_images/AQU-MAD-750.jpg new file mode 100644 index 00000000..7f23f9eb Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AQU-MAD-750.jpg differ diff --git a/engine/core/fixtures/demo_products_images/AQU-PAK-1520.jpg b/engine/core/fixtures/demo_products_images/AQU-PAK-1520.jpg new file mode 100644 index 00000000..a54b43ad Binary files /dev/null and b/engine/core/fixtures/demo_products_images/AQU-PAK-1520.jpg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-AS-220-E-VS1.jpeg b/engine/core/fixtures/demo_products_images/DIA-AS-220-E-VS1.jpeg new file mode 100644 index 00000000..30579f9f Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-AS-220-E-VS1.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/DIA-MQ-100-D-VVS2.jpg b/engine/core/fixtures/demo_products_images/DIA-MQ-100-D-VVS2.jpg new file mode 100644 index 00000000..16abcb8e Binary files /dev/null and b/engine/core/fixtures/demo_products_images/DIA-MQ-100-D-VVS2.jpg differ diff --git a/engine/core/fixtures/demo_products_images/EME-AFG-180-PJ.png b/engine/core/fixtures/demo_products_images/EME-AFG-180-PJ.png new file mode 100644 index 00000000..09be553c Binary files /dev/null and b/engine/core/fixtures/demo_products_images/EME-AFG-180-PJ.png differ diff --git a/engine/core/fixtures/demo_products_images/EME-COL-PAIR-100.jpeg b/engine/core/fixtures/demo_products_images/EME-COL-PAIR-100.jpeg new file mode 100644 index 00000000..54d8de22 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/EME-COL-PAIR-100.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/EME-ETH-250.jpg b/engine/core/fixtures/demo_products_images/EME-ETH-250.jpg new file mode 100644 index 00000000..9b8112af Binary files /dev/null and b/engine/core/fixtures/demo_products_images/EME-ETH-250.jpg differ diff --git a/engine/core/fixtures/demo_products_images/PRL-BAR-16MM-WH.jpg b/engine/core/fixtures/demo_products_images/PRL-BAR-16MM-WH.jpg new file mode 100644 index 00000000..6087d8da Binary files /dev/null and b/engine/core/fixtures/demo_products_images/PRL-BAR-16MM-WH.jpg differ diff --git a/engine/core/fixtures/demo_products_images/PRL-TAH-12MM-PC.jpeg b/engine/core/fixtures/demo_products_images/PRL-TAH-12MM-PC.jpeg new file mode 100644 index 00000000..aa9752b8 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/PRL-TAH-12MM-PC.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/RUB-BUR-100-UH.jpeg b/engine/core/fixtures/demo_products_images/RUB-BUR-100-UH.jpeg new file mode 100644 index 00000000..4824c5c4 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/RUB-BUR-100-UH.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/RUB-THA-150-VR.jpg b/engine/core/fixtures/demo_products_images/RUB-THA-150-VR.jpg new file mode 100644 index 00000000..92a1db1b Binary files /dev/null and b/engine/core/fixtures/demo_products_images/RUB-THA-150-VR.jpg differ diff --git a/engine/core/fixtures/demo_products_images/SAP-CEY-500-UH.jpeg b/engine/core/fixtures/demo_products_images/SAP-CEY-500-UH.jpeg new file mode 100644 index 00000000..303aaac1 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/SAP-CEY-500-UH.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/SAP-PAD-150.jpeg b/engine/core/fixtures/demo_products_images/SAP-PAD-150.jpeg new file mode 100644 index 00000000..be263b3e Binary files /dev/null and b/engine/core/fixtures/demo_products_images/SAP-PAD-150.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/SAP-PNK-PAIR-120.jpeg b/engine/core/fixtures/demo_products_images/SAP-PNK-PAIR-120.jpeg new file mode 100644 index 00000000..558ef5ce Binary files /dev/null and b/engine/core/fixtures/demo_products_images/SAP-PNK-PAIR-120.jpeg differ diff --git a/engine/core/fixtures/demo_products_images/TAN-320-BV.jpg b/engine/core/fixtures/demo_products_images/TAN-320-BV.jpg new file mode 100644 index 00000000..500a88f6 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TAN-320-BV.jpg differ diff --git a/engine/core/fixtures/demo_products_images/TOU-CHR-280.jpg b/engine/core/fixtures/demo_products_images/TOU-CHR-280.jpg new file mode 100644 index 00000000..31433475 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TOU-CHR-280.jpg differ diff --git a/engine/core/fixtures/demo_products_images/TOU-RUB-430.jpeg b/engine/core/fixtures/demo_products_images/TOU-RUB-430.jpeg new file mode 100644 index 00000000..06ba9dd7 Binary files /dev/null and b/engine/core/fixtures/demo_products_images/TOU-RUB-430.jpeg differ diff --git a/engine/core/graphene/object_types.py b/engine/core/graphene/object_types.py index 44ffffc3..cd0c9e0a 100644 --- a/engine/core/graphene/object_types.py +++ b/engine/core/graphene/object_types.py @@ -3,8 +3,7 @@ from contextlib import suppress from typing import Any from django.conf import settings -from django.core.cache import cache -from django.db.models import Max, Min, QuerySet +from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ from graphene import ( UUID, @@ -230,6 +229,7 @@ class CategoryType(DjangoObjectType): "minimum and maximum prices for products in this category, if available." ), ) + brands = List(lambda: BrandType, description=_("brands in this category")) tags = DjangoFilterConnectionField( lambda: CategoryTagType, description=_("tags for this category") ) @@ -249,6 +249,7 @@ class CategoryType(DjangoObjectType): "slug", "description", "image", + "brands", "min_max_prices", ) filter_fields = ["uuid"] @@ -277,23 +278,15 @@ class CategoryType(DjangoObjectType): return self.filterable_attributes 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 { - "min_price": min_max_prices["min_price"], - "max_price": min_max_prices["max_price"], + "min_price": self.min_price, + "max_price": self.max_price, } + def resolve_brands(self: Category, info) -> QuerySet[Brand]: + return self.brands + def resolve_seo_meta(self: Category, info): lang = graphene_current_lang() base = f"https://{settings.BASE_DOMAIN}" diff --git a/engine/core/management/commands/clear_unwanted.py b/engine/core/management/commands/clear_unwanted.py index c2ed53a8..d37853f9 100644 --- a/engine/core/management/commands/clear_unwanted.py +++ b/engine/core/management/commands/clear_unwanted.py @@ -14,7 +14,7 @@ class Command(BaseCommand): # Group stocks by (product, vendor) stocks_by_group = defaultdict(list) 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] = [] for group in stocks_by_group.values(): diff --git a/engine/core/management/commands/demo_data.py b/engine/core/management/commands/demo_data.py index 96e2fb73..3858ab6e 100644 --- a/engine/core/management/commands/demo_data.py +++ b/engine/core/management/commands/demo_data.py @@ -10,11 +10,14 @@ from django.core.files.base import ContentFile from django.core.management.base import BaseCommand from django.db import transaction from django.utils import timezone +from django.utils.translation import override +from engine.blog.models import Post, PostTag from engine.core.models import ( Address, Attribute, AttributeGroup, + AttributeValue, Brand, Category, CategoryTag, @@ -30,9 +33,9 @@ from engine.core.models import ( from engine.payments.models import Balance from engine.vibes_auth.models import Group, User -DEMO_EMAIL_DOMAIN = "demo.schon.store" -DEMO_VENDOR_NAME = "GemSource Global" +DEMO_EMAIL_DOMAIN = "wiseless.xyz" 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): @@ -84,24 +87,27 @@ class Command(BaseCommand): self._load_demo_data() - if action == "install": - self._install(options) - elif action == "remove": - self._remove() - else: - self.stdout.write(self.style.ERROR(f"Unknown action: {action}")) + with override("en"): + if action == "install": + self._install(options) + elif action == "remove": + self._remove() + else: + self.stdout.write(self.style.ERROR(f"Unknown action: {action}")) @property def staff_user(self): user, _ = User.objects.get_or_create( email=f"staff@{DEMO_EMAIL_DOMAIN}", - password="Staff!Demo888", first_name="Alice", last_name="Schon", is_staff=True, is_active=True, is_verified=True, ) + if _: + user.set_password("Staff!Demo888") + user.save() if not user.groups.filter(name="E-Commerce Admin").exists(): user.groups.add(Group.objects.get(name="E-Commerce Admin")) return user @@ -110,7 +116,6 @@ class Command(BaseCommand): def super_user(self): user, _ = User.objects.get_or_create( email=f"super@{DEMO_EMAIL_DOMAIN}", - password="Super!Demo888", first_name="Bob", last_name="Schon", is_superuser=True, @@ -118,6 +123,9 @@ class Command(BaseCommand): is_active=True, is_verified=True, ) + if _: + user.set_password("Super!Demo888") + user.save() return user def _load_demo_data(self) -> None: @@ -161,6 +169,10 @@ class Command(BaseCommand): wishlist_count = self._create_demo_wishlists(users, products) 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.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() + 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_vendor_name = self.demo_data["vendor"]["name"] try: - vendor = Vendor.objects.get(name=DEMO_VENDOR_NAME) + vendor = Vendor.objects.get(name=demo_vendor_name) Stock.objects.filter(vendor=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: pass @@ -213,6 +235,10 @@ class Command(BaseCommand): if product_dir.exists() and not any(product_dir.iterdir()): 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() 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" Categories: {Category.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" Orders created: {len(orders)}") self.stdout.write(f" Refunded orders: {refunded_count}") @@ -316,6 +343,17 @@ class Command(BaseCommand): attr.name_ru_ru = attr_data["name_ru"] 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 = {} for brand_data in data["brands"]: brand, created = Brand.objects.get_or_create( @@ -360,15 +398,15 @@ class Command(BaseCommand): "description": prod_data["description"], "category": category, "brand": brand, - "is_digital": False, + "is_digital": prod_data.get("is_digital", False), }, ) if created: 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: - product.description_ru_ru = prod_data["description_ru"] + product.description_ru_ru = prod_data["description_ru"] # ty: ignore[invalid-assignment] product.save() Stock.objects.create( @@ -383,27 +421,66 @@ class Command(BaseCommand): # Add product image self._add_product_image(product, prod_data["partnumber"]) - def _add_product_image(self, product: Product, partnumber: str) -> None: - image_path = DEMO_IMAGES_DIR / f"{partnumber}.jpg" - if not image_path.exists(): - image_path = DEMO_IMAGES_DIR / "placeholder.png" + # Add attribute values + for attr_name, av_data in prod_data.get("attribute_values", {}).items(): + attr = attr_lookup.get(attr_name) + 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.style.WARNING(f" No image found for {partnumber}, skipping...") ) 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: image_content = f.read() - filename = image_path.name product_image = ProductImage( product=product, 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 def _create_demo_users(self, count: int) -> list: @@ -585,3 +662,66 @@ class Command(BaseCommand): users_with_wishlists.add(user.id) 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() diff --git a/engine/core/migrations/0024_categorytag_category_tags.py b/engine/core/migrations/0024_categorytag_category_tags.py index 3de05258..7b696f40 100644 --- a/engine/core/migrations/0024_categorytag_category_tags.py +++ b/engine/core/migrations/0024_categorytag_category_tags.py @@ -1,7 +1,6 @@ import uuid import django_extensions.db.fields -import django_prometheus.models from django.db import migrations, models @@ -251,10 +250,7 @@ class Migration(migrations.Migration): "verbose_name": "category tag", "verbose_name_plural": "category tags", }, - bases=( - django_prometheus.models.ExportModelOperationsMixin("category_tag"), - models.Model, - ), + bases=(models.Model,), ), migrations.AddField( model_name="category", diff --git a/engine/core/migrations/0040_customerrelationshipmanagementprovider_ordercrmlink.py b/engine/core/migrations/0040_customerrelationshipmanagementprovider_ordercrmlink.py index a69e8f2d..45e2630e 100644 --- a/engine/core/migrations/0040_customerrelationshipmanagementprovider_ordercrmlink.py +++ b/engine/core/migrations/0040_customerrelationshipmanagementprovider_ordercrmlink.py @@ -2,7 +2,6 @@ import uuid import django.db.models.deletion import django_extensions.db.fields -import django_prometheus.models from django.db import migrations, models @@ -80,10 +79,7 @@ class Migration(migrations.Migration): "verbose_name": "order CRM link", "verbose_name_plural": "orders CRM links", }, - bases=( - django_prometheus.models.ExportModelOperationsMixin("crm_provider"), - models.Model, - ), + bases=(models.Model,), ), migrations.CreateModel( name="OrderCrmLink", @@ -148,9 +144,6 @@ class Migration(migrations.Migration): "verbose_name": "order CRM link", "verbose_name_plural": "orders CRM links", }, - bases=( - django_prometheus.models.ExportModelOperationsMixin("order_crm_link"), - models.Model, - ), + bases=(models.Model,), ), ] diff --git a/engine/core/migrations/0055_alter_brand_categories_alter_product_slug.py b/engine/core/migrations/0055_alter_brand_categories_alter_product_slug.py new file mode 100644 index 00000000..25cfb69b --- /dev/null +++ b/engine/core/migrations/0055_alter_brand_categories_alter_product_slug.py @@ -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", + ), + ), + ] diff --git a/engine/core/models.py b/engine/core/models.py index 0ad745aa..690f7014 100644 --- a/engine/core/models.py +++ b/engine/core/models.py @@ -2,7 +2,7 @@ import datetime import json import logging from contextlib import suppress -from typing import Any, Iterable, Self +from typing import TYPE_CHECKING, Any, Iterable, Self from constance import config from django.conf import settings @@ -29,6 +29,7 @@ from django.db.models import ( JSONField, ManyToManyField, Max, + Min, OneToOneField, PositiveIntegerField, QuerySet, @@ -46,7 +47,6 @@ from django.utils.functional import cached_property from django.utils.http import urlsafe_base64_encode from django.utils.translation import gettext_lazy as _ from django_extensions.db.fields import AutoSlugField -from django_prometheus.models import ExportModelOperationsMixin from mptt.fields import TreeForeignKey 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 schon.utils.misc import create_object +if TYPE_CHECKING: + from django.db.models import Manager + logger = logging.getLogger(__name__) -class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel): +class AttributeGroup(NiceModel): __doc__ = _( "Represents a group of attributes, which can be hierarchical." " 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." ) + if TYPE_CHECKING: + attributes: Manager["Attribute"] + is_publicly_visible = True parent = ForeignKey( "self", @@ -109,7 +115,7 @@ class AttributeGroup(ExportModelOperationsMixin("attribute_group"), NiceModel): verbose_name_plural = _("attribute groups") -class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): +class Vendor(NiceModel): __doc__ = _( "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." @@ -193,7 +199,7 @@ class Vendor(ExportModelOperationsMixin("vendor"), NiceModel): ] -class ProductTag(ExportModelOperationsMixin("product_tag"), NiceModel): +class ProductTag(NiceModel): __doc__ = _( "Represents a product tag used for classifying or identifying products." " 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") -class CategoryTag(ExportModelOperationsMixin("category_tag"), NiceModel): +class CategoryTag(NiceModel): __doc__ = _( "Represents a category tag used for 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") -class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): +class Category(NiceModel, MPTTModel): __doc__ = _( "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." @@ -267,6 +273,10 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): " as well as assign attributes like images, tags, or priority." ) + if TYPE_CHECKING: + products: Manager["Product"] + children: Manager["Category"] + is_publicly_visible = True image = ImageField( @@ -402,7 +412,8 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): @cached_property def filterable_attributes(self) -> list[FilterableAttribute]: rows = ( - AttributeValue.objects.annotate(value_length=Length("value")) + AttributeValue.objects.filter(is_active=True) + .annotate(value_length=Length("value")) .filter( product__is_active=True, product__category=self, @@ -442,13 +453,35 @@ class Category(ExportModelOperationsMixin("category"), NiceModel, MPTTModel): # Fallback to favicon.png from static files 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: verbose_name = _("category") verbose_name_plural = _("categories") ordering = ["tree_id", "lft"] -class Brand(ExportModelOperationsMixin("brand"), NiceModel): +class Brand(NiceModel): __doc__ = _( "Represents a Brand object in the system. " "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( "core.Category", blank=True, - help_text=_("optional categories that this brand is associated with"), - verbose_name=_("associated categories"), + help_text=_("DEPRECATED"), + verbose_name=_("DEPRECATED"), ) slug = AutoSlugField( populate_from=("name",), @@ -518,7 +551,7 @@ class Brand(ExportModelOperationsMixin("brand"), NiceModel): verbose_name_plural = _("brands") -class Stock(ExportModelOperationsMixin("stock"), NiceModel): +class Stock(NiceModel): __doc__ = _( "Represents the stock of a product managed in the system." " 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") -class Product(ExportModelOperationsMixin("product"), NiceModel): +class Product(NiceModel): __doc__ = _( "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." @@ -596,6 +629,12 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): " 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 category = ForeignKey( @@ -655,7 +694,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): help_text=_("part number for this product"), verbose_name=_("part number"), ) - slug = AutoSlugField( + slug = TweakedAutoSlugField( populate_from=( "name", "brand__slug", @@ -726,12 +765,17 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): @property 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 @cached_property 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 def total_orders(self): @@ -753,7 +797,7 @@ class Product(ExportModelOperationsMixin("product"), NiceModel): return self.images.exists() -class Attribute(ExportModelOperationsMixin("attribute"), NiceModel): +class Attribute(NiceModel): __doc__ = _( "Represents an attribute in the system." " This class is used to define and manage attributes," @@ -812,7 +856,7 @@ class Attribute(ExportModelOperationsMixin("attribute"), NiceModel): verbose_name_plural = _("attributes") -class AttributeValue(ExportModelOperationsMixin("attribute_value"), NiceModel): +class AttributeValue(NiceModel): __doc__ = _( "Represents a specific value for an attribute that is linked to a product. " "It links the 'attribute' to a unique 'value', allowing " @@ -852,7 +896,7 @@ class AttributeValue(ExportModelOperationsMixin("attribute_value"), NiceModel): verbose_name_plural = _("attribute values") -class ProductImage(ExportModelOperationsMixin("product_image"), NiceModel): +class ProductImage(NiceModel): __doc__ = _( "Represents a product image associated with a product in the system. " "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") -class Promotion(ExportModelOperationsMixin("promotion"), NiceModel): +class Promotion(NiceModel): __doc__ = _( "Represents a promotional campaign for products with a discount. " "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) -class Wishlist(ExportModelOperationsMixin("wishlist"), NiceModel): +class Wishlist(NiceModel): __doc__ = _( "Represents a user's wishlist for storing and managing desired products. " "The class provides functionality to manage a collection of products, " @@ -1023,7 +1067,7 @@ class Wishlist(ExportModelOperationsMixin("wishlist"), NiceModel): return self -class Documentary(ExportModelOperationsMixin("attribute_group"), NiceModel): +class Documentary(NiceModel): __doc__ = _( "Represents a documentary record tied to a product. " "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") -class Address(ExportModelOperationsMixin("address"), NiceModel): +class Address(NiceModel): __doc__ = _( "Represents an address entity that includes location details and associations with a user. " "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 -class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel): +class PromoCode(NiceModel): __doc__ = _( "Represents a promotional code that can be used for discounts, managing its validity, " "type of discount, and application. " @@ -1250,7 +1294,7 @@ class PromoCode(ExportModelOperationsMixin("promocode"), NiceModel): return promo_amount -class Order(ExportModelOperationsMixin("order"), NiceModel): +class Order(NiceModel): __doc__ = _( "Represents an order placed by a user." " 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." ) + if TYPE_CHECKING: + order_products: Manager["OrderProduct"] + payments_transactions: Manager[Transaction] + is_publicly_visible = False billing_address = ForeignKey( @@ -1429,7 +1477,8 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): if promotions.exists(): 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( @@ -1474,7 +1523,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): order_product.delete() return self if order_product.quantity == 1: - self.order_products.remove(order_product) + self.order_products.remove(order_product) # ty: ignore[unresolved-attribute] order_product.delete() else: 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") ) 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() return self @@ -1509,7 +1558,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): try: product = Product.objects.get(uuid=product_uuid) 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() except Product.DoesNotExist as dne: name = "Product" @@ -1775,6 +1824,8 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): crm_links = OrderCrmLink.objects.filter(order=self) if crm_links.exists(): crm_link = crm_links.first() + if not crm_link: + return False crm_integration = create_object( crm_link.crm.integration_location, crm_link.crm.name ) @@ -1820,7 +1871,7 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): return None -class Feedback(ExportModelOperationsMixin("feedback"), NiceModel): +class Feedback(NiceModel): __doc__ = _( "Manages user feedback for 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") -class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): +class OrderProduct(NiceModel): __doc__ = _( "Represents products associated with orders and their attributes. " "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." ) + if TYPE_CHECKING: + download: "DigitalAssetDownload" + is_publicly_visible = False buy_price = FloatField( @@ -2032,9 +2086,7 @@ class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): return None -class CustomerRelationshipManagementProvider( - ExportModelOperationsMixin("crm_provider"), NiceModel -): +class CustomerRelationshipManagementProvider(NiceModel): name = CharField(max_length=128, unique=True, verbose_name=_("name")) integration_url = URLField( blank=True, null=True, help_text=_("URL of the integration") @@ -2077,7 +2129,7 @@ class CustomerRelationshipManagementProvider( 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") crm = ForeignKey( to=CustomerRelationshipManagementProvider, @@ -2094,7 +2146,7 @@ class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel): verbose_name_plural = _("orders CRM links") -class DigitalAssetDownload(ExportModelOperationsMixin("attribute_group"), NiceModel): +class DigitalAssetDownload(NiceModel): __doc__ = _( "Represents the downloading functionality for digital assets associated with orders. " "The DigitalAssetDownload class provides the ability to manage and access " diff --git a/engine/core/serializers/detail.py b/engine/core/serializers/detail.py index ee3878a0..14c97487 100644 --- a/engine/core/serializers/detail.py +++ b/engine/core/serializers/detail.py @@ -26,6 +26,7 @@ from engine.core.models import ( Wishlist, ) from engine.core.serializers.simple import ( + BrandSimpleSerializer, CategorySimpleSerializer, ProductSimpleSerializer, ) @@ -60,6 +61,9 @@ class CategoryDetailListSerializer(ListSerializer): class CategoryDetailSerializer(ModelSerializer): children = SerializerMethodField() filterable_attributes = SerializerMethodField() + brands = BrandSimpleSerializer(many=True, read_only=True) + min_price = SerializerMethodField() + max_price = SerializerMethodField() class Meta: model = Category @@ -71,6 +75,7 @@ class CategoryDetailSerializer(ModelSerializer): "image", "markup_percent", "filterable_attributes", + "brands", "children", "slug", "created", @@ -95,6 +100,12 @@ class CategoryDetailSerializer(ModelSerializer): return list(serializer.data) 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): categories = CategorySimpleSerializer(many=True) diff --git a/engine/core/static/js/rapidoc-min.js b/engine/core/static/js/rapidoc-min.js new file mode 100644 index 00000000..c656086a --- /dev/null +++ b/engine/core/static/js/rapidoc-min.js @@ -0,0 +1,3915 @@ +/*! RapiDoc 9.3.8 | Author - Mrinmoy Majumdar | License information can be found in rapidoc-min.js.LICENSE.txt */ +(()=>{var e,t,r={557:(e,t,r)=>{"use strict";const s=globalThis,n=s.ShadowRoot&&(void 0===s.ShadyCSS||s.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),o=new WeakMap;class a{constructor(e,t,r){if(this._$cssResult$=!0,r!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(n&&void 0===e){const r=void 0!==t&&1===t.length;r&&(e=o.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&o.set(t,e))}return e}toString(){return this.cssText}}const l=e=>new a("string"==typeof e?e:e+"",void 0,i),c=(e,...t)=>{const r=1===e.length?e[0]:t.reduce(((t,r,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+e[s+1]),e[0]);return new a(r,e,i)},p=(e,t)=>{if(n)e.adoptedStyleSheets=t.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet));else for(const r of t){const t=document.createElement("style"),n=s.litNonce;void 0!==n&&t.setAttribute("nonce",n),t.textContent=r.cssText,e.appendChild(t)}},u=n?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const r of e.cssRules)t+=r.cssText;return l(t)})(e):e,{is:d,defineProperty:h,getOwnPropertyDescriptor:m,getOwnPropertyNames:f,getOwnPropertySymbols:g,getPrototypeOf:y}=Object,v=globalThis,b=v.trustedTypes,x=b?b.emptyScript:"",w=v.reactiveElementPolyfillSupport,$=(e,t)=>e,S={toAttribute(e,t){switch(t){case Boolean:e=e?x:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let r=e;switch(t){case Boolean:r=null!==e;break;case Number:r=null===e?null:Number(e);break;case Object:case Array:try{r=JSON.parse(e)}catch(e){r=null}}return r}},E=(e,t)=>!d(e,t),k={attribute:!0,type:String,converter:S,reflect:!1,hasChanged:E};Symbol.metadata??=Symbol("metadata"),v.litPropertyMetadata??=new WeakMap;class A extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=k){if(t.state&&(t.attribute=!1),this._$Ei(),this.elementProperties.set(e,t),!t.noAccessor){const r=Symbol(),s=this.getPropertyDescriptor(e,r,t);void 0!==s&&h(this.prototype,e,s)}}static getPropertyDescriptor(e,t,r){const{get:s,set:n}=m(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get(){return s?.call(this)},set(t){const i=s?.call(this);n.call(this,t),this.requestUpdate(e,i,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??k}static _$Ei(){if(this.hasOwnProperty($("elementProperties")))return;const e=y(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty($("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty($("properties"))){const e=this.properties,t=[...f(e),...g(e)];for(const r of t)this.createProperty(r,e[r])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,r]of t)this.elementProperties.set(e,r)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const r=this._$Eu(e,t);void 0!==r&&this._$Eh.set(r,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const r=new Set(e.flat(1/0).reverse());for(const e of r)t.unshift(u(e))}else void 0!==e&&t.push(u(e));return t}static _$Eu(e,t){const r=t.attribute;return!1===r?void 0:"string"==typeof r?r:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise((e=>this.enableUpdating=e)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((e=>e(this)))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return p(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach((e=>e.hostConnected?.()))}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach((e=>e.hostDisconnected?.()))}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$EC(e,t){const r=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,r);if(void 0!==s&&!0===r.reflect){const n=(void 0!==r.converter?.toAttribute?r.converter:S).toAttribute(t,r.type);this._$Em=e,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$Em=null}}_$AK(e,t){const r=this.constructor,s=r._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=r.getPropertyOptions(s),n="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:S;this._$Em=s,this[s]=n.fromAttribute(t,e.type),this._$Em=null}}requestUpdate(e,t,r){if(void 0!==e){if(r??=this.constructor.getPropertyOptions(e),!(r.hasChanged??E)(this[e],t))return;this.P(e,t,r)}!1===this.isUpdatePending&&(this._$ES=this._$ET())}P(e,t,r){this._$AL.has(e)||this._$AL.set(e,t),!0===r.reflect&&this._$Em!==e&&(this._$Ej??=new Set).add(e)}async _$ET(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,r]of e)!0!==r.wrapped||this._$AL.has(t)||void 0===this[t]||this.P(t,this[t],r)}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach((e=>e.hostUpdate?.())),this.update(t)):this._$EU()}catch(t){throw e=!1,this._$EU(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach((e=>e.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Ej&&=this._$Ej.forEach((e=>this._$EC(e,this[e]))),this._$EU()}updated(e){}firstUpdated(e){}}A.elementStyles=[],A.shadowRootOptions={mode:"open"},A[$("elementProperties")]=new Map,A[$("finalized")]=new Map,w?.({ReactiveElement:A}),(v.reactiveElementVersions??=[]).push("2.0.4");const O=globalThis,j=O.trustedTypes,T=j?j.createPolicy("lit-html",{createHTML:e=>e}):void 0,P="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,I="?"+C,_=`<${I}>`,R=document,F=()=>R.createComment(""),M=e=>null===e||"object"!=typeof e&&"function"!=typeof e,L=Array.isArray,D=e=>L(e)||"function"==typeof e?.[Symbol.iterator],B="[ \t\n\f\r]",q=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,N=/-->/g,U=/>/g,z=RegExp(`>|${B}(?:([^\\s"'>=/]+)(${B}*=${B}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),H=/'/g,V=/"/g,W=/^(?:script|style|textarea|title)$/i,G=e=>(t,...r)=>({_$litType$:e,strings:t,values:r}),J=G(1),K=(G(2),G(3),Symbol.for("lit-noChange")),Y=Symbol.for("lit-nothing"),X=new WeakMap,Z=R.createTreeWalker(R,129);function Q(e,t){if(!L(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==T?T.createHTML(t):t}const ee=(e,t)=>{const r=e.length-1,s=[];let n,i=2===t?"":3===t?"":"",o=q;for(let t=0;t"===l[0]?(o=n??q,c=-1):void 0===l[1]?c=-2:(c=o.lastIndex-l[2].length,a=l[1],o=void 0===l[3]?z:'"'===l[3]?V:H):o===V||o===H?o=z:o===N||o===U?o=q:(o=z,n=void 0);const u=o===z&&e[t+1].startsWith("/>")?" ":"";i+=o===q?r+_:c>=0?(s.push(a),r.slice(0,c)+P+r.slice(c)+C+u):r+C+(-2===c?t:u)}return[Q(e,i+(e[r]||"")+(2===t?"":3===t?"":"")),s]};class te{constructor({strings:e,_$litType$:t},r){let s;this.parts=[];let n=0,i=0;const o=e.length-1,a=this.parts,[l,c]=ee(e,t);if(this.el=te.createElement(l,r),Z.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=Z.nextNode())&&a.length0){s.textContent=j?j.emptyScript:"";for(let r=0;r2||""!==r[0]||""!==r[1]?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=Y}_$AI(e,t=this,r,s){const n=this.strings;let i=!1;if(void 0===n)e=re(this,e,t,0),i=!M(e)||e!==this._$AH&&e!==K,i&&(this._$AH=e);else{const s=e;let o,a;for(e=n[0],o=0;o{const s=r?.renderBefore??t;let n=s._$litPart$;if(void 0===n){const e=r?.renderBefore??null;s._$litPart$=n=new ne(t.insertBefore(F(),e),e,void 0,r??{})}return n._$AI(e),n})(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this.o?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this.o?.setConnected(!1)}render(){return K}}de._$litElement$=!0,de.finalized=!0,globalThis.litElementHydrateSupport?.({LitElement:de});const he=globalThis.litElementPolyfillSupport;he?.({LitElement:de});function me(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}(globalThis.litElementVersions??=[]).push("4.1.0");let fe={async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const ge=/[&<>"']/,ye=new RegExp(ge.source,"g"),ve=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,be=new RegExp(ve.source,"g"),xe={"&":"&","<":"<",">":">",'"':""","'":"'"},we=e=>xe[e];function $e(e,t){if(t){if(ge.test(e))return e.replace(ye,we)}else if(ve.test(e))return e.replace(be,we);return e}const Se=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function Ee(e){return e.replace(Se,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const ke=/(^|[^\[])\^/g;function Ae(e,t){e="string"==typeof e?e:e.source,t=t||"";const r={replace:(t,s)=>(s=(s=s.source||s).replace(ke,"$1"),e=e.replace(t,s),r),getRegex:()=>new RegExp(e,t)};return r}const Oe=/[^\w:]/g,je=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function Te(e,t,r){if(e){let e;try{e=decodeURIComponent(Ee(r)).replace(Oe,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!je.test(r)&&(r=function(e,t){Pe[" "+e]||(Ce.test(e)?Pe[" "+e]=e+"/":Pe[" "+e]=Me(e,"/",!0));e=Pe[" "+e];const r=-1===e.indexOf(":");return"//"===t.substring(0,2)?r?t:e.replace(Ie,"$1")+t:"/"===t.charAt(0)?r?t:e.replace(_e,"$1")+t:e+t}(t,r));try{r=encodeURI(r).replace(/%25/g,"%")}catch(e){return null}return r}const Pe={},Ce=/^[^:]+:\/*[^/]*$/,Ie=/^([^:]+:)[\s\S]*$/,_e=/^([^:]+:\/*[^/]*)[\s\S]*$/;const Re={exec:function(){}};function Fe(e,t){const r=e.replace(/\|/g,((e,t,r)=>{let s=!1,n=t;for(;--n>=0&&"\\"===r[n];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),r.length>t)r.splice(t);else for(;r.length1;)1&t&&(r+=e),t>>=1,e+=e;return r+e}function De(e,t,r,s){const n=t.href,i=t.title?$e(t.title):null,o=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:r,href:n,title:i,text:o,tokens:s.inlineTokens(o)};return s.state.inLink=!1,e}return{type:"image",raw:r,href:n,title:i,text:$e(o)}}class Be{constructor(e){this.options=e||fe}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:Me(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],r=function(e,t){const r=e.match(/^(\s+)(?:```)/);if(null===r)return t;const s=r[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[r]=t;return r.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline._escapes,"$1"):t[2],text:r}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=Me(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *>[ \t]?/gm,""),r=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=r,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let r,s,n,i,o,a,l,c,p,u,d,h,m=t[1].trim();const f=m.length>1,g={type:"list",raw:"",ordered:f,start:f?+m.slice(0,-1):"",loose:!1,items:[]};m=f?`\\d{1,9}\\${m.slice(-1)}`:`\\${m}`,this.options.pedantic&&(m=f?m:"[*+-]");const y=new RegExp(`^( {0,3}${m})((?:[\t ][^\\n]*)?(?:\\n|$))`);for(;e&&(h=!1,t=y.exec(e))&&!this.rules.block.hr.test(e);){if(r=t[0],e=e.substring(r.length),c=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),p=e.split("\n",1)[0],this.options.pedantic?(i=2,d=c.trimLeft()):(i=t[2].search(/[^ ]/),i=i>4?1:i,d=c.slice(i),i+=t[1].length),a=!1,!c&&/^ *$/.test(p)&&(r+=p+"\n",e=e.substring(p.length+1),h=!0),!h){const t=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),s=new RegExp(`^ {0,${Math.min(3,i-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),n=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:\`\`\`|~~~)`),o=new RegExp(`^ {0,${Math.min(3,i-1)}}#`);for(;e&&(u=e.split("\n",1)[0],p=u,this.options.pedantic&&(p=p.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!n.test(p))&&!o.test(p)&&!t.test(p)&&!s.test(e);){if(p.search(/[^ ]/)>=i||!p.trim())d+="\n"+p.slice(i);else{if(a)break;if(c.search(/[^ ]/)>=4)break;if(n.test(c))break;if(o.test(c))break;if(s.test(c))break;d+="\n"+p}a||p.trim()||(a=!0),r+=u+"\n",e=e.substring(u.length+1),c=p.slice(i)}}g.loose||(l?g.loose=!0:/\n *\n *$/.test(r)&&(l=!0)),this.options.gfm&&(s=/^\[[ xX]\] /.exec(d),s&&(n="[ ] "!==s[0],d=d.replace(/^\[[ xX]\] +/,""))),g.items.push({type:"list_item",raw:r,task:!!s,checked:n,loose:!1,text:d}),g.raw+=r}g.items[g.items.length-1].raw=r.trimRight(),g.items[g.items.length-1].text=d.trimRight(),g.raw=g.raw.trimRight();const v=g.items.length;for(o=0;o"space"===e.type)),t=e.length>0&&e.some((e=>/\n.*\n/.test(e.raw)));g.loose=t}if(g.loose)for(o=0;o$/,"$1").replace(this.rules.inline._escapes,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline._escapes,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:r,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:Fe(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let r,s,n,i,o=e.align.length;for(r=0;r({text:e})));for(o=e.header.length,s=0;s/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):$e(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=Me(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const r=e.length;let s=0,n=0;for(;n-1){const r=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let r=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r);e&&(r=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return r=r.trim(),/^$/.test(e)?r.slice(1):r.slice(1,-1)),De(t,{href:r?r.replace(this.rules.inline._escapes,"$1"):r,title:s?s.replace(this.rules.inline._escapes,"$1"):s},t[0],this.lexer)}}reflink(e,t){let r;if((r=this.rules.inline.reflink.exec(e))||(r=this.rules.inline.nolink.exec(e))){let e=(r[2]||r[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e){const e=r[0].charAt(0);return{type:"text",raw:e,text:e}}return De(r,e,r[0],this.lexer)}}emStrong(e,t,r=""){let s=this.rules.inline.emStrong.lDelim.exec(e);if(!s)return;if(s[3]&&r.match(/[\p{L}\p{N}]/u))return;const n=s[1]||s[2]||"";if(!n||n&&(""===r||this.rules.inline.punctuation.exec(r))){const r=s[0].length-1;let n,i,o=r,a=0;const l="*"===s[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(l.lastIndex=0,t=t.slice(-1*e.length+r);null!=(s=l.exec(t));){if(n=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!n)continue;if(i=n.length,s[3]||s[4]){o+=i;continue}if((s[5]||s[6])&&r%3&&!((r+i)%3)){a+=i;continue}if(o-=i,o>0)continue;i=Math.min(i,i+o+a);const t=e.slice(0,r+s.index+(s[0].length-n.length)+i);if(Math.min(r,i)%2){const e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}const l=t.slice(2,-2);return{type:"strong",raw:t,text:l,tokens:this.lexer.inlineTokens(l)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const r=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return r&&s&&(e=e.substring(1,e.length-1)),e=$e(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e,t){const r=this.rules.inline.autolink.exec(e);if(r){let e,s;return"@"===r[2]?(e=$e(this.options.mangle?t(r[1]):r[1]),s="mailto:"+e):(e=$e(r[1]),s=e),{type:"link",raw:r[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let r;if(r=this.rules.inline.url.exec(e)){let e,s;if("@"===r[2])e=$e(this.options.mangle?t(r[0]):r[0]),s="mailto:"+e;else{let t;do{t=r[0],r[0]=this.rules.inline._backpedal.exec(r[0])[0]}while(t!==r[0]);e=$e(r[0]),s="www."===r[1]?"http://"+r[0]:r[0]}return{type:"link",raw:r[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const r=this.rules.inline.text.exec(e);if(r){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(r[0]):$e(r[0]):r[0]:$e(this.options.smartypants?t(r[0]):r[0]),{type:"text",raw:r[0],text:e}}}}const qe={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:Re,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};qe.def=Ae(qe.def).replace("label",qe._label).replace("title",qe._title).getRegex(),qe.bullet=/(?:[*+-]|\d{1,9}[.)])/,qe.listItemStart=Ae(/^( *)(bull) */).replace("bull",qe.bullet).getRegex(),qe.list=Ae(qe.list).replace(/bull/g,qe.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+qe.def.source+")").getRegex(),qe._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",qe._comment=/|$)/,qe.html=Ae(qe.html,"i").replace("comment",qe._comment).replace("tag",qe._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),qe.paragraph=Ae(qe._paragraph).replace("hr",qe.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",qe._tag).getRegex(),qe.blockquote=Ae(qe.blockquote).replace("paragraph",qe.paragraph).getRegex(),qe.normal={...qe},qe.gfm={...qe.normal,table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"},qe.gfm.table=Ae(qe.gfm.table).replace("hr",qe.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",qe._tag).getRegex(),qe.gfm.paragraph=Ae(qe._paragraph).replace("hr",qe.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",qe.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",qe._tag).getRegex(),qe.pedantic={...qe.normal,html:Ae("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",qe._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Re,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:Ae(qe.normal._paragraph).replace("hr",qe.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",qe.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()};const Ne={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:Re,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:Re,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(r="x"+r.toString(16)),s+="&#"+r+";";return s}Ne._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",Ne.punctuation=Ae(Ne.punctuation).replace(/punctuation/g,Ne._punctuation).getRegex(),Ne.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,Ne.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,Ne._comment=Ae(qe._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),Ne.emStrong.lDelim=Ae(Ne.emStrong.lDelim).replace(/punct/g,Ne._punctuation).getRegex(),Ne.emStrong.rDelimAst=Ae(Ne.emStrong.rDelimAst,"g").replace(/punct/g,Ne._punctuation).getRegex(),Ne.emStrong.rDelimUnd=Ae(Ne.emStrong.rDelimUnd,"g").replace(/punct/g,Ne._punctuation).getRegex(),Ne._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,Ne._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,Ne._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,Ne.autolink=Ae(Ne.autolink).replace("scheme",Ne._scheme).replace("email",Ne._email).getRegex(),Ne._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,Ne.tag=Ae(Ne.tag).replace("comment",Ne._comment).replace("attribute",Ne._attribute).getRegex(),Ne._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ne._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,Ne._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,Ne.link=Ae(Ne.link).replace("label",Ne._label).replace("href",Ne._href).replace("title",Ne._title).getRegex(),Ne.reflink=Ae(Ne.reflink).replace("label",Ne._label).replace("ref",qe._label).getRegex(),Ne.nolink=Ae(Ne.nolink).replace("ref",qe._label).getRegex(),Ne.reflinkSearch=Ae(Ne.reflinkSearch,"g").replace("reflink",Ne.reflink).replace("nolink",Ne.nolink).getRegex(),Ne.normal={...Ne},Ne.pedantic={...Ne.normal,strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:Ae(/^!?\[(label)\]\((.*?)\)/).replace("label",Ne._label).getRegex(),reflink:Ae(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Ne._label).getRegex()},Ne.gfm={...Ne.normal,escape:Ae(Ne.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\t+" ".repeat(r.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(r=s.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.space(e))e=e.substring(r.raw.length),1===r.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(r);else if(r=this.tokenizer.code(e))e=e.substring(r.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(r):(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(r=this.tokenizer.fences(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.heading(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.hr(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.blockquote(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.list(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.html(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.def(e))e=e.substring(r.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title}):(s.raw+="\n"+r.raw,s.text+="\n"+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(r=this.tokenizer.table(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.lheading(e))e=e.substring(r.raw.length),t.push(r);else{if(n=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const r=e.slice(1);let s;this.options.extensions.startBlock.forEach((function(e){s=e.call({lexer:this},r),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(n=e.substring(0,t+1))}if(this.state.top&&(r=this.tokenizer.paragraph(n)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(r),i=n.length!==e.length,e=e.substring(r.raw.length);else if(r=this.tokenizer.text(e))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let r,s,n,i,o,a,l=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(l));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(l=l.slice(0,i.index)+"["+Le("a",i[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(l));)l=l.slice(0,i.index)+"["+Le("a",i[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.escapedEmSt.exec(l));)l=l.slice(0,i.index+i[0].length-2)+"++"+l.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex),this.tokenizer.rules.inline.escapedEmSt.lastIndex--;for(;e;)if(o||(a=""),o=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(r=s.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===r.type&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===r.type&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,l,a))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.autolink(e,ze))e=e.substring(r.raw.length),t.push(r);else if(this.state.inLink||!(r=this.tokenizer.url(e,ze))){if(n=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const r=e.slice(1);let s;this.options.extensions.startInline.forEach((function(e){s=e.call({lexer:this},r),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(n=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(n,Ue))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(a=r.raw.slice(-1)),o=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(r.raw.length),t.push(r);return t}}class Ve{constructor(e){this.options=e||fe}code(e,t,r){const s=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,s);null!=t&&t!==e&&(r=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",s?'
'+(r?e:$e(e,!0))+"
\n":"
"+(r?e:$e(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e){return e}heading(e,t,r,s){if(this.options.headerIds){return`${e}\n`}return`${e}\n`}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,r){const s=t?"ol":"ul";return"<"+s+(t&&1!==r?' start="'+r+'"':"")+">\n"+e+"\n"}listitem(e){return`
  • ${e}
  • \n`}checkbox(e){return" "}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const r=t.header?"th":"td";return(t.align?`<${r} align="${t.align}">`:`<${r}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return this.options.xhtml?"
    ":"
    "}del(e){return`${e}`}link(e,t,r){if(null===(e=Te(this.options.sanitize,this.options.baseUrl,e)))return r;let s='",s}image(e,t,r){if(null===(e=Te(this.options.sanitize,this.options.baseUrl,e)))return r;let s=`${r}":">",s}text(e){return e}}class We{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,r){return""+r}image(e,t,r){return""+r}br(){return""}}class Ge{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let r=e,s=0;if(this.seen.hasOwnProperty(r)){s=this.seen[e];do{s++,r=e+"-"+s}while(this.seen.hasOwnProperty(r))}return t||(this.seen[e]=s,this.seen[r]=0),r}slug(e,t={}){const r=this.serialize(e);return this.getNextSafeSlug(r,t.dryrun)}}class Je{constructor(e){this.options=e||fe,this.options.renderer=this.options.renderer||new Ve,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new We,this.slugger=new Ge}static parse(e,t){return new Je(t).parse(e)}static parseInline(e,t){return new Je(t).parseInline(e)}parse(e,t=!0){let r,s,n,i,o,a,l,c,p,u,d,h,m,f,g,y,v,b,x,w="";const $=e.length;for(r=0;r<$;r++)if(u=e[r],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[u.type]&&(x=this.options.extensions.renderers[u.type].call({parser:this},u),!1!==x||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(u.type)))w+=x||"";else switch(u.type){case"space":continue;case"hr":w+=this.renderer.hr();continue;case"heading":w+=this.renderer.heading(this.parseInline(u.tokens),u.depth,Ee(this.parseInline(u.tokens,this.textRenderer)),this.slugger);continue;case"code":w+=this.renderer.code(u.text,u.lang,u.escaped);continue;case"table":for(c="",l="",i=u.header.length,s=0;s0&&"paragraph"===g.tokens[0].type?(g.tokens[0].text=b+" "+g.tokens[0].text,g.tokens[0].tokens&&g.tokens[0].tokens.length>0&&"text"===g.tokens[0].tokens[0].type&&(g.tokens[0].tokens[0].text=b+" "+g.tokens[0].tokens[0].text)):g.tokens.unshift({type:"text",text:b}):f+=b),f+=this.parse(g.tokens,m),p+=this.renderer.listitem(f,v,y);w+=this.renderer.list(p,d,h);continue;case"html":w+=this.renderer.html(u.text);continue;case"paragraph":w+=this.renderer.paragraph(this.parseInline(u.tokens));continue;case"text":for(p=u.tokens?this.parseInline(u.tokens):u.text;r+1<$&&"text"===e[r+1].type;)u=e[++r],p+="\n"+(u.tokens?this.parseInline(u.tokens):u.text);w+=t?this.renderer.paragraph(p):p;continue;default:{const e='Token with "'+u.type+'" type was not found.';if(this.options.silent)return void console.error(e);throw new Error(e)}}return w}parseInline(e,t){t=t||this.renderer;let r,s,n,i="";const o=e.length;for(r=0;r{"function"==typeof s&&(n=s,s=null);const i={...s},o=function(e,t,r){return s=>{if(s.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+$e(s.message+"",!0)+"
    ";return t?Promise.resolve(e):r?void r(null,e):e}if(t)return Promise.reject(s);if(!r)throw s;r(s)}}((s={...Xe.defaults,...i}).silent,s.async,n);if(null==r)return o(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof r)return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(r)+", string expected"));if(function(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")}(s),s.hooks&&(s.hooks.options=s),n){const i=s.highlight;let a;try{s.hooks&&(r=s.hooks.preprocess(r)),a=e(r,s)}catch(e){return o(e)}const l=function(e){let r;if(!e)try{s.walkTokens&&Xe.walkTokens(a,s.walkTokens),r=t(a,s),s.hooks&&(r=s.hooks.postprocess(r))}catch(t){e=t}return s.highlight=i,e?o(e):n(null,r)};if(!i||i.length<3)return l();if(delete s.highlight,!a.length)return l();let c=0;return Xe.walkTokens(a,(function(e){"code"===e.type&&(c++,setTimeout((()=>{i(e.text,e.lang,(function(t,r){if(t)return l(t);null!=r&&r!==e.text&&(e.text=r,e.escaped=!0),c--,0===c&&l()}))}),0))})),void(0===c&&l())}if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(r):r).then((t=>e(t,s))).then((e=>s.walkTokens?Promise.all(Xe.walkTokens(e,s.walkTokens)).then((()=>e)):e)).then((e=>t(e,s))).then((e=>s.hooks?s.hooks.postprocess(e):e)).catch(o);try{s.hooks&&(r=s.hooks.preprocess(r));const n=e(r,s);s.walkTokens&&Xe.walkTokens(n,s.walkTokens);let i=t(n,s);return s.hooks&&(i=s.hooks.postprocess(i)),i}catch(e){return o(e)}}}function Xe(e,t,r){return Ye(He.lex,Je.parse)(e,t,r)}Xe.options=Xe.setOptions=function(e){var t;return Xe.defaults={...Xe.defaults,...e},t=Xe.defaults,fe=t,Xe},Xe.getDefaults=me,Xe.defaults=fe,Xe.use=function(...e){const t=Xe.defaults.extensions||{renderers:{},childTokens:{}};e.forEach((e=>{const r={...e};if(r.async=Xe.defaults.async||r.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const r=t.renderers[e.name];t.renderers[e.name]=r?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=r.apply(this,t)),s}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");t[e.level]?t[e.level].unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),r.extensions=t),e.renderer){const t=Xe.defaults.renderer||new Ve;for(const r in e.renderer){const s=t[r];t[r]=(...n)=>{let i=e.renderer[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.renderer=t}if(e.tokenizer){const t=Xe.defaults.tokenizer||new Be;for(const r in e.tokenizer){const s=t[r];t[r]=(...n)=>{let i=e.tokenizer[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.tokenizer=t}if(e.hooks){const t=Xe.defaults.hooks||new Ke;for(const r in e.hooks){const s=t[r];Ke.passThroughHooks.has(r)?t[r]=n=>{if(Xe.defaults.async)return Promise.resolve(e.hooks[r].call(t,n)).then((e=>s.call(t,e)));const i=e.hooks[r].call(t,n);return s.call(t,i)}:t[r]=(...n)=>{let i=e.hooks[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.hooks=t}if(e.walkTokens){const t=Xe.defaults.walkTokens;r.walkTokens=function(r){let s=[];return s.push(e.walkTokens.call(this,r)),t&&(s=s.concat(t.call(this,r))),s}}Xe.setOptions(r)}))},Xe.walkTokens=function(e,t){let r=[];for(const s of e)switch(r=r.concat(t.call(Xe,s)),s.type){case"table":for(const e of s.header)r=r.concat(Xe.walkTokens(e.tokens,t));for(const e of s.rows)for(const s of e)r=r.concat(Xe.walkTokens(s.tokens,t));break;case"list":r=r.concat(Xe.walkTokens(s.items,t));break;default:Xe.defaults.extensions&&Xe.defaults.extensions.childTokens&&Xe.defaults.extensions.childTokens[s.type]?Xe.defaults.extensions.childTokens[s.type].forEach((function(e){r=r.concat(Xe.walkTokens(s[e],t))})):s.tokens&&(r=r.concat(Xe.walkTokens(s.tokens,t)))}return r},Xe.parseInline=Ye(He.lexInline,Je.parseInline),Xe.Parser=Je,Xe.parser=Je.parse,Xe.Renderer=Ve,Xe.TextRenderer=We,Xe.Lexer=He,Xe.lexer=He.lex,Xe.Tokenizer=Be,Xe.Slugger=Ge,Xe.Hooks=Ke,Xe.parse=Xe;Xe.options,Xe.setOptions,Xe.use,Xe.walkTokens,Xe.parseInline,Je.parse,He.lex;var Ze=r(848),Qe=r.n(Ze);r(113),r(83),r(378),r(976),r(514),r(22),r(342),r(784),r(651);const et=c` + .hover-bg:hover { + background: var(--bg3); + } + ::selection { + background: var(--selection-bg); + color: var(--selection-fg); + } + .regular-font { + font-family:var(--font-regular); + } + .mono-font { + font-family:var(--font-mono); + } + .title { + font-size: calc(var(--font-size-small) + 18px); + font-weight: normal + } + .sub-title{ font-size: 20px; } + .req-res-title { + font-family: var(--font-regular); + font-size: calc(var(--font-size-small) + 4px); + font-weight:bold; + margin-bottom:8px; + text-align:left; + } + .tiny-title { + font-size:calc(var(--font-size-small) + 1px); + font-weight:bold; + } + .regular-font-size { font-size: var(--font-size-regular); } + .small-font-size { font-size: var(--font-size-small); } + .upper { text-transform: uppercase; } + .primary-text { color: var(--primary-color); } + .bold-text { font-weight:bold; } + .gray-text { color: var(--light-fg); } + .red-text { color: var(--red) } + .blue-text { color: var(--blue) } + .multiline { + overflow: scroll; + max-height: var(--resp-area-height, 400px); + color: var(--fg3); + } + .method-fg.put { color: var(--orange); } + .method-fg.post { color: var(--green); } + .method-fg.get { color: var(--blue); } + .method-fg.delete { color: var(--red); } + .method-fg.options, + .method-fg.head, + .method-fg.patch { + color: var(--yellow); + } + + h1 { font-family:var(--font-regular); font-size:28px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h2 { font-family:var(--font-regular); font-size:24px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h3 { font-family:var(--font-regular); font-size:18px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h4 { font-family:var(--font-regular); font-size:16px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h5 { font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h6 { font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + + h1,h2,h3,h4,h5,h5 { + margin-block-end: 0.2em; + } + p { margin-block-start: 0.5em; } + a { color: var(--blue); cursor:pointer; } + a.inactive-link { + color:var(--fg); + text-decoration: none; + cursor:text; + } + + code, + pre { + margin: 0px; + font-family: var(--font-mono); + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown, + .m-markdown-small { + display:block; + } + + .m-markdown p, + .m-markdown span { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 8px); + } + .m-markdown li { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 10px); + } + + .m-markdown-small p, + .m-markdown-small span, + .m-markdown-small li { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 6px); + } + .m-markdown-small li { + line-height: calc(var(--font-size-small) + 8px); + } + + .m-markdown p:not(:first-child) { + margin-block-start: 24px; + } + + .m-markdown-small p:not(:first-child) { + margin-block-start: 12px; + } + .m-markdown-small p:first-child { + margin-block-start: 0; + } + + .m-markdown p, + .m-markdown-small p { + margin-block-end: 0 + } + + .m-markdown code span { + font-size:var(--font-size-mono); + } + + .m-markdown-small code, + .m-markdown code { + padding: 1px 6px; + border-radius: 2px; + color: var(--inline-code-fg); + background-color: var(--bg3); + font-size: calc(var(--font-size-mono)); + line-height: 1.2; + } + + .m-markdown-small code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown-small pre, + .m-markdown pre { + white-space: pre-wrap; + overflow-x: auto; + line-height: normal; + border-radius: 2px; + border: 1px solid var(--code-border-color); + } + + .m-markdown pre { + padding: 12px; + background-color: var(--code-bg); + color:var(--code-fg); + } + + .m-markdown-small pre { + margin-top: 4px; + padding: 2px 4px; + background-color: var(--bg3); + color: var(--fg2); + } + + .m-markdown-small pre code, + .m-markdown pre code { + border:none; + padding:0; + } + + .m-markdown pre code { + color: var(--code-fg); + background-color: var(--code-bg); + background-color: transparent; + } + + .m-markdown-small pre code { + color: var(--fg2); + background-color: var(--bg3); + } + + .m-markdown ul, + .m-markdown ol { + padding-inline-start: 30px; + } + + .m-markdown-small ul, + .m-markdown-small ol { + padding-inline-start: 20px; + } + + .m-markdown-small a, + .m-markdown a { + color:var(--blue); + } + + .m-markdown-small img, + .m-markdown img { + max-width: 100%; + } + + /* Markdown table */ + + .m-markdown-small table, + .m-markdown table { + border-spacing: 0; + margin: 10px 0; + border-collapse: separate; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: calc(var(--font-size-small) + 1px); + line-height: calc(var(--font-size-small) + 4px); + max-width: 100%; + } + + .m-markdown-small table { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 2px); + margin: 8px 0; + } + + .m-markdown-small td, + .m-markdown-small th, + .m-markdown td, + .m-markdown th { + vertical-align: top; + border-top: 1px solid var(--border-color); + line-height: calc(var(--font-size-small) + 4px); + } + + .m-markdown-small tr:first-child th, + .m-markdown tr:first-child th { + border-top: 0 none; + } + + .m-markdown th, + .m-markdown td { + padding: 10px 12px; + } + + .m-markdown-small th, + .m-markdown-small td { + padding: 8px 8px; + } + + .m-markdown th, + .m-markdown-small th { + font-weight: 600; + background-color: var(--bg2); + vertical-align: middle; + } + + .m-markdown-small table code { + font-size: calc(var(--font-size-mono) - 2px); + } + + .m-markdown table code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown blockquote, + .m-markdown-small blockquote { + margin-inline-start: 0; + margin-inline-end: 0; + border-left: 3px solid var(--border-color); + padding: 6px 0 6px 6px; + } + .m-markdown hr{ + border: 1px solid var(--border-color); + } +`,tt=c` +/* Button */ +.m-btn { + border-radius: var(--border-radius); + font-weight: 600; + display: inline-block; + padding: 6px 16px; + font-size: var(--font-size-small); + outline: 0; + line-height: 1; + text-align: center; + white-space: nowrap; + border: 2px solid var(--primary-color); + background-color:transparent; + user-select: none; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + transition-duration: 0.75s; +} +.m-btn.primary { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.thin-border { border-width: 1px; } +.m-btn.large { padding:8px 14px; } +.m-btn.small { padding:5px 12px; } +.m-btn.tiny { padding:5px 6px; } +.m-btn.circle { border-radius: 50%; } +.m-btn:hover { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.nav { border: 2px solid var(--nav-accent-color); } +.m-btn.nav:hover { + background-color: var(--nav-accent-color); +} +.m-btn:disabled { + background-color: var(--bg3); + color: var(--fg3); + border-color: var(--fg3); + cursor: not-allowed; + opacity: 0.4; +} +.m-btn:active { + filter: brightness(75%); + transform: scale(0.95); + transition:scale 0s; +} +.toolbar-btn { + cursor: pointer; + padding: 4px; + margin:0 2px; + font-size: var(--font-size-small); + min-width: 50px; + color: var(--primary-color-invert); + border-radius: 2px; + border: none; + background-color: var(--primary-color); +} + +input, textarea, select, button, pre { + color:var(--fg); + outline: none; + background-color: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); +} +button { + font-family: var(--font-regular); +} + +/* Form Inputs */ +pre, +select, +textarea, +input[type="file"], +input[type="text"], +input[type="password"] { + font-family: var(--font-mono); + font-weight: 400; + font-size: var(--font-size-small); + transition: border .2s; + padding: 6px 5px; +} + +select { + font-family: var(--font-regular); + padding: 5px 30px 5px 5px; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%3E%3Cpath%20d%3D%22M10.3%203.3L6%207.6%201.7%203.3A1%201%200%2000.3%204.7l5%205a1%201%200%20001.4%200l5-5a1%201%200%2010-1.4-1.4z%22%20fill%3D%22%23777777%22%2F%3E%3C%2Fsvg%3E"); + background-position: calc(100% - 5px) center; + background-repeat: no-repeat; + background-size: 10px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; +} + +select:hover { + border-color: var(--primary-color); +} + +textarea::placeholder, +input[type="text"]::placeholder, +input[type="password"]::placeholder { + color: var(--placeholder-color); + opacity:1; +} + + +input[type="file"]{ + font-family: var(--font-regular); + padding:2px; + cursor:pointer; + border: 1px solid var(--primary-color); + min-height: calc(var(--font-size-small) + 18px); +} + +input[type="file"]::-webkit-file-upload-button { + font-family: var(--font-regular); + font-size: var(--font-size-small); + outline: none; + cursor:pointer; + padding: 3px 8px; + border: 1px solid var(--primary-color); + background-color: var(--primary-color); + color: var(--primary-color-invert); + border-radius: var(--border-radius);; + -webkit-appearance: none; +} + +pre, +textarea { + scrollbar-width: thin; + scrollbar-color: var(--border-color) var(--input-bg); +} + +pre::-webkit-scrollbar, +textarea::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +pre::-webkit-scrollbar-track, +textarea::-webkit-scrollbar-track { + background:var(--input-bg); +} + +pre::-webkit-scrollbar-thumb, +textarea::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: var(--border-color); +} + +.link { + font-size:var(--font-size-small); + text-decoration: underline; + color:var(--blue); + font-family:var(--font-mono); + margin-bottom:2px; +} + +/* Toggle Body */ +input[type="checkbox"] { + appearance: none; + display: inline-block; + background-color: var(--light-bg); + border: 1px solid var(--light-bg); + border-radius: 9px; + cursor: pointer; + height: 18px; + position: relative; + transition: border .25s .15s, box-shadow .25s .3s, padding .25s; + min-width: 36px; + width: 36px; + vertical-align: top; +} +/* Toggle Thumb */ +input[type="checkbox"]:after { + position: absolute; + background-color: var(--bg); + border: 1px solid var(--light-bg); + border-radius: 8px; + content: ''; + top: 0px; + left: 0px; + right: 16px; + display: block; + height: 16px; + transition: border .25s .15s, left .25s .1s, right .15s .175s; +} + +/* Toggle Body - Checked */ +input[type="checkbox"]:checked { + background-color: var(--green); + border-color: var(--green); +} +/* Toggle Thumb - Checked*/ +input[type="checkbox"]:checked:after { + border: 1px solid var(--green); + left: 16px; + right: 1px; + transition: border .25s, left .15s .25s, right .25s .175s; +}`,rt=c` +.row, .col { + display:flex; +} +.row { + align-items:center; + flex-direction: row; +} +.col { + align-items:stretch; + flex-direction: column; +} +`,st=c` +.m-table { + border-spacing: 0; + border-collapse: separate; + border: 1px solid var(--light-border-color); + border-radius: var(--border-radius); + margin: 0; + max-width: 100%; + direction: ltr; +} +.m-table tr:first-child td, +.m-table tr:first-child th { + border-top: 0 none; +} +.m-table td, +.m-table th { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 4px); + padding: 4px 5px 4px; + vertical-align: top; +} + +.m-table.padded-12 td, +.m-table.padded-12 th { + padding: 12px; +} + +.m-table td:not([align]), +.m-table th:not([align]) { + text-align: left; +} + +.m-table th { + color: var(--fg2); + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 18px); + font-weight: 600; + letter-spacing: normal; + background-color: var(--bg2); + vertical-align: bottom; + border-bottom: 1px solid var(--light-border-color); +} + +.m-table > tbody > tr > td, +.m-table > tr > td { + border-top: 1px solid var(--light-border-color); + text-overflow: ellipsis; + overflow: hidden; +} +.table-title { + font-size:var(--font-size-small); + font-weight:bold; + vertical-align: middle; + margin: 12px 0 4px 0; +} +`,nt=c` +:host { + container-type: inline-size; +} +.only-large-screen { display:none; } +.endpoint-head .path { + display: flex; + font-family:var(--font-mono); + font-size: var(--font-size-small); + align-items: center; + overflow-wrap: break-word; + word-break: break-all; +} + +.endpoint-head .descr { + font-size: var(--font-size-small); + color:var(--light-fg); + font-weight:400; + align-items: center; + overflow-wrap: break-word; + word-break: break-all; + display:none; +} + +.m-endpoint.expanded { margin-bottom:16px; } +.m-endpoint > .endpoint-head{ + border-width:1px 1px 1px 5px; + border-style:solid; + border-color:transparent; + border-top-color:var(--light-border-color); + display:flex; + padding:6px 16px; + align-items: center; + cursor: pointer; +} +.m-endpoint > .endpoint-head.put:hover, +.m-endpoint > .endpoint-head.put.expanded { + border-color:var(--orange); + background-color:var(--light-orange); +} +.m-endpoint > .endpoint-head.post:hover, +.m-endpoint > .endpoint-head.post.expanded { + border-color:var(--green); + background-color:var(--light-green); +} +.m-endpoint > .endpoint-head.get:hover, +.m-endpoint > .endpoint-head.get.expanded { + border-color:var(--blue); + background-color:var(--light-blue); +} +.m-endpoint > .endpoint-head.delete:hover, +.m-endpoint > .endpoint-head.delete.expanded { + border-color:var(--red); + background-color:var(--light-red); +} + +.m-endpoint > .endpoint-head.head:hover, +.m-endpoint > .endpoint-head.head.expanded, +.m-endpoint > .endpoint-head.patch:hover, +.m-endpoint > .endpoint-head.patch.expanded, +.m-endpoint > .endpoint-head.options:hover, +.m-endpoint > .endpoint-head.options.expanded { + border-color:var(--yellow); + background-color:var(--light-yellow); +} + +.m-endpoint > .endpoint-head.deprecated:hover, +.m-endpoint > .endpoint-head.deprecated.expanded { + border-color:var(--border-color); + filter:opacity(0.6); +} + +.m-endpoint .endpoint-body { + flex-wrap:wrap; + padding:16px 0px 0 0px; + border-width:0px 1px 1px 5px; + border-style:solid; + box-shadow: 0px 4px 3px -3px rgba(0, 0, 0, 0.15); +} +.m-endpoint .endpoint-body.delete{ border-color:var(--red); } +.m-endpoint .endpoint-body.put{ border-color:var(--orange); } +.m-endpoint .endpoint-body.post { border-color:var(--green); } +.m-endpoint .endpoint-body.get { border-color:var(--blue); } +.m-endpoint .endpoint-body.head, +.m-endpoint .endpoint-body.patch, +.m-endpoint .endpoint-body.options { + border-color:var(--yellow); +} + +.m-endpoint .endpoint-body.deprecated { + border-color:var(--border-color); + filter:opacity(0.6); +} + +.endpoint-head .deprecated { + color: var(--light-fg); + filter:opacity(0.6); +} + +.summary{ + padding:8px 8px; +} +.summary .title { + font-size:calc(var(--font-size-regular) + 2px); + margin-bottom: 6px; + word-break: break-all; +} + +.endpoint-head .method { + padding:2px 5px; + vertical-align: middle; + font-size:var(--font-size-small); + height: calc(var(--font-size-small) + 16px); + line-height: calc(var(--font-size-small) + 8px); + width: 60px; + border-radius: 2px; + display:inline-block; + text-align: center; + font-weight: bold; + text-transform:uppercase; + margin-right:5px; +} +.endpoint-head .method.delete{ border: 2px solid var(--red);} +.endpoint-head .method.put{ border: 2px solid var(--orange); } +.endpoint-head .method.post{ border: 2px solid var(--green); } +.endpoint-head .method.get{ border: 2px solid var(--blue); } +.endpoint-head .method.get.deprecated{ border: 2px solid var(--border-color); } +.endpoint-head .method.head, +.endpoint-head .method.patch, +.endpoint-head .method.options { + border: 2px solid var(--yellow); +} + +.req-resp-container { + display: flex; + margin-top:16px; + align-items: stretch; + flex-wrap: wrap; + flex-direction: column; + border-top:1px solid var(--light-border-color); +} + +.view-mode-request, +api-response.view-mode { + flex:1; + min-height:100px; + padding:16px 8px; + overflow:hidden; +} +.view-mode-request { + border-width:0 0 1px 0; + border-style:dashed; +} + +.head .view-mode-request, +.patch .view-mode-request, +.options .view-mode-request { + border-color:var(--yellow); +} +.put .view-mode-request { + border-color:var(--orange); +} +.post .view-mode-request { + border-color:var(--green); +} +.get .view-mode-request { + border-color:var(--blue); +} +.delete .view-mode-request { + border-color:var(--red); +} + +@container (min-width: 1024px) { + .only-large-screen { display:block; } + .endpoint-head .path{ + font-size: var(--font-size-regular); + } + .endpoint-head .descr{ + display: flex; + } + .endpoint-head .m-markdown-small, + .descr .m-markdown-small{ + display:block; + } + .req-resp-container{ + flex-direction: var(--layout, row); + flex-wrap: nowrap; + } + api-response.view-mode { + padding:16px; + } + .view-mode-request.row-layout { + border-width:0 1px 0 0; + padding:16px; + } + .summary{ + padding:8px 16px; + } +} +`,it=c` +code[class*="language-"], +pre[class*="language-"] { + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + tab-size: 2; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + white-space: normal; +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: var(--light-fg) +} + +.token.punctuation { + color: var(--fg); +} + +.token.tag, +.token.attr-name, +.token.namespace, +.token.deleted { + color:var(--pink); +} + +.token.function-name { + color: var(--blue); +} + +.token.boolean, +.token.number, +.token.function { + color: var(--red); +} + +.token.property, +.token.class-name, +.token.constant, +.token.symbol { + color: var(--code-property-color); +} + +.token.selector, +.token.important, +.token.atrule, +.token.keyword, +.token.builtin { + color: var(--code-keyword-color); +} + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.variable { + color: var(--green); +} + +.token.operator, +.token.entity, +.token.url { + color: var(--code-operator-color); +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: green; +} +`,ot=c` +.tab-panel { + border: none; +} +.tab-buttons { + height:30px; + padding: 4px 4px 0 4px; + border-bottom: 1px solid var(--light-border-color) ; + align-items: stretch; + overflow-y: hidden; + overflow-x: auto; + scrollbar-width: thin; +} +.tab-buttons::-webkit-scrollbar { + height: 1px; + background-color: var(--border-color); +} +.tab-btn { + border: none; + border-bottom: 3px solid transparent; + color: var(--light-fg); + background-color: transparent; + white-space: nowrap; + cursor:pointer; + outline:none; + font-family:var(--font-regular); + font-size:var(--font-size-small); + margin-right:16px; + padding:1px; +} +.tab-btn.active { + border-bottom: 3px solid var(--primary-color); + font-weight:bold; + color:var(--primary-color); +} + +.tab-btn:hover { + color:var(--primary-color); +} +.tab-content { + margin:-1px 0 0 0; + position:relative; + min-height: 50px; +} +`,at=c` +.nav-bar-info:focus-visible, +.nav-bar-tag:focus-visible, +.nav-bar-path:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: -4px; +} +.nav-bar-expand-all:focus-visible, +.nav-bar-collapse-all:focus-visible, +.nav-bar-tag-icon:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: 2px; +} +.nav-bar { + width:0; + height:100%; + overflow: hidden; + color:var(--nav-text-color); + background-color: var(--nav-bg-color); + background-blend-mode: multiply; + line-height: calc(var(--font-size-small) + 4px); + display:none; + position:relative; + flex-direction:column; + flex-wrap:nowrap; + word-break:break-word; +} +::slotted([slot=nav-logo]) { + padding:16px 16px 0 16px; +} +.nav-scroll { + overflow-x: hidden; + overflow-y: auto; + overflow-y: overlay; + scrollbar-width: thin; + scrollbar-color: var(--nav-hover-bg-color) transparent; +} + +.nav-bar-tag { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; +} +.nav-bar.read .nav-bar-tag-icon { + display:none; +} +.nav-bar-paths-under-tag { + overflow:hidden; + transition: max-height .2s ease-out, visibility .3s; +} +.collapsed .nav-bar-paths-under-tag { + visibility: hidden; +} + +.nav-bar-expand-all { + transform: rotate(90deg); + cursor:pointer; + margin-right:10px; +} +.nav-bar-collapse-all { + transform: rotate(270deg); + cursor:pointer; +} +.nav-bar-expand-all:hover, .nav-bar-collapse-all:hover { + color: var(--primary-color); +} + +.nav-bar-tag-icon { + color: var(--nav-text-color); + font-size: 20px; +} +.nav-bar-tag-icon:hover { + color:var(--nav-hover-text-color); +} +.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transform: rotate(-90deg); + transition: transform 0.2s ease-out 0s; +} +.nav-bar.focused .nav-bar-tag-and-paths.expanded .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transition: transform 0.2s ease-out 0s; +} +.nav-scroll::-webkit-scrollbar { + width: var(--scroll-bar-width, 8px); +} +.nav-scroll::-webkit-scrollbar-track { + background:transparent; +} +.nav-scroll::-webkit-scrollbar-thumb { + background-color: var(--nav-hover-bg-color); +} + +.nav-bar-tag { + font-size: var(--font-size-regular); + color: var(--nav-accent-color); + border-left:4px solid transparent; + font-weight:bold; + padding: 15px 15px 15px 10px; + text-transform: capitalize; +} + +.nav-bar-components, +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-info, +.nav-bar-tag, +.nav-bar-path { + display:flex; + cursor: pointer; + width: 100%; + border: none; + border-radius:4px; + color: var(--nav-text-color); + background: transparent; + border-left:4px solid transparent; +} + +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-path { + font-size: calc(var(--font-size-small) + 1px); + padding: var(--nav-item-padding); +} +.nav-bar-path.small-font { + font-size: var(--font-size-small); +} + +.nav-bar-info { + font-size: var(--font-size-regular); + padding: 16px 10px; + font-weight:bold; +} +.nav-bar-section { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: var(--font-size-small); + color: var(--nav-text-color); + padding: var(--nav-item-padding); + font-weight:bold; +} +.nav-bar-section.operations { + cursor:pointer; +} +.nav-bar-section.operations:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} + +.nav-bar-section:first-child { + display: none; +} +.nav-bar-h2 {margin-left:12px;} + +.nav-bar-h1.left-bar.active, +.nav-bar-h2.left-bar.active, +.nav-bar-info.left-bar.active, +.nav-bar-tag.left-bar.active, +.nav-bar-path.left-bar.active, +.nav-bar-section.left-bar.operations.active { + border-left:4px solid var(--nav-accent-color); + color:var(--nav-hover-text-color); +} + +.nav-bar-h1.colored-block.active, +.nav-bar-h2.colored-block.active, +.nav-bar-info.colored-block.active, +.nav-bar-tag.colored-block.active, +.nav-bar-path.colored-block.active, +.nav-bar-section.colored-block.operations.active { + background-color: var(--nav-accent-color); + color: var(--nav-accent-text-color); + border-radius: 0; +} + +.nav-bar-h1:hover, +.nav-bar-h2:hover, +.nav-bar-info:hover, +.nav-bar-tag:hover, +.nav-bar-path:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} +`,lt=c` +#api-info { + font-size: calc(var(--font-size-regular) - 1px); + margin-top: 8px; + margin-left: -15px; +} + +#api-info span:before { + content: "|"; + display: inline-block; + opacity: 0.5; + width: 15px; + text-align: center; +} +#api-info span:first-child:before { + content: ""; + width: 0px; +} +`,ct=c` + +`;const pt=/[\s#:?&={}]/g,ut="_rapidoc_api_key";function dt(e){return new Promise((t=>setTimeout(t,e)))}function ht(e,t){const r=t.target,s=document.createElement("textarea");s.value=e,s.style.position="fixed",document.body.appendChild(s),s.focus(),s.select();try{document.execCommand("copy"),r.innerText="Copied",setTimeout((()=>{r.innerText="Copy"}),5e3)}catch(e){console.error("Unable to copy",e)}document.body.removeChild(s)}function mt(e,t,r=""){return`${t.method} ${t.path} ${t.summary||""} ${t.description||""} ${t.operationId||""} ${r}`.toLowerCase().includes(e.toLowerCase())}function ft(e,t=new Set){return e?(Object.keys(e).forEach((r=>{var s;if(t.add(r),e[r].properties)ft(e[r].properties,t);else if(null!==(s=e[r].items)&&void 0!==s&&s.properties){var n;ft(null===(n=e[r].items)||void 0===n?void 0:n.properties,t)}})),t):t}function gt(e,t){if(e){const r=document.createElement("a");document.body.appendChild(r),r.style="display: none",r.href=e,r.download=t,r.click(),r.remove()}}function yt(e){if(e){const t=document.createElement("a");document.body.appendChild(t),t.style="display: none",t.href=e,t.target="_blank",t.click(),t.remove()}}const vt=Object.freeze({url:"/"}),{fetch:bt,Response:xt,Headers:wt,Request:$t,FormData:St,File:Et,Blob:kt}=globalThis;function At(e,t){return t||"undefined"==typeof navigator||(t=navigator),t&&"ReactNative"===t.product?!(!e||"object"!=typeof e||"string"!=typeof e.uri):"undefined"!=typeof File&&e instanceof File||"undefined"!=typeof Blob&&e instanceof Blob||!!ArrayBuffer.isView(e)||null!==e&&"object"==typeof e&&"function"==typeof e.pipe}function Ot(e,t){return Array.isArray(e)&&e.some((e=>At(e,t)))}void 0===globalThis.fetch&&(globalThis.fetch=bt),void 0===globalThis.Headers&&(globalThis.Headers=wt),void 0===globalThis.Request&&(globalThis.Request=$t),void 0===globalThis.Response&&(globalThis.Response=xt),void 0===globalThis.FormData&&(globalThis.FormData=St),void 0===globalThis.File&&(globalThis.File=Et),void 0===globalThis.Blob&&(globalThis.Blob=kt);class jt extends File{constructor(e,t="",r={}){super([e],t,r),this.data=e}valueOf(){return this.data}toString(){return this.valueOf()}}function Tt(e,t="reserved"){return[...e].map((e=>{if((e=>/^[a-z0-9\-._~]+$/i.test(e))(e))return e;if((e=>":/?#[]@!$&'()*+,;=".indexOf(e)>-1)(e)&&"unsafe"===t)return e;const r=new TextEncoder;return Array.from(r.encode(e)).map((e=>`0${e.toString(16).toUpperCase()}`.slice(-2))).map((e=>`%${e}`)).join("")})).join("")}function Pt(e){const{value:t}=e;return Array.isArray(t)?function({key:e,value:t,style:r,explode:s,escape:n}){if("simple"===r)return t.map((e=>Ct(e,n))).join(",");if("label"===r)return`.${t.map((e=>Ct(e,n))).join(".")}`;if("matrix"===r)return t.map((e=>Ct(e,n))).reduce(((t,r)=>!t||s?`${t||""};${e}=${r}`:`${t},${r}`),"");if("form"===r){const r=s?`&${e}=`:",";return t.map((e=>Ct(e,n))).join(r)}if("spaceDelimited"===r){const r=s?`${e}=`:"";return t.map((e=>Ct(e,n))).join(` ${r}`)}if("pipeDelimited"===r){const r=s?`${e}=`:"";return t.map((e=>Ct(e,n))).join(`|${r}`)}}(e):"object"==typeof t?function({key:e,value:t,style:r,explode:s,escape:n}){const i=Object.keys(t);return"simple"===r?i.reduce(((e,r)=>{const i=Ct(t[r],n);return`${e?`${e},`:""}${r}${s?"=":","}${i}`}),""):"label"===r?i.reduce(((e,r)=>{const i=Ct(t[r],n);return`${e?`${e}.`:"."}${r}${s?"=":"."}${i}`}),""):"matrix"===r&&s?i.reduce(((e,r)=>`${e?`${e};`:";"}${r}=${Ct(t[r],n)}`),""):"matrix"===r?i.reduce(((r,s)=>{const i=Ct(t[s],n);return`${r?`${r},`:`;${e}=`}${s},${i}`}),""):"form"===r?i.reduce(((e,r)=>{const i=Ct(t[r],n);return`${e?`${e}${s?"&":","}`:""}${r}${s?"=":","}${i}`}),""):void 0}(e):function({key:e,value:t,style:r,escape:s}){return"simple"===r?Ct(t,s):"label"===r?`.${Ct(t,s)}`:"matrix"===r?`;${e}=${Ct(t,s)}`:"form"===r||"deepObject"===r?Ct(t,s):void 0}(e)}function Ct(e,t=!1){return Array.isArray(e)||null!==e&&"object"==typeof e?e=JSON.stringify(e):"number"!=typeof e&&"boolean"!=typeof e||(e=String(e)),t&&e.length>0?Tt(e,t):e}const It={form:",",spaceDelimited:"%20",pipeDelimited:"|"},_t={csv:",",ssv:"%20",tsv:"%09",pipes:"|"};function Rt(e,t,r=!1){const{collectionFormat:s,allowEmptyValue:n,serializationOption:i,encoding:o}=t,a="object"!=typeof t||Array.isArray(t)?t:t.value,l=r?e=>e.toString():e=>encodeURIComponent(e),c=l(e);if(void 0===a&&n)return[[c,""]];if(At(a)||Ot(a))return[[c,a]];if(i)return Ft(e,a,r,i);if(o){if([typeof o.style,typeof o.explode,typeof o.allowReserved].some((e=>"undefined"!==e))){const{style:t,explode:s,allowReserved:n}=o;return Ft(e,a,r,{style:t,explode:s,allowReserved:n})}if("string"==typeof o.contentType){if(o.contentType.startsWith("application/json")){const e=l("string"==typeof a?a:JSON.stringify(a));return[[c,new jt(e,"blob",{type:o.contentType})]]}const e=l(String(a));return[[c,new jt(e,"blob",{type:o.contentType})]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)&&a.every((e=>"object"!=typeof e))?[[c,a.map(l).join(",")]]:[[c,l(JSON.stringify(a))]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)?"multi"===s?[[c,a.map(l)]]:[[c,a.map(l).join(_t[s||"csv"])]]:[[c,""]]}function Ft(e,t,r,s){const n=s.style||"form",i=void 0===s.explode?"form"===n:s.explode,o=!r&&(s&&s.allowReserved?"unsafe":"reserved"),a=e=>Ct(e,o),l=r?e=>e:e=>a(e);return"object"!=typeof t?[[l(e),a(t)]]:Array.isArray(t)?i?[[l(e),t.map(a)]]:[[l(e),t.map(a).join(It[n])]]:"deepObject"===n?Object.keys(t).map((r=>[l(`${e}[${r}]`),a(t[r])])):i?Object.keys(t).map((e=>[l(e),a(t[e])])):[[l(e),Object.keys(t).map((e=>[`${l(e)},${a(t[e])}`])).join(",")]]}function Mt(e){return((e,{encode:t=!0}={})=>{const r=(e,t,s)=>(null==s?e.append(t,""):Array.isArray(s)?s.reduce(((s,n)=>r(e,t,n)),e):s instanceof Date?e.append(t,s.toISOString()):"object"==typeof s?Object.entries(s).reduce(((s,[n,i])=>r(e,`${t}[${n}]`,i)),e):e.append(t,s),e),s=Object.entries(e).reduce(((e,[t,s])=>r(e,t,s)),new URLSearchParams),n=String(s);return t?n:decodeURIComponent(n)})(Object.keys(e).reduce(((t,r)=>{for(const[s,n]of Rt(r,e[r]))t[s]=n instanceof jt?n.valueOf():n;return t}),{}),{encode:!1})}function Lt(e={}){const{url:t="",query:r,form:s}=e;if(s){const t=Object.keys(s).some((e=>{const{value:t}=s[e];return At(t)||Ot(t)})),r=e.headers["content-type"]||e.headers["Content-Type"];if(t||/multipart\/form-data/i.test(r)){const t=(n=e.form,Object.entries(n).reduce(((e,[t,r])=>{for(const[s,n]of Rt(t,r,!0))if(Array.isArray(n))for(const t of n)if(ArrayBuffer.isView(t)){const r=new Blob([t]);e.append(s,r)}else e.append(s,t);else if(ArrayBuffer.isView(n)){const t=new Blob([n]);e.append(s,t)}else e.append(s,n);return e}),new FormData));e.formdata=t,e.body=t}else e.body=Mt(s);delete e.form}var n;if(r){const[s,n]=t.split("?");let i="";if(n){const e=new URLSearchParams(n);Object.keys(r).forEach((t=>e.delete(t))),i=String(e)}const o=((...e)=>{const t=e.filter((e=>e)).join("&");return t?`?${t}`:""})(i,Mt(r));e.url=s+o,delete e.query}return e}function Dt(e){return null==e}var Bt={isNothing:Dt,isObject:function(e){return"object"==typeof e&&null!==e},toArray:function(e){return Array.isArray(e)?e:Dt(e)?[]:[e]},repeat:function(e,t){var r,s="";for(r=0;ra&&(t=s-a+(i=" ... ").length),r-s>a&&(r=s+a-(o=" ...").length),{str:i+e.slice(t,r).replace(/\t/g,"→")+o,pos:s-t+i.length}}function Ht(e,t){return Bt.repeat(" ",t-e.length)+e}var Vt=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var r,s=/\r?\n|\r|\0/g,n=[0],i=[],o=-1;r=s.exec(e.buffer);)i.push(r.index),n.push(r.index+r[0].length),e.position<=r.index&&o<0&&(o=n.length-2);o<0&&(o=n.length-1);var a,l,c="",p=Math.min(e.line+t.linesAfter,i.length).toString().length,u=t.maxLength-(t.indent+p+3);for(a=1;a<=t.linesBefore&&!(o-a<0);a++)l=zt(e.buffer,n[o-a],i[o-a],e.position-(n[o]-n[o-a]),u),c=Bt.repeat(" ",t.indent)+Ht((e.line-a+1).toString(),p)+" | "+l.str+"\n"+c;for(l=zt(e.buffer,n[o],i[o],e.position,u),c+=Bt.repeat(" ",t.indent)+Ht((e.line+1).toString(),p)+" | "+l.str+"\n",c+=Bt.repeat("-",t.indent+p+3+l.pos)+"^\n",a=1;a<=t.linesAfter&&!(o+a>=i.length);a++)l=zt(e.buffer,n[o+a],i[o+a],e.position-(n[o]-n[o+a]),u),c+=Bt.repeat(" ",t.indent)+Ht((e.line+a+1).toString(),p)+" | "+l.str+"\n";return c.replace(/\n$/,"")},Wt=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],Gt=["scalar","sequence","mapping"],Jt=function(e,t){if(t=t||{},Object.keys(t).forEach((function(t){if(-1===Wt.indexOf(t))throw new Ut('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=function(e){var t={};return null!==e&&Object.keys(e).forEach((function(r){e[r].forEach((function(e){t[String(e)]=r}))})),t}(t.styleAliases||null),-1===Gt.indexOf(this.kind))throw new Ut('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')};function Kt(e,t){var r=[];return e[t].forEach((function(e){var t=r.length;r.forEach((function(r,s){r.tag===e.tag&&r.kind===e.kind&&r.multi===e.multi&&(t=s)})),r[t]=e})),r}function Yt(e){return this.extend(e)}Yt.prototype.extend=function(e){var t=[],r=[];if(e instanceof Jt)r.push(e);else if(Array.isArray(e))r=r.concat(e);else{if(!e||!Array.isArray(e.implicit)&&!Array.isArray(e.explicit))throw new Ut("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(r=r.concat(e.explicit))}t.forEach((function(e){if(!(e instanceof Jt))throw new Ut("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(e.loadKind&&"scalar"!==e.loadKind)throw new Ut("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(e.multi)throw new Ut("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),r.forEach((function(e){if(!(e instanceof Jt))throw new Ut("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var s=Object.create(Yt.prototype);return s.implicit=(this.implicit||[]).concat(t),s.explicit=(this.explicit||[]).concat(r),s.compiledImplicit=Kt(s,"implicit"),s.compiledExplicit=Kt(s,"explicit"),s.compiledTypeMap=function(){var e,t,r={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function s(e){e.multi?(r.multi[e.kind].push(e),r.multi.fallback.push(e)):r[e.kind][e.tag]=r.fallback[e.tag]=e}for(e=0,t=arguments.length;e=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0o"+e.toString(8):"-0o"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),ar=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),lr=/^[-+]?[0-9]+e/,cr=new Jt("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&!(!ar.test(e)||"_"===e[e.length-1])},construct:function(e){var t,r;return r="-"===(t=e.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===r?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:r*parseFloat(t,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&(e%1!=0||Bt.isNegativeZero(e))},represent:function(e,t){var r;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(Bt.isNegativeZero(e))return"-0.0";return r=e.toString(10),lr.test(r)?r.replace("e",".e"):r},defaultStyle:"lowercase"}),pr=tr.extend({implicit:[rr,sr,or,cr]}),ur=pr,dr=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),hr=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$"),mr=new Jt("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(e){return null!==e&&(null!==dr.exec(e)||null!==hr.exec(e))},construct:function(e){var t,r,s,n,i,o,a,l,c=0,p=null;if(null===(t=dr.exec(e))&&(t=hr.exec(e)),null===t)throw new Error("Date resolve error");if(r=+t[1],s=+t[2]-1,n=+t[3],!t[4])return new Date(Date.UTC(r,s,n));if(i=+t[4],o=+t[5],a=+t[6],t[7]){for(c=t[7].slice(0,3);c.length<3;)c+="0";c=+c}return t[9]&&(p=6e4*(60*+t[10]+ +(t[11]||0)),"-"===t[9]&&(p=-p)),l=new Date(Date.UTC(r,s,n,i,o,a,c)),p&&l.setTime(l.getTime()-p),l},instanceOf:Date,represent:function(e){return e.toISOString()}}),fr=new Jt("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}}),gr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r",yr=new Jt("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,r,s=0,n=e.length,i=gr;for(r=0;r64)){if(t<0)return!1;s+=6}return s%8==0},construct:function(e){var t,r,s=e.replace(/[\r\n=]/g,""),n=s.length,i=gr,o=0,a=[];for(t=0;t>16&255),a.push(o>>8&255),a.push(255&o)),o=o<<6|i.indexOf(s.charAt(t));return 0==(r=n%4*6)?(a.push(o>>16&255),a.push(o>>8&255),a.push(255&o)):18===r?(a.push(o>>10&255),a.push(o>>2&255)):12===r&&a.push(o>>4&255),new Uint8Array(a)},predicate:function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)},represent:function(e){var t,r,s="",n=0,i=e.length,o=gr;for(t=0;t>18&63],s+=o[n>>12&63],s+=o[n>>6&63],s+=o[63&n]),n=(n<<8)+e[t];return 0==(r=i%3)?(s+=o[n>>18&63],s+=o[n>>12&63],s+=o[n>>6&63],s+=o[63&n]):2===r?(s+=o[n>>10&63],s+=o[n>>4&63],s+=o[n<<2&63],s+=o[64]):1===r&&(s+=o[n>>2&63],s+=o[n<<4&63],s+=o[64],s+=o[64]),s}}),vr=Object.prototype.hasOwnProperty,br=Object.prototype.toString,xr=new Jt("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,r,s,n,i,o=[],a=e;for(t=0,r=a.length;t>10),56320+(e-65536&1023))}for(var qr=new Array(256),Nr=new Array(256),Ur=0;Ur<256;Ur++)qr[Ur]=Dr(Ur)?1:0,Nr[Ur]=Dr(Ur);function zr(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||kr,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function Hr(e,t){var r={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return r.snippet=Vt(r),new Ut(t,r)}function Vr(e,t){throw Hr(e,t)}function Wr(e,t){e.onWarning&&e.onWarning.call(null,Hr(e,t))}var Gr={YAML:function(e,t,r){var s,n,i;null!==e.version&&Vr(e,"duplication of %YAML directive"),1!==r.length&&Vr(e,"YAML directive accepts exactly one argument"),null===(s=/^([0-9]+)\.([0-9]+)$/.exec(r[0]))&&Vr(e,"ill-formed argument of the YAML directive"),n=parseInt(s[1],10),i=parseInt(s[2],10),1!==n&&Vr(e,"unacceptable YAML version of the document"),e.version=r[0],e.checkLineBreaks=i<2,1!==i&&2!==i&&Wr(e,"unsupported YAML version of the document")},TAG:function(e,t,r){var s,n;2!==r.length&&Vr(e,"TAG directive accepts exactly two arguments"),s=r[0],n=r[1],Pr.test(s)||Vr(e,"ill-formed tag handle (first argument) of the TAG directive"),Ar.call(e.tagMap,s)&&Vr(e,'there is a previously declared suffix for "'+s+'" tag handle'),Cr.test(n)||Vr(e,"ill-formed tag prefix (second argument) of the TAG directive");try{n=decodeURIComponent(n)}catch(t){Vr(e,"tag prefix is malformed: "+n)}e.tagMap[s]=n}};function Jr(e,t,r,s){var n,i,o,a;if(t1&&(e.result+=Bt.repeat("\n",t-1))}function ts(e,t){var r,s,n=e.tag,i=e.anchor,o=[],a=!1;if(-1!==e.firstTabInLine)return!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=o),s=e.input.charCodeAt(e.position);0!==s&&(-1!==e.firstTabInLine&&(e.position=e.firstTabInLine,Vr(e,"tab characters must not be used in indentation")),45===s)&&Fr(e.input.charCodeAt(e.position+1));)if(a=!0,e.position++,Zr(e,!0,-1)&&e.lineIndent<=t)o.push(null),s=e.input.charCodeAt(e.position);else if(r=e.line,ns(e,t,3,!1,!0),o.push(e.result),Zr(e,!0,-1),s=e.input.charCodeAt(e.position),(e.line===r||e.lineIndent>t)&&0!==s)Vr(e,"bad indentation of a sequence entry");else if(e.lineIndentt?m=1:e.lineIndent===t?m=0:e.lineIndentt?m=1:e.lineIndent===t?m=0:e.lineIndentt)&&(y&&(o=e.line,a=e.lineStart,l=e.position),ns(e,t,4,!0,n)&&(y?f=e.result:g=e.result),y||(Yr(e,d,h,m,f,g,o,a,l),m=f=g=null),Zr(e,!0,-1),c=e.input.charCodeAt(e.position)),(e.line===i||e.lineIndent>t)&&0!==c)Vr(e,"bad indentation of a mapping entry");else if(e.lineIndent=0))break;0===n?Vr(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):c?Vr(e,"repeat of an indentation width identifier"):(p=t+n-1,c=!0)}if(Rr(i)){do{i=e.input.charCodeAt(++e.position)}while(Rr(i));if(35===i)do{i=e.input.charCodeAt(++e.position)}while(!_r(i)&&0!==i)}for(;0!==i;){for(Xr(e),e.lineIndent=0,i=e.input.charCodeAt(e.position);(!c||e.lineIndentp&&(p=e.lineIndent),_r(i))u++;else{if(e.lineIndent0){for(n=o,i=0;n>0;n--)(o=Lr(a=e.input.charCodeAt(++e.position)))>=0?i=(i<<4)+o:Vr(e,"expected hexadecimal character");e.result+=Br(i),e.position++}else Vr(e,"unknown escape sequence");r=s=e.position}else _r(a)?(Jr(e,r,s,!0),es(e,Zr(e,!1,t)),r=s=e.position):e.position===e.lineStart&&Qr(e)?Vr(e,"unexpected end of the document within a double quoted scalar"):(e.position++,s=e.position)}Vr(e,"unexpected end of the stream within a double quoted scalar")}(e,d)?g=!0:function(e){var t,r,s;if(42!==(s=e.input.charCodeAt(e.position)))return!1;for(s=e.input.charCodeAt(++e.position),t=e.position;0!==s&&!Fr(s)&&!Mr(s);)s=e.input.charCodeAt(++e.position);return e.position===t&&Vr(e,"name of an alias node must contain at least one character"),r=e.input.slice(t,e.position),Ar.call(e.anchorMap,r)||Vr(e,'unidentified alias "'+r+'"'),e.result=e.anchorMap[r],Zr(e,!0,-1),!0}(e)?(g=!0,null===e.tag&&null===e.anchor||Vr(e,"alias node should not have any properties")):function(e,t,r){var s,n,i,o,a,l,c,p,u=e.kind,d=e.result;if(Fr(p=e.input.charCodeAt(e.position))||Mr(p)||35===p||38===p||42===p||33===p||124===p||62===p||39===p||34===p||37===p||64===p||96===p)return!1;if((63===p||45===p)&&(Fr(s=e.input.charCodeAt(e.position+1))||r&&Mr(s)))return!1;for(e.kind="scalar",e.result="",n=i=e.position,o=!1;0!==p;){if(58===p){if(Fr(s=e.input.charCodeAt(e.position+1))||r&&Mr(s))break}else if(35===p){if(Fr(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&Qr(e)||r&&Mr(p))break;if(_r(p)){if(a=e.line,l=e.lineStart,c=e.lineIndent,Zr(e,!1,-1),e.lineIndent>=t){o=!0,p=e.input.charCodeAt(e.position);continue}e.position=i,e.line=a,e.lineStart=l,e.lineIndent=c;break}}o&&(Jr(e,n,i,!1),es(e,e.line-a),n=i=e.position,o=!1),Rr(p)||(i=e.position+1),p=e.input.charCodeAt(++e.position)}return Jr(e,n,i,!1),!!e.result||(e.kind=u,e.result=d,!1)}(e,d,1===r)&&(g=!0,null===e.tag&&(e.tag="?")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===m&&(g=a&&ts(e,h))),null===e.tag)null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);else if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&Vr(e,'unacceptable node kind for ! tag; it should be "scalar", not "'+e.kind+'"'),l=0,c=e.implicitTypes.length;l"),null!==e.result&&u.kind!==e.kind&&Vr(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+u.kind+'", not "'+e.kind+'"'),u.resolve(e.result,e.tag)?(e.result=u.construct(e.result,e.tag),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):Vr(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||g}function is(e){var t,r,s,n,i=e.position,o=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(n=e.input.charCodeAt(e.position))&&(Zr(e,!0,-1),n=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==n));){for(o=!0,n=e.input.charCodeAt(++e.position),t=e.position;0!==n&&!Fr(n);)n=e.input.charCodeAt(++e.position);for(s=[],(r=e.input.slice(t,e.position)).length<1&&Vr(e,"directive name must not be less than one character in length");0!==n;){for(;Rr(n);)n=e.input.charCodeAt(++e.position);if(35===n){do{n=e.input.charCodeAt(++e.position)}while(0!==n&&!_r(n));break}if(_r(n))break;for(t=e.position;0!==n&&!Fr(n);)n=e.input.charCodeAt(++e.position);s.push(e.input.slice(t,e.position))}0!==n&&Xr(e),Ar.call(Gr,r)?Gr[r](e,r,s):Wr(e,'unknown document directive "'+r+'"')}Zr(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,Zr(e,!0,-1)):o&&Vr(e,"directives end mark is expected"),ns(e,e.lineIndent-1,4,!1,!0),Zr(e,!0,-1),e.checkLineBreaks&&jr.test(e.input.slice(i,e.position))&&Wr(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&Qr(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,Zr(e,!0,-1)):e.position=55296&&s<=56319&&t+1=56320&&r<=57343?1024*(s-55296)+r-56320+65536:s}function Ss(e){return/^\n* /.test(e)}function Es(e,t,r,s,n){e.dump=function(){if(0===t.length)return 2===e.quotingType?'""':"''";if(!e.noCompatMode&&(-1!==ds.indexOf(t)||hs.test(t)))return 2===e.quotingType?'"'+t+'"':"'"+t+"'";var i=e.indent*Math.max(1,r),o=-1===e.lineWidth?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-i),a=s||e.flowLevel>-1&&r>=e.flowLevel;switch(function(e,t,r,s,n,i,o,a){var l,c,p=0,u=null,d=!1,h=!1,m=-1!==s,f=-1,g=bs(c=$s(e,0))&&65279!==c&&!vs(c)&&45!==c&&63!==c&&58!==c&&44!==c&&91!==c&&93!==c&&123!==c&&125!==c&&35!==c&&38!==c&&42!==c&&33!==c&&124!==c&&61!==c&&62!==c&&39!==c&&34!==c&&37!==c&&64!==c&&96!==c&&function(e){return!vs(e)&&58!==e}($s(e,e.length-1));if(t||o)for(l=0;l=65536?l+=2:l++){if(!bs(p=$s(e,l)))return 5;g=g&&ws(p,u,a),u=p}else{for(l=0;l=65536?l+=2:l++){if(10===(p=$s(e,l)))d=!0,m&&(h=h||l-f-1>s&&" "!==e[f+1],f=l);else if(!bs(p))return 5;g=g&&ws(p,u,a),u=p}h=h||m&&l-f-1>s&&" "!==e[f+1]}return d||h?r>9&&Ss(e)?5:o?2===i?5:2:h?4:3:!g||o||n(e)?2===i?5:2:1}(t,a,e.indent,o,(function(t){return function(e,t){var r,s;for(r=0,s=e.implicitTypes.length;r"+ks(t,e.indent)+As(gs(function(e,t){for(var r,s,n,i=/(\n+)([^\n]*)/g,o=(n=-1!==(n=e.indexOf("\n"))?n:e.length,i.lastIndex=n,Os(e.slice(0,n),t)),a="\n"===e[0]||" "===e[0];s=i.exec(e);){var l=s[1],c=s[2];r=" "===c[0],o+=l+(a||r||""===c?"":"\n")+Os(c,t),a=r}return o}(t,o),i));case 5:return'"'+function(e){for(var t,r="",s=0,n=0;n=65536?n+=2:n++)s=$s(e,n),!(t=us[s])&&bs(s)?(r+=e[n],s>=65536&&(r+=e[n+1])):r+=t||ms(s);return r}(t)+'"';default:throw new Ut("impossible error: invalid scalar style")}}()}function ks(e,t){var r=Ss(e)?String(t):"",s="\n"===e[e.length-1];return r+(!s||"\n"!==e[e.length-2]&&"\n"!==e?s?"":"-":"+")+"\n"}function As(e){return"\n"===e[e.length-1]?e.slice(0,-1):e}function Os(e,t){if(""===e||" "===e[0])return e;for(var r,s,n=/ [^ ]/g,i=0,o=0,a=0,l="";r=n.exec(e);)(a=r.index)-i>t&&(s=o>i?o:a,l+="\n"+e.slice(i,s),i=s+1),o=a;return l+="\n",e.length-i>t&&o>i?l+=e.slice(i,o)+"\n"+e.slice(o+1):l+=e.slice(i),l.slice(1)}function js(e,t,r,s){var n,i,o,a="",l=e.tag;for(n=0,i=r.length;n tag resolver accepts not "'+l+'" style');s=a.represent[l](t,l)}e.dump=s}return!0}return!1}function Ps(e,t,r,s,n,i,o){e.tag=null,e.dump=r,Ts(e,r,!1)||Ts(e,r,!0);var a,l=cs.call(e.dump),c=s;s&&(s=e.flowLevel<0||e.flowLevel>t);var p,u,d="[object Object]"===l||"[object Array]"===l;if(d&&(u=-1!==(p=e.duplicates.indexOf(r))),(null!==e.tag&&"?"!==e.tag||u||2!==e.indent&&t>0)&&(n=!1),u&&e.usedDuplicates[p])e.dump="*ref_"+p;else{if(d&&u&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),"[object Object]"===l)s&&0!==Object.keys(e.dump).length?(function(e,t,r,s){var n,i,o,a,l,c,p="",u=e.tag,d=Object.keys(r);if(!0===e.sortKeys)d.sort();else if("function"==typeof e.sortKeys)d.sort(e.sortKeys);else if(e.sortKeys)throw new Ut("sortKeys must be a boolean or a function");for(n=0,i=d.length;n1024)&&(e.dump&&10===e.dump.charCodeAt(0)?c+="?":c+="? "),c+=e.dump,l&&(c+=ys(e,t)),Ps(e,t+1,a,!0,l)&&(e.dump&&10===e.dump.charCodeAt(0)?c+=":":c+=": ",p+=c+=e.dump));e.tag=u,e.dump=p||"{}"}(e,t,e.dump,n),u&&(e.dump="&ref_"+p+e.dump)):(function(e,t,r){var s,n,i,o,a,l="",c=e.tag,p=Object.keys(r);for(s=0,n=p.length;s1024&&(a+="? "),a+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),Ps(e,t,o,!1,!1)&&(l+=a+=e.dump));e.tag=c,e.dump="{"+l+"}"}(e,t,e.dump),u&&(e.dump="&ref_"+p+" "+e.dump));else if("[object Array]"===l)s&&0!==e.dump.length?(e.noArrayIndent&&!o&&t>0?js(e,t-1,e.dump,n):js(e,t,e.dump,n),u&&(e.dump="&ref_"+p+e.dump)):(function(e,t,r){var s,n,i,o="",a=e.tag;for(s=0,n=r.length;s",e.dump=a+" "+e.dump)}return!0}function Cs(e,t){var r,s,n=[],i=[];for(Is(e,n,i),r=0,s=i.length;r(e[t]=function(e){return e.includes(", ")?e.split(", "):e}(r),e)),{})}function Ys(e,t,{loadSpec:r=!1}={}){const s={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:Ks(e.headers)},n=s.headers["content-type"],i=r||((e="")=>/(json|xml|yaml|text)\b/.test(e))(n);return(i?e.text:e.blob||e.buffer).call(e).then((e=>{if(s.text=e,s.data=e,i)try{const t=function(e,t){return t&&(0===t.indexOf("application/json")||t.indexOf("+json")>0)?JSON.parse(e):Js.load(e)}(e,n);s.body=t,s.obj=t}catch(e){s.parseError=e}return s}))}async function Xs(e,t={}){"object"==typeof e&&(e=(t=e).url),t.headers=t.headers||{},(t=Lt(t)).headers&&Object.keys(t.headers).forEach((e=>{const r=t.headers[e];"string"==typeof r&&(t.headers[e]=r.replace(/\n+/g," "))})),t.requestInterceptor&&(t=await t.requestInterceptor(t)||t);const r=t.headers["content-type"]||t.headers["Content-Type"];let s;/multipart\/form-data/i.test(r)&&(delete t.headers["content-type"],delete t.headers["Content-Type"]);try{s=await(t.userFetch||fetch)(t.url,t),s=await Ys(s,e,t),t.responseInterceptor&&(s=await t.responseInterceptor(s)||s)}catch(e){if(!s)throw e;const t=new Error(s.statusText||`response status is ${s.status}`);throw t.status=s.status,t.statusCode=s.status,t.responseError=e,t}if(!s.ok){const e=new Error(s.statusText||`response status is ${s.status}`);throw e.status=s.status,e.statusCode=s.status,e.response=s,e}return s}function Zs(e,t={}){const{requestInterceptor:r,responseInterceptor:s}=t,n=e.withCredentials?"include":"same-origin";return t=>e({url:t,loadSpec:!0,requestInterceptor:r,responseInterceptor:s,headers:{Accept:"application/json, application/yaml"},credentials:n}).then((e=>e.body))}const Qs=e=>{var t,r;const{baseDoc:s,url:n}=e,i=null!==(t=null!=s?s:n)&&void 0!==t?t:"";return"string"==typeof(null===(r=globalThis.document)||void 0===r?void 0:r.baseURI)?String(new URL(i,globalThis.document.baseURI)):i},en=e=>{const{fetch:t,http:r}=e;return t||r||Xs};var tn,rn=(tn=function(e,t){return tn=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])},tn(e,t)},function(e,t){function r(){this.constructor=e}tn(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),sn=Object.prototype.hasOwnProperty;function nn(e,t){return sn.call(e,t)}function on(e){if(Array.isArray(e)){for(var t=new Array(e.length),r=0;r=48&&t<=57))return!1;r++}return!0}function cn(e){return-1===e.indexOf("/")&&-1===e.indexOf("~")?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}function pn(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")}function un(e){if(void 0===e)return!0;if(e)if(Array.isArray(e)){for(var t=0,r=e.length;t0&&"constructor"==a[c-1]))throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");if(r&&void 0===u&&(void 0===l[d]?u=a.slice(0,c).join("/"):c==p-1&&(u=t.path),void 0!==u&&h(t,0,e,u)),c++,Array.isArray(l)){if("-"===d)d=l.length;else{if(r&&!ln(d))throw new mn("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",i,t,e);ln(d)&&(d=~~d)}if(c>=p){if(r&&"add"===t.op&&d>l.length)throw new mn("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",i,t,e);if(!1===(o=yn[t.op].call(t,l,d,e)).test)throw new mn("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return o}}else if(c>=p){if(!1===(o=gn[t.op].call(t,l,d,e)).test)throw new mn("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return o}if(l=l[d],r&&c0)throw new mn('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",t,e,r);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new mn("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",t,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new mn("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",t,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&un(e.value))throw new mn("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",t,e,r);if(r)if("add"==e.op){var n=e.path.split("/").length,i=s.split("/").length;if(n!==i+1&&n!==i)throw new mn("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",t,e,r)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==s)throw new mn("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",t,e,r)}else if("move"===e.op||"copy"===e.op){var o=$n([{op:"_get",path:e.from,value:void 0}],r);if(o&&"OPERATION_PATH_UNRESOLVABLE"===o.name)throw new mn("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",t,e,r)}}function $n(e,t,r){try{if(!Array.isArray(e))throw new mn("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(t)xn(an(t),an(e),r||!0);else{r=r||wn;for(var s=0;s0&&(e.patches=[],e.callback&&e.callback(s)),s}function Tn(e,t,r,s,n){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var i=on(t),o=on(e),a=!1,l=o.length-1;l>=0;l--){var c=e[u=o[l]];if(!nn(t,u)||void 0===t[u]&&void 0!==c&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(n&&r.push({op:"test",path:s+"/"+cn(u),value:an(c)}),r.push({op:"remove",path:s+"/"+cn(u)}),a=!0):(n&&r.push({op:"test",path:s,value:e}),r.push({op:"replace",path:s,value:t}));else{var p=t[u];"object"==typeof c&&null!=c&&"object"==typeof p&&null!=p&&Array.isArray(c)===Array.isArray(p)?Tn(c,p,r,s+"/"+cn(u),n):c!==p&&(n&&r.push({op:"test",path:s+"/"+cn(u),value:an(c)}),r.push({op:"replace",path:s+"/"+cn(u),value:an(p)}))}}if(a||i.length!=o.length)for(l=0;lvoid 0!==t&&e?e[t]:e),e)},applyPatch:function(e,t,r){if(r=r||{},"merge"===(t={...t,path:t.path&&Nn(t.path)}).op){const r=ti(e,t.path);Object.assign(r,t.value),xn(e,[Un(t.path,r)])}else if("mergeDeep"===t.op){const r=ti(e,t.path),s=Bn(r,t.value);e=xn(e,[Un(t.path,s)]).newDocument}else if("add"===t.op&&""===t.path&&Kn(t.value)){const r=Object.keys(t.value).reduce(((e,r)=>(e.push({op:"add",path:`/${Nn(r)}`,value:t.value[r]}),e)),[]);xn(e,r)}else if("replace"===t.op&&""===t.path){let{value:s}=t;r.allowMetaPatches&&t.meta&&Qn(t)&&(Array.isArray(t.value)||Kn(t.value))&&(s={...s,...t.meta}),e=s}else if(xn(e,[t]),r.allowMetaPatches&&t.meta&&Qn(t)&&(Array.isArray(t.value)||Kn(t.value))){const r={...ti(e,t.path),...t.meta};xn(e,[Un(t.path,r)])}return e},parentPathMatch:function(e,t){if(!Array.isArray(t))return!1;for(let r=0,s=t.length;r(e+"").replace(/~/g,"~0").replace(/\//g,"~1"))).join("/")}`:e}function Un(e,t,r){return{op:"replace",path:e,value:t,meta:r}}function zn(e,t,r){return Jn(Gn(e.filter(Qn).map((e=>t(e.value,r,e.path)))||[]))}function Hn(e,t,r){return r=r||[],Array.isArray(e)?e.map(((e,s)=>Hn(e,t,r.concat(s)))):Kn(e)?Object.keys(e).map((s=>Hn(e[s],t,r.concat(s)))):t(e,r[r.length-1],r)}function Vn(e,t,r){let s=[];if((r=r||[]).length>0){const n=t(e,r[r.length-1],r);n&&(s=s.concat(n))}if(Array.isArray(e)){const n=e.map(((e,s)=>Vn(e,t,r.concat(s))));n&&(s=s.concat(n))}else if(Kn(e)){const n=Object.keys(e).map((s=>Vn(e[s],t,r.concat(s))));n&&(s=s.concat(n))}return s=Gn(s),s}function Wn(e){return Array.isArray(e)?e:[e]}function Gn(e){return[].concat(...e.map((e=>Array.isArray(e)?Gn(e):e)))}function Jn(e){return e.filter((e=>void 0!==e))}function Kn(e){return e&&"object"==typeof e}function Yn(e){return e&&"function"==typeof e}function Xn(e){if(ei(e)){const{op:t}=e;return"add"===t||"remove"===t||"replace"===t}return!1}function Zn(e){return Xn(e)||ei(e)&&"mutation"===e.type}function Qn(e){return Zn(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function ei(e){return e&&"object"==typeof e}function ti(e,t){try{return vn(e,t)}catch(e){return console.error(e),{}}}var ri=function(e){return e&&e.Math===Math&&e},si=ri("object"==typeof globalThis&&globalThis)||ri("object"==typeof window&&window)||ri("object"==typeof self&&self)||ri("object"==typeof global&&global)||ri(!1)||function(){return this}()||Function("return this")(),ni=function(e){try{return!!e()}catch(e){return!0}},ii=!ni((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")})),oi=ii,ai=Function.prototype,li=ai.apply,ci=ai.call,pi="object"==typeof Reflect&&Reflect.apply||(oi?ci.bind(li):function(){return ci.apply(li,arguments)}),ui=ii,di=Function.prototype,hi=di.call,mi=ui&&di.bind.bind(hi,hi),fi=ui?mi:function(e){return function(){return hi.apply(e,arguments)}},gi=fi,yi=gi({}.toString),vi=gi("".slice),bi=function(e){return vi(yi(e),8,-1)},xi=bi,wi=fi,$i=function(e){if("Function"===xi(e))return wi(e)},Si="object"==typeof document&&document.all,Ei=void 0===Si&&void 0!==Si?function(e){return"function"==typeof e||e===Si}:function(e){return"function"==typeof e},ki={},Ai=!ni((function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]})),Oi=ii,ji=Function.prototype.call,Ti=Oi?ji.bind(ji):function(){return ji.apply(ji,arguments)},Pi={},Ci={}.propertyIsEnumerable,Ii=Object.getOwnPropertyDescriptor,_i=Ii&&!Ci.call({1:2},1);Pi.f=_i?function(e){var t=Ii(this,e);return!!t&&t.enumerable}:Ci;var Ri,Fi,Mi=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},Li=ni,Di=bi,Bi=Object,qi=fi("".split),Ni=Li((function(){return!Bi("z").propertyIsEnumerable(0)}))?function(e){return"String"===Di(e)?qi(e,""):Bi(e)}:Bi,Ui=function(e){return null==e},zi=Ui,Hi=TypeError,Vi=function(e){if(zi(e))throw new Hi("Can't call method on "+e);return e},Wi=Ni,Gi=Vi,Ji=function(e){return Wi(Gi(e))},Ki=Ei,Yi=function(e){return"object"==typeof e?null!==e:Ki(e)},Xi={},Zi=Xi,Qi=si,eo=Ei,to=function(e){return eo(e)?e:void 0},ro=function(e,t){return arguments.length<2?to(Zi[e])||to(Qi[e]):Zi[e]&&Zi[e][t]||Qi[e]&&Qi[e][t]},so=fi({}.isPrototypeOf),no=si.navigator,io=no&&no.userAgent,oo=si,ao=io?String(io):"",lo=oo.process,co=oo.Deno,po=lo&&lo.versions||co&&co.version,uo=po&&po.v8;uo&&(Fi=(Ri=uo.split("."))[0]>0&&Ri[0]<4?1:+(Ri[0]+Ri[1])),!Fi&&ao&&(!(Ri=ao.match(/Edge\/(\d+)/))||Ri[1]>=74)&&(Ri=ao.match(/Chrome\/(\d+)/))&&(Fi=+Ri[1]);var ho=Fi,mo=ni,fo=si.String,go=!!Object.getOwnPropertySymbols&&!mo((function(){var e=Symbol("symbol detection");return!fo(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&ho&&ho<41})),yo=go&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,vo=ro,bo=Ei,xo=so,wo=Object,$o=yo?function(e){return"symbol"==typeof e}:function(e){var t=vo("Symbol");return bo(t)&&xo(t.prototype,wo(e))},So=String,Eo=function(e){try{return So(e)}catch(e){return"Object"}},ko=Ei,Ao=Eo,Oo=TypeError,jo=function(e){if(ko(e))return e;throw new Oo(Ao(e)+" is not a function")},To=jo,Po=Ui,Co=function(e,t){var r=e[t];return Po(r)?void 0:To(r)},Io=Ti,_o=Ei,Ro=Yi,Fo=TypeError,Mo={exports:{}},Lo=si,Do=Object.defineProperty,Bo=si,qo=Mo.exports=Bo.o||function(e,t){try{Do(Lo,e,{value:t,configurable:!0,writable:!0})}catch(r){Lo[e]=t}return t}("__core-js_shared__",{});(qo.versions||(qo.versions=[])).push({version:"3.38.1",mode:"pure",copyright:"© 2014-2024 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.38.1/LICENSE",source:"https://github.com/zloirock/core-js"});var No=Mo.exports,Uo=No,zo=function(e,t){return Uo[e]||(Uo[e]=t||{})},Ho=Vi,Vo=Object,Wo=function(e){return Vo(Ho(e))},Go=Wo,Jo=fi({}.hasOwnProperty),Ko=Object.hasOwn||function(e,t){return Jo(Go(e),t)},Yo=fi,Xo=0,Zo=Math.random(),Qo=Yo(1..toString),ea=function(e){return"Symbol("+(void 0===e?"":e)+")_"+Qo(++Xo+Zo,36)},ta=zo,ra=Ko,sa=ea,na=go,ia=yo,oa=si.Symbol,aa=ta("wks"),la=ia?oa.for||oa:oa&&oa.withoutSetter||sa,ca=function(e){return ra(aa,e)||(aa[e]=na&&ra(oa,e)?oa[e]:la("Symbol."+e)),aa[e]},pa=Ti,ua=Yi,da=$o,ha=Co,ma=TypeError,fa=ca("toPrimitive"),ga=function(e,t){if(!ua(e)||da(e))return e;var r,s=ha(e,fa);if(s){if(void 0===t&&(t="default"),r=pa(s,e,t),!ua(r)||da(r))return r;throw new ma("Can't convert object to primitive value")}return void 0===t&&(t="number"),function(e,t){var r,s;if("string"===t&&_o(r=e.toString)&&!Ro(s=Io(r,e)))return s;if(_o(r=e.valueOf)&&!Ro(s=Io(r,e)))return s;if("string"!==t&&_o(r=e.toString)&&!Ro(s=Io(r,e)))return s;throw new Fo("Can't convert object to primitive value")}(e,t)},ya=$o,va=function(e){var t=ga(e,"string");return ya(t)?t:t+""},ba=Yi,xa=si.document,wa=ba(xa)&&ba(xa.createElement),$a=function(e){return wa?xa.createElement(e):{}},Sa=$a,Ea=!Ai&&!ni((function(){return 7!==Object.defineProperty(Sa("div"),"a",{get:function(){return 7}}).a})),ka=Ai,Aa=Ti,Oa=Pi,ja=Mi,Ta=Ji,Pa=va,Ca=Ko,Ia=Ea,_a=Object.getOwnPropertyDescriptor;ki.f=ka?_a:function(e,t){if(e=Ta(e),t=Pa(t),Ia)try{return _a(e,t)}catch(e){}if(Ca(e,t))return ja(!Aa(Oa.f,e,t),e[t])};var Ra=ni,Fa=Ei,Ma=/#|\.prototype\./,La=function(e,t){var r=Ba[Da(e)];return r===Na||r!==qa&&(Fa(t)?Ra(t):!!t)},Da=La.normalize=function(e){return String(e).replace(Ma,".").toLowerCase()},Ba=La.data={},qa=La.NATIVE="N",Na=La.POLYFILL="P",Ua=La,za=jo,Ha=ii,Va=$i($i.bind),Wa=function(e,t){return za(e),void 0===t?e:Ha?Va(e,t):function(){return e.apply(t,arguments)}},Ga={},Ja=Ai&&ni((function(){return 42!==Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),Ka=Yi,Ya=String,Xa=TypeError,Za=function(e){if(Ka(e))return e;throw new Xa(Ya(e)+" is not an object")},Qa=Ai,el=Ea,tl=Ja,rl=Za,sl=va,nl=TypeError,il=Object.defineProperty,ol=Object.getOwnPropertyDescriptor;Ga.f=Qa?tl?function(e,t,r){if(rl(e),t=sl(t),rl(r),"function"==typeof e&&"prototype"===t&&"value"in r&&"writable"in r&&!r.writable){var s=ol(e,t);s&&s.writable&&(e[t]=r.value,r={configurable:"configurable"in r?r.configurable:s.configurable,enumerable:"enumerable"in r?r.enumerable:s.enumerable,writable:!1})}return il(e,t,r)}:il:function(e,t,r){if(rl(e),t=sl(t),rl(r),el)try{return il(e,t,r)}catch(e){}if("get"in r||"set"in r)throw new nl("Accessors not supported");return"value"in r&&(e[t]=r.value),e};var al=Ga,ll=Mi,cl=Ai?function(e,t,r){return al.f(e,t,ll(1,r))}:function(e,t,r){return e[t]=r,e},pl=si,ul=pi,dl=$i,hl=Ei,ml=ki.f,fl=Ua,gl=Xi,yl=Wa,vl=cl,bl=Ko,xl=function(e){var t=function(r,s,n){if(this instanceof t){switch(arguments.length){case 0:return new e;case 1:return new e(r);case 2:return new e(r,s)}return new e(r,s,n)}return ul(e,this,arguments)};return t.prototype=e.prototype,t},wl=function(e,t){var r,s,n,i,o,a,l,c,p,u=e.target,d=e.global,h=e.stat,m=e.proto,f=d?pl:h?pl[u]:pl[u]&&pl[u].prototype,g=d?gl:gl[u]||vl(gl,u,{})[u],y=g.prototype;for(i in t)s=!(r=fl(d?i:u+(h?".":"#")+i,e.forced))&&f&&bl(f,i),a=g[i],s&&(l=e.dontCallGetSet?(p=ml(f,i))&&p.value:f[i]),o=s&&l?l:t[i],(r||m||typeof a!=typeof o)&&(c=e.bind&&s?yl(o,pl):e.wrap&&s?xl(o):m&&hl(o)?dl(o):o,(e.sham||o&&o.sham||a&&a.sham)&&vl(c,"sham",!0),vl(g,i,c),m&&(bl(gl,n=u+"Prototype")||vl(gl,n,{}),vl(gl[n],i,o),e.real&&y&&(r||!y[i])&&vl(y,i,o)))},$l=ea,Sl=zo("keys"),El=function(e){return Sl[e]||(Sl[e]=$l(e))},kl=!ni((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype})),Al=Ko,Ol=Ei,jl=Wo,Tl=kl,Pl=El("IE_PROTO"),Cl=Object,Il=Cl.prototype,_l=Tl?Cl.getPrototypeOf:function(e){var t=jl(e);if(Al(t,Pl))return t[Pl];var r=t.constructor;return Ol(r)&&t instanceof r?r.prototype:t instanceof Cl?Il:null},Rl=fi,Fl=jo,Ml=Yi,Ll=String,Dl=TypeError,Bl=Yi,ql=Vi,Nl=function(e){if(function(e){return Ml(e)||null===e}(e))return e;throw new Dl("Can't set "+Ll(e)+" as a prototype")},Ul=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,r={};try{(e=function(e,t,r){try{return Rl(Fl(Object.getOwnPropertyDescriptor(e,t)[r]))}catch(e){}}(Object.prototype,"__proto__","set"))(r,[]),t=r instanceof Array}catch(e){}return function(r,s){return ql(r),Nl(s),Bl(r)?(t?e(r,s):r.__proto__=s,r):r}}():void 0),zl={},Hl=Math.ceil,Vl=Math.floor,Wl=Math.trunc||function(e){var t=+e;return(t>0?Vl:Hl)(t)},Gl=function(e){var t=+e;return t!=t||0===t?0:Wl(t)},Jl=Gl,Kl=Math.max,Yl=Math.min,Xl=Gl,Zl=Math.min,Ql=function(e){return function(e){var t=Xl(e);return t>0?Zl(t,9007199254740991):0}(e.length)},ec=Ji,tc=Ql,rc=function(e){return function(t,r,s){var n=ec(t),i=tc(n);if(0===i)return!e&&-1;var o,a=function(e,t){var r=Jl(e);return r<0?Kl(r+t,0):Yl(r,t)}(s,i);if(e&&r!=r){for(;i>a;)if((o=n[a++])!=o)return!0}else for(;i>a;a++)if((e||a in n)&&n[a]===r)return e||a||0;return!e&&-1}},sc={includes:rc(!0),indexOf:rc(!1)},nc={},ic=Ko,oc=Ji,ac=sc.indexOf,lc=nc,cc=fi([].push),pc=function(e,t){var r,s=oc(e),n=0,i=[];for(r in s)!ic(lc,r)&&ic(s,r)&&cc(i,r);for(;t.length>n;)ic(s,r=t[n++])&&(~ac(i,r)||cc(i,r));return i},uc=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],dc=pc,hc=uc.concat("length","prototype");zl.f=Object.getOwnPropertyNames||function(e){return dc(e,hc)};var mc={};mc.f=Object.getOwnPropertySymbols;var fc=ro,gc=zl,yc=mc,vc=Za,bc=fi([].concat),xc=fc("Reflect","ownKeys")||function(e){var t=gc.f(vc(e)),r=yc.f;return r?bc(t,r(e)):t},wc=Ko,$c=xc,Sc=ki,Ec=Ga,kc=function(e,t,r){for(var s=$c(t),n=Ec.f,i=Sc.f,o=0;oo;)Ic.f(e,r=n[o++],s[r]);return e};var Mc,Lc=ro("document","documentElement"),Dc=Za,Bc=Ac,qc=uc,Nc=nc,Uc=Lc,zc=$a,Hc=El("IE_PROTO"),Vc=function(){},Wc=function(e){return" + + + + + {{ title }} + + + diff --git a/engine/core/templates/unfold/helpers/fieldsets_tabs.html b/engine/core/templates/unfold/helpers/fieldsets_tabs.html new file mode 100644 index 00000000..668bc031 --- /dev/null +++ b/engine/core/templates/unfold/helpers/fieldsets_tabs.html @@ -0,0 +1,35 @@ +{% load unfold %} +{% load filters %} + +{% with tabs=adminform|tabs %} + {% if tabs %} + {% with active_tab=tabs|tabs_active_unicode %} +
    + {% endwith %} + {% endif %} +{% endwith %} diff --git a/engine/core/templatetags/filters.py b/engine/core/templatetags/filters.py index 4ef5c8de..c132eac4 100644 --- a/engine/core/templatetags/filters.py +++ b/engine/core/templatetags/filters.py @@ -1,4 +1,6 @@ from django import template +from django.contrib.admin.helpers import Fieldset +from django.utils.text import slugify register = template.Library() @@ -7,3 +9,30 @@ register = template.Library() def endswith(value, arg): """Returns True if the value ends with the argument.""" 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 diff --git a/engine/core/utils/commerce.py b/engine/core/utils/commerce.py index c52254e1..c5ca430d 100644 --- a/engine/core/utils/commerce.py +++ b/engine/core/utils/commerce.py @@ -174,7 +174,7 @@ def get_top_returned_products( p = product_by_id[pid] img = "" 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( { "name": p.name, diff --git a/engine/core/vendors/__init__.py b/engine/core/vendors/__init__.py index 1b10b38c..f027c6a5 100644 --- a/engine/core/vendors/__init__.py +++ b/engine/core/vendors/__init__.py @@ -8,7 +8,7 @@ from datetime import datetime from decimal import Decimal from io import BytesIO from math import ceil, log10 -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from constance import config from django.conf import settings @@ -36,6 +36,8 @@ from engine.payments.errors import RatesError from engine.payments.utils import get_rates from schon.utils.misc import LoggingError, LogLevel +_BrandOrCategory = TypeVar("_BrandOrCategory", Brand, Category) + if TYPE_CHECKING: from engine.core.models import OrderProduct @@ -320,8 +322,8 @@ class AbstractVendor(ABC): @staticmethod def _auto_resolver_helper( - model: type[Brand] | type[Category], resolving_name: str - ) -> Brand | Category | None: + model: type[_BrandOrCategory], resolving_name: str + ) -> _BrandOrCategory | None: """Internal helper for resolving Brand/Category by name with deduplication.""" queryset = model.objects.filter(name=resolving_name) if not queryset.exists(): @@ -453,7 +455,7 @@ class AbstractVendor(ABC): # step back 1 to land on a “9” ending 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: """ @@ -672,6 +674,8 @@ class AbstractVendor(ABC): .order_by("uuid") .first() ) + if not attribute: + return None fields_to_update: list[str] = [] if not attribute.is_active: attribute.is_active = True diff --git a/engine/core/views.py b/engine/core/views.py index 07d9404b..73fa36f9 100644 --- a/engine/core/views.py +++ b/engine/core/views.py @@ -30,13 +30,10 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import cache_page from django.views.decorators.csrf import csrf_exempt from django.views.decorators.vary import vary_on_headers +from django.views.generic import TemplateView from django_ratelimit.decorators import ratelimit from drf_spectacular.utils import extend_schema_view -from drf_spectacular.views import ( - SpectacularAPIView, - SpectacularRedocView, - SpectacularSwaggerView, -) +from drf_spectacular.views import SpectacularAPIView from graphene_file_upload.django import FileUploadGraphQLView from rest_framework import status from rest_framework.permissions import AllowAny @@ -100,7 +97,7 @@ def sitemap_index(request, *args, **kwargs): # 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. " "It ensures the response includes the appropriate content type header for XML." ) @@ -115,7 +112,7 @@ def sitemap_detail(request, *args, **kwargs): # noinspection PyTypeChecker -sitemap_detail.__doc__ = _( # pyright: ignore[reportUnknownVariableType] +sitemap_detail.__doc__ = _( # ty:ignore[invalid-assignment] "Handles the detailed view response for a sitemap. " "This function processes the request, fetches the appropriate " "sitemap detail response, and sets the Content-Type header for XML." @@ -133,19 +130,13 @@ class CustomSpectacularAPIView(SpectacularAPIView): return super().get(request, *args, **kwargs) -class CustomSwaggerView(SpectacularSwaggerView): - def get_context_data(self, **kwargs): - # noinspection PyUnresolvedReferences - context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute] - context["script_url"] = self.request.build_absolute_uri() - return context +class RapiDocView(TemplateView): + template_name = "rapidoc.html" - -class CustomRedocView(SpectacularRedocView): def get_context_data(self, **kwargs): - # noinspection PyUnresolvedReferences - context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute] - context["script_url"] = self.request.build_absolute_uri() + context = super().get_context_data(**kwargs) + context["title"] = settings.SPECTACULAR_SETTINGS.get("TITLE", "API") + context["schema_url"] = self.request.build_absolute_uri("/docs/schema/") return context @@ -616,7 +607,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: product = Product.objects.filter(pk=wished_first["products"]).first() if product: 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 = { "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: continue 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( { "name": p.name, @@ -696,10 +687,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: .order_by("total_qty")[:5] ) for p in products: - qty = int(p.total_qty or 0) + qty = int(p.total_qty or 0) # ty: ignore[possibly-missing-attribute] img = "" 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( { "name": p.name, @@ -743,7 +734,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context: product = Product.objects.filter(pk=popular_first["product"]).first() if product: 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 = { "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: continue 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( { "name": p.name, diff --git a/engine/core/viewsets.py b/engine/core/viewsets.py index d0519a17..c783dd0c 100644 --- a/engine/core/viewsets.py +++ b/engine/core/viewsets.py @@ -383,16 +383,18 @@ class BrandViewSet(SchonViewSet): return obj def get_queryset(self): - queryset = Brand.objects.all() - - if self.request.user.has_perm("view_category"): # ty:ignore[possibly-missing-attribute] - queryset = queryset.prefetch_related("categories") - else: - queryset = queryset.prefetch_related( + qs = super().get_queryset() + if self.request.user.has_perm("core.view_brand"): # ty:ignore[possibly-missing-attribute] + if self.request.user.has_perm("core.view_brand"): # ty:ignore[possibly-missing-attribute] + return qs.prefetch_related("categories") + return qs.prefetch_related( Prefetch("categories", queryset=Category.objects.filter(is_active=True)) ) - - return queryset + 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)) + ) # noinspection PyUnusedLocal @action( diff --git a/engine/vibes_auth/docs/drf/emailing.py b/engine/vibes_auth/docs/drf/emailing.py new file mode 100644 index 00000000..20139d2f --- /dev/null +++ b/engine/vibes_auth/docs/drf/emailing.py @@ -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." + ), + ), + }, + ), +} diff --git a/engine/vibes_auth/docs/drf/views.py b/engine/vibes_auth/docs/drf/views.py index a1c0f689..d810ad81 100644 --- a/engine/vibes_auth/docs/drf/views.py +++ b/engine/vibes_auth/docs/drf/views.py @@ -13,7 +13,7 @@ from engine.vibes_auth.serializers import ( TOKEN_OBTAIN_SCHEMA = { "post": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("obtain a token pair"), description=_("obtain a token pair (refresh and access) for authentication."), @@ -36,7 +36,7 @@ TOKEN_OBTAIN_SCHEMA = { TOKEN_REFRESH_SCHEMA = { "post": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("refresh a token pair"), description=_("refresh a token pair (refresh and access)."), @@ -59,7 +59,7 @@ TOKEN_REFRESH_SCHEMA = { TOKEN_VERIFY_SCHEMA = { "post": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("verify a token"), description=_("Verify a token (refresh or access)."), diff --git a/engine/vibes_auth/docs/drf/viewsets.py b/engine/vibes_auth/docs/drf/viewsets.py index 2362fafa..04480f61 100644 --- a/engine/vibes_auth/docs/drf/viewsets.py +++ b/engine/vibes_auth/docs/drf/viewsets.py @@ -14,7 +14,7 @@ from engine.vibes_auth.serializers import ( USER_SCHEMA = { "create": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("create a new user"), request=UserSerializer, @@ -22,14 +22,14 @@ USER_SCHEMA = { ), "retrieve": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("retrieve a user's details"), responses={status.HTTP_200_OK: UserSerializer, **BASE_ERRORS}, ), "update": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("update a user's details"), request=UserSerializer, @@ -37,7 +37,7 @@ USER_SCHEMA = { ), "partial_update": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("partially update a user's details"), request=UserSerializer, @@ -45,14 +45,14 @@ USER_SCHEMA = { ), "destroy": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("delete a user"), responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, ), "reset_password": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("reset a user's password by sending a reset password email"), request=ResetPasswordSerializer, @@ -60,7 +60,7 @@ USER_SCHEMA = { ), "upload_avatar": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("handle avatar upload for a user"), request={ @@ -78,7 +78,7 @@ USER_SCHEMA = { ), "confirm_password_reset": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("confirm a user's password reset"), request=ConfirmPasswordResetSerializer, @@ -90,7 +90,7 @@ USER_SCHEMA = { ), "activate": extend_schema( tags=[ - "vibesAuth", + "Auth", ], summary=_("activate a user's account"), request=ActivateEmailSerializer, diff --git a/engine/vibes_auth/emailing/views.py b/engine/vibes_auth/emailing/views.py index ba83c082..f3055e98 100644 --- a/engine/vibes_auth/emailing/views.py +++ b/engine/vibes_auth/emailing/views.py @@ -1,15 +1,17 @@ from uuid import UUID 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.permissions import AllowAny from rest_framework.response import Response 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 +@extend_schema_view(**UNSUBSCRIBE_SCHEMA) class UnsubscribeView(APIView): """ Public endpoint for one-click unsubscribe from email campaigns. @@ -20,44 +22,10 @@ class UnsubscribeView(APIView): permission_classes = [AllowAny] 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): """Handle GET request for unsubscribe (email link click).""" 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): """Handle POST request for one-click unsubscribe (RFC 8058).""" return self._process_unsubscribe(request) @@ -103,6 +71,7 @@ class UnsubscribeView(APIView): ) +@extend_schema_view(**TRACKING_SCHEMA) class TrackingView(APIView): """ Endpoint for tracking email opens and clicks. @@ -113,22 +82,6 @@ class TrackingView(APIView): permission_classes = [AllowAny] 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): """Track email open via tracking pixel.""" from django.utils import timezone diff --git a/engine/vibes_auth/serializers.py b/engine/vibes_auth/serializers.py index e8b4d3e2..294578b6 100644 --- a/engine/vibes_auth/serializers.py +++ b/engine/vibes_auth/serializers.py @@ -180,7 +180,7 @@ class TokenObtainSerializer(Serializer): @classmethod def get_token(cls, user: AuthUser) -> Token: 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: raise RuntimeError(_("must set token_class attribute on class.")) diff --git a/monitoring/generate_prometheus_password.py b/monitoring/generate_prometheus_password.py deleted file mode 100644 index 57824c3f..00000000 --- a/monitoring/generate_prometheus_password.py +++ /dev/null @@ -1,9 +0,0 @@ -import getpass - -import bcrypt - -print( - bcrypt.hashpw( - getpass.getpass("Password: ").encode("utf-8"), bcrypt.gensalt() - ).decode() -) diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml index d584994e..3bc38742 100644 --- a/monitoring/prometheus.yml +++ b/monitoring/prometheus.yml @@ -3,16 +3,6 @@ global: evaluation_interval: 15s 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' static_configs: - targets: [ 'database_exporter:9187' ] diff --git a/monitoring/web.yml b/monitoring/web.yml deleted file mode 100644 index 77702174..00000000 --- a/monitoring/web.yml +++ /dev/null @@ -1,2 +0,0 @@ -basic_auth_users: - schon: $2b$12$0HraDYmrZnJ089LcH9Vsn.Wv5V5a8oDlucTNm0.5obhULjPyLiYoy diff --git a/nginx.example.conf b/nginx.example.conf index 68b9b04e..2cbc9c91 100644 --- a/nginx.example.conf +++ b/nginx.example.conf @@ -12,10 +12,10 @@ upstream storefront_frontend { server { 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_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/schon.wiseless.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/schon.wiseless.xyz/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_session_cache shared:SSL:10m; @@ -72,10 +72,10 @@ server { server { 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_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/schon.wiseless.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/schon.wiseless.xyz/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_session_cache shared:SSL:10m; @@ -128,10 +128,10 @@ server { server { 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_key /etc/letsencrypt/live/schon.fureunoir.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/schon.wiseless.xyz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/schon.wiseless.xyz/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; ssl_session_cache shared:SSL:10m; @@ -173,6 +173,6 @@ server { server { 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; } diff --git a/pyproject.toml b/pyproject.toml index 430d5aa8..c6894bc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,70 +6,71 @@ authors = [{ name = "fureunoir", email = "contact@fureunoir.com" }] readme = "README.md" requires-python = ">=3.12,<=3.13" dependencies = [ - "aiogram==3.24.0", + "aiogram==3.25.0", "aiosmtpd==1.4.6", "channels==4.3.2", "channels-redis==4.3.0", "colorlog==6.10.1", - "coverage==7.13.2", + "coverage==7.13.4", "click==8.3.1", - "cryptography==46.0.3", - "django==5.2.9", + "cryptography==46.0.5", + "django==5.2.11", "django-cacheops==7.2", "django-constance==4.3.4", "django-cors-headers==4.9.0", - "django-dbbackup==5.1.2", + "django-dbbackup==5.2.0", "django-elasticsearch-dsl==8.2", "django-extensions==4.1", "django-filter==25.2", - "django-health-check==3.20.8", + "django-health-check==4.0.6", "django-import-export[all]==4.4.0", "django-json-widget==2.1.1", "django-model-utils==5.0.0", "django-md-field==0.1.0", "django-modeltranslation==0.19.19", "django-mptt==0.18.0", - "django-prometheus==2.4.1", "django-redis==6.0.0", "django-ratelimit==4.1.0", "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", "djangorestframework==3.16.1", "djangorestframework-recursive==0.1.2", "djangorestframework-simplejwt[crypto]==5.5.1", "djangorestframework-xml==2.0.0", "djangorestframework-yaml==2.0.0", - "djangoql==0.18.1", + "djangoql==0.19.1", "docutils==0.22.4", - "drf-spectacular[sidecar]==0.29.0", + "drf-spectacular==0.29.0", "drf-spectacular-websocket==1.3.1", "drf-orjson-renderer==1.8.0", "elasticsearch-dsl==8.18.0", - "filelock==3.20.3", + "filelock==3.24.3", "filetype==1.2.0", "graphene-django==3.2.3", "graphene-file-upload==1.3.0", "httpx==0.28.1", + "opentelemetry-instrumentation-django==0.60b1", "paramiko==4.0.0", - "pillow==12.1.0", - "pip==25.3", + "pillow==12.1.1", + "pip==26.0.1", "polib==1.2.0", - "PyJWT==2.10.1", + "PyJWT==2.11.0", "pytest==9.0.2", - "pytest-django==4.11.1", + "pytest-django==4.12.0", "python-slugify==8.0.4", - "psutil==7.2.1", - "psycopg[binary]==3.2.9", - "redis==7.1.0", + "psutil==7.2.2", + "psycopg[binary]==3.3.3", + "redis==7.2.1", "requests==2.32.5", - "sentry-sdk[django,celery,opentelemetry]==2.50.0", + "sentry-sdk[django,celery,opentelemetry]==2.53.0", "six==1.17.0", "swapper==1.4.0", - "uvicorn==0.40.0", + "uvicorn==0.41.0", "zeep==4.3.2", "websockets==16.0", - "whitenoise==6.11.0", + "whitenoise==6.12.0", ] [project.optional-dependencies] @@ -79,20 +80,20 @@ worker = [ "django-celery-results==2.6.0", ] linting = [ - "ty==0.0.13", - "ruff==0.14.14", + "ty==0.0.16", + "ruff==0.15.4", "celery-types==0.24.0", "django-stubs==5.2.9", - "djangorestframework-stubs==3.16.7", + "djangorestframework-stubs==3.16.8", "types-requests==2.32.4.20260107", "types-redis==4.6.0.20241004", "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-docutils==0.22.3.20251115", + "types-docutils==0.22.3.20260223", "types-six==1.17.0.20251009", ] -openai = ["openai==2.15.0"] +openai = ["openai==2.24.0"] jupyter = ["jupyter==1.1.1"] [tool.uv] diff --git a/schon/settings/base.py b/schon/settings/base.py index 9bd561bf..56fb8fcb 100644 --- a/schon/settings/base.py +++ b/schon/settings/base.py @@ -108,7 +108,6 @@ UNSAFE_CACHE_KEYS: list[str] = [] SITE_ID: int = 1 INSTALLED_APPS: list[str] = [ - "django_prometheus", "unfold", "unfold.contrib.filters", "unfold.contrib.forms", @@ -128,15 +127,6 @@ INSTALLED_APPS: list[str] = [ "django.contrib.gis", "django.contrib.humanize", "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", "django_celery_beat", "django_celery_results", @@ -168,11 +158,11 @@ INSTALLED_APPS: list[str] = [ if DEBUG: wn_app_index = INSTALLED_APPS.index("django.contrib.staticfiles") - 1 INSTALLED_APPS.insert(wn_app_index, "whitenoise.runserver_nostatic") + INSTALLED_APPS.append("debug_toolbar") MIDDLEWARE: list[str] = [ "schon.middleware.BlockInvalidHostMiddleware", "schon.middleware.RateLimitMiddleware", - "django_prometheus.middleware.PrometheusBeforeMiddleware", "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -185,9 +175,14 @@ MIDDLEWARE: list[str] = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "schon.middleware.CustomLocaleMiddleware", "schon.middleware.CamelCaseMiddleWare", - "django_prometheus.middleware.PrometheusAfterMiddleware", ] +if DEBUG: + MIDDLEWARE.insert( + MIDDLEWARE.index("django.contrib.sessions.middleware.SessionMiddleware"), + "debug_toolbar.middleware.DebugToolbarMiddleware", + ) + TEMPLATES: list[ dict[str, str | list[str | Path] | dict[str, str | list[str]] | Path | bool] ] = [ @@ -251,7 +246,7 @@ LANGUAGES: tuple[tuple[str, str], ...] = ( ("zh-hans", "简体中文"), ) -LANGUAGE_CODE: str = "en-gb" +LANGUAGE_CODE: str = getenv("SCHON_LANGUAGE_CODE", "en-gb") LANGUAGES_FLAGS: dict[str, str] = { "ar-ar": "🇸🇦", @@ -402,6 +397,16 @@ INTERNAL_IPS: list[str] = [ "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"): import sentry_sdk from sentry_sdk.integrations.celery import CeleryIntegration diff --git a/schon/settings/caches.py b/schon/settings/caches.py index 024c2e7a..fe6f6334 100644 --- a/schon/settings/caches.py +++ b/schon/settings/caches.py @@ -5,7 +5,7 @@ from schon.settings.base import REDIS_PASSWORD CACHES = { "default": { - "BACKEND": "django_prometheus.cache.backends.redis.RedisCache", + "BACKEND": "django_redis.cache.RedisCache", "LOCATION": getenv( "CELERY_BROKER_URL", f"redis://:{REDIS_PASSWORD}@redis:6379/0" ), diff --git a/schon/settings/database.py b/schon/settings/database.py index 80a8858f..21e7bdbb 100644 --- a/schon/settings/database.py +++ b/schon/settings/database.py @@ -2,7 +2,7 @@ from os import getenv DATABASES = { "default": { - "ENGINE": "django_prometheus.db.backends.postgis", + "ENGINE": "django.contrib.gis.db.backends.postgis", "NAME": getenv("POSTGRES_DB"), "USER": getenv("POSTGRES_USER"), "PASSWORD": getenv("POSTGRES_PASSWORD"), diff --git a/schon/settings/drf.py b/schon/settings/drf.py index 84727a4f..2a52c554 100644 --- a/schon/settings/drf.py +++ b/schon/settings/drf.py @@ -88,7 +88,6 @@ The API supports multiple response formats: ## Health & Monitoring - Health checks: `/health/` -- Prometheus metrics: `/prometheus/metrics/` ## Version Current API version: {version} @@ -118,20 +117,12 @@ SPECTACULAR_SETTINGS = { "DESCRIPTION": SPECTACULAR_DESCRIPTION, "VERSION": SCHON_VERSION, # noqa: F405 "TOS": "https://schon.wiseless.xyz/terms-of-service", - "SWAGGER_UI_DIST": "SIDECAR", "CAMELIZE_NAMES": True, "POSTPROCESSING_HOOKS": [ "schon.utils.renderers.camelize_serializer_fields", "drf_spectacular.hooks.postprocess_schema_enums", ], - "REDOC_DIST": "SIDECAR", "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": [ { "url": f"https://api.{BASE_DOMAIN}/", diff --git a/schon/settings/unfold.py b/schon/settings/unfold.py index 3e6387a9..1f9eac98 100644 --- a/schon/settings/unfold.py +++ b/schon/settings/unfold.py @@ -49,7 +49,7 @@ UNFOLD: dict[str, Any] = { { "icon": "health_metrics", "title": _("Health"), - "link": reverse_lazy("health_check:health_check_home"), + "link": reverse_lazy("health_check"), }, { "title": _("Support"), @@ -116,14 +116,9 @@ UNFOLD: dict[str, Any] = { "link": reverse_lazy("core:sitemap-index"), }, { - "title": "Swagger", - "icon": "integration_instructions", - "link": reverse_lazy("swagger-ui-platform"), - }, - { - "title": "Redoc", - "icon": "integration_instructions", - "link": reverse_lazy("redoc-ui-platform"), + "title": "API Docs", + "icon": "api", + "link": reverse_lazy("rapidoc-platform"), }, { "title": "GraphQL", diff --git a/schon/urls.py b/schon/urls.py index c6ef2a26..5be556fe 100644 --- a/schon/urls.py +++ b/schon/urls.py @@ -3,13 +3,14 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path 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.views import ( CustomGraphQLView, - CustomRedocView, CustomSpectacularAPIView, - CustomSwaggerView, + RapiDocView, favicon_view, index, ) @@ -22,16 +23,28 @@ urlpatterns = [ index, ), path( - r"health/", - include( - "health_check.urls", - ), - ), - path( - r"prometheus/", - include( - "django_prometheus.urls", + "health/", + HealthCheckView.as_view( + 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 + ) + }, + ), + ], ), + name="health_check", ), path( r"i18n/setlang/", @@ -55,19 +68,14 @@ urlpatterns = [ ### DOCUMENTATION URLS ### path( r"docs/", + RapiDocView.as_view(), + name="rapidoc-platform", + ), + path( + r"docs/schema/", CustomSpectacularAPIView.as_view(urlconf="schon.urls"), 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 ### path( r"b2b/", @@ -101,4 +109,7 @@ urlpatterns = [ ] 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) diff --git a/schon/wsgi.py b/schon/wsgi.py index ed51a3e0..80d47000 100644 --- a/schon/wsgi.py +++ b/schon/wsgi.py @@ -10,7 +10,10 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ import os from django.core.wsgi import get_wsgi_application +from opentelemetry.instrumentation.django import DjangoInstrumentor os.environ.setdefault("DJANGO_SETTINGS_MODULE", "schon.settings") +DjangoInstrumentor().instrument() + application = get_wsgi_application() diff --git a/scripts/Unix/generate-environment-file.sh b/scripts/Unix/generate-environment-file.sh index f907e9d7..9a1a740c 100755 --- a/scripts/Unix/generate-environment-file.sh +++ b/scripts/Unix/generate-environment-file.sh @@ -44,17 +44,18 @@ if [ -f .env ]; then fi SCHON_PROJECT_NAME=$(prompt_default SCHON_PROJECT_NAME Schon) -SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.fureunoir.com) -SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.fureunoir.com) +SCHON_STOREFRONT_DOMAIN=$(prompt_default SCHON_STOREFRONT_DOMAIN schon.wiseless.xyz) +SCHON_BASE_DOMAIN=$(prompt_default SCHON_BASE_DOMAIN schon.wiseless.xyz) SENTRY_DSN=$(prompt_default SENTRY_DSN "") DEBUG=$(prompt_default DEBUG 1) 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) JWT_SIGNING_KEY=$(prompt_autogen JWT_SIGNING_KEY 64) -ALLOWED_HOSTS=$(prompt_default ALLOWED_HOSTS "schon.fureunoir.com api.schon.fureunoir.com") -CSRF_TRUSTED_ORIGINS=$(prompt_default CSRF_TRUSTED_ORIGINS "https://schon.fureunoir.com https://api.schon.fureunoir.com https://www.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.wiseless.xyz https://api.schon.wiseless.xyz https://www.schon.wiseless.xyz") CORS_ALLOWED_ORIGINS=$(prompt_default CORS_ALLOWED_ORIGINS "$CSRF_TRUSTED_ORIGINS") 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) 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_USE_TLS=$(prompt_default EMAIL_USE_TLS 0) 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_HOST_PASSWORD=$(prompt_default EMAIL_HOST_PASSWORD SUPERSECRETEMAILHOSTPASSWORD) @@ -95,6 +96,8 @@ SCHON_STOREFRONT_DOMAIN="${SCHON_STOREFRONT_DOMAIN}" SCHON_BASE_DOMAIN="${SCHON_BASE_DOMAIN}" SENTRY_DSN="${SENTRY_DSN}" DEBUG=${DEBUG} +TIME_ZONE="${TIME_ZONE}" +SCHON_LANGUAGE_CODE="${SCHON_LANGUAGE_CODE}" SECRET_KEY="${SECRET_KEY}" JWT_SIGNING_KEY="${JWT_SIGNING_KEY}" diff --git a/scripts/Unix/install.sh b/scripts/Unix/install.sh index 7ef3e258..6d6fe9b7 100755 --- a/scripts/Unix/install.sh +++ b/scripts/Unix/install.sh @@ -81,6 +81,10 @@ case "$install_choice" in fi log_success "Images built successfully" + # Generate Prometheus web config from .env + log_step "Generating Prometheus web config..." + generate_prometheus_web_config + echo log_result "Docker installation complete!" log_info "You can now use: make run" diff --git a/scripts/Unix/restart.sh b/scripts/Unix/restart.sh index 5c5b94dc..5023ceeb 100755 --- a/scripts/Unix/restart.sh +++ b/scripts/Unix/restart.sh @@ -3,6 +3,10 @@ set -euo pipefail source ./scripts/Unix/starter.sh +# Generate Prometheus web config from .env +log_step "Generating Prometheus web config..." +generate_prometheus_web_config + # Shutdown services log_step "Shutting down..." if ! output=$(docker compose down 2>&1); then diff --git a/scripts/Unix/run.sh b/scripts/Unix/run.sh index 26cc9161..0702e4ad 100755 --- a/scripts/Unix/run.sh +++ b/scripts/Unix/run.sh @@ -20,6 +20,10 @@ else log_warning "jq is not installed; skipping image verification step." fi +# Generate Prometheus web config from .env +log_step "Generating Prometheus web config..." +generate_prometheus_web_config + # Start services log_step "Spinning services up..." if ! output=$(docker compose up --no-build --detach --wait 2>&1); then diff --git a/scripts/Windows/generate-environment-file.ps1 b/scripts/Windows/generate-environment-file.ps1 index ed128720..fa158e71 100644 --- a/scripts/Windows/generate-environment-file.ps1 +++ b/scripts/Windows/generate-environment-file.ps1 @@ -50,17 +50,18 @@ if (Test-Path '.env') } $SCHON_PROJECT_NAME = Prompt-Default 'SCHON_PROJECT_NAME' 'Schon' -$SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.fureunoir.com' -$SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.fureunoir.com' +$SCHON_STOREFRONT_DOMAIN = Prompt-Default 'SCHON_STOREFRONT_DOMAIN' 'schon.wiseless.xyz' +$SCHON_BASE_DOMAIN = Prompt-Default 'SCHON_BASE_DOMAIN' 'schon.wiseless.xyz' $SENTRY_DSN = Prompt-Default 'SENTRY_DSN' '' $DEBUG = Prompt-Default 'DEBUG' '1' $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 $JWT_SIGNING_KEY = Prompt-AutoGen 'JWT_SIGNING_KEY' 64 -$ALLOWED_HOSTS = Prompt-Default 'ALLOWED_HOSTS' 'schon.fureunoir.com api.schon.fureunoir.com' -$CSRF_TRUSTED_ORIGINS = Prompt-Default 'CSRF_TRUSTED_ORIGINS' 'https://schon.fureunoir.com https://api.schon.fureunoir.com https://www.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.wiseless.xyz https://api.schon.wiseless.xyz https://www.schon.wiseless.xyz' $CORS_ALLOWED_ORIGINS = Prompt-Default 'CORS_ALLOWED_ORIGINS' $CSRF_TRUSTED_ORIGINS $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 $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_USE_TLS = Prompt-Default 'EMAIL_USE_TLS' '0' $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_HOST_PASSWORD = Prompt-Default 'EMAIL_HOST_PASSWORD' 'SUPERSECRETEMAILHOSTPASSWORD' @@ -102,6 +103,8 @@ $lines = @( "SCHON_BASE_DOMAIN=""$SCHON_BASE_DOMAIN""" "SENTRY_DSN=""$SENTRY_DSN""" "DEBUG=$DEBUG" + "TIME_ZONE=""$TIME_ZONE""" + "SCHON_LANGUAGE_CODE=""$SCHON_LANGUAGE_CODE""" "" "SECRET_KEY=""$SECRET_KEY""" "JWT_SIGNING_KEY=""$JWT_SIGNING_KEY""" diff --git a/scripts/Windows/install.ps1 b/scripts/Windows/install.ps1 index 12a59948..e066c89d 100644 --- a/scripts/Windows/install.ps1 +++ b/scripts/Windows/install.ps1 @@ -42,5 +42,9 @@ if ($LASTEXITCODE -ne 0) { } Write-Success "Images built successfully" +# Generate Prometheus web config from .env +Write-Step "Generating Prometheus web config..." +New-PrometheusWebConfig + Write-Result "" Write-Result "You can now use run.ps1 script or run: make run" diff --git a/scripts/Windows/restart.ps1 b/scripts/Windows/restart.ps1 index b16493f7..fd248985 100644 --- a/scripts/Windows/restart.ps1 +++ b/scripts/Windows/restart.ps1 @@ -10,6 +10,10 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +# Generate Prometheus web config from .env +Write-Step "Generating Prometheus web config..." +New-PrometheusWebConfig + # Shutdown services Write-Step "Shutting down..." $output = docker compose down 2>&1 diff --git a/scripts/Windows/run.ps1 b/scripts/Windows/run.ps1 index 53e08b91..9c75cf3e 100644 --- a/scripts/Windows/run.ps1 +++ b/scripts/Windows/run.ps1 @@ -35,6 +35,10 @@ foreach ($prop in $config.services.PSObject.Properties) Write-Info " Found image: $image" } +# Generate Prometheus web config from .env +Write-Step "Generating Prometheus web config..." +New-PrometheusWebConfig + # Start services Write-Step "Spinning services up..." $output = docker compose up --no-build --detach --wait 2>&1 diff --git a/scripts/lib/utils.ps1 b/scripts/lib/utils.ps1 index bc332c52..e320f22e 100644 --- a/scripts/lib/utils.ps1 +++ b/scripts/lib/utils.ps1 @@ -277,6 +277,43 @@ function Test-SystemRequirements return $true } +# Generate monitoring/web.yml from PROMETHEUS_USER and PROMETHEUS_PASSWORD in .env +function New-PrometheusWebConfig +{ + if (-not (Test-Path '.env')) { return } + + $envContent = Get-Content '.env' + $promUserLine = $envContent | Where-Object { $_ -match '^PROMETHEUS_USER=' } | Select-Object -First 1 + $promPassLine = $envContent | Where-Object { $_ -match '^PROMETHEUS_PASSWORD=' } | Select-Object -First 1 + + if (-not $promUserLine -or -not $promPassLine) { + Write-Warning-Custom "PROMETHEUS_USER or PROMETHEUS_PASSWORD not set in .env, skipping web.yml generation" + return + } + + $promUser = ($promUserLine -replace '^PROMETHEUS_USER=', '').Trim('"') + $promPassword = ($promPassLine -replace '^PROMETHEUS_PASSWORD=', '').Trim('"') + + if ([string]::IsNullOrEmpty($promUser) -or [string]::IsNullOrEmpty($promPassword)) { + Write-Warning-Custom "PROMETHEUS_USER or PROMETHEUS_PASSWORD is empty, skipping web.yml generation" + return + } + + $rawHash = docker run --rm httpd:2-alpine htpasswd -nbBC 12 "" "$promPassword" 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($rawHash)) { + Write-Warning-Custom "Failed to generate Prometheus password hash" + return + } + + # htpasswd outputs ":$2y$..." - strip leading colon and whitespace + $hash = $rawHash.TrimStart(':').Trim() + + $content = "basic_auth_users:`n ${promUser}: ${hash}`n" + [System.IO.File]::WriteAllText((Join-Path $PWD 'monitoring/web.yml'), $content) + + Write-Success "Prometheus web config generated" +} + # Confirm action function Confirm-Action { diff --git a/scripts/lib/utils.sh b/scripts/lib/utils.sh index 2ad2426c..0805f3bf 100644 --- a/scripts/lib/utils.sh +++ b/scripts/lib/utils.sh @@ -201,6 +201,41 @@ check_system_requirements() { return 0 } +# Generate monitoring/web.yml from PROMETHEUS_USER and PROMETHEUS_PASSWORD in .env +generate_prometheus_web_config() { + if [ ! -f .env ]; then + return 0 + fi + + local prom_user prom_password + prom_user=$(grep '^PROMETHEUS_USER=' .env | head -1 | cut -d= -f2- | tr -d '"') + prom_password=$(grep '^PROMETHEUS_PASSWORD=' .env | head -1 | cut -d= -f2- | tr -d '"') + + if [ -z "$prom_user" ] || [ -z "$prom_password" ]; then + log_warning "PROMETHEUS_USER or PROMETHEUS_PASSWORD not set in .env, skipping web.yml generation" + return 0 + fi + + local raw_hash hash + raw_hash=$(docker run --rm httpd:2-alpine htpasswd -nbBC 12 "" "$prom_password" 2>/dev/null) + + if [ -z "$raw_hash" ]; then + log_warning "Failed to generate Prometheus password hash" + return 0 + fi + + # htpasswd outputs ":$2y$..." — strip leading colon and trailing whitespace + hash="${raw_hash#:}" + hash="${hash%%[[:space:]]*}" + + cat > monitoring/web.yml <