Wednesday, December 28, 2011

Re: Constraints on a one-to-many relationship and related problems

Just looking at the models, I'd like to make a couple of suggestions.

Instead of using a Foreign Key relationship in Customer to indicate
the billing address, I would include a flag called 'billing_address'
in the Address table that would be set to True for the customer
selected address.

An override of save() in the Address model is then used to enforce the
'only one billing address' rule.

def save(self):
''' This will turn off the billing address flag for all other
addresses for this customer if the new record is selected '''
if self.billing_address:
old_billing_address =
Address.objects.filter(customer=self.customer).filter(billing_flag=
True)
for s in old_billing_address:
s.billing_flag = False
s.save()
super(Address, self).save()

Hope this helps,

Dan

On Dec 28, 6:02 am, Bart Nagel <b...@tremby.net> wrote:
> I'm new to Django. I'm finding it very impressive so far but have hit
> a cluster of problems. I'm using the SVN version.
>
> Sorry for the length of this, I'm just trying to explain as fully as I
> can what I'm trying to do, what I have and what's going wrong.
>
> The relevant parts of my model, in the simplest terms, look like this.
>
>     from django.db import models
>
>     class Address(models.Model):
>         customer = models.ForeignKey("Customer")
>         street = models.CharField(max_length=128)
>         city = models.CharField(max_length=128)
>
>         def __unicode__(self):
>             return "%s, %s" % (self.street, self.city)
>
>     class Customer(models.Model):
>         last_name = models.CharField(blank=True, max_length=64)
>         first_name = models.CharField(blank=True, max_length=64)
>         billing_address = models.ForeignKey("Address", related_name="+")
>
>         def __unicode__(self):
>             return "%s %s" % (self.first_name, self.last_name)
>
> So customers can have many addresses, and one of those addresses is
> pointed to by the customer as being the billing address.
>
> I then have the Customer admin page set up so that Address entries are
> edited inline on the same form.
>
> 1. The billing address should be required, but obviously when it's a
>    new Customer there won't be any addresses on file, so there will be
>    no choices on the billing address dropdown.
>
>    So I need a way to accept a blank selection for billing address,
>    maybe have it labelled as "use first address given below", and then
>    just complain if no addresses are given below.
>
>    Later when there needs to be something to stop the billing address
>    from being deleted.
>
> 2. Related to the previous, there needs to be a constraint so there
>    must be at least one Address for each customer.
>
> 3. When editing an existing customer, only that customer's addresses
>    should be shown in the dropdown for billing address.
>
> Here's what I've tried...
>
> I set the billing address field to be optional for now.
>
> Problem 3 seemed easiest so I decided to tackle that first, and made a
> bunch of customers and addresses so I could test with the database
> somewhat populated.
>
> I found the ForeignKey.limit_choices_to in the documentation but since
> there's no "self" when I'm setting up the database fields I've no idea
> how I'd tell it to limit the options to something like
> self.addresses_set.all(), let alone have that updated as addresses are
> added, removed, edited in the inline form below.
>
> I first posted this problem on Stacko Overflow
> (http://stackoverflow.com/questions/8637912/) and a suggestion was to
> use a ModelChoiceField. I had a look at the documentation and it
> wasn't obvious what the difference is between that an ForeignKey, plus
> it looks like I'd have exactly the same problem as above.
>
> So I'm totally stuck on that one.
>
> Next I had a go at the other two problems. It seemed to me (bearing in
> mind I'm a newbie here) it'd be best to add that logic to the
> Customer.clean() method -- I could check the number of addresses which
> had been entered there and raise an Exception if it was zero. At the
> same time I could set the billing address, if not already set, to the
> first address given. All sounds simple enough, but apparently not.
>
> In the Customer.clean() method, I can't seem to get at what was posted
> as addresses. In there, self.address_set.all().count() is zero. I
> don't really see why -- I can get at the other data which was posted
> to the form as an object, why not the child objects which are being
> posted too?
>
> Perhaps just too early. Following a suggestion in the same Stack
> Overflow thread mentioned above, I figured out how to set up a
> listeners for the pre_save and post_save signals and inspected the
> count of addresses at those points. It's still zero in both cases.
> Even after the save. That was very confusing but from what I've found
> while Googling it's something to do with the database transaction
> having not been finished yet. It seems counter-intuitive. Ideally I'd
> like to get at the Address objects before they're committed to the
> database so that I can roll back if necessary (in the case that there
> are no addresses), but after they're committed would do if there was
> no other way -- I could change the Customer object as necessary and
> re-save it or delete it. Not sure how I'd let the user know in that
> case.
>
> But no -- empty address_set.all() at post_save time. I found a monkey
> patch to add a signal for post_transaction
> (https://gist.github.com/247844) and with a small tweak (possibly my
> Python or my Django is too old or new or something) it worked. I set
> up the listener and now I can get at the Address objects from the
> Customer object and edit the Customer if necessary (adding the first
> address as the billing address). But at this point it's too late to
> throw pretty exceptions if something goes wrong, which is a shame.
>
> There's another problem there too. Once the billing address is set to
> one of the addresses and then the customer's addresses are later
> edited again I get horrific errors saying that the billing address is
> set to an unacceptable option. I think what's happening is that the
> addresses are being deleted and recreated, and so the reference in the
> billing address field now points to a non-existent primary key in the
> addresses table.
>
> Since the above doesn't let me warn the user if they haven't entered
> any addresses, I needed another approach to let me do that. What I
> came up with was setting up a custom ModelForm for the Customer
> object's admin interface, and checking in the clean() method there.
> The best I could think to do was to print out dir() of various things
> to find likely looking methods and to see what data I had. The only
> reference to the addresses being entered I could find was in the
> self.data dict there. And it's messy, but I got a solution
> half-working by looking at self.data["address_set-TOTAL_FORMS"] and
> checking the number. But that doesn't cover all possibilities -- it
> might not exist (easy to deal with) and, slightly harder, some of the
> forms sent might have the "delete" box checked. So I had to count up
> to the number of forms and look for "address_set-%n-DELETE" keys. It's
> a mess and there's still a case it doesn't catch -- when a form is
> sent with the default (untouched) values and so doesn't actually end
> up creating an address. I'd see it as being an address and let it
> through, but then the billing address wouldn't have anything to point
> to. I didn't try to write code to handle that case because it's just
> getting way too messy.
>
> So this solution is not working for me.
>
> In fact, looking back, none of my solutions for any of the problems
> are adequate. I'm stuck.
>
> Please help! Any suggestions are appreciated. Hopefully there are just
> some really easy things I've missed in the documents which will solve
> everything, but if the solution is a bit more in depth, so be it.
>
> --bart nagel

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To post to this group, send email to django-users@googlegroups.com.
To unsubscribe from this group, send email to django-users+unsubscribe@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-users?hl=en.

No comments:

Post a Comment