Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added tools to track which fields_to_update after mapping. #55

Merged
merged 1 commit into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have priority field for product model?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup


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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to remove parent field from this list?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in this list twice

]

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),
[
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe define this list in a variable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this is a test so ...

"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