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

Implement productrecommendations #19

Merged
merged 6 commits into from
Feb 26, 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
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
Loading