Opened 10 years ago

Last modified 3 weeks 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: master
Severity: Normal Keywords: proxy ForeignKey reverse relationship manager
Cc: Raphaël Hertzog, Julien Enselme 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 (12)

comment:1 Changed 10 years ago by Tai Lee

The real models above should also have slug fields for the example:

class Country(models.Model):
	name = models.CharField(max_length=50, unique=True)
	slug = models.SlugField()

class State(models.Model):
	name = models.CharField(max_length=50)
	slug = models.SlugField()
	country = models.ForeignKey(Country)

class Region(models.Model):
	name = models.CharField(max_length=50)
	slug = models.SlugField()
	state = models.ForeignKey(State)

class Suburb(models.Model):
	name = models.CharField(max_length=50)
	slug = models.SlugField()
	region = models.ForeignKey(Region)

And the work around queries should have been CountryProxy.objects.get(pk=state.country.pk) and StateProxy.objects.filter(country=country).

comment:2 Changed 10 years ago by Tai Lee

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 Changed 10 years ago by Alex Gaynor

Triage Stage: UnreviewedDesign decision needed

This speaks to a large issue of wanting to overide non-db level properties, like help_text.

comment:4 Changed 8 years ago by Julien Phalip

Severity: Normal
Type: New feature

comment:5 Changed 8 years ago by Aymeric Augustin

UI/UX: unset

Change UI/UX from NULL to False.

comment:6 Changed 8 years ago by Aymeric Augustin

Easy pickings: unset

Change Easy pickings from NULL to False.

comment:7 Changed 6 years ago by Anssi Kääriäinen

Triage Stage: Design decision neededAccepted

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 Changed 6 years ago by Pi Delport

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 Changed 5 years ago by Raphaël Hertzog

Cc: Raphaël Hertzog 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 Changed 9 months ago by Forest Gregg

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 Changed 5 weeks ago by Julien Enselme

Cc: Julien Enselme added

comment:12 Changed 3 weeks ago by Nate Dudenhoeffer

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.

Note: See TracTickets for help on using tickets.
Back to Top