diff --git a/.gitignore b/.gitignore index 1109375..5f744c7 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,4 @@ dmypy.json .idea .DS_Store *.sublime-workspace -.vscode \ No newline at end of file +.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 c46c280..94b57ed 100644 --- a/djantic/main.py +++ b/djantic/main.py @@ -43,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) diff --git a/tests/test_main.py b/tests/test_main.py index aed9a12..7581d15 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -11,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 index e20d412..d6c24cb 100644 --- a/tests/test_multiple_level_relations.py +++ b/tests/test_multiple_level_relations.py @@ -3,7 +3,7 @@ from typing import List import pytest -from testapp.order import Order, OrderItem, OrderItemDetail, OrderUser, OrderUserFactory +from testapp.order import Order, OrderItem, OrderItemDetail, OrderUser, OrderUserFactory, OrderUserProfile from djantic import ModelSchema @@ -26,8 +26,14 @@ class OrderSchema(ModelSchema): class Config: model = Order + class OrderUserProfileSchema(ModelSchema): + + class Config: + model = OrderUserProfile + class OrderUserSchema(ModelSchema): orders: List[OrderSchema] + profile: OrderUserProfileSchema class Config: model = OrderUser @@ -39,6 +45,11 @@ class Config: 'first_name': '', 'last_name': None, 'email': '', + 'profile': { + 'id': 1, + 'address': '', + 'user': 1 + }, 'orders': [ { 'id': 1, @@ -152,6 +163,9 @@ class Config: "description": "OrderUser(id, first_name, last_name, email)", "type": "object", "properties": { + "profile": { + "$ref": "#/definitions/OrderUserProfileSchema" + }, "orders": { "title": "Orders", "type": "array", @@ -184,11 +198,39 @@ class Config: } }, "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)", diff --git a/tests/testapp/order.py b/tests/testapp/order.py index 162b871..7089991 100644 --- a/tests/testapp/order.py +++ b/tests/testapp/order.py @@ -8,6 +8,12 @@ class OrderUser(models.Model): 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) @@ -81,6 +87,12 @@ def items(self, create, items, **kwargs): for i in range(0, 2)] +class OrderUserProfileFactory(DjangoModelFactory): + + class Meta: + model = OrderUserProfile + + class OrderUserFactory(DjangoModelFactory): class Meta: @@ -90,3 +102,8 @@ class Meta: 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)