Skip to content

Commit

Permalink
Implement productrecommendations (#19)
Browse files Browse the repository at this point in the history
* Implement productrecommendations

* equal ofcourse

* Make flag to not update related objects from the same type

* Change around the logic

* Raise odin exception when trough object creation fails

* Add test for deleting recommendation
  • Loading branch information
viggo-devries authored Feb 26, 2024
1 parent 67446e6 commit c9fcb74
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 44 deletions.
12 changes: 12 additions & 0 deletions oscar_odin/mappings/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,13 @@ def stockrecords(

return []

@odin.map_list_field
def recommended_products(self, values):
if values:
return RecommendedProductToModel.apply(values)

return []

@odin.map_field
def product_class(self, value) -> ProductClassModel:
if not value or self.source.structure == ProductModel.CHILD:
Expand All @@ -297,6 +304,11 @@ def product_class(self, value) -> ProductClassModel:
return ProductClassToModel.apply(value)


class RecommendedProductToModel(OscarBaseMapping):
from_obj = resources.catalogue.ProductRecommentation
to_obj = ProductModel


class ParentToModel(OscarBaseMapping):
from_obj = resources.catalogue.ParentProduct
to_obj = ProductModel
Expand Down
115 changes: 71 additions & 44 deletions oscar_odin/mappings/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class ModelMapperContext(dict):
Model = None
errors = None

update_related_models_same_type = True

def __init__(self, Model, *args, delete_related=False, **kwargs):
super().__init__(*args, **kwargs)
self.foreign_key_items = defaultdict(list)
Expand Down Expand Up @@ -180,19 +182,14 @@ def bulk_update_or_create_foreign_keys(self):
field.related_model.objects.bulk_create(validated_fk_instances)

for field, instances in instances_to_update.items():
# We don't update parent details. If we want this then we will have to
# provide other product fields in the ParentProductResource too along with
# the upc, which is not useful in most cases.
if not field.name == "parent":
Model = field.related_model
Model = field.related_model
if self.update_related_models_same_type or Model != self.Model:
fields = self.get_fields_to_update(Model)
if fields is not None:
validated_instances_to_update = self.validate_instances(
instances_to_update = self.validate_instances(
instances, fields=fields
)
Model.objects.bulk_update(
validated_instances_to_update, fields=fields
)
Model.objects.bulk_update(instances_to_update, fields=fields)

def bulk_update_or_create_instances(self, instances):
(
Expand Down Expand Up @@ -232,23 +229,29 @@ def bulk_update_or_create_one_to_many(self):

instances_to_create, instances_to_update, identities = self.get_o2m_relations

for relation, instances in instances_to_create.items():
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
validated_instances_to_create = self.validate_instances(instances)
relation.related_model.objects.bulk_create(
validated_instances_to_create
)

for relation, instances in instances_to_update.items():
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
validated_instances_to_update = self.validate_instances(
instances, fields=fields
)
relation.related_model.objects.bulk_update(
validated_instances_to_update, fields=fields
)
for relation, instances_to_create in instances_to_create.items():
if (
self.update_related_models_same_type
or relation.related_model != self.Model
):
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
instances_to_create = self.validate_instances(instances_to_create)
relation.related_model.objects.bulk_create(instances_to_create)

for relation, instances_to_update in instances_to_update.items():
if (
self.update_related_models_same_type
or relation.related_model != self.Model
):
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
instances_to_update = self.validate_instances(
instances_to_update, fields=fields
)
relation.related_model.objects.bulk_update(
instances_to_update, fields=fields
)

if self.delete_related:
for relation, keys in identities.items():
Expand Down Expand Up @@ -278,22 +281,30 @@ def bulk_update_or_create_many_to_many(self):
m2m_to_create, m2m_to_update, _ = self.get_all_m2m_relations

# Create many to many's
for relation, instances in m2m_to_create.items():
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
validated_m2m_instances = self.validate_instances(instances)
relation.related_model.objects.bulk_create(validated_m2m_instances)
for relation, instances_to_create in m2m_to_create.items():
if (
self.update_related_models_same_type
or relation.related_model != self.Model
):
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
instances_to_create = self.validate_instances(instances_to_create)
relation.related_model.objects.bulk_create(instances_to_create)

# Update many to many's
for relation, instances in m2m_to_update.items():
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
validated_instances_to_update = self.validate_instances(
instances, fields=fields
)
relation.related_model.objects.bulk_update(
validated_instances_to_update, fields=fields
)
for relation, instances_to_update in m2m_to_update.items():
if (
self.update_related_models_same_type
or relation.related_model != self.Model
):
fields = self.get_fields_to_update(relation.related_model)
if fields is not None:
instances_to_update = self.validate_instances(
instances_to_update, fields=fields
)
relation.related_model.objects.bulk_update(
instances_to_update, fields=fields
)

for relation, values in self.many_to_many_items.items():
fields = self.get_fields_to_update(relation.related_model)
Expand All @@ -319,7 +330,10 @@ def bulk_update_or_create_many_to_many(self):
# Delete throughs if no instances are passed for the field
if self.delete_related:
Through.objects.filter(
product_id__in=to_delete_throughs_product_ids
**{
"%s_id__in"
% relation.m2m_field_name(): to_delete_throughs_product_ids
}
).all().delete()

if throughs:
Expand All @@ -341,11 +355,22 @@ def bulk_update_or_create_many_to_many(self):
# Delete remaining non-existing through models
if self.delete_related:
Through.objects.filter(
product_id__in=[item[0] for item in bulk_troughs.keys()]
**{
"%s_id__in"
% relation.m2m_field_name(): [
item[0] for item in bulk_troughs.keys()
]
}
).exclude(id__in=bulk_troughs.values()).delete()

# Save only new through models
Through.objects.bulk_create(throughs.values())
try:
# Save only new through models
Through.objects.bulk_create(throughs.values())
except ValueError as e:
raise OscarOdinException(
"Failed creating Trough models for %s. Maybe the related model does NOT exist?"
% relation.name
) from e

def bulk_save(self, instances, fields_to_update, identifier_mapping):
self.fields_to_update = fields_to_update
Expand All @@ -364,6 +389,8 @@ def bulk_save(self, instances, fields_to_update, identifier_mapping):


class ProductModelMapperContext(ModelMapperContext):
update_related_models_same_type = False

@property
def get_fk_relations(self):
to_create, to_update = super().get_fk_relations
Expand Down
6 changes: 6 additions & 0 deletions oscar_odin/resources/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class ParentProduct(OscarCatalogue):
upc: str


class ProductRecommentation(OscarCatalogue):
upc: str


class Product(OscarCatalogue):
"""A product within Django Oscar."""

Expand All @@ -111,6 +115,8 @@ class Product(OscarCatalogue):
attributes: Dict[str, Union[Any, None]]
categories: List[Category]

recommended_products: List[ProductRecommentation]

date_created: Optional[datetime]
date_updated: Optional[datetime]

Expand Down
91 changes: 91 additions & 0 deletions tests/reverse/test_catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ProductClass as ProductClassResource,
Category as CategoryResource,
ParentProduct as ParentProductResource,
ProductRecommentation as ProductRecommentationResource,
)
from oscar_odin.exceptions import OscarOdinException
from oscar_odin.mappings.constants import (
Expand All @@ -23,6 +24,7 @@
STOCKRECORD_NUM_ALLOCATED,
PRODUCTIMAGE_ORIGINAL,
PRODUCT_TITLE,
PRODUCT_UPC,
PRODUCT_DESCRIPTION,
PRODUCTCLASS_REQUIRESSHIPPING,
)
Expand All @@ -33,6 +35,7 @@
ProductImage = get_model("catalogue", "ProductImage")
Category = get_model("catalogue", "Category")
Partner = get_model("partner", "Partner")
ProductRecommendation = get_model("catalogue", "ProductRecommendation")


class SingleProductReverseTest(TestCase):
Expand Down Expand Up @@ -482,6 +485,94 @@ def test_create_product_with_related_fields(self):
self.assertEqual(prd2.attr.harrie, 1)


class ProductRecommendationTest(TestCase):
def setUp(self):
super().setUp()
ProductClass.objects.create(
name="Klaas", slug="klaas", requires_shipping=True, track_stock=True
)
Partner.objects.create(name="klaas")

def test_recommendation(self):
product_resource = [
ProductResource(
upc="recommended_product1",
title="asdf2",
slug="asdf-asdfasdf2",
description="description",
structure=Product.STANDALONE,
product_class=ProductClassResource(slug="klaas"),
),
ProductResource(
upc="recommended_product2",
title="asdf2",
slug="asdf-asdasdfasdf2",
description="description",
structure=Product.STANDALONE,
product_class=ProductClassResource(slug="klaas"),
),
]

_, errors = products_to_db(product_resource)
self.assertEqual(len(errors), 0)

product_resource = ProductResource(
upc="harses",
title="asdf2",
slug="asdf-asdfas23df2",
description="description",
structure=Product.STANDALONE,
product_class=ProductClassResource(slug="klaas"),
recommended_products=[
ProductRecommentationResource(upc="recommended_product1"),
ProductRecommentationResource(upc="recommended_product2"),
],
)

_, errors = products_to_db(product_resource, fields_to_update=[PRODUCT_UPC])
self.assertEqual(len(errors), 0)

prd = Product.objects.get(upc="harses")

self.assertEqual(ProductRecommendation.objects.count(), 2)
self.assertEqual(prd.recommended_products.count(), 2)
self.assertEqual(
sorted(list(prd.recommended_products.values_list("upc", flat=True))),
sorted(["recommended_product1", "recommended_product2"]),
)

def test_recommendation_non_existing(self):
product_resource = [
ProductResource(
upc="recommended_product1",
title="asdf2",
slug="asdf-asdfasdf2",
description="description",
structure=Product.STANDALONE,
product_class=ProductClassResource(slug="klaas"),
),
]

_, errors = products_to_db(product_resource)
self.assertEqual(len(errors), 0)

product_resource = ProductResource(
upc="harses",
title="asdf2",
slug="asdf-asdfas23df2",
description="description",
structure=Product.STANDALONE,
product_class=ProductClassResource(slug="klaas"),
recommended_products=[
ProductRecommentationResource(upc="recommended_product1"),
ProductRecommentationResource(upc="recommended_product2"),
],
)

with self.assertRaises(OscarOdinException):
products_to_db(product_resource)


class ParentChildTest(TestCase):
def setUp(self):
super().setUp()
Expand Down
8 changes: 8 additions & 0 deletions tests/reverse/test_deleting_related.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Image as ImageResource,
ProductClass as ProductClassResource,
Category as CategoryResource,
ProductRecommentation as ProductRecommentationResource,
)
from oscar_odin.mappings.constants import (
CATEGORY_CODE,
Expand Down Expand Up @@ -247,6 +248,8 @@ def test_deleting_product_related_models(self):

def test_deleting_all_related_models(self):
partner = Partner.objects.get(name="klaas")

Product.objects.create(upc="recommended_product1")

product_resource = ProductResource(
upc="1234323-2",
Expand Down Expand Up @@ -274,6 +277,9 @@ def test_deleting_all_related_models(self):
original=File(self.image, name="vats.jpg"),
),
],
recommended_products=[
ProductRecommentationResource(upc="recommended_product1"),
],
categories=[CategoryResource(code="1"), CategoryResource(code="2")],
attributes={"henk": "Klaas", "harrie": 1},
)
Expand All @@ -286,6 +292,7 @@ def test_deleting_all_related_models(self):
self.assertEqual(prd.stockrecords.count(), 1)
self.assertEqual(prd.categories.count(), 2)
self.assertEqual(prd.attribute_values.count(), 2)
self.assertEqual(prd.recommended_products.count(), 1)

product_resource = ProductResource(
upc="1234323-2",
Expand All @@ -303,6 +310,7 @@ def test_deleting_all_related_models(self):
self.assertEqual(prd.stockrecords.count(), 0)
self.assertEqual(prd.categories.count(), 0)
self.assertEqual(prd.attribute_values.count(), 0)
self.assertEqual(prd.recommended_products.count(), 0)

def test_partial_deletion_of_one_to_many_related_models(self):
partner = Partner.objects.get(name="klaas")
Expand Down

0 comments on commit c9fcb74

Please sign in to comment.