From 3dafbd52d8169fbc4734cec3a208c284335f6fc7 Mon Sep 17 00:00:00 2001 From: Joey Date: Fri, 1 Nov 2024 09:49:39 +0100 Subject: [PATCH] Oscarify django-oscar-odin (#46) * Load resources with get_class and register model field resolver in __init__ * Add inheritable resources that allow the usage of get_class * Add the 'Resource' suffis to all resources, as right now its confusing to work with * More suffixes * New major release because breaking changes * Load all resources now with get_class * Load more things with get_classes. Remove '_' prefix from some files names, as they're not just used internally. Rename inheritable resources to same odin name, as this is just what should be used for oscar resources if you want them to be able to be subclassed. * Load ModelMapping & ParentProductResource with get_class too * fix modelmapping import --------- Co-authored-by: Joey Jurjens --- oscar_odin/__init__.py | 8 ++ oscar_odin/apps.py | 10 +- oscar_odin/mappings/address.py | 24 ++-- oscar_odin/mappings/auth.py | 13 ++- oscar_odin/mappings/catalogue.py | 61 +++++++---- oscar_odin/mappings/{_common.py => common.py} | 0 .../{_model_mapper.py => model_mapper.py} | 0 oscar_odin/mappings/order.py | 103 +++++++++++++----- oscar_odin/resources/__init__.py | 3 +- oscar_odin/resources/address.py | 16 +-- oscar_odin/resources/auth.py | 6 +- oscar_odin/resources/{_base.py => base.py} | 4 +- oscar_odin/resources/catalogue.py | 37 ++++--- oscar_odin/resources/inheritable.py | 64 +++++++++++ oscar_odin/resources/order.py | 70 ++++++------ pyproject.toml | 2 +- tests/resources/test_catalogue.py | 2 +- tests/reverse/test_catalogue.py | 56 +++++----- tests/reverse/test_deleting_related.py | 70 ++++++------ tests/reverse/test_reallifecase.py | 12 +- 20 files changed, 354 insertions(+), 207 deletions(-) rename oscar_odin/mappings/{_common.py => common.py} (100%) rename oscar_odin/mappings/{_model_mapper.py => model_mapper.py} (100%) rename oscar_odin/resources/{_base.py => base.py} (84%) create mode 100644 oscar_odin/resources/inheritable.py diff --git a/oscar_odin/__init__.py b/oscar_odin/__init__.py index 7434aed..250bfc7 100644 --- a/oscar_odin/__init__.py +++ b/oscar_odin/__init__.py @@ -2,3 +2,11 @@ Odin Resources and mappings to Oscar models. """ + +from django.db.models import Model + +from odin import registration + +from .django_resolver import ModelFieldResolver + +registration.register_field_resolver(ModelFieldResolver, Model) diff --git a/oscar_odin/apps.py b/oscar_odin/apps.py index 087c851..c41d192 100644 --- a/oscar_odin/apps.py +++ b/oscar_odin/apps.py @@ -1,14 +1,11 @@ """Django App Config for Oscar Odin.""" -from django.apps import AppConfig -from django.db.models import Model from django.utils.translation import gettext_lazy as _ -from odin import registration -from .django_resolver import ModelFieldResolver +from oscar.core.application import OscarConfig -class OscarOdinAppConfig(AppConfig): +class OscarOdinAppConfig(OscarConfig): name = "oscar_odin" label = "oscar_odin" verbose_name = _("Oscar Odin") @@ -16,9 +13,6 @@ class OscarOdinAppConfig(AppConfig): def ready(self): """Hook that Django apps have been loaded.""" - # Register the Django model field resolver - registration.register_field_resolver(ModelFieldResolver, Model) - # Register the default prefetches for the product queryset from oscar_odin.mappings.prefetching.prefetch import register_default_prefetches diff --git a/oscar_odin/mappings/address.py b/oscar_odin/mappings/address.py index e25fae1..be58c55 100644 --- a/oscar_odin/mappings/address.py +++ b/oscar_odin/mappings/address.py @@ -1,9 +1,6 @@ """Mappings between odin and django-oscar models.""" import odin -from oscar.core.loading import get_model - -from .. import resources -from ._common import OscarBaseMapping +from oscar.core.loading import get_model, get_class, get_classes __all__ = ( "BillingAddressToResource", @@ -14,22 +11,31 @@ ShippingAddressModel = get_model("order", "ShippingAddress") CountryModel = get_model("address", "Country") +# mappings +OscarBaseMapping = get_class("oscar_odin.mappings.common", "OscarBaseMapping") + +# resources +CountryResource, BillingAddressResource, ShippingAddressResource = get_classes( + "oscar_odin.resources.address", + ["CountryResource", "BillingAddressResource", "ShippingAddressResource"], +) + class CountryToResource(OscarBaseMapping): """Mapping from country model to resource.""" from_obj = CountryModel - to_obj = resources.address.Country + to_obj = CountryResource class BillingAddressToResource(OscarBaseMapping): """Mapping from billing address model to resource.""" from_obj = BillingAddressModel - to_obj = resources.address.BillingAddress + to_obj = BillingAddressResource @odin.assign_field - def country(self) -> resources.address.Country: + def country(self) -> CountryResource: """Map country.""" return CountryToResource.apply(self.source.country) @@ -38,9 +44,9 @@ class ShippingAddressToResource(OscarBaseMapping): """Mapping from shipping address model to resource.""" from_obj = ShippingAddressModel - to_obj = resources.address.ShippingAddress + to_obj = ShippingAddressResource @odin.assign_field - def country(self) -> resources.address.Country: + def country(self) -> CountryResource: """Map country.""" return CountryToResource.apply(self.source.country) diff --git a/oscar_odin/mappings/auth.py b/oscar_odin/mappings/auth.py index 495f190..5a2745f 100644 --- a/oscar_odin/mappings/auth.py +++ b/oscar_odin/mappings/auth.py @@ -1,16 +1,19 @@ """Mappings between odin and django auth models.""" -from oscar.core.loading import get_model - -from .. import resources -from ._common import OscarBaseMapping +from oscar.core.loading import get_model, get_class __all__ = ("UserToResource",) UserModel = get_model("auth", "User") +# mappings +OscarBaseMapping = get_class("oscar_odin.mappings.common", "OscarBaseMapping") + +# resources +UserResource = get_class("oscar_odin.resources.auth", "UserResource") + class UserToResource(OscarBaseMapping): """Mapping from user model to resource.""" from_obj = UserModel - to_obj = resources.auth.User + to_obj = UserResource diff --git a/oscar_odin/mappings/catalogue.py b/oscar_odin/mappings/catalogue.py index 8f6a336..b6611ae 100644 --- a/oscar_odin/mappings/catalogue.py +++ b/oscar_odin/mappings/catalogue.py @@ -11,13 +11,10 @@ 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_model +from oscar.core.loading import get_class, get_classes, get_model from datetime import datetime -from .. import resources -from ._common import map_queryset, OscarBaseMapping -from ._model_mapper import ModelMapping from ..utils import validate_resources from .prefetching.prefetch import prefetch_product_queryset @@ -41,12 +38,38 @@ StockRecordModel = get_model("partner", "StockRecord") ProductAttributeValueModel = get_model("catalogue", "ProductAttributeValue") +# mappings +ModelMapping = get_class("oscar_odin.mappings.model_mapper", "ModelMapping") +map_queryset, OscarBaseMapping = get_classes( + "oscar_odin.mappings.common", ["map_queryset", "OscarBaseMapping"] +) + +# resources +( + ProductImageResource, + CategoryResource, + ProductClassResource, + ProductResource, + ParentProductResource, + ProductRecommentationResource, +) = get_classes( + "oscar_odin.resources.catalogue", + [ + "ProductImageResource", + "CategoryResource", + "ProductClassResource", + "ProductResource", + "ParentProductResource", + "ProductRecommentationResource", + ], +) + class ProductImageToResource(OscarBaseMapping): """Map from an image model to a resource.""" from_obj = ProductImageModel - to_obj = resources.catalogue.Image + to_obj = ProductImageResource @odin.map_field def original(self, value: ImageFieldFile) -> str: @@ -61,7 +84,7 @@ def original(self, value: ImageFieldFile) -> str: class ProductImageToModel(OscarBaseMapping): """Map from an image resource to a model.""" - from_obj = resources.catalogue.Image + from_obj = ProductImageResource to_obj = ProductImageModel @odin.map_field @@ -76,7 +99,7 @@ class CategoryToResource(OscarBaseMapping): """Map from a category model to a resource.""" from_obj = CategoryModel - to_obj = resources.catalogue.Category + to_obj = CategoryResource @odin.assign_field def meta_title(self) -> str: @@ -94,7 +117,7 @@ def image(self, value: ImageFieldFile) -> Optional[str]: class CategoryToModel(OscarBaseMapping): """Map from a category resource to a model.""" - from_obj = resources.catalogue.Category + from_obj = CategoryResource to_obj = CategoryModel @odin.map_field @@ -132,13 +155,13 @@ class ProductClassToResource(OscarBaseMapping): """Map from a product class model to a resource.""" from_obj = ProductClassModel - to_obj = resources.catalogue.ProductClass + to_obj = ProductClassResource class ProductClassToModel(OscarBaseMapping): """Map from a product class resource to a model.""" - from_obj = resources.catalogue.ProductClass + from_obj = ProductClassResource to_obj = ProductClassModel @@ -146,7 +169,7 @@ class ProductToResource(OscarBaseMapping): """Map from a product model to a resource.""" from_obj = ProductModel - to_obj = resources.catalogue.Product + to_obj = ProductResource @odin.assign_field def title(self) -> str: @@ -159,7 +182,7 @@ def meta_title(self) -> str: return self.source.get_meta_title() @odin.assign_field(to_list=True) - def images(self) -> List[resources.catalogue.Image]: + def images(self) -> List[ProductImageResource]: """Map related image.""" items = self.source.get_all_images() return map_queryset(ProductImageToResource, items, context=self.context) @@ -219,7 +242,7 @@ def attributes(self) -> Dict[str, Any]: } @odin.assign_field - def children(self) -> Tuple[Optional[List[resources.catalogue.Product]]]: + def children(self) -> Tuple[Optional[List[ProductResource]]]: """Children of parent products.""" if self.context.get("include_children", False) and self.source.is_parent: @@ -253,7 +276,7 @@ def map_stock_price(self) -> Tuple[Decimal, str, int, bool]: class ProductToModel(ModelMapping): """Map from a product resource to a model.""" - from_obj = resources.catalogue.Product + from_obj = ProductResource to_obj = ProductModel mappings = (odin.define(from_field="children", skip_if_none=True),) @@ -322,12 +345,12 @@ def product_class(self, value) -> ProductClassModel: class RecommendedProductToModel(OscarBaseMapping): - from_obj = resources.catalogue.ProductRecommentation + from_obj = ProductRecommentationResource to_obj = ProductModel class ParentToModel(OscarBaseMapping): - from_obj = resources.catalogue.ParentProduct + from_obj = ParentProductResource to_obj = ProductModel @odin.assign_field @@ -368,7 +391,7 @@ def product_to_resource( include_children: bool = False, product_mapper: OscarBaseMapping = ProductToResource, **kwargs, -) -> Union[resources.catalogue.Product, Iterable[resources.catalogue.Product]]: +) -> Union[ProductResource, Iterable[ProductResource]]: """Map a product model to a resource. This method will accept either a single product or an iterable of product @@ -397,7 +420,7 @@ def product_queryset_to_resources( include_children: bool = False, product_mapper=ProductToResource, **kwargs, -) -> Iterable[resources.catalogue.Product]: +) -> 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 @@ -423,7 +446,7 @@ def product_queryset_to_resources( def products_to_model( - products: List[resources.catalogue.Product], + products: List[ProductResource], product_mapper=ProductToModel, delete_related=False, ) -> Tuple[List[ProductModel], Dict]: diff --git a/oscar_odin/mappings/_common.py b/oscar_odin/mappings/common.py similarity index 100% rename from oscar_odin/mappings/_common.py rename to oscar_odin/mappings/common.py diff --git a/oscar_odin/mappings/_model_mapper.py b/oscar_odin/mappings/model_mapper.py similarity index 100% rename from oscar_odin/mappings/_model_mapper.py rename to oscar_odin/mappings/model_mapper.py diff --git a/oscar_odin/mappings/order.py b/oscar_odin/mappings/order.py index acd798b..73d4c73 100644 --- a/oscar_odin/mappings/order.py +++ b/oscar_odin/mappings/order.py @@ -3,13 +3,7 @@ import odin from django.http import HttpRequest -from oscar.core.loading import get_model -from oscar_odin.resources.order import DiscountPerTaxCodeResource - -from .. import resources -from ._common import map_queryset, OscarBaseMapping -from .address import BillingAddressToResource, ShippingAddressToResource -from .auth import UserToResource +from oscar.core.loading import get_model, get_class, get_classes from decimal import Decimal from django.db.models import Sum @@ -31,19 +25,68 @@ OrderLineDiscountModel = get_model("order", "OrderLineDiscount") SurchargeModel = get_model("order", "Surcharge") +# mappings +map_queryset, OscarBaseMapping = get_classes( + "oscar_odin.mappings.common", ["map_queryset", "OscarBaseMapping"] +) +BillingAddressToResource, ShippingAddressToResource = get_classes( + "oscar_odin.mappings.address", + ["BillingAddressToResource", "ShippingAddressToResource"], +) +UserToResource = get_class("oscar_odin.mappings.auth", "UserToResource") + +# resources +UserResource = get_class("oscar_odin.resources.auth", "UserResource") + +( + SurchargeResource, + DiscountResource, + DiscountLineResource, + DiscountCategoryResource, + OrderResource, + NoteResource, + StatusChangeResource, + LineResource, + LinePriceResource, + PaymentEventResource, + ShippingEventResource, + DiscountPerTaxCodeResource, +) = get_classes( + "oscar_odin.resources.order", + [ + "SurchargeResource", + "DiscountResource", + "DiscountLineResource", + "DiscountCategoryResource", + "OrderResource", + "NoteResource", + "StatusChangeResource", + "LineResource", + "LinePriceResource", + "PaymentEventResource", + "ShippingEventResource", + "DiscountPerTaxCodeResource", + ], +) + +BillingAddressResource, ShippingAddressResource = get_classes( + "oscar_odin.resources.address", + ["BillingAddressResource", "ShippingAddressResource"], +) + class SurchargeToResource(OscarBaseMapping): """Mapping from a surcharge model to a resource.""" from_obj = SurchargeModel - to_obj = resources.order.Surcharge + to_obj = SurchargeResource class DiscountLineToResource(OscarBaseMapping): """Mapping from an order discount line model to a resource""" from_obj = OrderLineDiscountModel - to_obj = resources.order.DiscountLine + to_obj = DiscountLineResource @odin.assign_field def line(self): @@ -55,12 +98,12 @@ class DiscountToResource(OscarBaseMapping): """Mapping from an order discount model to a resource.""" from_obj = OrderDiscountModel - to_obj = resources.order.Discount + to_obj = DiscountResource @odin.map_field def category(self, value: str): """Map category.""" - return resources.order.DiscountCategory(value) + return DiscountCategoryResource(value) @odin.assign_field def is_basket_discount(self) -> bool: @@ -112,31 +155,31 @@ class ShippingEventToResource(OscarBaseMapping): """Mapping from a shipping event model to a resource.""" from_obj = ShippingEventModel - to_obj = resources.order.ShippingEvent + to_obj = ShippingEventResource class PaymentEventToResource(OscarBaseMapping): """Mapping from a payment event model to a resource.""" from_obj = PaymentEventModel - to_obj = resources.order.PaymentEvent + to_obj = PaymentEventResource class LinePriceToResource(OscarBaseMapping): """Mapping from Line price to resource.""" from_obj = LinePriceModel - to_obj = resources.order.LinePrice + to_obj = LinePriceResource class LineToResource(OscarBaseMapping): """Mapping from Line model to resource.""" from_obj = LineModel - to_obj = resources.order.Line + to_obj = LineResource @odin.assign_field(to_list=True) - def prices(self) -> List[resources.order.LinePrice]: + def prices(self) -> List[LinePriceResource]: """Map price resources.""" items = self.source.prices.all() return map_queryset(LinePriceToResource, items, context=self.context) @@ -151,21 +194,21 @@ class StatusChangeToResource(OscarBaseMapping): """Mapping from order status change model to resource.""" from_obj = OrderStatusChangeModel - to_obj = resources.order.StatusChange + to_obj = StatusChangeResource class NoteToResource(OscarBaseMapping): """Mapping from order note model to resource.""" from_obj = OrderNoteModel - to_obj = resources.order.Note + to_obj = NoteResource class OrderToResource(OscarBaseMapping): """Mapping from order model to resource.""" from_obj = OrderModel - to_obj = resources.order.Order + to_obj = OrderResource @odin.assign_field def email(self) -> str: @@ -173,61 +216,61 @@ def email(self) -> str: return self.source.email @odin.assign_field - def user(self) -> Optional[resources.auth.User]: + def user(self) -> Optional[UserResource]: """Map user.""" if self.source.user: return UserToResource.apply(self.source.user) @odin.assign_field - def billing_address(self) -> Optional[resources.address.BillingAddress]: + def billing_address(self) -> Optional[BillingAddressResource]: """Map billing address.""" if self.source.billing_address: return BillingAddressToResource.apply(self.source.billing_address) @odin.assign_field - def shipping_address(self) -> Optional[resources.address.ShippingAddress]: + def shipping_address(self) -> Optional[ShippingAddressResource]: """Map shipping address.""" if self.source.shipping_address: return ShippingAddressToResource.apply(self.source.shipping_address) @odin.assign_field(to_list=True) - def lines(self) -> List[resources.order.Line]: + def lines(self) -> List[LineResource]: """Map order lines.""" items = self.source.lines return map_queryset(LineToResource, items, context=self.context) @odin.assign_field(to_list=True) - def notes(self) -> List[resources.order.Note]: + def notes(self) -> List[NoteResource]: """Map order notes.""" items = self.source.notes return map_queryset(NoteToResource, items, context=self.context) @odin.assign_field(to_list=True) - def status_changes(self) -> List[resources.order.StatusChange]: + def status_changes(self) -> List[StatusChangeResource]: """Map order status changes.""" items = self.source.status_changes return map_queryset(StatusChangeToResource, items, context=self.context) @odin.assign_field(to_list=True) - def discounts(self) -> List[resources.order.Discount]: + def discounts(self) -> List[DiscountResource]: """Map order discounts.""" items = self.source.discounts return map_queryset(DiscountToResource, items, context=self.context) @odin.assign_field(to_list=True) - def surcharges(self) -> List[resources.order.Surcharge]: + def surcharges(self) -> List[SurchargeResource]: """Map order surcharges.""" items = self.source.surcharges return map_queryset(SurchargeToResource, items, context=self.context) @odin.assign_field(to_list=True) - def shipping_events(self) -> List[resources.order.ShippingEvent]: + def shipping_events(self) -> List[ShippingEventResource]: """Map order shipping events.""" items = self.source.shipping_events return map_queryset(ShippingEventToResource, items, context=self.context) @odin.assign_field(to_list=True) - def payment_events(self) -> List[resources.order.PaymentEvent]: + def payment_events(self) -> List[PaymentEventResource]: """Map order payment events.""" items = self.source.payment_events return map_queryset(PaymentEventToResource, items, context=self.context) @@ -236,7 +279,7 @@ def payment_events(self) -> List[resources.order.PaymentEvent]: def order_to_resource( order: Union[OrderModel, Iterable[OrderModel]], request: Optional[HttpRequest] = None, -) -> Union[resources.order.Order, Iterable[resources.order.Order]]: +) -> Union[OrderResource, Iterable[OrderResource]]: """Map an order model to a resource. This method will except either a single order or an iterable of order diff --git a/oscar_odin/resources/__init__.py b/oscar_odin/resources/__init__.py index 5193196..721cbc1 100644 --- a/oscar_odin/resources/__init__.py +++ b/oscar_odin/resources/__init__.py @@ -1,6 +1,7 @@ """Resources for Oscar Odin.""" from . import address, auth, catalogue, order -from ._base import OscarResource + +from .base import OscarResource __all__ = ["OscarResource", "address", "catalogue", "order", "auth"] diff --git a/oscar_odin/resources/address.py b/oscar_odin/resources/address.py index 309e343..3adfce1 100644 --- a/oscar_odin/resources/address.py +++ b/oscar_odin/resources/address.py @@ -4,10 +4,12 @@ import odin.validators from odin.utils import iter_to_choices -from ._base import OscarResource +from oscar.core.loading import get_class +OscarResource = get_class("oscar_odin.resources.base", "OscarResource") -class OscarAddress(OscarResource, abstract=True): + +class OscarAddressResource(OscarResource, abstract=True): """Base resource for Oscar order application.""" class Meta: @@ -15,7 +17,7 @@ class Meta: namespace = "oscar.address" -class Country(OscarAddress): +class CountryResource(OscarAddressResource): """A country.""" iso_3166_1_a2: str = odin.Options( @@ -52,7 +54,7 @@ class Country(OscarAddress): TITLE_CHOICES: Final[Sequence[str]] = ("Mr", "Miss", "Mrs", "Ms", "Dr") -class Address(OscarAddress, abstract=True): +class AddressResource(OscarAddressResource, abstract=True): """Base address resource.""" title: str = odin.Options(empty=True, choices=iter_to_choices(TITLE_CHOICES)) @@ -64,10 +66,10 @@ class Address(OscarAddress, abstract=True): line4: str = odin.Options(empty=True, verbose_name="City") state: str = odin.Options(empty=True, verbose_name="State/Country") postcode: str = odin.Options(empty=True, verbose_name="Post/Zip-code") - country: Country + country: CountryResource -class BillingAddress(Address): +class BillingAddressResource(AddressResource): """Address for billing.""" class Meta: @@ -76,7 +78,7 @@ class Meta: verbose_name_plural = "Billing addresses" -class ShippingAddress(Address): +class ShippingAddressResource(AddressResource): """Address for shipping.""" class Meta: diff --git a/oscar_odin/resources/auth.py b/oscar_odin/resources/auth.py index 4781a44..d9a019f 100644 --- a/oscar_odin/resources/auth.py +++ b/oscar_odin/resources/auth.py @@ -1,5 +1,7 @@ """Resources for Oscar categories.""" -from ._base import OscarResource +from oscar.core.loading import get_class + +OscarResource = get_class("oscar_odin.resources.base", "OscarResource") class _User(OscarResource, abstract=True): @@ -10,7 +12,7 @@ class Meta: namespace = "oscar.user" -class User(_User): +class UserResource(_User): """User resource.""" id: int diff --git a/oscar_odin/resources/_base.py b/oscar_odin/resources/base.py similarity index 84% rename from oscar_odin/resources/_base.py rename to oscar_odin/resources/base.py index f26bc2e..dc1bb0c 100644 --- a/oscar_odin/resources/_base.py +++ b/oscar_odin/resources/base.py @@ -1,8 +1,8 @@ """Common base resource for all Oscar resources.""" -import odin +from .inheritable import AnnotatedResource -class OscarResource(odin.AnnotatedResource, abstract=True): +class OscarResource(AnnotatedResource, abstract=True): """Base resource for Oscar models.""" @property diff --git a/oscar_odin/resources/catalogue.py b/oscar_odin/resources/catalogue.py index cbb59bb..063bbd6 100644 --- a/oscar_odin/resources/catalogue.py +++ b/oscar_odin/resources/catalogue.py @@ -3,17 +3,18 @@ from decimal import Decimal from typing import Any, Dict, List, Optional, Union -from oscar.core.loading import get_model +from oscar.core.loading import get_model, get_class import odin from odin.fields import StringField from ..fields import DecimalField -from ._base import OscarResource + +OscarResource = get_class("oscar_odin.resources.base", "OscarResource") ProductModel = get_model("catalogue", "Product") -class OscarCatalogue(OscarResource, abstract=True): +class OscarCatalogueResource(OscarResource, abstract=True): """Base resource for Oscar catalogue application.""" class Meta: @@ -21,7 +22,7 @@ class Meta: namespace = "oscar.catalogue" -class Image(OscarCatalogue): +class ProductImageResource(OscarCatalogueResource): """An image for a product.""" class Meta: @@ -43,7 +44,7 @@ class Meta: date_created: Optional[datetime] -class Category(OscarCatalogue): +class CategoryResource(OscarCatalogueResource): """A category within Django Oscar.""" id: Optional[int] @@ -60,7 +61,7 @@ class Category(OscarCatalogue): path: Optional[str] -class ProductClass(OscarCatalogue): +class ProductClassResource(OscarCatalogueResource): """A product class within Django Oscar.""" name: Optional[str] @@ -69,7 +70,7 @@ class ProductClass(OscarCatalogue): track_stock: Optional[bool] -class StockRecord(OscarCatalogue): +class StockRecordResource(OscarCatalogueResource): id: Optional[int] partner_sku: str num_in_stock: Optional[int] @@ -78,20 +79,20 @@ class StockRecord(OscarCatalogue): currency: Optional[str] -class ProductAttributeValue(OscarCatalogue): +class ProductAttributeValueResource(OscarCatalogueResource): code: str value: Any -class ParentProduct(OscarCatalogue): +class ParentProductResource(OscarCatalogueResource): upc: str -class ProductRecommentation(OscarCatalogue): +class ProductRecommentationResource(OscarCatalogueResource): upc: str -class Product(OscarCatalogue): +class ProductResource(OscarCatalogueResource): """A product within Django Oscar.""" id: Optional[int] @@ -101,11 +102,11 @@ class Product(OscarCatalogue): slug: Optional[str] description: Optional[str] = "" meta_title: Optional[str] - images: List[Image] = odin.Options(empty=True) + images: List[ProductImageResource] = odin.Options(empty=True) rating: Optional[float] is_discountable: bool = True is_public: bool = True - parent: Optional[ParentProduct] + parent: Optional[ParentProductResource] priority: int = 0 # Price information @@ -115,15 +116,15 @@ class Product(OscarCatalogue): is_available_to_buy: Optional[bool] partner: Optional[Any] - product_class: Optional[ProductClass] = None + product_class: Optional[ProductClassResource] = None attributes: Dict[str, Union[Any, None]] - categories: List[Category] + categories: List[CategoryResource] - recommended_products: List[ProductRecommentation] + recommended_products: List[ProductRecommentationResource] date_created: Optional[datetime] date_updated: Optional[datetime] - children: Optional[List["Product"]] = odin.ListOf.delayed( - lambda: Product, null=True + children: Optional[List["ProductResource"]] = odin.ListOf.delayed( + lambda: ProductResource, null=True ) diff --git a/oscar_odin/resources/inheritable.py b/oscar_odin/resources/inheritable.py new file mode 100644 index 0000000..be1efb1 --- /dev/null +++ b/oscar_odin/resources/inheritable.py @@ -0,0 +1,64 @@ +from typing import Type + +from odin.annotated_resource import AnnotatedResourceType +from odin.fields import NotProvided +from odin.resources import ResourceBase, ResourceType, ResourceOptions, MOT + + +class InheritableResourceOptions(ResourceOptions): + """ + odin heavily caches resources. It does this by adding a resource to a resource + registry the minute the resource gets initiated (__new__) --> metaclass. + + The cache/registry key is from the resource name(classname) and name_space (meta) combined. + However, when a resource inherits from another resource, the namespace also gets inherited. + + This is problematic in context of oscar's `get_class` method, as the class name will be the same + and since namespaces are inherited, this will also be the same. This results in odin returning the old resource class from + the cache / registry. + + This class sets the namespace to NotProvided, odin will in this case generate the namespace + from the module where the resource is defined. This will make sure that the namespace is unique + for each resource, even if they inherit from each other. + """ + + def inherit_from(self, base): + super().inherit_from(base) + self.name_space = NotProvided + + +class InheritableResourceType(ResourceType): + meta_options = InheritableResourceOptions + + +class InheritableAnnotatedResourceType(AnnotatedResourceType): + def __new__( + mcs, + name: str, + bases, + attrs: dict, + # Set our inheritable meta options type + meta_options_type: Type[MOT] = InheritableResourceOptions, + abstract: bool = False, + ): + return super().__new__(mcs, name, bases, attrs, meta_options_type, abstract) + + +class Resource(ResourceBase, metaclass=InheritableResourceType): + """ + from oscar_odin.resources.inheritable import Resource + class MyResource(Resource): + pass + """ + + +class AnnotatedResource( + ResourceBase, + metaclass=InheritableAnnotatedResourceType, + meta_options_type=InheritableResourceOptions, +): + """ + from oscar_odin.resources.inheritable import AnnotatedResource + class MyAnnotatedResource(AnnotatedResource): + pass + """ diff --git a/oscar_odin/resources/order.py b/oscar_odin/resources/order.py index 8dba4b7..ebd6d72 100644 --- a/oscar_odin/resources/order.py +++ b/oscar_odin/resources/order.py @@ -6,13 +6,19 @@ import odin +from oscar.core.loading import get_class, get_classes + from ..fields import DecimalField -from ._base import OscarResource -from .address import BillingAddress, ShippingAddress -from .auth import User + +OscarResource = get_class("oscar_odin.resources.base", "OscarResource") +BillingAddressResource, ShippingAddressResource = get_classes( + "oscar_odin.resources.address", + ["BillingAddressResource", "ShippingAddressResource"], +) +UserResource = get_class("oscar_odin.resources.auth", "UserResource") -class OscarOrder(OscarResource, abstract=True): +class OscarOrderResource(OscarResource, abstract=True): """Base resource for Oscar order application.""" class Meta: @@ -20,7 +26,7 @@ class Meta: namespace = "oscar.order" -class LinePrice(OscarOrder): +class LinePriceResource(OscarOrderResource): """For tracking the prices paid for each unit within a line. This is necessary as offers can lead to units within a line @@ -45,7 +51,7 @@ class LinePrice(OscarOrder): ) -class Line(OscarOrder): +class LineResource(OscarOrderResource): """A line within an order.""" partner_id: Optional[int] @@ -65,7 +71,7 @@ class Line(OscarOrder): upc: Optional[str] quantity: int = 1 attributes: Dict[str, Any] - prices: List[LinePrice] + prices: List[LinePriceResource] # Price information before discounts are applied price_before_discounts_incl_tax: Decimal = DecimalField( @@ -90,7 +96,7 @@ class Line(OscarOrder): status: str = odin.Options(empty=True) -class PaymentEvent(OscarOrder): +class PaymentEventResource(OscarOrderResource): """A payment event for an order. For example: @@ -106,7 +112,7 @@ class PaymentEvent(OscarOrder): # shipping_event -class ShippingEvent(OscarOrder): +class ShippingEventResource(OscarOrderResource): """An event is something which happens to a group of lines such as 1 item being dispatched. """ @@ -117,7 +123,7 @@ class ShippingEvent(OscarOrder): date_created: datetime -class DiscountCategory(str, Enum): +class DiscountCategoryResource(str, Enum): """Category of discount.""" BASKET = "Basket" @@ -125,27 +131,27 @@ class DiscountCategory(str, Enum): DEFERRED = "Deferred" -class DiscountLine(OscarOrder): +class DiscountLineResource(OscarOrderResource): """Line of a discount""" - line: Line + line: LineResource order_discount_id: int is_incl_tax: bool amount: Decimal = DecimalField() -class DiscountPerTaxCodeResource(OscarOrder): +class DiscountPerTaxCodeResource(OscarOrderResource): """Total discount for each tax code in a discount""" amount: Decimal = DecimalField() tax_code: str -class Discount(OscarOrder): +class DiscountResource(OscarOrderResource): """A discount against an order.""" id: int - category: DiscountCategory + category: DiscountCategoryResource offer_id: Optional[int] offer_name: Optional[str] voucher_id: Optional[int] @@ -153,7 +159,7 @@ class Discount(OscarOrder): frequency: Optional[int] amount: Decimal = DecimalField() message: str = odin.Options(empty=True) - discount_lines: List[DiscountLine] + discount_lines: List[DiscountLineResource] is_basket_discount: bool is_shipping_discount: bool is_post_order_action: bool @@ -161,7 +167,7 @@ class Discount(OscarOrder): discount_lines_per_tax_code: DiscountPerTaxCodeResource -class Surcharge(OscarOrder): +class SurchargeResource(OscarOrderResource): """A surcharge against an order.""" name: str @@ -177,7 +183,7 @@ class Surcharge(OscarOrder): ) -class NoteType(str, Enum): +class NoteTypeResource(str, Enum): """Type of order note.""" INFO = "Info" @@ -186,17 +192,17 @@ class NoteType(str, Enum): SYSTEM = "System" -class Note(OscarOrder): +class NoteResource(OscarOrderResource): """A note against an order.""" # user_id: int - note_type: Optional[NoteType] + note_type: Optional[NoteTypeResource] message: str date_created: datetime date_updated: datetime -class StatusChange(OscarOrder): +class StatusChangeResource(OscarOrderResource): """A status change for an order.""" old_status: str = odin.Options(empty=True) @@ -204,7 +210,7 @@ class StatusChange(OscarOrder): date_created: datetime -class Order(OscarOrder): +class OrderResource(OscarOrderResource): """An order within Django Oscar.""" class Meta: @@ -219,9 +225,9 @@ class Meta: verbose_name="Site ID", doc_text="Site that the order was made through.", ) - user: Optional[User] + user: Optional[UserResource] email: str = odin.Options(empty=True) # Map off the order property on model - billing_address: Optional[BillingAddress] + billing_address: Optional[BillingAddressResource] currency: str total_incl_tax: Decimal = DecimalField( verbose_name="Order total (inc. tax)", @@ -238,16 +244,16 @@ class Meta: shipping_tax_code: Optional[str] = odin.Options( verbose_name="Shipping VAT rate code" ) - shipping_address: Optional[ShippingAddress] + shipping_address: Optional[ShippingAddressResource] shipping_method: str = odin.Options(empty=True) shipping_code: str = odin.Options(empty=True) - lines: List[Line] + lines: List[LineResource] status: str = odin.Options(empty=True) date_placed: datetime - notes: List[Note] - status_changes: List[StatusChange] - discounts: List[Discount] - surcharges: List[Surcharge] - payment_events: List[PaymentEvent] - shipping_events: List[ShippingEvent] + notes: List[NoteResource] + status_changes: List[StatusChangeResource] + discounts: List[DiscountResource] + surcharges: List[SurchargeResource] + payment_events: List[PaymentEventResource] + shipping_events: List[ShippingEventResource] diff --git a/pyproject.toml b/pyproject.toml index 6b8abc5..c3fab08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-oscar-odin" -version = "0.2.2" +version = "0.3.0" description = "Odin Resources and mappings to Oscar models" authors = ["Tim Savage "] maintainers = [ diff --git a/tests/resources/test_catalogue.py b/tests/resources/test_catalogue.py index 9468621..27e7dc8 100644 --- a/tests/resources/test_catalogue.py +++ b/tests/resources/test_catalogue.py @@ -5,6 +5,6 @@ class TestProduct(TestCase): def test_init(self): - target = resources.catalogue.Product() + target = resources.catalogue.ProductResource() self.assertIsNotNone(target) diff --git a/tests/reverse/test_catalogue.py b/tests/reverse/test_catalogue.py index 9e8bcb1..db2a4c0 100644 --- a/tests/reverse/test_catalogue.py +++ b/tests/reverse/test_catalogue.py @@ -10,12 +10,12 @@ from oscar_odin.mappings.catalogue import products_to_db from oscar_odin.resources.catalogue import ( - Product as ProductResource, - Image as ImageResource, - ProductClass as ProductClassResource, - Category as CategoryResource, - ParentProduct as ParentProductResource, - ProductRecommentation as ProductRecommentationResource, + ProductResource, + ProductImageResource, + ProductClassResource, + CategoryResource, + ParentProductResource, + ProductRecommentationResource, ) from oscar_odin.exceptions import OscarOdinException from oscar_odin.mappings.constants import ( @@ -84,13 +84,13 @@ def test_create_product_with_related_fields(self): partner=partner, product_class=ProductClassResource(slug="klaas"), images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", original=File(self.image, name="harrie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vats", @@ -131,13 +131,13 @@ def test_create_product_with_related_fields(self): partner=partner, product_class=ProductClassResource(slug="klaas"), images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harriebatsie", original=File(self.image, name="harriebatsie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vatsie", @@ -184,13 +184,13 @@ def test_create_productclass_with_product(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", original=File(self.image, name="harrie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vats", @@ -264,13 +264,13 @@ def test_idempotent(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, original=File(self.image, name="harrie.jpg"), code="12", ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, original=File(self.image, name="vats.jpg"), @@ -314,13 +314,13 @@ def test_idempotent(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, original=File(self.image, name="harrie.jpg"), code="12", ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, original=File(self.image, name="vats.jpg"), @@ -420,13 +420,13 @@ def test_create_product_with_related_fields(self): currency="EUR", product_class=ProductClassResource(slug="klaas"), images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="klaas", original=File(self.image, name="klaas.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="harrie", @@ -448,13 +448,13 @@ def test_create_product_with_related_fields(self): partner=Partner.objects.create(name="klaas"), product_class=ProductClassResource(slug="klaas"), images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="klass-2", original=File(self.image, name="klaas.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="harrie-2", @@ -695,13 +695,13 @@ def test_error_handling_on_product_operations(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order="top", code="harrie", original=File(self.image, name="harrie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vats", @@ -731,13 +731,13 @@ def test_error_handling_on_product_operations(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", original=File(self.image, name="harrie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vats", @@ -763,8 +763,8 @@ def test_error_handling_on_product_operations(self): partner=partner, product_class=product_class, images=[ - ImageResource(code="harrie"), - ImageResource( + ProductImageResource(code="harrie"), + ProductImageResource( caption="gekke caption 2", display_order="Alphabet", code="vats", @@ -827,13 +827,13 @@ def test_fields_to_update_on_product_operations(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", original=File(self.image, name="harrie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vats", diff --git a/tests/reverse/test_deleting_related.py b/tests/reverse/test_deleting_related.py index d9827d2..d9c991a 100644 --- a/tests/reverse/test_deleting_related.py +++ b/tests/reverse/test_deleting_related.py @@ -10,11 +10,11 @@ from oscar_odin.mappings.catalogue import products_to_db from oscar_odin.resources.catalogue import ( - Product as ProductResource, - Image as ImageResource, - ProductClass as ProductClassResource, - Category as CategoryResource, - ProductRecommentation as ProductRecommentationResource, + ProductResource, + ProductImageResource, + ProductClassResource, + CategoryResource, + ProductRecommentationResource, ) from oscar_odin.mappings.constants import ( CATEGORY_CODE, @@ -84,13 +84,13 @@ def test_deleting_product_related_models(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", original=File(self.image, name="harrie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vats", @@ -113,7 +113,7 @@ def test_deleting_product_related_models(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="robin", display_order=0, code="robin", @@ -173,7 +173,7 @@ def test_deleting_product_related_models(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", @@ -192,7 +192,7 @@ def test_deleting_product_related_models(self): partner=partner, product_class=product_class, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", @@ -271,13 +271,13 @@ def test_deleting_all_related_models(self): partner=partner, product_class=ProductClassResource(slug="klaas"), images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", original=File(self.image, name="harrie.jpg"), ), - ImageResource( + ProductImageResource( caption="gekke caption 2", display_order=1, code="vats", @@ -334,7 +334,7 @@ def test_partial_deletion_of_one_to_many_related_models(self): currency="EUR", partner=partner, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", @@ -353,7 +353,7 @@ def test_partial_deletion_of_one_to_many_related_models(self): currency="EUR", partner=partner, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="bat", @@ -372,7 +372,7 @@ def test_partial_deletion_of_one_to_many_related_models(self): currency="EUR", partner=partner, images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="hat", @@ -402,12 +402,12 @@ def test_partial_deletion_of_one_to_many_related_models(self): # Other products' related models of stay unaffected self.assertTrue(Stockrecord.objects.count(), 2) self.assertTrue(ProductImage.objects.count(), 2) - + def test_only_category_related(self): partner = Partner.objects.get(name="klaas") product_class = ProductClassResource(slug="klaas", name="Klaas") Category.add_root(code="3", name="3") - + product_resources = [ ProductResource( upc="harrie", @@ -420,11 +420,11 @@ def test_only_category_related(self): currency="EUR", partner=partner, categories=[ - CategoryResource(code="1"), - CategoryResource(code="3"), + CategoryResource(code="1"), + CategoryResource(code="3"), ], images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="harrie", @@ -442,12 +442,9 @@ def test_only_category_related(self): availability=2, currency="EUR", partner=partner, - categories=[ - CategoryResource(code="2"), - CategoryResource(code="1") - ], + categories=[CategoryResource(code="2"), CategoryResource(code="1")], images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="bat", @@ -465,11 +462,9 @@ def test_only_category_related(self): availability=1, currency="EUR", partner=partner, - categories=[ - CategoryResource(code="3") - ], + categories=[CategoryResource(code="3")], images=[ - ImageResource( + ProductImageResource( caption="gekke caption", display_order=0, code="hat", @@ -483,14 +478,14 @@ def test_only_category_related(self): self.assertEqual(Product.objects.count(), 3) self.assertEqual(ProductCategory.objects.count(), 5) self.assertEqual(ProductImage.objects.count(), 3) - + product_resources = [ ProductResource( upc="harrie", title="harrie", structure=Product.STANDALONE, categories=[ - CategoryResource(code="1"), + CategoryResource(code="1"), ], ), ProductResource( @@ -498,22 +493,21 @@ def test_only_category_related(self): title="bat", structure=Product.STANDALONE, categories=[ - CategoryResource(code="2"), + CategoryResource(code="2"), ], ), ProductResource( upc="hat", title="hat", structure=Product.STANDALONE, - categories=[ - CategoryResource(code="3") - ], + categories=[CategoryResource(code="3")], ), ] - _, errors = products_to_db(product_resources, fields_to_update=["Category.code"], delete_related=True) + _, errors = products_to_db( + product_resources, fields_to_update=["Category.code"], delete_related=True + ) self.assertEqual(len(errors), 0) - + self.assertEqual(ProductCategory.objects.count(), 3) self.assertEqual(ProductImage.objects.count(), 3) - \ No newline at end of file diff --git a/tests/reverse/test_reallifecase.py b/tests/reverse/test_reallifecase.py index f3e48ea..cc0d599 100644 --- a/tests/reverse/test_reallifecase.py +++ b/tests/reverse/test_reallifecase.py @@ -19,10 +19,10 @@ from oscar_odin.fields import DecimalField from oscar_odin.mappings.catalogue import products_to_db from oscar_odin.resources.catalogue import ( - Product as ProductResource, - Image as ImageResource, - ProductClass as ProductClassResource, - Category as CategoryResource, + ProductResource, + ProductImageResource, + ProductClassResource, + CategoryResource, ) Product = get_model("catalogue", "Product") @@ -96,7 +96,7 @@ def images(self, image, app_image): a = urlparse(image) img = File(io.BytesIO(response.content), name=path.basename(a.path)) images.append( - ImageResource( + ProductImageResource( display_order=0, code="%s?upc=%s" % (self.source.number, image), caption="", @@ -109,7 +109,7 @@ def images(self, image, app_image): a = urlparse(app_image) img = File(io.BytesIO(response.content), name=path.basename(a.path)) images.append( - ImageResource( + ProductImageResource( display_order=1, caption="", code="%s?upc=%s-2" % (self.source.number, image),