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

Specific django mapping #4

Closed
wants to merge 20 commits into from
Closed
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
2 changes: 1 addition & 1 deletion oscar_odin/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.apps import AppConfig
from django.db.models import Model
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from odin import registration

from .django_resolver import ModelFieldResolver
Expand Down
8 changes: 7 additions & 1 deletion oscar_odin/django_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ class ModelFieldResolver(FieldResolverBase):
def get_field_dict(self) -> Dict[str, Optional[Field]]:
"""Get a dictionary of fields from the source object."""
meta = getmeta(self.obj)
return {f.attname: f for f in meta.fields}
fields = {f.attname: f for f in meta.fields}
fields.update(
(r.related_name, r.field)
for r in meta.related_objects
if r.related_name != "+"
)
return fields
2 changes: 1 addition & 1 deletion oscar_odin/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class DecimalField(ScalarField):
}
scalar_type = Decimal

def __init__(self, places: int = 4, **kwargs):
def __init__(self, places: int = 2, **kwargs):
"""Initialise the field."""
super().__init__(**kwargs)
self.places = places
Expand Down
85 changes: 85 additions & 0 deletions oscar_odin/mappings/_model_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Extended model mapper for Django models."""
from typing import Sequence

from django.db.models.fields.related import ForeignKey

from odin.mapping import MappingBase, MappingMeta
from odin.utils import getmeta


class ModelMappingMeta(MappingMeta):
"""Extended type of mapping meta."""

def __new__(cls, name, bases, attrs):
mapping_type = super().__new__(cls, name, bases, attrs)

if mapping_type.to_obj is None:
return mapping_type

# Extract out foreign field types.
mapping_type.one_to_one_fields = one_to_one_fields = []
mapping_type.many_to_one_fields = many_to_one_fields = []
mapping_type.many_to_many_fields = many_to_many_fields = []
mapping_type.foreign_key_fields = foreign_key_fields = []

meta = getmeta(mapping_type.to_obj)

for relation in meta.related_objects:
if relation.many_to_many:
many_to_many_fields.append(
(relation.related_name, relation.field.attname)
)
elif relation.many_to_one:
many_to_one_fields.append(
(relation.related_name, relation.field.attname)
)
elif relation.one_to_many:
many_to_many_fields.append(
(relation.related_name, relation.field.attname)
)

for field in meta.fields:
if isinstance(field, ForeignKey):
foreign_key_fields.append(("%s_id" % field.name, field.attname))

return mapping_type


class ModelMapping(MappingBase, metaclass=ModelMappingMeta):
"""Definition of a mapping between two Objects."""

exclude_fields = []
mappings = []
one_to_one_fields: Sequence[str] = []
many_to_one_fields: Sequence[str] = []
many_to_many_fields: Sequence[str] = []
foreign_key_fields: Sequence[str] = []

def create_object(self, **field_values):
"""Create a new product model."""

self.context["one_to_one_items"] = one_to_one_items = [
(name, table_name, field_values.pop(name))
for name, table_name in self.one_to_one_fields
if name in field_values
]
self.context["many_to_one_items"] = many_to_one_items = [
(name, table_name, field_values.pop(name))
for name, table_name in self.many_to_one_fields
if name in field_values
]
self.context["many_to_many_items"] = many_to_many_items = [
(name, table_name, field_values.pop(name))
for name, table_name in self.many_to_many_fields
if name in field_values
]
self.context["foreign_key_items"] = foreign_key_items = [
(name, table_name, field_values.pop(name))
for name, table_name in self.foreign_key_fields
if name in field_values
]
obj = super().create_object(**field_values)

setattr(obj, "odin_context", self.context)

return obj
45 changes: 45 additions & 0 deletions oscar_odin/mappings/address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Mappings between odin and django-oscar models."""
import odin
from oscar.core.loading import get_model

from .. import resources

__all__ = (
"BillingAddressToResource",
"ShippingAddressToResource",
)

BillingAddressModel = get_model("order", "BillingAddress")
ShippingAddressModel = get_model("order", "ShippingAddress")
CountryModel = get_model("address", "Country")


class CountryToResource(odin.Mapping):
"""Mapping from country model to resource."""

from_obj = CountryModel
to_obj = resources.address.Country


class BillingAddressToResource(odin.Mapping):
"""Mapping from billing address model to resource."""

from_obj = BillingAddressModel
to_obj = resources.address.BillingAddress

@odin.assign_field
def country(self) -> resources.address.Country:
"""Map country."""
return CountryToResource.apply(self.source.country)


class ShippingAddressToResource(odin.Mapping):
"""Mapping from shipping address model to resource."""

from_obj = ShippingAddressModel
to_obj = resources.address.ShippingAddress

@odin.assign_field
def country(self) -> resources.address.Country:
"""Map country."""
return CountryToResource.apply(self.source.country)
16 changes: 16 additions & 0 deletions oscar_odin/mappings/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Mappings between odin and django auth models."""
import odin
from oscar.core.loading import get_model

from .. import resources

__all__ = ("UserToResource",)

UserModel = get_model("auth", "User")


class UserToResource(odin.Mapping):
"""Mapping from user model to resource."""

from_obj = UserModel
to_obj = resources.auth.User
105 changes: 105 additions & 0 deletions oscar_odin/mappings/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

import odin
from django.contrib.auth.models import AbstractUser
from django.db import transaction
from django.db.models import QuerySet
from django.db.models.fields.files import ImageFieldFile
from django.http import HttpRequest
from oscar.apps.partner.strategy import Default as DefaultStrategy
from oscar.core.loading import get_class, get_model

from datetime import datetime

from .. import resources
from ..resources.catalogue import Structure
from ._common import map_queryset
from ._model_mapper import ModelMapping

__all__ = (
"ProductImageToResource",
Expand Down Expand Up @@ -43,6 +47,26 @@ def original(self, value: ImageFieldFile) -> str:
return value.url


class ProductImageToModel(odin.Mapping):
"""Map from an image resource to a model."""

from_obj = resources.catalogue.Image
to_obj = ProductImageModel

@odin.map_field
def original(self, value: str) -> str:
"""Convert value into a pure URL."""
# TODO convert into a form that can be accepted by a model
return value

@odin.map_field
def date_created(self, value: datetime) -> datetime:
if value:
return value

return datetime.now()


class CategoryToResource(odin.Mapping):
"""Map from a category model to a resource."""

Expand All @@ -62,13 +86,33 @@ def image(self, value: ImageFieldFile) -> Optional[str]:
return value.url


class CategoryToModel(odin.Mapping):
"""Map from a category resource to a model."""

from_obj = resources.catalogue.Category
to_obj = CategoryModel

@odin.map_field
def image(self, value: Optional[str]) -> Optional[str]:
"""Convert value into a pure URL."""
# TODO convert into a form that can be accepted by a model
return value


class ProductClassToResource(odin.Mapping):
"""Map from a product class model to a resource."""

from_obj = ProductClassModel
to_obj = resources.catalogue.ProductClass


class ProductClassToModel(odin.Mapping):
"""Map from a product class resource to a model."""

from_obj = resources.catalogue.ProductClass
to_obj = ProductClassModel


class ProductToResource(odin.Mapping):
"""Map from a product model to a resource."""

Expand Down Expand Up @@ -172,6 +216,32 @@ def map_stock_price(self) -> Tuple[Decimal, str, int]:
return Decimal(0), "", 0


class ProductToModel(ModelMapping):
"""Map from a product resource to a model."""

from_obj = resources.catalogue.Product
to_obj = ProductModel

@odin.map_list_field
def images(self, values) -> List[ProductImageModel]:
"""Map related image. We save these later in bulk"""
return ProductImageToModel.apply(values)

@odin.map_list_field
def children(self, values) -> List[ProductModel]:
"""Map related image."""
return []

@odin.map_field(from_field="product_class", to_field="product_class_id")
def product_class_id(self, value) -> ProductClassModel:
return ProductClassToModel.apply(value)

# @odin.assign_field
# def categories(self) -> List[CategoryModel]:
# """Map related categories."""
# return list(CategoryToModel.apply(self.source.categories, context=self.context))


def product_to_resource_with_strategy(
product: Union[ProductModel, Iterable[ProductModel]],
stock_strategy: DefaultStrategy,
Expand Down Expand Up @@ -249,3 +319,38 @@ def product_queryset_to_resources(
return product_to_resource(
query_set, request, user, include_children=include_children, **kwargs
)


def product_to_model(
product: resources.catalogue.Product,
) -> ProductModel:
"""Map a product resource to a model."""

return ProductToModel.apply(product)


def product_to_db(
product: resources.catalogue.Product,
) -> ProductModel:
"""Map a product resource to a model and store in the database.

The method will handle the nested database saves required to store the entire resource
within a single transaction.
"""
obj = product_to_model(product)

context = obj.odin_context

with transaction.atomic():
for fk_name, fk_attname, fk_instance in context.get("foreign_key_items", []):
fk_instance.save()
setattr(obj, fk_name, fk_instance.pk)

obj.save()

for mtm_name, mtm_attname, instances in context.get("many_to_many_items", []):
for mtm_instance in instances:
setattr(mtm_instance, mtm_attname, obj.pk)
mtm_instance.save()

return obj
Loading
Loading