Opened 16 years ago
Last modified 11 months ago
#10961 new New feature
Allow users to override forward and reverse relationships on proxy models with !ForeignKey fields.
Reported by: | Tai Lee | Owned by: | nobody |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | dev |
Severity: | Normal | Keywords: | proxy ForeignKey reverse relationship manager |
Cc: | Raphaël Hertzog, Julien Enselme, Dmytro Litvinov, ivanov17 | Triage Stage: | Accepted |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
I have a few generic models with foreign keys (simplified for this example):
class Country(turbia_models.Model): name = turbia_models.CharField(max_length=50, unique=True) class State(turbia_models.Model): name = turbia_models.CharField(max_length=50) country = models.ForeignKey(Country) class Region(turbia_models.Model): name = turbia_models.CharField(max_length=50) state = models.ForeignKey(State) class Suburb(turbia_models.Model): name = turbia_models.CharField(max_length=50) region = models.ForeignKey(Region)
I want to create proxy models for each of these when used with particular applications:
class Venue(models.Model): name = models.CharField(max_length=50) slug = models.SlugField() suburb = models.ForeignKey(Suburb) class CountryProxy(Country): class Meta: proxy = True @property def venue_set(self): return Venue.objects.filter(suburb__region__state__country=self) def get_absolute_url(self): return '/directory/%s/' % self.slug class StateProxy(State): class Meta: proxy = True @property def venue_set(self): return Venue.objects.filter(suburb__region__state=self) def get_absolute_url(self): return '%s%s/' % (self.country.get_absolute_url(), self.slug) class RegionProxy(Region): class Meta: proxy = True @property def venue_set(self): return Venue.objects.filter(suburb__region=self) def get_absolute_url(self): return '%s%s/' % (self.state.get_absolute_url(), self.slug) class SuburbProxy(Suburb): class Meta: proxy = True @property def venue_set(self): return Venue.objects.filter(suburb=self) def get_absolute_url(self): return '%s%s/' % (self.region.get_absolute_url(), self.slug)
This works if I never use the ForeignKey fields or reverse relationship managers to get related objects. E.g. if state
is a StateProxy object and I try state.country
, I'll end up with a !Country object instead of a CountryProxy object. Likewise if country
is a CountryProxy object and I try country.state_set.all()
, I'll end up with a queryset of !State objects instead of StateProxy objects.
This can be worked around with queries like Country.objects.get(pk=state.country.pk)
and State.objects.filter(country=country)
, but this kinda defeats the purpose of the ORM, and is not practical inside templates.
Where you should be able to just pass a single CountryProxy object to your template and do:
<h1>{{ country }}</h1> {% for state in country.state_set.all %} <h2>{{ state }}</h2> {% for region in state.region_set.all %} <h3>{{ region }}</h3> <p> {% for suburb in region.suburb_set.all %} <a href="{{ suburb.get_absolute_url }}">{{ suburb }}</a><br> {% endfor %} </p> {% endfor %} {% endfor %}
You need to pass all these objects and querysets from the view into the template context in some kind of nested structure.
def directory(request, slug): country = get_object_or_404(CountryProxy, slug=slug) state_data = ((state, ( (region, SuburbProxy.objects.filter(region=region)) for region in RegionProxy.objects.filter(state=state) )) for state in StateProxy.objects.filter(country=country)) render_to_response('directory.html', {'country': country, 'data': data})
and:
<h1>{{ country }}</h1> {% for state, region_data in state_data %} <h2>{{ state }}</h2> {% for region, suburb_set in region_data %} <h3>{{ region }}</h3> <p> {% for suburb in suburb_set %} <a href="{{ suburb.get_absolute_url }}">{{ suburb }}</a><br> {% endfor %} </p> {% endfor %} {% endfor %}
Change History (17)
comment:1 by , 16 years ago
comment:2 by , 16 years ago
The reverse relationship manager can be overriden with:
class CountryProxy(Country): class Meta: proxy = True @property def state_set(self): return StateProxy.objects.filter(country=self)
But I don't see any way to override the ForeignKey field. It seems that if state
is a StateProxy object, state.country
will always return a Country object, not a CountryProxy object.
comment:3 by , 15 years ago
Triage Stage: | Unreviewed → Design decision needed |
---|
This speaks to a large issue of wanting to overide non-db level properties, like help_text.
comment:4 by , 14 years ago
Severity: | → Normal |
---|---|
Type: | → New feature |
comment:7 by , 12 years ago
Triage Stage: | Design decision needed → Accepted |
---|
I am marking this as accepted, I am not sure what is the best approach to solve this issue, but ability to customize proxy model relations seems like a good idea (as well as the override help_text idea, too).
comment:8 by , 11 years ago
This would definitely be useful.
For my use case, my workaround is to reset the fields to customized versions after the proxy classes are defined.
For example:
StateProxy.add_to_class('country', models.ForeignKey(CountryProxy)) RegionProxy.add_to_class('state', models.ForeignKey(StateProxy)) SuburbProxy.add_to_class('region', models.ForeignKey(RegionProxy))
comment:9 by , 11 years ago
Cc: | added |
---|
pjdelport, this doesn't seem to be enough to update the related fields (eg. country.state_set). There's a discussion of this on http://stackoverflow.com/questions/3891880/django-proxy-model-and-foreignkey and for my needs I have also added something like this:
class CountryProxy(Country): ... @property def state_set(self): qs = super(CountryProxy, self).state_set qs.model = StateProxy return qs
comment:10 by , 6 years ago
This approach looks interesting. Would something like this be a possible way forward?
https://schinckel.net/2015/05/13/django-proxy-model-relations/
comment:11 by , 5 years ago
Cc: | added |
---|
comment:12 by , 5 years ago
Forest Gregg has updated the code from his comment for django 2 compatibility here: https://github.com/datamade/django-proxy-overrides
I am beginning to use this, and it seems like a workable approach, but native support shouldn't require this "late binding" IMHO.
comment:13 by , 3 years ago
I've run up against this issue on several projects, in fact nearly every time I use proxy models to solve a problem. Different workarounds each time, depending on use-case.
Currently working with M2M relations on proxy models, same issue can occur.
This is solved nicely with prefetch_related using a Prefetch object, and specifying the queryset from the related proxy model.
prefetch=models.Prefetch('related_set', queryset=RelatedProxyModel.objects.all()) obj = MyModel.objects.prefetch_related(prefetch).first() assert all( [type(r) == RelatedProxyModel for r in obj.related_set.all()] )
The same approach should work for accessing a reverse relation.
This can all be hidden in a model Manager or Queryset to make code prettier.
Got me to wonder if a similar approach could be used by allowing a queryset to be specified on select_related? In fact, only need the model class, since a queryset for select_related would over-specify.
Not as elegant as a declaration on the model, but seems like it might be pretty flexible and if it's easy to implement, a quick-and-dirty work-around that avoids any hackery.
comment:14 by , 3 years ago
Re: https://github.com/datamade/django-proxy-overrides
This approach seems to fail when using .select_related('related')
, where 'related' field is defined with a proxy override.
This is counter intuitive, since myProxyInstance.related
returns a different type of object depending on whether the original queryset used .select_related
or not.
Hope its useful to document that here.
comment:15 by , 14 months ago
For others who land here looking for solutions...
IF there is a field on your model that can be used to determine which Proxy subclass to initialize, this approach works beautifully:
https://schinckel.net/2013/06/13/django-proxy-model-state-machine/
No fuss, no overhead, every concrete object gets its correct proxy subclass type. Limited use-case, but a nifty solution if you happen to have it.
comment:16 by , 13 months ago
Cc: | added |
---|
comment:17 by , 11 months ago
Cc: | added |
---|
The real models above should also have
slug
fields for the example:And the work around queries should have been
CountryProxy.objects.get(pk=state.country.pk)
andStateProxy.objects.filter(country=country)
.