sforkowany z mtyton/comfy
				
			added product loading feature
							rodzic
							
								
									a3c148fd70
								
							
						
					
					
						commit
						1a257341aa
					
				|  | @ -228,3 +228,5 @@ LOGGING = { | |||
|         "level": "WARNING", | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| PRODUCTS_CSV_PATH = os.environ.get("PRODUCTS_CSV_PATH", "products.csv") | ||||
|  |  | |||
|  | @ -1,3 +0,0 @@ | |||
| from django.contrib import admin | ||||
| 
 | ||||
| # Register your models here. | ||||
|  | @ -1,6 +0,0 @@ | |||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class DocgeneratorConfig(AppConfig): | ||||
|     default_auto_field = 'django.db.models.BigAutoField' | ||||
|     name = 'docgenerator' | ||||
|  | @ -1,44 +0,0 @@ | |||
| from abc import ( | ||||
|     ABC, | ||||
|     abstractmethod | ||||
| ) | ||||
| from typing import ( | ||||
|     Dict, | ||||
|     Any | ||||
| ) | ||||
| 
 | ||||
| from django.db.models import Model | ||||
| from docxtpl import DocxTemplate | ||||
| 
 | ||||
| 
 | ||||
| class DocumentGeneratorInterface(ABC): | ||||
|     @abstractmethod | ||||
|     def load_template(self, path: str): | ||||
|         ... | ||||
|      | ||||
|     @abstractmethod | ||||
|     def get_extra_context(self) -> Dict[str, Any]: | ||||
|         ... | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def generate_file(self, context: Dict[str, Any] = None): | ||||
|         ... | ||||
| 
 | ||||
| 
 | ||||
| class BaseDocumentGenerator(DocumentGeneratorInterface): | ||||
|      | ||||
|     def __init__(self, instance: Model) -> None: | ||||
|         super().__init__() | ||||
|         self.instance = instance | ||||
| 
 | ||||
|     def load_template(self, path: str): | ||||
|         return DocxTemplate(path) | ||||
|      | ||||
|     def get_extra_context(self): | ||||
|         return {} | ||||
| 
 | ||||
| 
 | ||||
| class PdfFromDocGenerator(BaseDocumentGenerator): | ||||
|     def generate_file(self, context: Dict[str, Any] = None): | ||||
|         template = self.load_template() | ||||
|         context.update(self.get_extra_context()) | ||||
|  | @ -1,10 +0,0 @@ | |||
| from django.db import models | ||||
| from django.core.files.storage import Storage | ||||
| 
 | ||||
| 
 | ||||
| class DocumentTemplate(models.Model): | ||||
|     name = models.CharField(max_length=255) | ||||
|     file = models.FileField(upload_to="doc_templates/", ) | ||||
| 
 | ||||
|     def __str__(self) -> str: | ||||
|         return self.name | ||||
|  | @ -1,5 +0,0 @@ | |||
| from django.test import TestCase | ||||
| 
 | ||||
| 
 | ||||
| class PdfFromDocGeneratorTestCase(TestCase): | ||||
|     ... | ||||
|  | @ -1,3 +0,0 @@ | |||
| from django.shortcuts import render | ||||
| 
 | ||||
| # Create your views here. | ||||
|  | @ -10,3 +10,4 @@ factory-boy==3.2.1 | |||
| pdfkit==1.0.0 | ||||
| num2words==0.5.12 | ||||
| sentry-sdk==1.28.0 | ||||
| pandas==2.0.3 | ||||
|  |  | |||
|  | @ -0,0 +1,52 @@ | |||
| import logging | ||||
| import pandas as pd | ||||
| 
 | ||||
| from store.models import ( | ||||
|     ProductTemplate, | ||||
|     ProductCategoryParamValue, | ||||
|     Product | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class BaseLoader: | ||||
|     def __init__(self, path): | ||||
|         self.path = path | ||||
| 
 | ||||
|     def load_data(self): | ||||
|         return pd.read_csv(self.path) | ||||
|          | ||||
| 
 | ||||
| class ProductLoader(BaseLoader): | ||||
|      | ||||
|     def _process_row(self, row): | ||||
|         template = ProductTemplate.objects.get(code=row["template"]) | ||||
|         price = float(row["price"]) | ||||
|         name = row["name"] | ||||
|         available = bool(row["available"]) | ||||
|         params = [] | ||||
|         for param in row["params"]: | ||||
|             key, value = param | ||||
|             param = ProductCategoryParamValue.objects.get(param__key=key, value=value) | ||||
|             params.append(param) | ||||
|         product = Product.objects.get_or_create_by_params(template=template, params=params) | ||||
|         product.price = price | ||||
|         product.name = name | ||||
|         product.available = available | ||||
|         product.save() | ||||
|         return product | ||||
| 
 | ||||
|     def process(self): | ||||
|         data = self.load_data() | ||||
|         products = [] | ||||
|         for _, row in data.iterrows(): | ||||
|             try: | ||||
|                 product = self._process_row(row) | ||||
|             except Exception as e: | ||||
|                 # catch any error and log it, GlitchTip will catch it | ||||
|                 logger.exception(str(e)) | ||||
|             else: | ||||
|                 products.append(product) | ||||
|         logger.info(f"Loaded {len(products)} products") | ||||
|  | @ -0,0 +1,13 @@ | |||
| from django.core.management import BaseCommand | ||||
| from django.conf import settings | ||||
| 
 | ||||
| from store.loader import ProductLoader | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class Command(BaseCommand): | ||||
|     help = "Load products from csv file" | ||||
| 
 | ||||
|     def handle(self, *args, **options): | ||||
|         loader = ProductLoader(settings.PRODUCTS_CSV_PATH) | ||||
|         loader.process() | ||||
|  | @ -22,6 +22,7 @@ from django.template import ( | |||
| ) | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db.models.signals import m2m_changed | ||||
| from django.forms import CheckboxSelectMultiple | ||||
| 
 | ||||
| from modelcluster.models import ClusterableModel | ||||
| from modelcluster.fields import ParentalKey | ||||
|  | @ -217,7 +218,7 @@ class Product(ClusterableModel): | |||
|     panels = [ | ||||
|         FieldPanel("template"), | ||||
|         FieldPanel("price"), | ||||
|         FieldPanel("params"), | ||||
|         FieldPanel("params", widget=CheckboxSelectMultiple), | ||||
|         FieldPanel("available"), | ||||
|         FieldPanel("name"), | ||||
|         InlinePanel("product_images", label="Variant Images"), | ||||
|  |  | |||
|  | @ -0,0 +1,97 @@ | |||
| import pandas as pd | ||||
| from django.test import TestCase | ||||
| from unittest.mock import patch | ||||
| 
 | ||||
| from store.tests import factories | ||||
| from store.loader import ProductLoader | ||||
| 
 | ||||
| 
 | ||||
| class TestProductLoader(TestCase): | ||||
|     def setUp(self) -> None: | ||||
|         self.category = factories.ProductCategoryFactory() | ||||
|         self.template = factories.ProductTemplateFactory(category=self.category) | ||||
|         self.category_params = [factories.ProductCategoryParamFactory(category=self.category) for _ in range(3)] | ||||
|         self.category_param_values = [factories.ProductCategoryParamValueFactory(param=param) for param in self.category_params] | ||||
|      | ||||
|     def test_load_products_single_product_success(self): | ||||
|         fake_df = pd.DataFrame({ | ||||
|             "template": [self.template.code], | ||||
|             "price": [10.0], | ||||
|             "name": ["Test product"], | ||||
|             "available": [True], | ||||
|             "params": [[ | ||||
|                 (self.category_params[0].key, self.category_param_values[0].value),  | ||||
|                 (self.category_params[1].key, self.category_param_values[1].value), | ||||
|                 (self.category_params[2].key, self.category_param_values[2].value), | ||||
|             ]] | ||||
|         }) | ||||
|         with patch("store.loader.BaseLoader.load_data", return_value=fake_df): | ||||
|             loader = ProductLoader("fake_path") | ||||
|             loader.process() | ||||
| 
 | ||||
|         self.assertEqual(self.template.products.count(), 1) | ||||
|         product = self.template.products.first() | ||||
|         self.assertEqual(product.price, 10.0) | ||||
|         self.assertEqual(product.name, "Test product") | ||||
|         self.assertEqual(product.available, True) | ||||
| 
 | ||||
|     @patch("store.loader.logger") | ||||
|     def test_load_incorrect_data_types_failure(self, mock_logger): | ||||
|         fake_df = pd.DataFrame({ | ||||
|             "template": [self.template.code], | ||||
|             "price": ["FASDSADQAW"], | ||||
|             "name": ["Test product"], | ||||
|             "available": [True], | ||||
|             "params": [[ | ||||
|                 (self.category_params[0].key, self.category_param_values[0].value),  | ||||
|                 (self.category_params[1].key, self.category_param_values[1].value), | ||||
|                 (self.category_params[2].key, self.category_param_values[2].value), | ||||
|             ]] | ||||
|         }) | ||||
|         with patch("store.loader.BaseLoader.load_data", return_value=fake_df): | ||||
|             loader = ProductLoader("fake_path") | ||||
|             loader.process() | ||||
| 
 | ||||
|         self.assertEqual(self.template.products.count(), 0) | ||||
|         mock_logger.exception.assert_called_with("could not convert string to float: 'FASDSADQAW'") | ||||
| 
 | ||||
|     @patch("store.loader.logger") | ||||
|     def test_load_no_existing_template_code_failure(self, mock_logger): | ||||
|         fake_df = pd.DataFrame({ | ||||
|             "template": ["NOTEEXISTINGTEMPLATE"], | ||||
|             "price": [10.0], | ||||
|             "name": ["Test product"], | ||||
|             "available": [True], | ||||
|             "params": [[ | ||||
|                 (self.category_params[0].key, self.category_param_values[0].value),  | ||||
|                 (self.category_params[1].key, self.category_param_values[1].value), | ||||
|                 (self.category_params[2].key, self.category_param_values[2].value), | ||||
|             ]] | ||||
|         }) | ||||
|         with patch("store.loader.BaseLoader.load_data", return_value=fake_df): | ||||
|             loader = ProductLoader("fake_path") | ||||
|             loader.process() | ||||
|          | ||||
|         self.assertEqual(self.template.products.count(), 0) | ||||
|         mock_logger.exception.assert_called_with("ProductTemplate matching query does not exist.") | ||||
| 
 | ||||
|     @patch("store.loader.logger") | ||||
|     def test_not_existing_params_key_value_pairs_failure(self, mock_logger): | ||||
|         fake_df = pd.DataFrame({ | ||||
|             "template": [self.template.code], | ||||
|             "price": [10.0], | ||||
|             "name": ["Test product"], | ||||
|             "available": [True], | ||||
|             "params": [[ | ||||
|                 (self.category_params[0].key, self.category_param_values[2].value),  | ||||
|                 (self.category_params[1].key, self.category_param_values[0].value), | ||||
|                 (self.category_params[2].key, self.category_param_values[1].value), | ||||
|             ]] | ||||
|         }) | ||||
|         with patch("store.loader.BaseLoader.load_data", return_value=fake_df): | ||||
|             loader = ProductLoader("fake_path") | ||||
|             loader.process() | ||||
|          | ||||
|         self.assertEqual(self.template.products.count(), 0) | ||||
|         mock_logger.exception.assert_called_with("ProductCategoryParamValue matching query does not exist.") | ||||
|      | ||||
		Ładowanie…
	
		Reference in New Issue
	
	 mtyton
						mtyton