diff --git a/.gitignore b/.gitignore index 28736b2..5f744c7 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,5 @@ dmypy.json .idea .DS_Store -*.sublime-workspace \ No newline at end of file +*.sublime-workspace +.vscode diff --git a/README.md b/README.md index 4782a49..e1a6f2d 100644 --- a/README.md +++ b/README.md @@ -114,4 +114,112 @@ print(user_schema.json(indent=2)) } ``` -See https://pydantic-docs.helpmanual.io/usage/exporting_models/ for more. \ No newline at end of file +See https://pydantic-docs.helpmanual.io/usage/exporting_models/ for more. + +### Use multiple level relations + +Djantic supports multiple level relations. Given the following models: + +```python +class OrderUser(models.Model): + email = models.EmailField(unique=True) + + +class OrderUserProfile(models.Model): + address = models.CharField(max_length=255) + user = models.OneToOneField(OrderUser, on_delete=models.CASCADE, related_name='profile') + + +class Order(models.Model): + total_price = models.DecimalField(max_digits=8, decimal_places=5, default=0) + user = models.ForeignKey( + OrderUser, on_delete=models.CASCADE, related_name="orders" + ) + + +class OrderItem(models.Model): + price = models.DecimalField(max_digits=8, decimal_places=5, default=0) + quantity = models.IntegerField(default=0) + order = models.ForeignKey( + Order, on_delete=models.CASCADE, related_name="items" + ) + + +class OrderItemDetail(models.Model): + name = models.CharField(max_length=30) + order_item = models.ForeignKey( + OrderItem, on_delete=models.CASCADE, related_name="details" + ) +``` + +Inverse ForeignKey relation (or M2M relation) type is a list of the Schema of this related object. + +OneToOne relation type is the Schema of this related object. + +```python +class OrderItemDetailSchema(ModelSchema): + class Config: + model = OrderItemDetail + +class OrderItemSchema(ModelSchema): + details: List[OrderItemDetailSchema] + + class Config: + model = OrderItem + +class OrderSchema(ModelSchema): + items: List[OrderItemSchema] + + class Config: + model = Order + +class OrderUserProfileSchema(ModelSchema): + class Config: + model = OrderUserProfile + +class OrderUserSchema(ModelSchema): + orders: List[OrderSchema] + profile: OrderUserProfileSchema +``` + +**Calling:** + +```python +user = OrderUser.objects.first() +print(OrderUserSchema.from_orm(user).json(ident=4)) +``` + +**Output:** +```json +{ + "profile": { + "id": 1, + "address": "", + "user": 1 + }, + "orders": [ + { + "items": [ + { + "details": [ + { + "id": 1, + "name": "", + "order_item": 1 + } + ], + "id": 1, + "price": 0.0, + "quantity": 0, + "order": 1 + } + ], + "id": 1, + "total_price": 0.0, + "user": 1 + } + ], + "id": 1, + "email": "" +} +``` \ No newline at end of file diff --git a/djantic/main.py b/djantic/main.py index 3761bd2..94b57ed 100644 --- a/djantic/main.py +++ b/djantic/main.py @@ -1,19 +1,20 @@ -from inspect import isclass +from functools import reduce from itertools import chain -from typing import Optional, Union, Any, List, no_type_check +from typing import Any, Dict, List, Optional, no_type_check -from pydantic import BaseModel, create_model, validate_model, ConfigError -from pydantic.main import ModelMetaclass - - -import django -from django.utils.functional import Promise -from django.utils.encoding import force_str from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import Manager, Model +from django.db.models.fields.files import ImageFieldFile +from django.db.models.fields.reverse_related import (ForeignObjectRel, + OneToOneRel) +from django.utils.encoding import force_str +from django.utils.functional import Promise +from pydantic import BaseModel, ConfigError, create_model +from pydantic.main import ModelMetaclass +from pydantic.utils import GetterDict from .fields import ModelSchemaField - _is_base_model_class_defined = False @@ -42,7 +43,13 @@ def __new__( and base == ModelSchema ): - config = namespace["Config"] + try: + config = namespace["Config"] + except KeyError as exc: + raise ConfigError( + f"{exc} (Is `Config` class defined?)" + ) + include = getattr(config, "include", None) exclude = getattr(config, "exclude", None) @@ -61,13 +68,17 @@ def __new__( f"{exc} (Is `Config.model` a valid Django model class?)" ) + if include is None and exclude is None: + cls.__config__.include = [f.name for f in fields] + field_values = {} _seen = set() for field in chain(fields, annotations.copy()): - field_name = getattr( - field, "name", getattr(field, "related_name", field) - ) + if issubclass(field.__class__, ForeignObjectRel) and not issubclass(field.__class__, OneToOneRel): + field_name = getattr(field, "related_name", None) or f"{field.name}_set" + else: + field_name = getattr(field, "name", field) if ( field_name in _seen @@ -107,14 +118,43 @@ def __new__( name, __base__=cls, __module__=cls.__module__, **field_values ) - setattr(model_schema, "instance", None) - return model_schema return cls +class ProxyGetterNestedObj(GetterDict): + def __init__(self, obj: Any, schema_class): + self._obj = obj + self.schema_class = schema_class + + def get(self, key: Any, default: Any = None) -> Any: + if "__" in key: + # Allow double underscores aliases: `first_name: str = Field(alias="user__first_name")` + keys_map = key.split("__") + attr = reduce(lambda a, b: getattr(a, b, default), keys_map, self._obj) + outer_type_ = self.schema_class.__fields__["user"].outer_type_ + else: + attr = getattr(self._obj, key) + outer_type_ = self.schema_class.__fields__[key].outer_type_ + + is_manager = issubclass(attr.__class__, Manager) + + if is_manager and outer_type_ == List[Dict[str, int]]: + attr = list(attr.all().values("id")) + elif is_manager: + attr = list(attr.all()) + elif outer_type_ == int and issubclass(type(attr), Model): + attr = attr.id + elif issubclass(attr.__class__, ImageFieldFile) and issubclass(outer_type_, str): + attr = attr.name + return attr + + class ModelSchema(BaseModel, metaclass=ModelSchemaMetaclass): + class Config: + orm_mode = True + @classmethod def schema_json( cls, @@ -131,145 +171,30 @@ def schema_json( @classmethod @no_type_check def get_field_names(cls) -> List[str]: - model_fields = [field.name for field in cls.__config__.model._meta.get_fields()] - if hasattr(cls.__config__, "include"): - model_fields = [ - name for name in model_fields if name in cls.__config__.include + if hasattr(cls.__config__, "exclude"): + django_model_fields = cls.__config__.model._meta.get_fields() + all_fields = [f.name for f in django_model_fields] + return [ + name for name in all_fields if name not in cls.__config__.exclude ] - elif hasattr(cls.__config__, "exclude"): - model_fields = [ - name for name in model_fields if name not in cls.__config__.exclude - ] - - return model_fields + return cls.__config__.include @classmethod - def _get_object_model(cls, obj_data: dict) -> "ModelSchema": - values, fields_set, validation_error = validate_model(cls, obj_data) - if validation_error: # pragma: nocover - raise validation_error - - model_schema = cls.__new__(cls) - object.__setattr__(model_schema, "__dict__", values) - object.__setattr__(model_schema, "__fields_set__", fields_set) - - return model_schema + def from_orm(cls, *args, **kwargs): + return cls.from_django(*args, **kwargs) @classmethod - def from_django( - cls, - instance: Union[django.db.models.Model, django.db.models.QuerySet], - many: bool = False, - store: bool = True, - ) -> Union["ModelSchema", list]: - - if not many: - obj_data = {} - annotations = cls.__annotations__ - fields = [ - field - for field in instance._meta.get_fields() - if field.name in cls.get_field_names() - ] - for field in fields: - schema_cls = None - related_field_names = None - - # Check if this field is a related model schema to handle the data - # according to specific schema rules. - if ( - field.name in annotations - and isclass(cls.__fields__[field.name].type_) - and issubclass(cls.__fields__[field.name].type_, ModelSchema) - ): - schema_cls = cls.__fields__[field.name].type_ - related_field_names = schema_cls.get_field_names() - - if not field.concrete and field.auto_created: - accessor_name = field.get_accessor_name() - related_obj = getattr(instance, accessor_name, None) - if field.one_to_many: - related_qs = related_obj.all() - - if schema_cls: - related_obj_data = [ - schema_cls.construct(**obj_vals) - for obj_vals in related_qs.values(*related_field_names) - ] - - else: - related_obj_data = list(related_obj.all().values("id")) - - elif field.one_to_one: - if schema_cls: - related_obj_data = schema_cls.construct( - **{ - name: getattr(related_obj, name) - for name in related_field_names - } - ) - else: - related_obj_data = related_obj.pk - - elif field.many_to_many: - related_qs = getattr(instance, accessor_name) - if schema_cls: - related_obj_data = [ - schema_cls.construct(**obj_vals) - for obj_vals in related_qs.values(*related_field_names) - ] - else: - related_obj_data = list(related_qs.values("pk")) - - obj_data[accessor_name] = related_obj_data - - elif field.one_to_many or field.many_to_many: - related_qs = getattr(instance, field.name) - if schema_cls: - - # FIXME: This seems incorrect, should probably handle generic - # relations specifically. - related_fields = [ - field - for field in related_field_names - if field != "content_object" - ] - related_obj_data = [ - schema_cls.construct(**obj_vals) - for obj_vals in related_qs.values(*related_fields) - ] - else: - related_obj_data = list(related_qs.values("pk")) - - obj_data[field.name] = related_obj_data - - elif field.many_to_one: - related_obj = getattr(instance, field.name) - if schema_cls: - related_obj_data = schema_cls.from_django(related_obj).dict() - else: - related_obj_data = field.value_from_object(instance) - obj_data[field.name] = related_obj_data - - else: - # Handle field and image fields. - field_data = field.value_from_object(instance) - if hasattr(field_data, "field"): - field_data = str(field_data.field.value_from_object(instance)) - obj_data[field.name] = field_data - - model_schema = cls._get_object_model(obj_data) - - if store: - cls.instance = instance - - return model_schema - - model_schema_qs = [ - cls.from_django(obj, store=False, many=False) for obj in instance - ] - - return model_schema_qs + def from_django(cls, objs, many=False, context={}): + cls.context = context + if many: + result_objs = [] + for obj in objs: + cls.instance = obj + result_objs.append(super().from_orm(ProxyGetterNestedObj(obj, cls))) + return result_objs + + cls.instance = objs + return super().from_orm(ProxyGetterNestedObj(objs, cls)) _is_base_model_class_defined = True diff --git a/tests/test_fields.py b/tests/test_fields.py index c8d43ee..074b532 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,6 +1,6 @@ import pytest +from testapp.models import Configuration, Listing, Preference, Record, Searchable -from testapp.models import Record, Configuration, Preference, Searchable, Listing from djantic import ModelSchema diff --git a/tests/test_files.py b/tests/test_files.py index 1ea91f7..60f8b6c 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,8 +1,8 @@ from tempfile import NamedTemporaryFile import pytest - from testapp.models import Attachment + from djantic import ModelSchema diff --git a/tests/test_main.py b/tests/test_main.py index 8b04f80..7581d15 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,8 +1,7 @@ import pytest - +from pydantic import ConfigError from testapp.models import User -from pydantic import ConfigError from djantic import ModelSchema @@ -12,6 +11,13 @@ def test_config_errors(): Test the model config error exceptions. """ + with pytest.raises( + ConfigError, match="(Is `Config` class defined?)" + ): + + class InvalidModelErrorSchema(ModelSchema): + pass + with pytest.raises( ConfigError, match="(Is `Config.model` a valid Django model class?)" ): diff --git a/tests/test_multiple_level_relations.py b/tests/test_multiple_level_relations.py new file mode 100644 index 0000000..d6c24cb --- /dev/null +++ b/tests/test_multiple_level_relations.py @@ -0,0 +1,362 @@ + +from decimal import Decimal +from typing import List + +import pytest +from testapp.order import Order, OrderItem, OrderItemDetail, OrderUser, OrderUserFactory, OrderUserProfile + +from djantic import ModelSchema + + +@pytest.mark.django_db +def test_multiple_level_relations(): + class OrderItemDetailSchema(ModelSchema): + class Config: + model = OrderItemDetail + + class OrderItemSchema(ModelSchema): + details: List[OrderItemDetailSchema] + + class Config: + model = OrderItem + + class OrderSchema(ModelSchema): + items: List[OrderItemSchema] + + class Config: + model = Order + + class OrderUserProfileSchema(ModelSchema): + + class Config: + model = OrderUserProfile + + class OrderUserSchema(ModelSchema): + orders: List[OrderSchema] + profile: OrderUserProfileSchema + + class Config: + model = OrderUser + + user = OrderUserFactory.create() + + assert OrderUserSchema.from_django(user).dict() == { + 'id': 1, + 'first_name': '', + 'last_name': None, + 'email': '', + 'profile': { + 'id': 1, + 'address': '', + 'user': 1 + }, + 'orders': [ + { + 'id': 1, + 'total_price': Decimal('0.00000'), + 'shipping_address': '', + 'user': 1, + 'items': [ + { + 'id': 1, + 'name': '', + 'price': Decimal('0.00000'), + 'quantity': 0, + 'order': 1, + 'details': [ + { + 'id': 1, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 1 + }, + { + 'id': 2, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 1 + } + ] + }, + { + 'details': [ + { + 'id': 3, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 2 + }, + { + 'id': 4, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 2 + } + ], + 'id': 2, + 'name': '', + 'price': Decimal('0.00000'), + 'quantity': 0, + 'order': 1 + } + ], + + }, + { + 'id': 2, + 'total_price': Decimal('0.00000'), + 'shipping_address': '', + 'user': 1, + 'items': [ + { + 'id': 3, + 'name': '', + 'price': Decimal('0.00000'), + 'quantity': 0, + 'order': 2, + 'details': [ + { + 'id': 5, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 3}, + { + 'id': 6, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 3} + ] + }, + { + 'id': 4, + 'name': '', + 'price': Decimal('0.00000'), + 'quantity': 0, + 'order': 2, + 'details': [ + { + 'id': 7, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 4}, + { + 'id': 8, + 'name': '', + 'value': 0, + 'quantity': 0, + 'order_item': 4}] + } + ] + } + ] + } + + assert OrderUserSchema.schema() == { + "title": "OrderUserSchema", + "description": "OrderUser(id, first_name, last_name, email)", + "type": "object", + "properties": { + "profile": { + "$ref": "#/definitions/OrderUserProfileSchema" + }, + "orders": { + "title": "Orders", + "type": "array", + "items": { + "$ref": "#/definitions/OrderSchema" + } + }, + "id": { + "title": "Id", + "description": "id", + "type": "integer" + }, + "first_name": { + "title": "First Name", + "description": "first_name", + "maxLength": 50, + "type": "string" + }, + "last_name": { + "title": "Last Name", + "description": "last_name", + "maxLength": 50, + "type": "string" + }, + "email": { + "title": "Email", + "description": "email", + "maxLength": 254, + "type": "string" + } + }, + "required": [ + "profile", + "orders", + "first_name", + "email" + ], + "definitions": { + "OrderUserProfileSchema": { + "title": "OrderUserProfileSchema", + "description": "OrderUserProfile(id, address, user)", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "id", + "type": "integer" + }, + "address": { + "title": "Address", + "description": "address", + "maxLength": 255, + "type": "string" + }, + "user": { + "title": "User", + "description": "id", + "type": "integer" + } + }, + "required": [ + "address", + "user" + ] + }, + "OrderItemDetailSchema": { + "title": "OrderItemDetailSchema", + "description": "OrderItemDetail(id, name, value, quantity, order_item)", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "id", + "type": "integer" + }, + "name": { + "title": "Name", + "description": "name", + "maxLength": 30, + "type": "string" + }, + "value": { + "title": "Value", + "description": "value", + "default": 0, + "type": "integer" + }, + "quantity": { + "title": "Quantity", + "description": "quantity", + "default": 0, + "type": "integer" + }, + "order_item": { + "title": "Order Item", + "description": "id", + "type": "integer" + } + }, + "required": [ + "name", + "order_item" + ] + }, + "OrderItemSchema": { + "title": "OrderItemSchema", + "description": "OrderItem(id, name, price, quantity, order)", + "type": "object", + "properties": { + "details": { + "title": "Details", + "type": "array", + "items": { + "$ref": "#/definitions/OrderItemDetailSchema" + } + }, + "id": { + "title": "Id", + "description": "id", + "type": "integer" + }, + "name": { + "title": "Name", + "description": "name", + "maxLength": 30, + "type": "string" + }, + "price": { + "title": "Price", + "description": "price", + "default": 0, + "type": "number" + }, + "quantity": { + "title": "Quantity", + "description": "quantity", + "default": 0, + "type": "integer" + }, + "order": { + "title": "Order", + "description": "id", + "type": "integer" + } + }, + "required": [ + "details", + "name", + "order" + ] + }, + "OrderSchema": { + "title": "OrderSchema", + "description": "Order(id, total_price, shipping_address, user)", + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "array", + "items": { + "$ref": "#/definitions/OrderItemSchema" + } + }, + "id": { + "title": "Id", + "description": "id", + "type": "integer" + }, + "total_price": { + "title": "Total Price", + "description": "total_price", + "default": 0, + "type": "number" + }, + "shipping_address": { + "title": "Shipping Address", + "description": "shipping_address", + "maxLength": 255, + "type": "string" + }, + "user": { + "title": "User", + "description": "id", + "type": "integer" + } + }, + "required": [ + "items", + "shipping_address", + "user" + ] + } + } + } diff --git a/tests/test_queries.py b/tests/test_queries.py index e59acc0..735ee62 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -1,9 +1,7 @@ from typing import List import pytest - - -from testapp.models import User, Profile, Thread, Message, Tagged, Bookmark +from testapp.models import Bookmark, Message, Profile, Tagged, Thread, User from djantic import ModelSchema @@ -49,6 +47,7 @@ class Config: "id": 1, "tags": [ { + 'content_object': 1, "content_type": 20, "id": 1, "object_id": 1, @@ -190,7 +189,8 @@ class Config: model = Thread thread_schema_qs = ThreadSchema.from_django(threads, many=True) - assert thread_schema_qs == [ + thread_schemas = [t.dict() for t in thread_schema_qs] + assert thread_schemas == [ { "messages": [{"id": 2}, {"id": 4}, {"id": 6}], "id": 2, @@ -256,5 +256,5 @@ class Config: schema.dict() == { "id": 1, "url": "https://github.com", - "tags": [{"pk": 1}, {"pk": 2}], + "tags": [{"id": 1}, {"id": 2}], } diff --git a/tests/test_relations.py b/tests/test_relations.py index 66740ec..25ed086 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,20 +1,20 @@ import datetime -from typing import List, Dict, Optional +from typing import Dict, List, Optional import pytest - +from pydantic import Field from testapp.models import ( - User, - Profile, - Thread, - Message, - Publication, Article, - Item, - Tagged, Bookmark, - Expert, Case, + Expert, + Item, + Message, + Profile, + Publication, + Tagged, + Thread, + User ) from djantic import ModelSchema @@ -102,8 +102,8 @@ class Config: "description": "A news publication.", "type": "object", "properties": { - "article": { - "title": "Article", + "article_set": { + "title": "Article Set", "description": "id", "type": "array", "items": { @@ -135,7 +135,7 @@ class Config: "id": 1, "headline": "My Headline", "pub_date": datetime.date(2021, 3, 20), - "publications": [{"article": 1, "id": 1, "title": "My Publication"}], + "publications": [{"article_set": [{'id': 1}], "id": 1, "title": "My Publication"}], } @@ -783,12 +783,12 @@ class Config: case_schema = CaseSchema.from_django(case) expert_schema = ExpertSchema.from_django(expert) assert case_schema.dict() == { - "related_experts": [{"pk": 1}], + "related_experts": [{"id": 1}], "id": 1, "name": "My Case", "details": "Some text data.", } - assert expert_schema.dict() == {"id": 1, "name": "My Expert", "cases": [{"pk": 1}]} + assert expert_schema.dict() == {"id": 1, "name": "My Expert", "cases": [{"id": 1}]} class CustomExpertSchema(ModelSchema): """Custom schema""" @@ -849,8 +849,62 @@ class Config: case_schema = CaseSchema.from_django(case) assert case_schema.dict() == { - "related_experts": [{"id": 1, "name": "My Expert", "cases": 1}], + "related_experts": [{"id": 1, "name": "My Expert", "cases": [{'id': 1}]}], "id": 1, "name": "My Case", "details": "Some text data.", } + + +@pytest.mark.django_db +def test_alias(): + class ProfileSchema(ModelSchema): + first_name: str = Field(alias='user__first_name') + + class Config: + model = Profile + + assert ProfileSchema.schema() == { + 'title': 'ProfileSchema', + 'description': "A user's profile.", + 'type': 'object', + 'properties': { + 'id': { + 'title': 'Id', + 'description': 'id', + 'type': 'integer' + }, + 'user': { + 'title': 'User', + 'description': 'id', + 'type': 'integer' + }, + 'website': { + 'title': 'Website', + 'description': 'website', + 'default': '', 'maxLength': 200, + 'type': 'string' + }, + 'location': { + 'title': 'Location', + 'description': 'location', + 'default': '', + 'maxLength': 100, + 'type': 'string' + }, + 'user__first_name': { + 'title': 'User First Name', + 'type': 'string' + } + }, + 'required': ['user', 'user__first_name'] + } + + user = User.objects.create(first_name="Jack") + profile = Profile.objects.create( + user=user, website='www.github.com', location='Europe') + assert ProfileSchema.from_django(profile).dict() == {'first_name': 'Jack', + 'id': 1, + 'location': 'Europe', + 'user': 1, + 'website': 'www.github.com'} diff --git a/tests/testapp/order.py b/tests/testapp/order.py new file mode 100644 index 0000000..7089991 --- /dev/null +++ b/tests/testapp/order.py @@ -0,0 +1,109 @@ +import factory +from django.db import models +from factory.django import DjangoModelFactory + + +class OrderUser(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50, null=True, blank=True) + email = models.EmailField(unique=True) + + +class OrderUserProfile(models.Model): + address = models.CharField(max_length=255) + user = models.OneToOneField(OrderUser, on_delete=models.CASCADE, related_name='profile') + + +class Order(models.Model): + total_price = models.DecimalField(max_digits=8, decimal_places=5, default=0) + shipping_address = models.CharField(max_length=255) + user = models.ForeignKey( + OrderUser, on_delete=models.CASCADE, related_name="orders" + ) + + class Meta: + ordering = ["total_price"] + + def __str__(self): + return f'{self.order.id} - {self.name}' + + +class OrderItem(models.Model): + name = models.CharField(max_length=30) + price = models.DecimalField(max_digits=8, decimal_places=5, default=0) + quantity = models.IntegerField(default=0) + order = models.ForeignKey( + Order, on_delete=models.CASCADE, related_name="items" + ) + + class Meta: + ordering = ["order"] + + def __str__(self): + return f'{self.order.id} - {self.name}' + + +class OrderItemDetail(models.Model): + name = models.CharField(max_length=30) + value = models.IntegerField(default=0) + quantity = models.IntegerField(default=0) + order_item = models.ForeignKey( + OrderItem, on_delete=models.CASCADE, related_name="details" + ) + + class Meta: + ordering = ["order_item"] + + def __str__(self): + return f'{self.order_item.id} - {self.name}' + + +class OrderItemDetailFactory(DjangoModelFactory): + + class Meta: + model = OrderItemDetail + + +class OrderItemFactory(DjangoModelFactory): + + class Meta: + model = OrderItem + + @factory.post_generation + def details(self, create, details, **kwargs): + if details is None: + details = [OrderItemDetailFactory.create(order_item=self, **kwargs) for i in range(0, 2)] + + +class OrderFactory(DjangoModelFactory): + + class Meta: + model = Order + + @factory.post_generation + def items(self, create, items, **kwargs): + if items is None: + items = [OrderItemFactory.create(order=self, **kwargs) + for i in range(0, 2)] + + +class OrderUserProfileFactory(DjangoModelFactory): + + class Meta: + model = OrderUserProfile + + +class OrderUserFactory(DjangoModelFactory): + + class Meta: + model = OrderUser + + @factory.post_generation + def orders(self, create, orders, **kwargs): + if orders is None: + orders = [OrderFactory.create(user=self, **kwargs) for i in range(0, 2)] + + @factory.post_generation + def profile(self, create, profile, **kwargs): + if profile is None: + profile = OrderUserProfileFactory.create(user=self, **kwargs) diff --git a/tox.ini b/tox.ini index 9c07a90..1d34238 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,7 @@ deps = django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 + factory-boy setenv = PYTHONPATH = {toxinidir} commands =