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),