views:

833

answers:

2

I have the following:

class AccountAdmin(models.Model):

    account = models.ForeignKey(Account)
    is_master = models.BooleanField()
    name = models.CharField(max_length=255)
    email = models.EmailField()

    class Meta:
        unique_together = (('Account', 'is_master'), ('Account', 'username'),)

If I then create a new AccoutnAdmin with the same username as another on the same account, instead of it giving me an error to display in the template, it breaks with an IntegrityError and the page dies. I wish that in my view, I could just go:

if new_accountadmin_form.is_valid():
    new_accountadmin_form.save()

How do I conquer this problem. Is there a second is_valid() type of method that checks the DB for violation of the "unique_together = (('Account', 'is_master'), ('Account', 'username'),)" part?

I would like not to have to catch an IntegrityError in my view. That's domain logic mixed with presentation logic. It violates DRY because if I display the same form on 2 pages, I'll have to repeat the same block. It also violates DRY because if I have two forms for the same thing, I have to write the same except: again.

+1  A: 

You have two options:

a) Have a try block where you save your model and capture the IntegrityError and deal with it. Something like:

try:
    new_accountadmin_form.save()
except IntegrityError:
    new_accountadmin_form._errors["account"] = "some message"
    new_accountadmin_form._errors["is_master"] = "some message"

    del new_accountadmin_form.cleaned_data["account"]
    del new_accountadmin_form.cleaned_data["is_master"]

b) In the clean() method of your form, check if the a row exists and raise a forms.ValidationError with an appropriate message. Example here.


So, b) it is... That is why I referenced the documentation; all you need is there.

But it would be something like:

class YouForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
       """ This is the form's clean method, not a particular field's clean method """
       cleaned_data = self.cleaned_data

       account = cleaned_data.get("account")
       is_master = cleaned_data.get("is_master")
       username = cleaned_data.get("username")

       if AccountAdmin.objects.filter(account=account, is_master=is_master).count() > 0:
           del cleaned_data["account"]
           del cleaned_data["is_master"]
           raise forms.ValidationError("Account and is_master combination already exists.")

       if AccountAdmin.objects.filter(account=account, username=username).count() > 0:
           del cleaned_data["account"]
           del cleaned_data["username"]
           raise forms.ValidationError("Account and username combination already exists.")

    # Always return the full collection of cleaned data.
    return cleaned_data

For what it is worth - I just realized that your unique together above is referencing a field called username that is not represented in the model.

The clean method above is called after all clean methods for the individual fields are called.

celopes
How can I do that in the clean method and attach it to the normal clean method. I know about super(MyForm, self).clean() but how can I attach them together so that all validations happen at the same time, so that I could display this on the page:- "Sorry your email is not valid"- "Sorry your username already exists for this account",etc.
orokusaki
A: 

Model.Meta.unique_together creates a constraint limited to the database, while ModelForm.is_valid() is primarily based on correct types. Event if it did check constraints you would have a race condition that could still cause an IntegrityError in the save() call.

You probably want to be catching IntegrityError:

if new_accountadmin_form.is_valid():
    try:
        newaccountadmin_form.save()
    except IntegrityError, error:
        # here's your error handling code
teepark
1) One problem with that is that I have to import IntegrityError from the correct database, which makes for more configuration.2) The other problem with that is that I don't want validation logic in my views.3) (rhetoric, aimed towards Django itself) What's the point of putting unique_together constraints on something if there's no logical way of handling it during validation.
orokusaki
Your 3rd point is answered by the documentation: it is there to ensure the creation of a database constraint and for the admin interface.
celopes
1) You can import IntegrityError from django.db and it will get the correct backend's IntegrityError based on your settings. 2) I don't either. You should probably write it in a method of your ModelForm subclass. 3) Because the only alternative is to offer no way of specifying unique-together constraints.
teepark