Code

Opened 5 years ago

Last modified 2 months ago

#10961 new New feature

Allow users to override forward and reverse relationships on proxy models with !ForeignKey fields.

Reported by: mrmachine Owned by: nobody
Component: Database layer (models, ORM) Version: master
Severity: Normal Keywords: proxy ForeignKey reverse relationship manager
Cc: rhertzog 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 %}

Attachments (0)

Change History (9)

comment:1 Changed 5 years ago by mrmachine

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

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 5 years ago by mrmachine

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 5 years ago by Alex

  • Triage Stage changed from Unreviewed to Design decision needed

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

comment:4 Changed 3 years ago by julien

  • Severity set to Normal
  • Type set to New feature

comment:5 Changed 2 years ago by aaugustin

  • UI/UX unset

Change UI/UX from NULL to False.

comment:6 Changed 2 years ago by aaugustin

  • Easy pickings unset

Change Easy pickings from NULL to False.

comment:7 Changed 16 months ago by akaariai

  • Triage Stage changed from Design decision needed to 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 Changed 12 months ago by pjdelport

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 2 months ago by rhertzog

  • Cc rhertzog 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

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as new
The owner will be changed from nobody to anonymous. Next status will be 'assigned'
as The resolution will be set. Next status will be 'closed'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.