Skip to content

Commit

Permalink
move helper functions to helpers.py
Browse files Browse the repository at this point in the history
  • Loading branch information
viggo-devries committed Feb 20, 2025
1 parent 117127f commit dcead8f
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 142 deletions.
130 changes: 2 additions & 128 deletions oscar_odin/mappings/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)


Expand All @@ -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"]
Expand Down Expand Up @@ -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,
)
139 changes: 139 additions & 0 deletions oscar_odin/mappings/helpers.py
Original file line number Diff line number Diff line change
@@ -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,
)
22 changes: 11 additions & 11 deletions tests/mappings/test_catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand All @@ -33,23 +37,23 @@ 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)

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)
self.assertEqual(3, len(actual.children))

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))

Expand All @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/reverse/test_catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/reverse/test_deleting_related.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit dcead8f

Please sign in to comment.