Opened 15 years ago

Last modified 3 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 Tai Lee, 15 years ago

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 by Tai Lee, 15 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 Alex Gaynor, 15 years ago

Triage Stage: UnreviewedDesign decision needed

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

comment:4 by Julien Phalip, 13 years ago

Severity: Normal
Type: New feature

comment:5 by Aymeric Augustin, 12 years ago

UI/UX: unset

Change UI/UX from NULL to False.

comment:6 by Aymeric Augustin, 12 years ago

Easy pickings: unset

Change Easy pickings from NULL to False.

comment:7 by Anssi Kääriäinen, 11 years ago

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 by Pi Delport, 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 Raphaël Hertzog, 10 years ago

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 by Forest Gregg, 5 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 Julien Enselme, 5 years ago

Cc: Julien Enselme added

comment:12 by Nate Dudenhoeffer, 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 powderflask, 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 powderflask, 2 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 powderflask, 6 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 Dmytro Litvinov, 6 months ago

Cc: Dmytro Litvinov added

comment:17 by ivanov17, 3 months ago

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