views:

49

answers:

2

I'm sorta having a django / db brainfart.

In words I'm trying to come up with a better way to represent Address objects in my project. I have 5 or 6 different models that have one or more associations to an Address (Billing / Shipping / etc) and I need to allow these addresses to be created/modified/deleted. My first thought was to use the Admin for this since it seems like a natural fit.

However, I can't seem to figure out how to tell the Admin to restrict the visible set of addresses to the specific model in question(Account/Partner/Invoice). I found a really nasty, not viable, and incredibility horrible to maintain way to do it, which I will show below.

How can I do this efficiently (preferably in the Admin)? I am open to going to a m2m relationship which feels much more natural to me, and using a custom view/form, but I wanted to see if I was missing some Admin trick before I went that route. If I go that route I'm thinking I will need to use a GenericRelation so that I don't end up with a ton of lookup tables (one for each different entity).

EDIT: The same address may be used for different models, and for different instances of a particular model, BUT if an address is reused we must track who is using what to maintain independence between models and/or instances. Or in other words in a m2m relationship we can track who is using what with the intermediate table. If we aren't using a lookup table then we need to always copy the Address instance so that both objects have their own copy. (If there is an edit we need to make sure we aren't changing the preexisting relationships of anyone else. In other words edits are actually creates and reassigns in the m2m case.)

Here is an example that should pretty much work as an empty project that sorta shows how I want the address(es) isolated during add/edit/delete, but it also shows how horrible the solution is.

models.py

from django.db import models

class Account(models.Model):
    name = models.CharField(max_length=200,blank=True)
    #...
    def __unicode__(self):
        return u"%s - (%s)" %(self.name, self.address_set.all())

class Partner(models.Model):
    name = models.CharField(max_length=200,blank=True) 
    discount = models.DecimalField(max_digits= 3, decimal_places=1, default=0)
    #...
    def __unicode__(self):
        return u"%s - (%s)" %(self.name, self.address_set.all())

class Invoice(models.Model):
    invoice_number = models.IntegerField(default=1)
    #...
    def __unicode__(self):
        return u"%s - (%s)" %(self.invoice_number, self.address_set.all())

class Address(models.Model):
    street = models.CharField(max_length=200,blank=True)
    zip = models.CharField(max_length=10, verbose_name='Zip Code')
    account = models.ForeignKey(Account, blank=True, null=True)
    partner = models.ForeignKey(Partner, blank=True, null=True)
    invoice = models.ForeignKey(Invoice, blank=True, null=True)
    type = models.CharField(max_length=25, choices=(('B','Billing'),('S','Shipping')))

    class Meta:
        unique_together = (('type', 'account' ),
                           ('type', 'partner' ),
                           ('type', 'invoice' ),)

    def __unicode__(self):
        return "(%s) - %s %s" %(self.get_type_display(), self.street, self.zip)

admin.py

from django.contrib import admin
from relationships.rels.models import Partner, Account, Address, Invoice

class AcctAddrInline(admin.TabularInline):
    model = Address
    extra = 1
    max_num =3
    exclude = ('partner', 'invoice')

class PartAddrInline(admin.TabularInline):
    model = Address
    extra = 1
    max_num =3
    exclude = ('account', 'invoice')

class InvAddrInline(admin.TabularInline):
    model = Address
    extra = 1
    max_num =2
    exclude = ('account', 'partner')        

class AccountAdmin(admin.ModelAdmin):
    inlines = [AcctAddrInline,]

class PartnerAdmin(admin.ModelAdmin):
    inlines = [PartAddrInline,]

class InvoiceAdmin(admin.ModelAdmin):
    inlines = [InvAddrInline,]

admin.site.register(Invoice, InvoiceAdmin)         
admin.site.register(Partner, PartnerAdmin)
admin.site.register(Account, AccountAdmin)
admin.site.register(Address)
A: 

I, personally, would put foreign keys to your Address model on Account, Partner and Invoice instead of having Address aware of what it is the address to. That, MAY, solve your problem.

Matthew J Morrison
well that is where they **should** go I agree. But if I do that then ALL addresses are available to ALL entities, which is exactly what I am trying to avoid.
JT
@JT have you looked at using limit_choices_to on your ForeignKey fields?
Matthew J Morrison
@Matthew, I looked at limit_choices_to per your suggestion, but I couldn't figure out a way to use that in the class definition to refer to the specific instance. That seems to work well for things like dates or other things that don't depend on models. http://stackoverflow.com/questions/160009/django-model-limit-choices-touser-user is another use case that didn't work well :)
JT
+1  A: 

EDIT: The same address may be used for different models, and for different instances of a particular model, BUT if an address is reused we must track who is using what to maintain independence between models and/or instances

It looks like you want to use COW pattern for addresses, but I don't think it plays nice with the whole idea of database integrity.

If you just want to separate account addresses from invoice addresses I'd suggest to use Multi-table model inheritance. This way you will have several sets of addresses while being able to browse all addresses at once.

Here's an example.

models.py

from django.db import models

class Account(models.Model):
    name = models.CharField(max_length=200, blank=True)

    def __unicode__(self):
        return u"%s - (%s)" % (self.name, self.address_set.all())

class Partner(models.Model):
    name = models.CharField(max_length=200, blank=True) 
    discount = models.DecimalField(max_digits= 3, decimal_places=1, default=0)

    def __unicode__(self):
        return u"%s - (%s)" % (self.name, self.address_set.all())

class Invoice(models.Model):
    invoice_number = models.IntegerField(default=1)

    def __unicode__(self):
        return u"%s - (%s)" % (self.invoice_number, self.address_set.all())

class Address(models.Model):
    street = models.CharField(max_length=200, blank=True)
    zip = models.CharField(max_length=10, verbose_name='Zip Code')

    def __unicode__(self):
        return "%s %s" % (self.street, self.zip)

class AccountAddress(Address):
    account = models.ForeignKey(Account, related_name='address_set')

class InvoiceAddress(Address):
    invoice = models.ForeignKey(Invoice, related_name='address_set')

class PartnerAddress(Address):
    partner = models.ForeignKey(Partner, related_name='address_set')

admin.py

from django.contrib import admin
# Wildcard import used for brevity
from relationships.rels.models *

class AccountAddressInline(admin.TabularInline):
    model = AccountAddress
    extra = 1
    max_num = 3

class PartnerAddressInline(admin.TabularInline):
    model = PartnerAddress
    extra = 1
    max_num = 3

class InvoiceAddressInline(admin.TabularInline):
    model = InvoiceAddress
    extra = 1
    max_num = 3

class AccountAdmin(admin.ModelAdmin):
    inlines = [AccountAddressInline,]

class PartnerAdmin(admin.ModelAdmin):
    inlines = [PartnerAddressInline,]

class InvoiceAdmin(admin.ModelAdmin):
    inlines = [InvoiceAddressInline,]

admin.site.register(Account, AccountAdmin)
admin.site.register(Partner, PartnerAdmin)
admin.site.register(Invoice, InvoiceAdmin)

admin.site.register(AccountAddress)
admin.site.register(InvoiceAddress)
admin.site.register(PartnerAddress)

# Uncomment if you want to browse all addresses available at once
# admin.site.register(PartnerAddress)

Note the related_name='address_set' hack. I don't know why, but it is the only way inline editing works when using foreign key from an inherited model. It seems that it's a bug in Django similar to (but rather with a reversed use-case) #11120 and #11121.

Ihor Kaharlichenko
I went in this direction last night, but couldn't get it working. I missed the related_name hack. Now it works as expected, thanks!
JT