From adfa2f20ddf4ff96c1d9b123367ef487a32801e1 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Mon, 26 Jan 2026 17:44:21 +0300 Subject: [PATCH] feat(feeds): add marketplace-specific feed generators add feed generators for Google Merchant, Amazon Seller, Yandex Market, and Yandex Products. Includes a shared base class (`BaseFeedGenerator`) for common functionality, such as product retrieval and validation. Each generator supports format-specific output and handles platform-specific requirements. This change simplifies the creation of product feeds for multiple marketplaces. --- ...ntent_alter_post_content_ar_ar_and_more.py | 235 +-- engine/core/feeds/__init__.py | 21 + engine/core/feeds/amazon_seller.py | 241 ++++ engine/core/feeds/base.py | 168 +++ engine/core/feeds/google_merchant.py | 162 +++ engine/core/feeds/yandex_market.py | 219 +++ engine/core/feeds/yandex_products.py | 141 ++ engine/core/tasks.py | 57 + ...emplate_user_unsubscribe_token_and_more.py | 465 +++++- ...ailtemplate_html_content_ar_ar_and_more.py | 1259 +++++++++++------ schon/settings/celery.py | 5 + schon/settings/constance.py | 10 + 12 files changed, 2405 insertions(+), 578 deletions(-) create mode 100644 engine/core/feeds/__init__.py create mode 100644 engine/core/feeds/amazon_seller.py create mode 100644 engine/core/feeds/base.py create mode 100644 engine/core/feeds/google_merchant.py create mode 100644 engine/core/feeds/yandex_market.py create mode 100644 engine/core/feeds/yandex_products.py diff --git a/engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py b/engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py index 7c51490d..a6b6278d 100644 --- a/engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py +++ b/engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py @@ -4,155 +4,212 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('blog', '0007_post_is_static_page'), + ("blog", "0007_post_is_static_page"), ] operations = [ migrations.AlterField( - model_name='post', - name='content', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_ar_ar', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_ar_ar", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_cs_cz', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_cs_cz", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_da_dk', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_da_dk", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_de_de', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_de_de", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_en_gb', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_en_gb", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_en_us', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_en_us", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_es_es', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_es_es", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_fa_ir', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_fa_ir", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_fr_fr', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_fr_fr", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_he_il', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_he_il", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_hi_in', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_hi_in", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_hr_hr', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_hr_hr", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_id_id', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_id_id", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_it_it', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_it_it", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_ja_jp', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_ja_jp", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_kk_kz', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_kk_kz", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_ko_kr', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_ko_kr", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_nl_nl', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_nl_nl", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_no_no', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_no_no", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_pl_pl', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_pl_pl", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_pt_br', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_pt_br", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_ro_ro', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_ro_ro", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_ru_ru', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_ru_ru", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_sv_se', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_sv_se", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_th_th', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_th_th", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_tr_tr', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_tr_tr", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_vi_vn', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_vi_vn", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), migrations.AlterField( - model_name='post', - name='content_zh_hans', - field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'), + model_name="post", + name="content_zh_hans", + field=models.TextField( + blank=True, help_text="post content", null=True, verbose_name="content" + ), ), ] diff --git a/engine/core/feeds/__init__.py b/engine/core/feeds/__init__.py new file mode 100644 index 00000000..87955c75 --- /dev/null +++ b/engine/core/feeds/__init__.py @@ -0,0 +1,21 @@ +from engine.core.feeds.amazon_seller import AmazonSellerFeedGenerator +from engine.core.feeds.base import BaseFeedGenerator +from engine.core.feeds.google_merchant import GoogleMerchantFeedGenerator +from engine.core.feeds.yandex_market import YandexMarketFeedGenerator +from engine.core.feeds.yandex_products import YandexProductsFeedGenerator + +FEED_GENERATORS: dict[str, type[BaseFeedGenerator]] = { + "google_merchant": GoogleMerchantFeedGenerator, + "yandex_market": YandexMarketFeedGenerator, + "yandex_products": YandexProductsFeedGenerator, + "amazon_seller": AmazonSellerFeedGenerator, +} + +__all__ = [ + "BaseFeedGenerator", + "GoogleMerchantFeedGenerator", + "YandexMarketFeedGenerator", + "YandexProductsFeedGenerator", + "AmazonSellerFeedGenerator", + "FEED_GENERATORS", +] diff --git a/engine/core/feeds/amazon_seller.py b/engine/core/feeds/amazon_seller.py new file mode 100644 index 00000000..a06903e7 --- /dev/null +++ b/engine/core/feeds/amazon_seller.py @@ -0,0 +1,241 @@ +from datetime import datetime +from typing import Any +from xml.etree.ElementTree import Element, SubElement + +from constance import config +from django.conf import settings +from django.db.models import QuerySet + +from engine.core.feeds.base import BaseFeedGenerator +from engine.core.models import Product + + +class AmazonSellerFeedGenerator(BaseFeedGenerator): + """ + Amazon Seller Central feed generator. + + Generates product feeds in Amazon's XML format for Seller Central. + Reference: https://developer-docs.amazon.com/sp-api/docs/feeds-api-v2021-06-30-reference + """ + + name: str = "amazon_seller" + supported_formats: tuple[str, ...] = ("xml", "json", "yaml") + default_format: str = "xml" + + AMAZON_NS = "http://www.amazon.com/schema/merchant/product/2024-01" + + def generate_feed_data(self, products: QuerySet[Product]) -> dict[str, Any]: + """Generate feed data as a structured dictionary.""" + messages = [] + for idx, product in enumerate(products, start=1): + message = self._build_message(product, idx) + if message: + messages.append(message) + + return { + "header": { + "documentVersion": "1.0", + "merchantIdentifier": config.COMPANY_NAME or settings.PROJECT_NAME, + }, + "messageType": "Product", + "purgeAndReplace": False, + "messages": messages, + "generated_at": datetime.now().isoformat(), + } + + def _build_message( + self, product: Product, message_id: int + ) -> dict[str, Any] | None: + """Build a message dictionary for a product.""" + if not product.price or product.price <= 0: + return None + + images = self.get_product_images(product) + + description_data: dict[str, Any] = { + "title": product.name[:200], + "brand": product.brand.name if product.brand else "", + "description": (product.description or "")[:2000], + "bulletPoint": self._get_bullet_points(product), + "manufacturer": product.brand.name + if product.brand + else config.COMPANY_NAME or "", + "itemType": self._get_item_type(product), + } + + if images: + description_data["mainImage"] = { + "imageType": "Main", + "imageLocation": images[0], + } + if len(images) > 1: + description_data["otherImages"] = [ + {"imageType": f"PT{i}", "imageLocation": img} + for i, img in enumerate(images[1:9], start=1) + ] + + message: dict[str, Any] = { + "messageID": message_id, + "operationType": "Update", + "product": { + "sku": product.sku, + "standardProductID": self._get_standard_product_id(product), + "productTaxCode": "A_GEN_TAX", + "descriptionData": description_data, + "productData": self._get_product_data(product), + }, + } + + return message + + def _get_standard_product_id(self, product: Product) -> dict[str, str]: + """Get standard product identifier (EAN/UPC/GTIN).""" + id_types = [ + ("ean", "EAN"), + ("upc", "UPC"), + ("gtin", "GTIN"), + ("isbn", "ISBN"), + ] + + for attr_value in product.attributes.all(): + attr_name_lower = attr_value.attribute.name.lower() + for attr_key, amazon_type in id_types: + if attr_name_lower == attr_key: + return {"type": amazon_type, "value": attr_value.value} + + if product.partnumber: + return {"type": "PrivateLabel", "value": product.partnumber} + + return {"type": "PrivateLabel", "value": product.sku} + + def _get_bullet_points(self, product: Product) -> list[str]: + """Generate bullet points from product attributes.""" + bullet_points = [] + + for attr_value in product.attributes.all()[:5]: + bullet_points.append(f"{attr_value.attribute.name}: {attr_value.value}") + + return bullet_points + + def _get_item_type(self, product: Product) -> str: + """Get the item type from category.""" + if product.category: + return product.category.name + return "General" + + def _get_product_data(self, product: Product) -> dict[str, Any]: + """Get additional product data.""" + data = { + "price": { + "standardPrice": { + "value": product.price, + "currency": self.get_currency(), + }, + }, + "inventory": { + "quantity": product.quantity, + "fulfillmentLatency": 3, + }, + } + + if product.discount_price: + sale_price = product.price - product.discount_price + if sale_price > 0: + data["price"]["salePrice"] = { + "value": sale_price, + "currency": self.get_currency(), + } + + return data + + def to_xml(self, data: dict[str, Any]) -> str: + """Convert feed data to Amazon XML format.""" + envelope = Element("AmazonEnvelope") + envelope.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + envelope.set("xsi:noNamespaceSchemaLocation", "amzn-envelope.xsd") + + header = SubElement(envelope, "Header") + doc_version = SubElement(header, "DocumentVersion") + doc_version.text = data["header"]["documentVersion"] + merchant_id = SubElement(header, "MerchantIdentifier") + merchant_id.text = data["header"]["merchantIdentifier"] + + message_type = SubElement(envelope, "MessageType") + message_type.text = data["messageType"] + + purge = SubElement(envelope, "PurgeAndReplace") + purge.text = "true" if data["purgeAndReplace"] else "false" + + for msg_data in data["messages"]: + self._add_message_to_xml(envelope, msg_data) + + return '\n' + self.prettify_xml(envelope) + + def _add_message_to_xml(self, envelope: Element, msg_data: dict[str, Any]) -> None: + """Add a message to the XML envelope.""" + message = SubElement(envelope, "Message") + + message_id = SubElement(message, "MessageID") + message_id.text = str(msg_data["messageID"]) + + operation_type = SubElement(message, "OperationType") + operation_type.text = msg_data["operationType"] + + product = SubElement(message, "Product") + product_data = msg_data["product"] + + sku = SubElement(product, "SKU") + sku.text = product_data["sku"] + + std_product_id = SubElement(product, "StandardProductID") + type_elem = SubElement(std_product_id, "Type") + type_elem.text = product_data["standardProductID"]["type"] + value_elem = SubElement(std_product_id, "Value") + value_elem.text = product_data["standardProductID"]["value"] + + tax_code = SubElement(product, "ProductTaxCode") + tax_code.text = product_data["productTaxCode"] + + desc_data = SubElement(product, "DescriptionData") + self._add_description_data_to_xml(desc_data, product_data["descriptionData"]) + + def _add_description_data_to_xml( + self, parent: Element, desc_data: dict[str, Any] + ) -> None: + """Add description data to the XML element.""" + title = SubElement(parent, "Title") + title.text = desc_data["title"] + + if desc_data.get("brand"): + brand = SubElement(parent, "Brand") + brand.text = desc_data["brand"] + + if desc_data.get("description"): + description = SubElement(parent, "Description") + description.text = desc_data["description"] + + for bullet in desc_data.get("bulletPoint", []): + bullet_point = SubElement(parent, "BulletPoint") + bullet_point.text = bullet + + if desc_data.get("manufacturer"): + manufacturer = SubElement(parent, "Manufacturer") + manufacturer.text = desc_data["manufacturer"] + + if desc_data.get("itemType"): + item_type = SubElement(parent, "ItemType") + item_type.text = desc_data["itemType"] + + if desc_data.get("mainImage"): + main_image = SubElement(parent, "MainImage") + img_type = SubElement(main_image, "ImageType") + img_type.text = desc_data["mainImage"]["imageType"] + img_loc = SubElement(main_image, "ImageLocation") + img_loc.text = desc_data["mainImage"]["imageLocation"] + + for other_img in desc_data.get("otherImages", []): + other_image = SubElement(parent, "OtherImage") + img_type = SubElement(other_image, "ImageType") + img_type.text = other_img["imageType"] + img_loc = SubElement(other_image, "ImageLocation") + img_loc.text = other_img["imageLocation"] diff --git a/engine/core/feeds/base.py b/engine/core/feeds/base.py new file mode 100644 index 00000000..65ee96b4 --- /dev/null +++ b/engine/core/feeds/base.py @@ -0,0 +1,168 @@ +import json +import logging +import os +from abc import ABC, abstractmethod +from datetime import datetime +from typing import Any +from xml.dom import minidom +from xml.etree.ElementTree import Element, tostring + +import yaml +from django.conf import settings +from django.db.models import QuerySet + +from engine.core.models import Product + +logger = logging.getLogger(__name__) + + +class BaseFeedGenerator(ABC): + """ + Base class for marketplace feed generators. + + Each marketplace feed generator should inherit from this class and implement + the required methods for generating feed data in the appropriate format. + """ + + name: str = "base" + supported_formats: tuple[str, ...] = ("xml", "json", "yaml") + default_format: str = "xml" + + def __init__(self, locale: str = "en-gb"): + self.locale = locale + self.generated_at = datetime.now() + + def get_products(self) -> QuerySet[Product]: + """Get products that should be exported to marketplaces.""" + return ( + Product.objects.filter( + is_active=True, + export_to_marketplaces=True, + ) + .select_related( + "category", + "brand", + ) + .prefetch_related( + "images", + "stocks", + "attributes__attribute", + "tags", + ) + ) + + def get_product_url(self, product: Product) -> str: + """Generate the frontend URL for a product.""" + return ( + f"https://{settings.STOREFRONT_DOMAIN}/{self.locale}/product/{product.slug}" + ) + + def get_product_image_url(self, product: Product) -> str: + """Get the primary image URL for a product.""" + image = product.images.order_by("priority").first() + if image: + return image.image_url + return "" + + def get_product_images(self, product: Product) -> list[str]: + """Get all image URLs for a product.""" + return [ + img.image_url + for img in product.images.order_by("priority") + if img.image_url + ] + + def get_availability(self, product: Product) -> str: + """Get availability status for a product.""" + return "in stock" if product.quantity > 0 else "out of stock" + + def get_currency(self) -> str: + """Get the currency code.""" + return settings.CURRENCY_CODE + + def get_output_path(self, format_type: str) -> str: + """Get the output file path for the feed.""" + feeds_dir = os.path.join(settings.MEDIA_ROOT, "feeds") + os.makedirs(feeds_dir, exist_ok=True) + + extension = format_type if format_type != "yaml" else "yml" + return os.path.join(feeds_dir, f"{self.name}.{extension}") + + @abstractmethod + def generate_feed_data(self, products: QuerySet[Product]) -> Any: + """ + Generate the feed data structure. + + This method should be implemented by each marketplace-specific generator + to create the appropriate data structure for that marketplace. + """ + raise NotImplementedError + + def to_xml(self, data: Any) -> str: + """Convert feed data to XML format.""" + raise NotImplementedError( + f"{self.__class__.__name__} does not support XML format" + ) + + def to_json(self, data: Any) -> str: + """Convert feed data to JSON format.""" + return json.dumps(data, ensure_ascii=False, indent=2) + + def to_yaml(self, data: Any) -> str: + """Convert feed data to YAML format.""" + return yaml.dump(data, allow_unicode=True, default_flow_style=False) + + def generate(self, format_type: str | None = None) -> str: + """ + Generate the feed and save it to a file. + + Args: + format_type: The output format (xml, json, yaml). Defaults to the generator's default. + + Returns: + The path to the generated feed file. + """ + if format_type is None: + format_type = self.default_format + + if format_type not in self.supported_formats: + raise ValueError( + f"Format '{format_type}' is not supported by {self.__class__.__name__}. " + f"Supported formats: {self.supported_formats}" + ) + + products = self.get_products() + product_count = products.count() + + if product_count == 0: + logger.warning("No products to export for %s feed", self.name) + + logger.info("Generating %s feed with %d products", self.name, product_count) + + feed_data = self.generate_feed_data(products) + + match format_type: + case "xml": + content = self.to_xml(feed_data) + case "json": + content = self.to_json(feed_data) + case "yaml" | "yml": + content = self.to_yaml(feed_data) + case _: + raise ValueError(f"Unknown format: {format_type}") + + output_path = self.get_output_path(format_type) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(content) + + logger.info("Generated %s feed at %s", self.name, output_path) + + return output_path + + @staticmethod + def prettify_xml(elem: Element) -> str: + """Return a pretty-printed XML string for the Element.""" + rough_string = tostring(elem, encoding="unicode") + reparsed = minidom.parseString(rough_string) + return reparsed.toprettyxml(indent=" ", encoding=None) diff --git a/engine/core/feeds/google_merchant.py b/engine/core/feeds/google_merchant.py new file mode 100644 index 00000000..fc5f7136 --- /dev/null +++ b/engine/core/feeds/google_merchant.py @@ -0,0 +1,162 @@ +from datetime import datetime +from typing import Any +from xml.etree.ElementTree import Element, SubElement + +from constance import config +from django.conf import settings +from django.db.models import QuerySet + +from engine.core.feeds.base import BaseFeedGenerator +from engine.core.models import Product + + +class GoogleMerchantFeedGenerator(BaseFeedGenerator): + """ + Google Merchant Center feed generator. + + Generates product feeds in Atom/RSS format compatible with Google Shopping. + Reference: https://support.google.com/merchants/answer/7052112 + """ + + name: str = "google_merchant" + supported_formats: tuple[str, ...] = ("xml", "json") + default_format: str = "xml" + + GOOGLE_NS = "http://base.google.com/ns/1.0" + + def generate_feed_data(self, products: QuerySet[Product]) -> list[dict[str, Any]]: + """Generate feed data as a list of product dictionaries.""" + items = [] + + for product in products: + item = self._build_product_item(product) + if item: + items.append(item) + + return items + + def _build_product_item(self, product: Product) -> dict[str, Any] | None: + """Build a product item dictionary for the feed.""" + if not product.price or product.price <= 0: + return None + + images = self.get_product_images(product) + primary_image = images[0] if images else "" + additional_images = images[1:10] if len(images) > 1 else [] + + item = { + "id": product.sku, + "title": product.name[:150], + "description": (product.description or "")[:5000], + "link": self.get_product_url(product), + "image_link": primary_image, + "availability": self.get_availability(product), + "price": f"{product.price:.2f} {self.get_currency()}", + "brand": product.brand.name if product.brand else "", + "condition": "new", + "product_type": self._get_product_type(product), + } + + if additional_images: + item["additional_image_link"] = additional_images + + if product.partnumber: + item["mpn"] = product.partnumber + + if product.discount_price: + sale_price = product.price - product.discount_price + if sale_price > 0: + item["sale_price"] = f"{sale_price:.2f} {self.get_currency()}" + + gtin = self._get_gtin(product) + if gtin: + item["gtin"] = gtin + else: + item["identifier_exists"] = "no" + + return item + + def _get_product_type(self, product: Product) -> str: + """Build the product type hierarchy from category.""" + if not product.category: + return "" + + ancestors = product.category.get_ancestors(include_self=True) + return " > ".join([cat.name for cat in ancestors]) + + def _get_gtin(self, product: Product) -> str | None: + """Extract GTIN/EAN/UPC from product attributes.""" + gtin_names = ["gtin", "ean", "upc", "isbn", "barcode"] + + for attr_value in product.attributes.all(): + if attr_value.attribute.name.lower() in gtin_names: + return attr_value.value + + return None + + def to_xml(self, data: list[dict[str, Any]]) -> str: + """Convert feed data to Google Merchant XML format.""" + rss = Element("rss") + rss.set("version", "2.0") + rss.set("xmlns:g", self.GOOGLE_NS) + + channel = SubElement(rss, "channel") + + title = SubElement(channel, "title") + title.text = config.COMPANY_NAME or settings.PROJECT_NAME + + link = SubElement(channel, "link") + link.text = f"https://{settings.STOREFRONT_DOMAIN}" + + description = SubElement(channel, "description") + description.text = ( + f"Product feed for {config.COMPANY_NAME or settings.PROJECT_NAME}" + ) + + for product_data in data: + item = SubElement(channel, "item") + self._add_product_to_xml(item, product_data) + + return self.prettify_xml(rss) + + def _add_product_to_xml(self, item: Element, product_data: dict[str, Any]) -> None: + """Add a product's data to an XML item element.""" + simple_fields = [ + ("id", "g:id"), + ("title", "g:title"), + ("description", "g:description"), + ("link", "g:link"), + ("image_link", "g:image_link"), + ("availability", "g:availability"), + ("price", "g:price"), + ("brand", "g:brand"), + ("condition", "g:condition"), + ("product_type", "g:product_type"), + ("mpn", "g:mpn"), + ("gtin", "g:gtin"), + ("sale_price", "g:sale_price"), + ("identifier_exists", "g:identifier_exists"), + ] + + for data_key, xml_tag in simple_fields: + if data_key in product_data and product_data[data_key]: + elem = SubElement(item, xml_tag) + elem.text = str(product_data[data_key]) + + additional_images = product_data.get("additional_image_link", []) + for img_url in additional_images: + elem = SubElement(item, "g:additional_image_link") + elem.text = img_url + + def to_json(self, data: list[dict[str, Any]]) -> str: + """Convert feed data to JSON format.""" + feed = { + "channel": { + "title": config.COMPANY_NAME or settings.PROJECT_NAME, + "link": f"https://{settings.STOREFRONT_DOMAIN}", + "description": f"Product feed for {config.COMPANY_NAME or settings.PROJECT_NAME}", + "generated_at": datetime.now().isoformat(), + }, + "items": data, + } + return super().to_json(feed) diff --git a/engine/core/feeds/yandex_market.py b/engine/core/feeds/yandex_market.py new file mode 100644 index 00000000..8ce91e42 --- /dev/null +++ b/engine/core/feeds/yandex_market.py @@ -0,0 +1,219 @@ +from datetime import datetime +from typing import Any +from xml.etree.ElementTree import Element, SubElement + +from constance import config +from django.conf import settings +from django.db.models import QuerySet + +from engine.core.feeds.base import BaseFeedGenerator +from engine.core.models import Product + + +class YandexMarketFeedGenerator(BaseFeedGenerator): + """ + Yandex Market feed generator (YML format). + + Generates product feeds in Yandex Market Language (YML) format. + Reference: https://yandex.ru/support/partnermarket/export/yml.html + """ + + name: str = "yandex_market" + supported_formats: tuple[str, ...] = ("xml", "json", "yaml") + default_format: str = "xml" + + def generate_feed_data(self, products: QuerySet[Product]) -> dict[str, Any]: + """Generate feed data as a structured dictionary.""" + categories = self._get_categories_from_products(products) + currencies = self._get_currencies() + + offers = [] + for product in products: + offer = self._build_offer(product) + if offer: + offers.append(offer) + + return { + "shop": { + "name": config.COMPANY_NAME or settings.PROJECT_NAME, + "company": config.COMPANY_NAME or settings.PROJECT_NAME, + "url": f"https://{settings.STOREFRONT_DOMAIN}", + "currencies": currencies, + "categories": categories, + "offers": offers, + }, + "date": datetime.now().strftime("%Y-%m-%d %H:%M"), + } + + def _get_categories_from_products( + self, products: QuerySet[Product] + ) -> list[dict[str, Any]]: + """Extract unique categories from products with their hierarchy.""" + category_ids = set() + categories_data = [] + + for product in products: + if product.category_id: + ancestors = product.category.get_ancestors(include_self=True) + for cat in ancestors: + if cat.id not in category_ids: + category_ids.add(cat.id) + cat_data = { + "id": cat.id, + "name": cat.name, + } + if cat.parent_id: + cat_data["parentId"] = cat.parent_id + categories_data.append(cat_data) + + return categories_data + + def _get_currencies(self) -> list[dict[str, str]]: + """Get supported currencies.""" + return [ + {"id": self.get_currency(), "rate": "1"}, + ] + + def _build_offer(self, product: Product) -> dict[str, Any] | None: + """Build an offer dictionary for a product.""" + if not product.price or product.price <= 0: + return None + + images = self.get_product_images(product) + + offer = { + "id": product.sku, + "available": product.quantity > 0, + "url": self.get_product_url(product), + "price": product.price, + "currencyId": self.get_currency(), + "categoryId": product.category_id, + "name": product.name[:120], + } + + if images: + offer["picture"] = images[:10] + + if product.brand: + offer["vendor"] = product.brand.name + + if product.partnumber: + offer["vendorCode"] = product.partnumber + + if product.description: + offer["description"] = product.description[:3000] + + if product.discount_price: + offer["oldprice"] = product.price + offer["price"] = product.price - product.discount_price + + barcode = self._get_barcode(product) + if barcode: + offer["barcode"] = barcode + + params = self._get_params(product) + if params: + offer["param"] = params + + return offer + + def _get_barcode(self, product: Product) -> str | None: + """Extract barcode/EAN from product attributes.""" + barcode_names = ["barcode", "ean", "gtin", "upc"] + + for attr_value in product.attributes.all(): + if attr_value.attribute.name.lower() in barcode_names: + return attr_value.value + + return None + + def _get_params(self, product: Product) -> list[dict[str, str]]: + """Extract product parameters from attributes.""" + params = [] + skip_names = ["barcode", "ean", "gtin", "upc", "isbn"] + + for attr_value in product.attributes.all(): + attr_name = attr_value.attribute.name + if attr_name.lower() not in skip_names: + params.append( + { + "name": attr_name, + "value": attr_value.value, + } + ) + + return params + + def to_xml(self, data: dict[str, Any]) -> str: + """Convert feed data to YML XML format.""" + yml_catalog = Element("yml_catalog") + yml_catalog.set("date", data["date"]) + + shop = SubElement(yml_catalog, "shop") + + name = SubElement(shop, "name") + name.text = data["shop"]["name"] + + company = SubElement(shop, "company") + company.text = data["shop"]["company"] + + url = SubElement(shop, "url") + url.text = data["shop"]["url"] + + currencies = SubElement(shop, "currencies") + for curr in data["shop"]["currencies"]: + currency = SubElement(currencies, "currency") + currency.set("id", curr["id"]) + currency.set("rate", curr["rate"]) + + categories = SubElement(shop, "categories") + for cat in data["shop"]["categories"]: + category = SubElement(categories, "category") + category.set("id", str(cat["id"])) + if "parentId" in cat: + category.set("parentId", str(cat["parentId"])) + category.text = cat["name"] + + offers = SubElement(shop, "offers") + for offer_data in data["shop"]["offers"]: + self._add_offer_to_xml(offers, offer_data) + + return ( + '\n\n' + + self.prettify_xml(yml_catalog) + ) + + def _add_offer_to_xml(self, offers: Element, offer_data: dict[str, Any]) -> None: + """Add an offer to the XML offers element.""" + offer = SubElement(offers, "offer") + offer.set("id", str(offer_data["id"])) + offer.set("available", "true" if offer_data["available"] else "false") + + simple_fields = [ + "url", + "price", + "oldprice", + "currencyId", + "categoryId", + "name", + "vendor", + "vendorCode", + "description", + "barcode", + ] + + for field in simple_fields: + if field in offer_data and offer_data[field] is not None: + elem = SubElement(offer, field) + elem.text = str(offer_data[field]) + + pictures = offer_data.get("picture", []) + for pic_url in pictures: + picture = SubElement(offer, "picture") + picture.text = pic_url + + params = offer_data.get("param", []) + for param_data in params: + param = SubElement(offer, "param") + param.set("name", param_data["name"]) + param.text = param_data["value"] diff --git a/engine/core/feeds/yandex_products.py b/engine/core/feeds/yandex_products.py new file mode 100644 index 00000000..343e3df1 --- /dev/null +++ b/engine/core/feeds/yandex_products.py @@ -0,0 +1,141 @@ +from datetime import datetime +from typing import Any +from xml.etree.ElementTree import Element, SubElement + +from constance import config +from django.conf import settings +from django.db.models import QuerySet + +from engine.core.feeds.base import BaseFeedGenerator +from engine.core.models import Product + + +class YandexProductsFeedGenerator(BaseFeedGenerator): + """ + Yandex Products (Yandex Webmaster) feed generator. + + Generates product feeds for Yandex Webmaster product snippets. + Reference: https://yandex.ru/support/webmaster/goods-prices/technical-requirements.html + """ + + name: str = "yandex_products" + supported_formats: tuple[str, ...] = ("xml", "json", "yaml") + default_format: str = "xml" + + def generate_feed_data(self, products: QuerySet[Product]) -> dict[str, Any]: + """Generate feed data as a structured dictionary.""" + offers = [] + for product in products: + offer = self._build_offer(product) + if offer: + offers.append(offer) + + return { + "shop": { + "name": config.COMPANY_NAME or settings.PROJECT_NAME, + "company": config.COMPANY_NAME or settings.PROJECT_NAME, + "url": f"https://{settings.STOREFRONT_DOMAIN}", + "email": config.EMAIL_HOST_USER or "", + "offers": offers, + }, + "date": datetime.now().strftime("%Y-%m-%d %H:%M"), + } + + def _build_offer(self, product: Product) -> dict[str, Any] | None: + """Build an offer dictionary for a product.""" + if not product.price or product.price <= 0: + return None + + images = self.get_product_images(product) + + offer = { + "url": self.get_product_url(product), + "price": product.price, + "currencyId": self.get_currency(), + "name": product.name[:120], + "available": product.quantity > 0, + } + + if images: + offer["picture"] = images[0] + + if product.brand: + offer["vendor"] = product.brand.name + + if product.category: + ancestors = product.category.get_ancestors(include_self=True) + offer["category"] = " / ".join([cat.name for cat in ancestors]) + + if product.description: + offer["description"] = product.description[:500] + + if product.discount_price: + offer["oldprice"] = product.price + offer["price"] = product.price - product.discount_price + + barcode = self._get_barcode(product) + if barcode: + offer["barcode"] = barcode + + return offer + + def _get_barcode(self, product: Product) -> str | None: + """Extract barcode/EAN from product attributes.""" + barcode_names = ["barcode", "ean", "gtin", "upc"] + + for attr_value in product.attributes.all(): + if attr_value.attribute.name.lower() in barcode_names: + return attr_value.value + + return None + + def to_xml(self, data: dict[str, Any]) -> str: + """Convert feed data to Yandex Products XML format.""" + yml_catalog = Element("yml_catalog") + yml_catalog.set("date", data["date"]) + + shop = SubElement(yml_catalog, "shop") + + name = SubElement(shop, "name") + name.text = data["shop"]["name"] + + company = SubElement(shop, "company") + company.text = data["shop"]["company"] + + url = SubElement(shop, "url") + url.text = data["shop"]["url"] + + if data["shop"].get("email"): + email = SubElement(shop, "email") + email.text = data["shop"]["email"] + + offers = SubElement(shop, "offers") + for offer_data in data["shop"]["offers"]: + self._add_offer_to_xml(offers, offer_data) + + return '\n' + self.prettify_xml( + yml_catalog + ) + + def _add_offer_to_xml(self, offers: Element, offer_data: dict[str, Any]) -> None: + """Add an offer to the XML offers element.""" + offer = SubElement(offers, "offer") + offer.set("available", "true" if offer_data["available"] else "false") + + simple_fields = [ + "url", + "price", + "oldprice", + "currencyId", + "name", + "vendor", + "category", + "picture", + "description", + "barcode", + ] + + for field in simple_fields: + if field in offer_data and offer_data[field] is not None: + elem = SubElement(offer, field) + elem.text = str(offer_data[field]) diff --git a/engine/core/tasks.py b/engine/core/tasks.py index f45eae79..7851b74a 100644 --- a/engine/core/tasks.py +++ b/engine/core/tasks.py @@ -14,6 +14,7 @@ from django.conf import settings from django.core.cache import cache from django.core.management import call_command +from engine.core.feeds import FEED_GENERATORS from engine.core.models import Product, Promotion from engine.core.utils.caching import set_default_cache from engine.core.utils.vendors import get_vendors_integrations @@ -238,3 +239,59 @@ def process_promotions() -> tuple[bool, str]: promotion.products.add(product) return True, "Promotions updated successfully." + + +@shared_task(queue="default") +def generate_marketplace_feeds_task() -> tuple[bool, str]: + """ + Generate product feeds for configured marketplaces. + + This task reads the EXPORT_TO_MARKETPLACES constance setting and generates + feeds for each specified marketplace. Supported marketplaces: + - google_merchant: Google Merchant Center (XML/JSON) + - yandex_market: Yandex Market YML format + - yandex_products: Yandex Products/Webmaster format + - amazon_seller: Amazon Seller Central format + + The feeds are saved to /media/feeds/. + """ + export_config = config.EXPORT_TO_MARKETPLACES + if not export_config or not export_config.strip(): + logger.info("EXPORT_TO_MARKETPLACES is empty, skipping feed generation") + return True, "No marketplaces configured for export" + + marketplaces = [m.strip().lower() for m in export_config.split(",") if m.strip()] + + if not marketplaces: + logger.info("No valid marketplaces found in EXPORT_TO_MARKETPLACES") + return True, "No valid marketplaces configured" + + generated_feeds: list[str] = [] + errors: list[str] = [] + + for marketplace in marketplaces: + if marketplace not in FEED_GENERATORS: + error_msg = f"Unknown marketplace: {marketplace}" + logger.warning(error_msg) + errors.append(error_msg) + continue + + try: + generator_class = FEED_GENERATORS[marketplace] + generator = generator_class(locale=settings.LANGUAGE_CODE) + output_path = generator.generate() + generated_feeds.append(f"{marketplace}: {output_path}") + logger.info("Successfully generated feed for %s", marketplace) + except Exception as e: + error_msg = f"Failed to generate {marketplace} feed: {e!s}" + logger.error(error_msg, exc_info=True) + errors.append(error_msg) + + if errors and not generated_feeds: + return False, f"All feeds failed: {'; '.join(errors)}" + + result_msg = f"Generated feeds: {', '.join(generated_feeds)}" + if errors: + result_msg += f"; Errors: {'; '.join(errors)}" + + return True, result_msg diff --git a/engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py b/engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py index 32666092..5b0d1ee0 100644 --- a/engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py +++ b/engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py @@ -1,111 +1,438 @@ # Generated by Django 5.2.9 on 2026-01-26 12:33 +import uuid + import django.db.models.deletion import django_extensions.db.fields -import engine.vibes_auth.emailing.models -import uuid from django.conf import settings from django.db import migrations, models +import engine.vibes_auth.emailing.models + class Migration(migrations.Migration): - dependencies = [ - ('vibes_auth', '0006_chatthread_chatmessage_and_more'), + ("vibes_auth", "0006_chatthread_chatmessage_and_more"), ] operations = [ migrations.CreateModel( - name='EmailImage', + name="EmailImage", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')), - ('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')), - ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')), - ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')), - ('name', models.CharField(help_text='descriptive name for the image', max_length=100, verbose_name='name')), - ('image', models.ImageField(help_text='image file to use in email templates', upload_to=engine.vibes_auth.emailing.models.get_email_image_path, verbose_name='image')), - ('alt_text', models.CharField(blank=True, default='', help_text='alternative text for accessibility', max_length=255, verbose_name='alt text')), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="unique id is used to surely identify any database object", + primary_key=True, + serialize=False, + verbose_name="unique id", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="if set to false, this object can't be seen by users without needed permission", + verbose_name="is active", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, + help_text="when the object first appeared on the database", + verbose_name="created", + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, + help_text="when the object was last modified", + verbose_name="modified", + ), + ), + ( + "name", + models.CharField( + help_text="descriptive name for the image", + max_length=100, + verbose_name="name", + ), + ), + ( + "image", + models.ImageField( + help_text="image file to use in email templates", + upload_to=engine.vibes_auth.emailing.models.get_email_image_path, + verbose_name="image", + ), + ), + ( + "alt_text", + models.CharField( + blank=True, + default="", + help_text="alternative text for accessibility", + max_length=255, + verbose_name="alt text", + ), + ), ], options={ - 'verbose_name': 'email image', - 'verbose_name_plural': 'email images', - 'ordering': ('-created',), + "verbose_name": "email image", + "verbose_name_plural": "email images", + "ordering": ("-created",), }, ), migrations.CreateModel( - name='EmailTemplate', + name="EmailTemplate", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')), - ('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')), - ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')), - ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')), - ('name', models.CharField(help_text='internal name for the template', max_length=100, verbose_name='name')), - ('slug', models.SlugField(help_text='unique identifier for the template', unique=True, verbose_name='slug')), - ('subject', models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, verbose_name='subject')), - ('html_content', models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', verbose_name='HTML content')), - ('plain_content', models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', verbose_name='plain text content')), - ('available_variables', models.TextField(blank=True, default='user.first_name, user.last_name, user.email, project_name, unsubscribe_url', help_text='documentation of available template variables', verbose_name='available variables')), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="unique id is used to surely identify any database object", + primary_key=True, + serialize=False, + verbose_name="unique id", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="if set to false, this object can't be seen by users without needed permission", + verbose_name="is active", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, + help_text="when the object first appeared on the database", + verbose_name="created", + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, + help_text="when the object was last modified", + verbose_name="modified", + ), + ), + ( + "name", + models.CharField( + help_text="internal name for the template", + max_length=100, + verbose_name="name", + ), + ), + ( + "slug", + models.SlugField( + help_text="unique identifier for the template", + unique=True, + verbose_name="slug", + ), + ), + ( + "subject", + models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + verbose_name="subject", + ), + ), + ( + "html_content", + models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + verbose_name="HTML content", + ), + ), + ( + "plain_content", + models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + verbose_name="plain text content", + ), + ), + ( + "available_variables", + models.TextField( + blank=True, + default="user.first_name, user.last_name, user.email, project_name, unsubscribe_url", + help_text="documentation of available template variables", + verbose_name="available variables", + ), + ), ], options={ - 'verbose_name': 'email template', - 'verbose_name_plural': 'email templates', - 'ordering': ('name',), + "verbose_name": "email template", + "verbose_name_plural": "email templates", + "ordering": ("name",), }, ), migrations.AddField( - model_name='user', - name='unsubscribe_token', - field=models.UUIDField(default=uuid.uuid4, help_text='token for secure one-click unsubscribe from campaigns', verbose_name='unsubscribe token'), + model_name="user", + name="unsubscribe_token", + field=models.UUIDField( + default=uuid.uuid4, + help_text="token for secure one-click unsubscribe from campaigns", + verbose_name="unsubscribe token", + ), ), migrations.CreateModel( - name='EmailCampaign', + name="EmailCampaign", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')), - ('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')), - ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')), - ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')), - ('name', models.CharField(help_text='internal name for the campaign', max_length=200, verbose_name='name')), - ('status', models.CharField(choices=[('draft', 'Draft'), ('scheduled', 'Scheduled'), ('sending', 'Sending'), ('sent', 'Sent'), ('cancelled', 'Cancelled')], default='draft', max_length=16, verbose_name='status')), - ('scheduled_at', models.DateTimeField(blank=True, help_text='when to send the campaign (leave empty for manual send)', null=True, verbose_name='scheduled at')), - ('sent_at', models.DateTimeField(blank=True, help_text='when the campaign was actually sent', null=True, verbose_name='sent at')), - ('total_recipients', models.PositiveIntegerField(default=0, verbose_name='total recipients')), - ('sent_count', models.PositiveIntegerField(default=0, verbose_name='sent count')), - ('failed_count', models.PositiveIntegerField(default=0, verbose_name='failed count')), - ('opened_count', models.PositiveIntegerField(default=0, verbose_name='opened count')), - ('clicked_count', models.PositiveIntegerField(default=0, verbose_name='clicked count')), - ('template', models.ForeignKey(help_text='email template to use for this campaign', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='campaigns', to='vibes_auth.emailtemplate', verbose_name='template')), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="unique id is used to surely identify any database object", + primary_key=True, + serialize=False, + verbose_name="unique id", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="if set to false, this object can't be seen by users without needed permission", + verbose_name="is active", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, + help_text="when the object first appeared on the database", + verbose_name="created", + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, + help_text="when the object was last modified", + verbose_name="modified", + ), + ), + ( + "name", + models.CharField( + help_text="internal name for the campaign", + max_length=200, + verbose_name="name", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("draft", "Draft"), + ("scheduled", "Scheduled"), + ("sending", "Sending"), + ("sent", "Sent"), + ("cancelled", "Cancelled"), + ], + default="draft", + max_length=16, + verbose_name="status", + ), + ), + ( + "scheduled_at", + models.DateTimeField( + blank=True, + help_text="when to send the campaign (leave empty for manual send)", + null=True, + verbose_name="scheduled at", + ), + ), + ( + "sent_at", + models.DateTimeField( + blank=True, + help_text="when the campaign was actually sent", + null=True, + verbose_name="sent at", + ), + ), + ( + "total_recipients", + models.PositiveIntegerField( + default=0, verbose_name="total recipients" + ), + ), + ( + "sent_count", + models.PositiveIntegerField(default=0, verbose_name="sent count"), + ), + ( + "failed_count", + models.PositiveIntegerField(default=0, verbose_name="failed count"), + ), + ( + "opened_count", + models.PositiveIntegerField(default=0, verbose_name="opened count"), + ), + ( + "clicked_count", + models.PositiveIntegerField( + default=0, verbose_name="clicked count" + ), + ), + ( + "template", + models.ForeignKey( + help_text="email template to use for this campaign", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="campaigns", + to="vibes_auth.emailtemplate", + verbose_name="template", + ), + ), ], options={ - 'verbose_name': 'email campaign', - 'verbose_name_plural': 'email campaigns', - 'ordering': ('-created',), + "verbose_name": "email campaign", + "verbose_name_plural": "email campaigns", + "ordering": ("-created",), }, ), migrations.CreateModel( - name='CampaignRecipient', + name="CampaignRecipient", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')), - ('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')), - ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')), - ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')), - ('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('failed', 'Failed'), ('opened', 'Opened'), ('clicked', 'Clicked')], default='pending', max_length=16, verbose_name='status')), - ('sent_at', models.DateTimeField(blank=True, null=True, verbose_name='sent at')), - ('opened_at', models.DateTimeField(blank=True, null=True, verbose_name='opened at')), - ('clicked_at', models.DateTimeField(blank=True, null=True, verbose_name='clicked at')), - ('tracking_id', models.UUIDField(default=uuid.uuid4, help_text='unique ID for tracking opens and clicks', unique=True, verbose_name='tracking ID')), - ('error_message', models.TextField(blank=True, default='', help_text='error details if sending failed', verbose_name='error message')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaign_emails', to=settings.AUTH_USER_MODEL, verbose_name='user')), - ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='vibes_auth.emailcampaign', verbose_name='campaign')), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="unique id is used to surely identify any database object", + primary_key=True, + serialize=False, + verbose_name="unique id", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="if set to false, this object can't be seen by users without needed permission", + verbose_name="is active", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, + help_text="when the object first appeared on the database", + verbose_name="created", + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, + help_text="when the object was last modified", + verbose_name="modified", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("pending", "Pending"), + ("sent", "Sent"), + ("failed", "Failed"), + ("opened", "Opened"), + ("clicked", "Clicked"), + ], + default="pending", + max_length=16, + verbose_name="status", + ), + ), + ( + "sent_at", + models.DateTimeField(blank=True, null=True, verbose_name="sent at"), + ), + ( + "opened_at", + models.DateTimeField( + blank=True, null=True, verbose_name="opened at" + ), + ), + ( + "clicked_at", + models.DateTimeField( + blank=True, null=True, verbose_name="clicked at" + ), + ), + ( + "tracking_id", + models.UUIDField( + default=uuid.uuid4, + help_text="unique ID for tracking opens and clicks", + unique=True, + verbose_name="tracking ID", + ), + ), + ( + "error_message", + models.TextField( + blank=True, + default="", + help_text="error details if sending failed", + verbose_name="error message", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="campaign_emails", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), + ( + "campaign", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="recipients", + to="vibes_auth.emailcampaign", + verbose_name="campaign", + ), + ), ], options={ - 'verbose_name': 'campaign recipient', - 'verbose_name_plural': 'campaign recipients', - 'ordering': ('-created',), - 'indexes': [models.Index(fields=['campaign', 'status'], name='recipient_camp_status_idx'), models.Index(fields=['tracking_id'], name='recipient_tracking_idx')], + "verbose_name": "campaign recipient", + "verbose_name_plural": "campaign recipients", + "ordering": ("-created",), + "indexes": [ + models.Index( + fields=["campaign", "status"], name="recipient_camp_status_idx" + ), + models.Index(fields=["tracking_id"], name="recipient_tracking_idx"), + ], }, ), migrations.AddIndex( - model_name='emailcampaign', - index=models.Index(fields=['status', 'scheduled_at'], name='campaign_status_sched_idx'), + model_name="emailcampaign", + index=models.Index( + fields=["status", "scheduled_at"], name="campaign_status_sched_idx" + ), ), ] diff --git a/engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py b/engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py index f4cce6fd..1330778e 100644 --- a/engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py +++ b/engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py @@ -4,430 +4,849 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('vibes_auth', '0007_emailimage_emailtemplate_user_unsubscribe_token_and_more'), + ("vibes_auth", "0007_emailimage_emailtemplate_user_unsubscribe_token_and_more"), ] operations = [ migrations.AddField( - model_name='emailtemplate', - name='html_content_ar_ar', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_cs_cz', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_da_dk', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_de_de', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_en_gb', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_en_us', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_es_es', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_fa_ir', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_fr_fr', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_he_il', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_hi_in', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_hr_hr', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_id_id', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_it_it', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_ja_jp', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_kk_kz', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_ko_kr', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_nl_nl', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_no_no', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_pl_pl', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_pt_br', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_ro_ro', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_ru_ru', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_sv_se', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_th_th', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_tr_tr', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_vi_vn', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='html_content_zh_hans', - field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_ar_ar', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_cs_cz', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_da_dk', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_de_de', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_en_gb', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_en_us', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_es_es', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_fa_ir', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_fr_fr', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_he_il', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_hi_in', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_hr_hr', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_id_id', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_it_it', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_ja_jp', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_kk_kz', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_ko_kr', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_nl_nl', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_no_no', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_pl_pl', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_pt_br', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_ro_ro', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_ru_ru', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_sv_se', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_th_th', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_tr_tr', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_vi_vn', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='plain_content_zh_hans', - field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_ar_ar', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_cs_cz', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_da_dk', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_de_de', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_en_gb', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_en_us', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_es_es', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_fa_ir', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_fr_fr', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_he_il', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_hi_in', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_hr_hr', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_id_id', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_it_it', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_ja_jp', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_kk_kz', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_ko_kr', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_nl_nl', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_no_no', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_pl_pl', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_pt_br', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_ro_ro', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_ru_ru', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_sv_se', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_th_th', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_tr_tr', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_vi_vn', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), - ), - migrations.AddField( - model_name='emailtemplate', - name='subject_zh_hans', - field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'), + model_name="emailtemplate", + name="html_content_ar_ar", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_cs_cz", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_da_dk", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_de_de", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_en_gb", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_en_us", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_es_es", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_fa_ir", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_fr_fr", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_he_il", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_hi_in", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_hr_hr", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_id_id", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_it_it", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_ja_jp", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_kk_kz", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_ko_kr", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_nl_nl", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_no_no", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_pl_pl", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_pt_br", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_ro_ro", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_ru_ru", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_sv_se", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_th_th", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_tr_tr", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_vi_vn", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="html_content_zh_hans", + field=models.TextField( + help_text="email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}", + null=True, + verbose_name="HTML content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_ar_ar", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_cs_cz", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_da_dk", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_de_de", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_en_gb", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_en_us", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_es_es", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_fa_ir", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_fr_fr", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_he_il", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_hi_in", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_hr_hr", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_id_id", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_it_it", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_ja_jp", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_kk_kz", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_ko_kr", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_nl_nl", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_no_no", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_pl_pl", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_pt_br", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_ro_ro", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_ru_ru", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_sv_se", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_th_th", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_tr_tr", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_vi_vn", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="plain_content_zh_hans", + field=models.TextField( + blank=True, + default="", + help_text="plain text fallback (auto-generated if empty)", + null=True, + verbose_name="plain text content", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_ar_ar", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_cs_cz", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_da_dk", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_de_de", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_en_gb", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_en_us", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_es_es", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_fa_ir", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_fr_fr", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_he_il", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_hi_in", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_hr_hr", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_id_id", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_it_it", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_ja_jp", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_kk_kz", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_ko_kr", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_nl_nl", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_no_no", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_pl_pl", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_pt_br", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_ro_ro", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_ru_ru", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_sv_se", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_th_th", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_tr_tr", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_vi_vn", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="emailtemplate", + name="subject_zh_hans", + field=models.CharField( + help_text="email subject line - supports {{ variables }}", + max_length=255, + null=True, + verbose_name="subject", + ), ), ] diff --git a/schon/settings/celery.py b/schon/settings/celery.py index 05f3f3ad..ec133324 100644 --- a/schon/settings/celery.py +++ b/schon/settings/celery.py @@ -111,4 +111,9 @@ CELERY_BEAT_SCHEDULE = { "schedule": timedelta(minutes=5), "options": {"queue": "default"}, }, + "generate_marketplace_feeds_task": { + "task": "engine.core.tasks.generate_marketplace_feeds_task", + "schedule": timedelta(days=1), + "options": {"queue": "default"}, + }, } diff --git a/schon/settings/constance.py b/schon/settings/constance.py index ad1b5b86..b350d25b 100644 --- a/schon/settings/constance.py +++ b/schon/settings/constance.py @@ -71,6 +71,15 @@ CONSTANCE_CONFIG = OrderedDict( ), ("EMAIL_FROM", (getenv("EMAIL_FROM", "Schon"), _("Mail from option"))), ### Features Options ### + ( + "EXPORT_TO_MARKETPLACES", + ( + "", + _( + "Export products to specified marketplaces. Comma-separated list from " + ), + ), + ), ( "DAYS_TO_STORE_ANON_MSGS", (1, _("How many days we store messages from anonymous users")), @@ -149,6 +158,7 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict( "EMAIL_FROM", ), _("Features Options"): ( + "EXPORT_TO_MARKETPLACES", "DAYS_TO_STORE_ANON_MSGS", "DAYS_TO_STORE_AUTH_MSGS", "DISABLED_COMMERCE",