Skip to content
Open
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
11 changes: 6 additions & 5 deletions ninja/orm/factory.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import itertools
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type, Union, cast
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type, Union

from django.db.models import Field as DjangoField
from django.db.models import ManyToManyRel, ManyToOneRel, Model
from django.db.models import ForeignObjectRel, Model
from pydantic import create_model as create_pydantic_model

from ninja.errors import ConfigError
Expand Down Expand Up @@ -158,10 +158,11 @@ def _selected_model_fields(
def _model_fields(self, model: Type[Model]) -> Iterator[DjangoField]:
"returns iterator with all the fields that can be part of schema"
for fld in model._meta.get_fields():
if isinstance(fld, (ManyToOneRel, ManyToManyRel)):
# skipping relations
if isinstance(fld, ForeignObjectRel):
# skipping reverse relations (ManyToOneRel, ManyToManyRel, and the
# bare ForeignObjectRel produced by a ForeignObject field)
continue
yield cast(DjangoField, fld)
yield fld


factory = SchemaFactory()
Expand Down
32 changes: 32 additions & 0 deletions tests/test_orm_relations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db import models
from django.test.utils import isolate_apps

from ninja import NinjaAPI
from ninja.orm import create_schema
Expand Down Expand Up @@ -35,3 +36,34 @@ def post_with_m2m(request, payload: WithM2MSchema):
response = client.post("/bar", json={"m2m": []})
assert response.status_code == 200, str(response.json())
assert response.json() == {"m2m": []}


@isolate_apps("tests")
def test_reverse_foreign_object_relation_is_skipped():
"""A ForeignObject's reverse accessor is a bare ForeignObjectRel. Building a
schema for the referenced model must skip it like any other reverse relation
instead of crashing on the missing ``help_text`` attribute (see #1530)."""

class Order(models.Model):
class Meta:
app_label = "tests"

class OrderDetail(models.Model):
order_id = models.PositiveIntegerField()
order = models.ForeignObject(
Order,
on_delete=models.CASCADE,
from_fields=["order_id"],
to_fields=["id"],
related_name="details",
)

class Meta:
app_label = "tests"

# Order gets a reverse ForeignObjectRel "details"; schema generation must not raise.
OrderSchema = create_schema(Order)

# The reverse relation is skipped, so it does not appear as a schema field.
assert "details" not in OrderSchema.model_fields
assert "id" in OrderSchema.model_fields