Skip to content

Commit

Permalink
Added tools to track which fields_to_update after mapping.
Browse files Browse the repository at this point in the history
  • Loading branch information
specialunderwear committed Nov 29, 2024
1 parent ec784e1 commit 388d251
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 5 deletions.
61 changes: 58 additions & 3 deletions oscar_odin/mappings/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

from .prefetching.prefetch import prefetch_product_queryset

from . import constants
from .context import ProductModelMapperContext
from .constants import ALL_CATALOGUE_FIELDS, MODEL_IDENTIFIERS_MAPPING
from ..settings import RESOURCES_TO_DB_CHUNK_SIZE

__all__ = (
Expand Down Expand Up @@ -357,6 +357,61 @@ def product_class(self, value) -> ProductClassModel:

return ProductClassToModel.apply(value)

IMPACTED_FIELDS = {
"images": set(constants.ALL_PRODUCTIMAGE_FIELDS),
"parent": {constants.PRODUCT_PARENT},
"categories": {constants.CATEGORY_CODE},
"stockrecords": set(constants.ALL_STOCKRECORD_FIELDS),
"recommended_products": {constants.PRODUCT_UPC},
"product_class": {constants.PRODUCTCLASS_SLUG},
"price": {
constants.STOCKRECORD_PARTNER,
constants.STOCKRECORD_PARTNER_SKU,
constants.STOCKRECORD_PRICE_CURRENCY,
constants.STOCKRECORD_PRICE,
},
"currency": {
constants.STOCKRECORD_PARTNER,
constants.STOCKRECORD_PARTNER_SKU,
constants.STOCKRECORD_PRICE_CURRENCY,
},
"availability": {
constants.STOCKRECORD_PARTNER,
constants.STOCKRECORD_PARTNER_SKU,
constants.STOCKRECORD_PRICE_CURRENCY,
constants.STOCKRECORD_NUM_IN_STOCK,
},
"partner": {constants.STOCKRECORD_PARTNER, constants.STOCKRECORD_PARTNER_SKU},
"attributes": set(),
"children": set(),
"structure": {constants.PRODUCT_STRUCTURE},
"title": {constants.PRODUCT_TITLE},
"upc": {constants.PRODUCT_UPC},
"slug": {constants.PRODUCT_SLUG},
"meta_title": {constants.PRODUCT_META_TITLE},
"meta_description": {constants.PRODUCT_META_DESCRIPTION},
"is_public": {constants.PRODUCT_IS_PUBLIC},
"description": {constants.PRODUCT_DESCRIPTION},
"is_discountable": {constants.PRODUCT_IS_DISCOUNTABLE},
"priority": {constants.PRODUCT_PRIORITY},
}

@classmethod
def get_fields_impacted_by_mapping(cls, *from_obj_field_names):
model_field_names = set()
# remove the excluded fields from consideration and then determine the
# impacted model field names.
for field_name in from_obj_field_names:
if field_name in cls.exclude_fields:
continue
# if there is no registration in IMPACTED_FIELDS, we
# just take the original fieldname
fields = cls.IMPACTED_FIELDS.get(field_name)
if fields is not None:
model_field_names.update(fields)

return list(model_field_names)


class RecommendedProductToModel(OscarBaseMapping):
from_obj = ProductRecommentationResource
Expand Down Expand Up @@ -461,8 +516,8 @@ def product_queryset_to_resources(

def products_to_db(
products,
fields_to_update=ALL_CATALOGUE_FIELDS,
identifier_mapping=MODEL_IDENTIFIERS_MAPPING,
fields_to_update=constants.ALL_CATALOGUE_FIELDS,
identifier_mapping=constants.MODEL_IDENTIFIERS_MAPPING,
product_mapper=ProductToModel,
delete_related=False,
clean_instances=True,
Expand Down
3 changes: 2 additions & 1 deletion oscar_odin/mappings/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
PRODUCT_META_TITLE = "Product.meta_title"
PRODUCT_META_DESCRIPTION = "Product.meta_description"
PRODUCT_IS_DISCOUNTABLE = "Product.is_discountable"
PRODUCT_PRIORITY = "Product.priority"

PRODUCTCLASS_SLUG = "ProductClass.slug"
PRODUCTCLASS_REQUIRESSHIPPING = "ProductClass.requires_shipping"
Expand Down Expand Up @@ -55,7 +56,7 @@
PRODUCT_META_TITLE,
PRODUCT_META_DESCRIPTION,
PRODUCT_IS_DISCOUNTABLE,
PRODUCT_PARENT,
PRODUCT_PRIORITY,
]

ALL_PRODUCTCLASS_FIELDS = [
Expand Down
30 changes: 29 additions & 1 deletion oscar_odin/resources/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
from decimal import Decimal
from typing import Any, Dict, List, Optional, Union

from django.conf import settings
from oscar.core.loading import get_model, get_class

import odin
from odin.fields import StringField
from odin.exceptions import ValidationError as OdinValidationError, NON_FIELD_ERRORS

from ..fields import DecimalField

Expand Down Expand Up @@ -113,7 +116,7 @@ class ProductResource(OscarCatalogueResource):

# Price information
price: Decimal = DecimalField(null=True)
currency: Optional[str]
currency: Optional[str] = odin.Options(default=settings.OSCAR_DEFAULT_CURRENCY)
availability: Optional[int]
is_available_to_buy: Optional[bool]
partner: Optional[Any]
Expand All @@ -133,3 +136,28 @@ class ProductResource(OscarCatalogueResource):
children: Optional[List["ProductResource"]] = odin.ListOf.delayed(
lambda: ProductResource, null=True
)

def clean(self):
if (
not self.stockrecords
and (self.price is not None or self.availability is not None)
and not (
self.upc is not None
and self.currency is not None
and self.partner is not None
)
):
errors = {
NON_FIELD_ERRORS: [
"upc, currency and partner are required when specifying price or availability"
]
}
# upc is allready required so we don't need to check for it here
if (
self.currency is None
): # currency has a default but it can be set to null by accident
errors["currency"] = ["Currency can not be empty."]
if self.partner is None:
errors["partner"] = ["Partner can not be empty."]

raise OdinValidationError(errors, code="simpleprice")
20 changes: 20 additions & 0 deletions oscar_odin/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from collections import defaultdict
from functools import reduce
from operator import itemgetter, or_
import contextlib
import time
import math
Expand Down Expand Up @@ -126,3 +128,21 @@ def chunked(iterable, size=RESOURCES_TO_DB_CHUNK_SIZE, startindex=0):
if chunklen < size:
break
startindex += size


def get_mapped_fields(mapping, *from_field_names):
keyed_mapping = defaultdict(set)
exclude_fields = getattr(mapping, "exclude_fields", set())
# pylint: disable=protected-access
for mapping_rule in mapping._mapping_rules:
if mapping_rule.from_field:
for field_name in mapping_rule.from_field:
if field_name not in exclude_fields:
keyed_mapping[field_name] |= set(mapping_rule.to_field)
else:
keyed_mapping[None] |= set(mapping_rule.to_field)

if from_field_names:
return reduce(or_, itemgetter(*from_field_names)(keyed_mapping))

return reduce(or_, keyed_mapping.values())
126 changes: 126 additions & 0 deletions tests/mappings/test_catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from oscar_odin.mappings import catalogue

from oscar_odin.utils import get_mapped_fields

Product = get_model("catalogue", "Product")


Expand Down Expand Up @@ -75,3 +77,127 @@ def test_queryset_to_resources_include_children_num_queries(self):
queryset, include_children=True
)
dict_codec.dump(resources, include_type_field=False)

def test_get_mapped_fields(self):
product_to_model_fields = get_mapped_fields(catalogue.ProductToModel)
self.assertListEqual(
sorted(product_to_model_fields),
[
"attributes",
"categories",
"children",
"date_created",
"date_updated",
"description",
"id",
"images",
"is_discountable",
"is_public",
"meta_description",
"meta_title",
"parent",
"priority",
"product_class",
"rating",
"recommended_products",
"slug",
"stockrecords",
"structure",
"title",
"upc",
],
)

model_to_product_fields = get_mapped_fields(catalogue.ProductToResource)
self.assertListEqual(
sorted(model_to_product_fields),
[
"attributes",
"availability",
"categories",
"children",
"currency",
"date_created",
"date_updated",
"description",
"id",
"images",
"is_available_to_buy",
"is_discountable",
"is_public",
"meta_description",
"meta_title",
"parent",
"price",
"priority",
"product_class",
"rating",
"recommended_products",
"slug",
"stockrecords",
"structure",
"title",
"upc",
],
)

fieldz = get_mapped_fields(catalogue.ProductToModel, *model_to_product_fields)
self.assertListEqual(
sorted(fieldz),
[
"attributes",
"categories",
"children",
"date_created",
"date_updated",
"description",
"id",
"images",
"is_discountable",
"is_public",
"meta_description",
"meta_title",
"parent",
"priority",
"product_class",
"rating",
"recommended_products",
"slug",
"stockrecords",
"structure",
"title",
"upc",
],
)

demfields = catalogue.ProductToModel.get_fields_impacted_by_mapping(
*model_to_product_fields
)
self.assertListEqual(
sorted(demfields),
[
"Category.code",
"Product.description",
"Product.is_discountable",
"Product.is_public",
"Product.meta_description",
"Product.meta_title",
"Product.parent",
"Product.priority",
"Product.slug",
"Product.structure",
"Product.title",
"Product.upc",
"ProductClass.slug",
"ProductImage.caption",
"ProductImage.code",
"ProductImage.display_order",
"ProductImage.original",
"StockRecord.num_allocated",
"StockRecord.num_in_stock",
"StockRecord.partner",
"StockRecord.partner_sku",
"StockRecord.price",
"StockRecord.price_currency",
],
)
Loading

0 comments on commit 388d251

Please sign in to comment.