diff --git a/oscar_odin/mappings/catalogue.py b/oscar_odin/mappings/catalogue.py index b366572..ca879a7 100644 --- a/oscar_odin/mappings/catalogue.py +++ b/oscar_odin/mappings/catalogue.py @@ -3,31 +3,22 @@ import odin from decimal import Decimal -from typing import Any, Dict, Iterable, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple -from django.contrib.auth.models import AbstractUser -from django.db.models import QuerySet from django.db.models.fields.files import ImageFieldFile -from django.http import HttpRequest from odin.mapping import ImmediateResult from oscar.apps.partner.strategy import Default as DefaultStrategy from oscar.core.loading import get_class, get_classes, get_model from datetime import datetime -from .prefetching.prefetch import prefetch_product_queryset - from . import constants -from .context import ProductModelMapperContext -from ..settings import RESOURCES_TO_DB_CHUNK_SIZE __all__ = ( "ProductImageToResource", "CategoryToResource", "ProductClassToResource", "ProductToResource", - "product_to_resource", - "product_queryset_to_resources", ) @@ -36,11 +27,8 @@ ProductClassModel = get_model("catalogue", "ProductClass") ProductModel = get_model("catalogue", "Product") StockRecordModel = get_model("partner", "StockRecord") -ProductAttributeValueModel = get_model("catalogue", "ProductAttributeValue") - -resources_to_db = get_class("oscar_odin.mappings.resources", "resources_to_db") -# mappings +# # mappings ModelMapping = get_class("oscar_odin.mappings.model_mapper", "ModelMapping") map_queryset, OscarBaseMapping = get_classes( "oscar_odin.mappings.common", ["map_queryset", "OscarBaseMapping"] @@ -426,117 +414,3 @@ class ParentToModel(OscarBaseMapping): @odin.assign_field def structure(self): return ProductModel.PARENT - - -def product_to_resource_with_strategy( - product: Union[ProductModel, Iterable[ProductModel]], - stock_strategy: DefaultStrategy, - include_children: bool = False, - product_mapper: OscarBaseMapping = ProductToResource, -): - """Map a product model to a resource. - - This method will accept either a single product or an iterable of product - models (eg a QuerySet), and will return the corresponding resource(s). - The request and user are optional, but if provided they are supplied to the - partner strategy selector. - - :param product: A single product model or iterable of product models (eg a QuerySet). - :param stock_strategy: The current HTTP request - :param include_children: Include children of parent products. - """ - return product_mapper.apply( - product, - context={ - "stock_strategy": stock_strategy, - "include_children": include_children, - }, - ) - - -def product_to_resource( - product: Union[ProductModel, Iterable[ProductModel]], - request: Optional[HttpRequest] = None, - user: Optional[AbstractUser] = None, - include_children: bool = False, - product_mapper: OscarBaseMapping = ProductToResource, - **kwargs, -) -> Union[ProductResource, Iterable[ProductResource]]: - """Map a product model to a resource. - - This method will accept either a single product or an iterable of product - models (eg a QuerySet), and will return the corresponding resource(s). - The request and user are optional, but if provided they are supplied to the - partner strategy selector. - - :param product: A single product model or iterable of product models (eg a QuerySet). - :param request: The current HTTP request - :param user: The current user - :param include_children: Include children of parent products. - :param kwargs: Additional keyword arguments to pass to the strategy selector. - """ - - selector_type = get_class("partner.strategy", "Selector") - stock_strategy = selector_type().strategy(request=request, user=user, **kwargs) - return product_to_resource_with_strategy( - product, stock_strategy, include_children, product_mapper=product_mapper - ) - - -def product_queryset_to_resources( - queryset: QuerySet, - request: Optional[HttpRequest] = None, - user: Optional[AbstractUser] = None, - include_children: bool = False, - product_mapper=ProductToResource, - **kwargs, -) -> Iterable[ProductResource]: - """Map a queryset of product models to a list of resources. - - The request and user are optional, but if provided they are supplied to the - partner strategy selector. - - :param queryset: A queryset of product models. - :param request: The current HTTP request - :param user: The current user - :param include_children: Include children of parent products. - :param kwargs: Additional keyword arguments to pass to the strategy selector. - """ - - queryset = prefetch_product_queryset(queryset, include_children) - - return product_to_resource( - queryset, - request, - user, - include_children, - product_mapper, - **kwargs, - ) - - -def products_to_db( - products, - fields_to_update=constants.ALL_CATALOGUE_FIELDS, - identifier_mapping=constants.MODEL_IDENTIFIERS_MAPPING, - product_mapper=ProductToModel, - delete_related=False, - clean_instances=True, - chunk_size=RESOURCES_TO_DB_CHUNK_SIZE, -) -> Tuple[List[ProductModel], Dict]: - """Map mulitple products to a model and store them in the database. - - The method will first bulk update or create the foreign keys like parent products and productclasses - After that all the products will be bulk saved. - At last all related models like images, stockrecords, and related_products can will be saved and set on the product. - """ - return resources_to_db( - products, - fields_to_update, - identifier_mapping, - model_mapper=product_mapper, - context_mapper=ProductModelMapperContext, - delete_related=delete_related, - clean_instances=clean_instances, - chunk_size=chunk_size, - ) diff --git a/oscar_odin/mappings/helpers.py b/oscar_odin/mappings/helpers.py new file mode 100644 index 0000000..f1de78d --- /dev/null +++ b/oscar_odin/mappings/helpers.py @@ -0,0 +1,139 @@ +from typing import Dict, Union, Iterable, Optional, Tuple, List + +from django.http import HttpRequest +from django.db.models import QuerySet +from django.contrib.auth.models import AbstractUser + +from oscar.core.loading import get_model, get_class, get_classes +from oscar.apps.partner.strategy import Default as DefaultStrategy + +from . import constants +from .context import ProductModelMapperContext +from ..settings import RESOURCES_TO_DB_CHUNK_SIZE +from .prefetching.prefetch import prefetch_product_queryset + +ProductModel = get_model("catalogue", "Product") + +ProductResource = get_class("oscar_odin.resources.catalogue", "ProductResource") +resources_to_db = get_class("oscar_odin.mappings.resources", "resources_to_db") + +ProductToResource, ProductToModel = get_classes( + "oscar_odin.mappings.catalogue", ["ProductToResource", "ProductToModel"] +) +map_queryset, OscarBaseMapping = get_classes( + "oscar_odin.mappings.common", ["map_queryset", "OscarBaseMapping"] +) + + +def product_to_resource_with_strategy( + product: Union[ProductModel, Iterable[ProductModel]], + stock_strategy: DefaultStrategy, + include_children: bool = False, + product_mapper: OscarBaseMapping = ProductToResource, +): + """Map a product model to a resource. + + This method will accept either a single product or an iterable of product + models (eg a QuerySet), and will return the corresponding resource(s). + The request and user are optional, but if provided they are supplied to the + partner strategy selector. + + :param product: A single product model or iterable of product models (eg a QuerySet). + :param stock_strategy: The current HTTP request + :param include_children: Include children of parent products. + """ + return product_mapper.apply( + product, + context={ + "stock_strategy": stock_strategy, + "include_children": include_children, + }, + ) + + +def product_to_resource( + product: Union[ProductModel, Iterable[ProductModel]], + request: Optional[HttpRequest] = None, + user: Optional[AbstractUser] = None, + include_children: bool = False, + product_mapper: OscarBaseMapping = ProductToResource, + **kwargs, +) -> Union[ProductResource, Iterable[ProductResource]]: + """Map a product model to a resource. + + This method will accept either a single product or an iterable of product + models (eg a QuerySet), and will return the corresponding resource(s). + The request and user are optional, but if provided they are supplied to the + partner strategy selector. + + :param product: A single product model or iterable of product models (eg a QuerySet). + :param request: The current HTTP request + :param user: The current user + :param include_children: Include children of parent products. + :param kwargs: Additional keyword arguments to pass to the strategy selector. + """ + + selector_type = get_class("partner.strategy", "Selector") + stock_strategy = selector_type().strategy(request=request, user=user, **kwargs) + return product_to_resource_with_strategy( + product, stock_strategy, include_children, product_mapper=product_mapper + ) + + +def product_queryset_to_resources( + queryset: QuerySet, + request: Optional[HttpRequest] = None, + user: Optional[AbstractUser] = None, + include_children: bool = False, + product_mapper=ProductToResource, + **kwargs, +) -> Iterable[ProductResource]: + """Map a queryset of product models to a list of resources. + + The request and user are optional, but if provided they are supplied to the + partner strategy selector. + + :param queryset: A queryset of product models. + :param request: The current HTTP request + :param user: The current user + :param include_children: Include children of parent products. + :param kwargs: Additional keyword arguments to pass to the strategy selector. + """ + + queryset = prefetch_product_queryset(queryset, include_children) + + return product_to_resource( + queryset, + request, + user, + include_children, + product_mapper, + **kwargs, + ) + + +def products_to_db( + products, + fields_to_update=constants.ALL_CATALOGUE_FIELDS, + identifier_mapping=constants.MODEL_IDENTIFIERS_MAPPING, + product_mapper=ProductToModel, + delete_related=False, + clean_instances=True, + chunk_size=RESOURCES_TO_DB_CHUNK_SIZE, +) -> Tuple[List[ProductModel], Dict]: + """Map mulitple products to a model and store them in the database. + + The method will first bulk update or create the foreign keys like parent products and productclasses + After that all the products will be bulk saved. + At last all related models like images, stockrecords, and related_products can will be saved and set on the product. + """ + return resources_to_db( + products, + fields_to_update, + identifier_mapping, + model_mapper=product_mapper, + context_mapper=ProductModelMapperContext, + delete_related=delete_related, + clean_instances=clean_instances, + chunk_size=chunk_size, + ) diff --git a/tests/mappings/test_catalogue.py b/tests/mappings/test_catalogue.py index c2081a7..66625bf 100644 --- a/tests/mappings/test_catalogue.py +++ b/tests/mappings/test_catalogue.py @@ -5,6 +5,10 @@ from oscar.core.loading import get_model from oscar_odin.mappings import catalogue +from oscar_odin.mappings.helpers import ( + product_queryset_to_resources, + product_to_resource, +) from oscar_odin.utils import get_mapped_fields @@ -17,14 +21,14 @@ class TestProduct(TestCase): def test_product_to_resource__basic_model_to_resource(self): product = Product.objects.first() - actual = catalogue.product_to_resource(product) + actual = product_to_resource(product) self.assertEqual(product.title, actual.title) def test_product_to_resource__basic_product_with_out_of_stock_children(self): product = Product.objects.get(id=1) - actual = catalogue.product_to_resource(product) + actual = product_to_resource(product) self.assertEqual(product.title, actual.title) @@ -33,7 +37,7 @@ def test_product_to_resource__where_is_a_parent_product_do_not_include_children( ): product = Product.objects.get(id=8) - actual = catalogue.product_to_resource(product) + actual = product_to_resource(product) self.assertEqual(product.title, actual.title) self.assertIsNone(actual.children) @@ -41,7 +45,7 @@ def test_product_to_resource__where_is_a_parent_product_do_not_include_children( def test_mapping__where_is_a_parent_product_include_children(self): product = Product.objects.get(id=8) - actual = catalogue.product_to_resource(product, include_children=True) + actual = product_to_resource(product, include_children=True) self.assertEqual(product.title, actual.title) self.assertIsNotNone(actual.children) @@ -49,7 +53,7 @@ def test_mapping__where_is_a_parent_product_include_children(self): def test_queryset_to_resources(self): queryset = Product.objects.all() - product_resources = catalogue.product_queryset_to_resources(queryset) + product_resources = product_queryset_to_resources(queryset) self.assertEqual(queryset.count(), len(product_resources)) @@ -62,9 +66,7 @@ def test_queryset_to_resources_num_queries(self): # However, the query shouldn't increase too much, if it does, it means you got a # n+1 query problem and that should be fixed instead by prefetching, annotating etc. with self.assertNumQueries(14): - resources = catalogue.product_queryset_to_resources( - queryset, include_children=False - ) + resources = product_queryset_to_resources(queryset, include_children=False) dict_codec.dump(resources, include_type_field=False) def test_queryset_to_resources_include_children_num_queries(self): @@ -73,9 +75,7 @@ def test_queryset_to_resources_include_children_num_queries(self): # It should only go up by a few queries. with self.assertNumQueries(20): - resources = catalogue.product_queryset_to_resources( - queryset, include_children=True - ) + resources = product_queryset_to_resources(queryset, include_children=True) dict_codec.dump(resources, include_type_field=False) def test_get_mapped_fields(self): diff --git a/tests/reverse/test_catalogue.py b/tests/reverse/test_catalogue.py index 10c5872..f04b1ec 100644 --- a/tests/reverse/test_catalogue.py +++ b/tests/reverse/test_catalogue.py @@ -8,7 +8,7 @@ from oscar.core.loading import get_model -from oscar_odin.mappings.catalogue import products_to_db +from oscar_odin.mappings.helpers import products_to_db from oscar_odin.resources.catalogue import ( ProductResource, ProductImageResource, diff --git a/tests/reverse/test_deleting_related.py b/tests/reverse/test_deleting_related.py index d9c991a..fea0f10 100644 --- a/tests/reverse/test_deleting_related.py +++ b/tests/reverse/test_deleting_related.py @@ -8,7 +8,7 @@ from oscar.core.loading import get_model -from oscar_odin.mappings.catalogue import products_to_db +from oscar_odin.mappings.helpers import products_to_db from oscar_odin.resources.catalogue import ( ProductResource, ProductImageResource, diff --git a/tests/reverse/test_reallifecase.py b/tests/reverse/test_reallifecase.py index cc0d599..da7fe28 100644 --- a/tests/reverse/test_reallifecase.py +++ b/tests/reverse/test_reallifecase.py @@ -17,7 +17,7 @@ from django.utils.text import slugify from oscar_odin.fields import DecimalField -from oscar_odin.mappings.catalogue import products_to_db +from oscar_odin.mappings.helpers import products_to_db from oscar_odin.resources.catalogue import ( ProductResource, ProductImageResource,