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

Support base class relations and reverse for proxy models #1380

Conversation

TomsOverBaghdad
Copy link
Contributor

Includes the proxy's base model when determining the reverse fields for a django model

The potential fix for this issue #1379

@firaskafri
Copy link
Collaborator

It would be great if the solution would be more generic to handle possible base classes such as django-polymorphic

    for base in model.__bases__:
        if is_valid_django_model(base):
            model_ancestry.append(base)

as opposed to:

    if model._meta.proxy:
        model_ancestry.append(model._meta.proxy_for_model)

@TomsOverBaghdad
Copy link
Contributor Author

@firaskafri Thanks for the suggestion! I implemented it with tests. Let me know if the test changes imply correctly what you were expecting.

@firaskafri
Copy link
Collaborator

@TomsOverBaghdad Will definitely do!

Though I wanted to ask you, what the behavior would be if base class had an m2m field, not a reverse one?

@TomsOverBaghdad
Copy link
Contributor Author

@firaskafri if base class A has many B then class C(A) that inherits from A will also have many B, I updated the query_test to show.

@firaskafri
Copy link
Collaborator

firaskafri commented Jan 10, 2023

@firaskafri if base class A has many B then class C(A) that inherits from A will also have many B, I updated the query_test to show.

Yes reverse m2m is working correctly with this PR. But have you got the chance to deal with base m2m relation?

This PR addresses the reverse relation issue correctly I guess where if Z has an m2m with A, A and `B(A) will have the field added to their nodes.

But the example you provided is different, you are assuming that the field exists in A as an original field not a reverse relation.

related = getattr(attr, "rel", None) or getattr(attr, "related", None)
if isinstance(related, models.ManyToOneRel):
yield (name, related)
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TomsOverBaghdad this being inside a loop for base classes will break relations in the case of m2m, I think its safer to isolate getting base fields in a different method as in this case ManyToManyRel is two sided, in the case of A it is ManyToManyRel.related while in the case of B(A) it is ManyToManyRel.field. Check this out

Try having the m2m field in the base class just like the example you mentioned in the comment, not a reverse relation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this:

elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
    if _model is not model:
        yield (name, getattr(attr, "field", None))
        continue

But my preference would still be to get base models relations and reverse relations in a different method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heard! Good catch @firaskafri ! Let me know what you think of the changes I pushed. I separated out getting local fields and included the models ancestry similar to the reverse fields.

Returns a tuple of (field.name, field)
"""
local_fields = get_local_fields(model)
local_field_names = {field[0] for field in local_fields}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to a set, but I can totally change it back to list to not break anything. It looks like get_reverse_fields is public so I didn't want to mess with the signature too much.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like that (switching to a set here) was the right move to me, since you were able to keep the get_reverse_fields signature the same but improve lookup time-complexity for this internal use-case in get_model_fields; nice!

@firaskafri firaskafri changed the title Support reverse relationship for proxy models Support base class relations and reverse for proxy models Jan 11, 2023
@TomsOverBaghdad
Copy link
Contributor Author

@firaskafri is there anything else I can do for this PR based on the changes requested tag? Seems like the failing tests are expected based on failing with previous merged prs.

@firaskafri
Copy link
Collaborator

@firaskafri is there anything else I can do for this PR based on the changes requested tag? Seems like the failing tests are expected based on failing with previous merged prs.

I just updated the tags. We're just waiting for more reviews.

@firaskafri firaskafri requested review from kiendang and jaw9c May 3, 2023 10:25
@jaw9c
Copy link
Collaborator

jaw9c commented May 3, 2023

Great PR - would it be possible to use something from the Django lib like this to pull the fields from the model rather than rolling our own?

@firaskafri
Copy link
Collaborator

firaskafri commented May 4, 2023

Great PR - would it be possible to use something from the Django lib like this to pull the fields from the model rather than rolling our own?

I investigated that, the problem with get_fields is that it does not get the local ManyToMany relationships.
@TomsOverBaghdad any idea if this could be done?

Copy link
Collaborator

@sjdemartini sjdemartini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Appreciate the thorough tests, and your effort to make the functions in utils.py better documented and more modular.

Returns a tuple of (field.name, field)
"""
local_fields = get_local_fields(model)
local_field_names = {field[0] for field in local_fields}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like that (switching to a set here) was the right move to me, since you were able to keep the get_reverse_fields signature the same but improve lookup time-complexity for this internal use-case in get_model_fields; nice!

@firaskafri firaskafri merged commit b1abebd into graphql-python:main Jul 18, 2023
1 of 13 checks passed
superlevure pushed a commit to loft-orbital/graphene-django that referenced this pull request Jul 19, 2023
…thon#1380)

* support reverse relationship for proxy models

* support multi table inheritence

* update query test for multi table inheritance

* remove debugger

* support local many to many in model inheritance

* format and lint

---------

Co-authored-by: Firas K <3097061+firaskafri@users.noreply.github.com>
@lucas-bremond
Copy link
Contributor

lucas-bremond commented Jul 21, 2023

I believe this PR may have introduced a "regression" when combined with django-polymorphic.

In Django Polymorphic, the parent type exposes accessors to the derived types, which are now picked up by the updated get_reverse_fields logic and added to the derived type fields.

Here's an example of what I mean. Assuming the following models:

from polymorphic.models import PolymorphicModel

class Vehicle(PolymorphicModel):
    id = UUIDField()

class Car(Vehicle):
    ...

class Truck(Vehicle):
    ...

now the Car and Truck types will expose car and truck fields (that came from the parent). And these fields don't have associated resolvers, so the following GraphQL query will raise an Exception:

query {
  cars {
    id
    # The presence of these two fields makes no logical sense, and resolving them raises
    car { id }
    truck { id }
  }
}

I suppose this is not really Graphene Django's concern to ensure downstream compatibility, but since Django Polymorphic is a quite popular library, I believe this is worth reporting here?

A quick workaround is to add these duplicated fields to the exclude lists of the derived types:

class CarType(DjangoObjectType):
    class Meta:
        exclude = ["car", "truck"]

but this is not most elegant.

@firaskafri
Copy link
Collaborator

I believe this PR may have introduced a "regression" when combined with django-polymorphic.

In Django Polymorphic, the parent type exposes accessors to the derived types, which are now picked up by the updated get_reverse_fields logic and added to the derived type fields.

Here's an example of what I mean. Assuming the following models:

from polymorphic.models import PolymorphicModel

class Vehicle(PolymorphicModel):
    id = UUIDField()

class Car(Vehicle):
    ...

class Truck(Vehicle):
    ...

now the Car and Truck types will expose car and truck fields (that came from the parent). And these fields don't have associated resolvers, so the following GraphQL query will raise an Exception:

query {
  cars {
    id
    # The presence of these two fields makes no logical sense, and resolving them raises
    car { id }
    truck { id }
  }
}

I suppose this is not really Graphene Django's concern to ensure downstream compatibility, but since Django Polymorphic is a quite popular library, I believe this is worth reporting here?

A quick workaround is to add these duplicated fields to the exclude lists of the derived types:

class CarType(DjangoObjectType):
    class Meta:
        exclude = ["car", "truck"]

but this is not most elegant.

@lucas-bremond I believe we are having the same issue as well. Can you create an issue with your comment referencing this PR please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants