How to limit a ManyToManyField to three choices?

5,010 views
Skip to first unread message

greenie2600

unread,
Mar 11, 2011, 12:06:43 PM3/11/11
to Django users
Hi all -

I have two models with a many-to-many relationship: Restaurant and
Cuisine. The Cuisine table contains, e.g., "Italian", "Mexican",
"Chinese", etc. Each Restaurant record can be associated with one or
more Cuisines.

Here's the thing: I'd like to limit this to three Cuisines per
Restaurant. So when editing the record for "Bob's Pan-Asian Buffet",
the user would be able to check "Japanese", "Chinese", and "Korean",
but wouldn't be able to check a fourth box.

My question: can this be enforced within the model, or is this
something I'd have to build into my interface layer?

Here's my models.py.

from django.db import models
from django.contrib.auth.models import User

class Restaurant( models.Model ):

user = models.ForeignKey( User )
name = models.CharField( max_length = 128 )
slug = models.CharField( max_length = 24, unique = True )
cuisines = models.ManyToManyField( 'Cuisine' )

def __unicode__(self):
return self.name

class Cuisine( models.Model ):

name = models.CharField( max_length = 32 )

def __unicode__(self):
return self.name

class Meta:
ordering = ['name']

gontran

unread,
Mar 11, 2011, 2:11:45 PM3/11/11
to Django users
Hi greenie,

you just need to override the save method from your model Restaurant.
Before saving each instance, you check if the restaurant already has
3 Cuisine's objects associated. If yes, you don't save the instance
and raise an error for exemple, if no, just call the standard method
of the super class Model.
You will find more infos in the django documentation:
http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods

gontran

unread,
Mar 11, 2011, 2:14:49 PM3/11/11
to Django users
edit: you don't need to raise an error if the restaurant already has 3
Cuisine's objects associated, you just need to return a string with
the explanation of the error.

On 11 mar, 20:11, gontran <geoffroydecorb...@gmail.com> wrote:
> Hi greenie,
>
> you just need to override the save method from your model Restaurant.
> Before saving each instance, you check if the restaurant already  has
> 3 Cuisine's objects associated. If yes, you don't save the instance
> and raise an error for exemple, if no, just call the standard method
> of the super class Model.
> You will find more infos in the django documentation:http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-pre...

bagheera

unread,
Mar 11, 2011, 2:35:47 PM3/11/11
to django...@googlegroups.com
Dnia 11-03-2011 o 18:06:43 greenie2600 <green...@gmail.com> napisał(a):

> Hi all -
>
> I have two models with a many-to-many relationship: Restaurant and
> Cuisine. The Cuisine table contains, e.g., "Italian", "Mexican",
> "Chinese", etc. Each Restaurant record can be associated with one or
> more Cuisines.
>
> Here's the thing: I'd like to limit this to three Cuisines per
> Restaurant. So when editing the record for "Bob's Pan-Asian Buffet",
> the user would be able to check "Japanese", "Chinese", and "Korean",
> but wouldn't be able to check a fourth box.
>
> My question: can this be enforced within the model, or is this
> something I'd have to build into my interface layer?
>


You can limit choices on model level:

http://docs.djangoproject.com/en/1.2/ref/models/fields/#django.db.models.ForeignKey.limit_choices_to

If query is too complicated (cuz u want to access object's data, witch
isn't yet available), you still can limit choices on form level, by
overriding __init__


class RestaurantForm(forms.ModelForm):
cuisines = forms.ModelMultipleChoiceField(Sklep)

class Meta:
model = Restaurant

def __init__(self, *args, **kwargs):
super(RestaurantForm, self).__init__(*args, **kwargs)
self.fields[cuisines].queryset =
Cuisine.objects.filter(pk__in=[fancy query])


--
Linux user

greenie2600

unread,
Mar 11, 2011, 3:23:38 PM3/11/11
to Django users
gontran -

Thanks.

However, I tried the sample code in your link, and I don't think it
will work. Returning a string from the overridden save() method
prevents the record from being saved to the database, but by the time
the save() method is invoked, my ModelForm (and consequently, I
presume, the underlying Model) has already been tested as valid. The
Restaurant isn't saved, but the form isn't redisplayed and no error
message is shown, and code execution proceeds as if the form were
valid (because it *is* valid; it just wasn't saved).

I think I need to override the model validation instead. Perhaps I
need to override Model.clean_fields()?

greenie2600

unread,
Mar 11, 2011, 3:29:29 PM3/11/11
to Django users
bagheera -

I had seen the limit_choices_to parameter, but I thought it controlled
*which* choices are available to the user - not *how many* they're
allowed to choose.

I want to show the user a list of 20 or 30 cuisines, but forbid them
from checking more than three.

Can you show me an example of how I'd use limit_choices_to to limit
the *number* of choices the user can select?



On Mar 11, 2:35 pm, bagheera <neost...@go2.pl> wrote:
> Dnia 11-03-2011 o 18:06:43 greenie2600 <greenie2...@gmail.com> napisał(a):
>
> > Hi all -
>
> > I have two models with a many-to-many relationship: Restaurant and
> > Cuisine. The Cuisine table contains, e.g., "Italian", "Mexican",
> > "Chinese", etc. Each Restaurant record can be associated with one or
> > more Cuisines.
>
> > Here's the thing: I'd like to limit this to three Cuisines per
> > Restaurant. So when editing the record for "Bob's Pan-Asian Buffet",
> > the user would be able to check "Japanese", "Chinese", and "Korean",
> > but wouldn't be able to check a fourth box.
>
> > My question: can this be enforced within the model, or is this
> > something I'd have to build into my interface layer?
>
> You can limit choices on model level:
>
> http://docs.djangoproject.com/en/1.2/ref/models/fields/#django.db.mod...

bagheera

unread,
Mar 11, 2011, 3:34:10 PM3/11/11
to django...@googlegroups.com

Right, i just get to that point, there is a cave rat about limiting
choices on form level.
Depending on limiting query it may make problems if you edit this object.

afik U can't validate m2m fields on model level, but u can do it on form
level and rise forms.ValidationError if needed in clean_field().

--
Linux user

bagheera

unread,
Mar 11, 2011, 3:39:01 PM3/11/11
to django...@googlegroups.com
Dnia 11-03-2011 o 21:29:29 greenie2600 <green...@gmail.com> napisał(a):

> bagheera -
>
> I had seen the limit_choices_to parameter, but I thought it controlled
> *which* choices are available to the user - not *how many* they're
> allowed to choose.
>
> I want to show the user a list of 20 or 30 cuisines, but forbid them
> from checking more than three.
>
> Can you show me an example of how I'd use limit_choices_to to limit
> the *number* of choices the user can select?
>

Form validation.

this should work

class RestaurantForm(forms.ModelForm):
cuisines = forms.ModelMultipleChoiceField(Sklep)

class Meta:
model = Restaurant

def clean_sklepy(self):
cuisines_clean = self.cleaned_data[cuisines]
if len(cuisines_clean) > 3:
raise forms.ValidationError('You can't choose more than
three items!')
return cuisines_clean


--
Linux user

bagheera

unread,
Mar 11, 2011, 3:43:12 PM3/11/11
to django...@googlegroups.com
Dnia 11-03-2011 o 21:29:29 greenie2600 <green...@gmail.com> napisał(a):

> bagheera -
>
> I had seen the limit_choices_to parameter, but I thought it controlled
> *which* choices are available to the user - not *how many* they're
> allowed to choose.
>
> I want to show the user a list of 20 or 30 cuisines, but forbid them
> from checking more than three.
>
> Can you show me an example of how I'd use limit_choices_to to limit
> the *number* of choices the user can select?
>
>
>
> On Mar 11, 2:35 pm, bagheera <neost...@go2.pl> wrote:
>> Dnia 11-03-2011 o 18:06:43 greenie2600 <greenie2...@gmail.com>
>> napisał(a):
>>
>> > Hi all -
>>
>> > I have two models with a many-to-many relationship: Restaurant and
>> > Cuisine. The Cuisine table contains, e.g., "Italian", "Mexican",
>> > "Chinese", etc. Each Restaurant record can be associated with one or
>> > more Cuisines.
>>
>> > Here's the thing: I'd like to limit this to three Cuisines per
>> > Restaurant. So when editing the record for "Bob's Pan-Asian Buffet",
>> > the user would be able to check "Japanese", "Chinese", and "Korean",
>> > but wouldn't be able to check a fourth box.
>>
>> > My question: can this be enforced within the model, or is this
>> > something I'd have to build into my interface layer?
>>

or u can add a js script to this form field that will disallow selecting
more than three items instead validating model (or do both).

--
Linux user

bagheera

unread,
Mar 11, 2011, 3:50:36 PM3/11/11
to django...@googlegroups.com

Sorry, i left some of my code :P But u got the idea.

--
Linux user

werefr0g

unread,
Mar 11, 2011, 4:03:17 PM3/11/11
to django...@googlegroups.com
Hello,

Can Model.clean() method help you? [1] You'll still have to pay attention to validation before trying to save your instances.[2]

Regards,

[1] http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.clean
[2] http://docs.djangoproject.com/en/dev/releases/1.2/#model-validation

gontran

unread,
Mar 11, 2011, 4:11:42 PM3/11/11
to Django users
I didn't try it, but werefr0g may be right. It seems that
Model.clean() is the method do you need. And you can still add a
javascript control for more convenience without the risk that if a
user deactivate javascript, the error will be saved.

Let us know greenie if it's ok with this method.



On 11 mar, 22:03, werefr0g <weref...@yahoo.fr> wrote:
> Hello,
>
> Can Model.clean() method help you? [1] You'll still have to pay
> attention to validation before trying to save your instances.[2]
>
> Regards,
>
> [1]http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db....
> [2]http://docs.djangoproject.com/en/dev/releases/1.2/#model-validation

greenie2600

unread,
Mar 11, 2011, 4:45:34 PM3/11/11
to Django users
werefr0g—

Yep, that's actually the solution I'm looking into right now. However,
I'm getting an error when trying to save a new instance of the
Restaurant model:

"'Restaurant' instance needs to have a primary key value before a many-
to-many relationship can be used."

Here's the new code that's triggering this error:


from django.db import models
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

class Restaurant( models.Model ):

user = models.ForeignKey( User )
name = models.CharField( max_length = 128 )
slug = models.CharField( max_length = 24, unique = True )
cuisines = models.ManyToManyField( 'Cuisine', help_text = 'Choose
up to three' )

def __unicode__(self):
return self.name

def clean( self ):
raise ValidationError( type( self.cuisines ).__name__ )

class Cuisine( models.Model ):
name = models.CharField( max_length = 32 )
def __unicode__(self):
return self.name
class Meta:
ordering = ['name']


As you can see, my custom clean() method simply raises an error
containing the type of the cuisines property. And even this is enough
to trigger the error shown above. Doing len( self.cuisines ) gives the
same result. (Raising an error containing a literal string, without
attempting to inspect the cuisines property, works as expected.)

Any ideas? It really seems like it should be possible to perform this
check at the model level. (bagheera, I'll resort to form-level
validation if I have to—but only if I have to. Client-side checks are
for UI convenience only, which is a peripheral issue.)

I'm new to Django and Python, and at this point I'm more interested in
learning the Right Way to do this (if there is one), or *why* I can't
do it at the model level (if, in fact, I can't).

bagheera

unread,
Mar 12, 2011, 4:38:40 AM3/12/11
to django...@googlegroups.com
Dnia 11-03-2011 o 22:45:34 greenie2600 <green...@gmail.com> napisał(a):

> 'Restaurant' instance needs to have a primary key value before a many-
> to-many relationship can be used.

That is the answer You need to understand. afiak You can't perform m2m
validation on model level due that very reason.
That's why i did it on form level, and i think that's the only way to do
this. I know such things should be performed on server-side and model
level, but i this case, is validation on form level causing any problems?

In my project i use only admin interface, so for end-user that validation
is completely transparent.
--
Linux user

werefr0g

unread,
Mar 12, 2011, 10:08:53 AM3/12/11
to django...@googlegroups.com
Well,

MAX_CUISINES = 3
   
    def clean(self):
        # three cuisines max. allowed
        if self.pk is None:
            # That's the case that raise the error
            # you're inserting a new Restaurant
            pass
        else:
            # Here, you're editing an existing Restaurant
            # (including its relashionship with Cuisine)
            if self.cuisines.count() > MAX_CUISINES:
                raise ValidationError('I said, "Choose up to three" cusines!')

As we're aiming to prevent, at model's level, saving a new Restaurant when too many related Cuisine are provided, I'm afraid that doesn't fit the needs... unless saving restaurant's intance, run a validation then delete it if it fails the validation :) I'd like to place the validation at 'cusines' field level. I'm not confortable placing it at the "whole" model's level, despite my suggestion.

I failed for the moment to find a way and I'll resume tomorrow. Maybe the following unsuccessful tries can help you (with a mix of admin interface playing and shell, both on dev server):

  * use validator on models.ManyToManyField:

      I can't even trigger a simple print('test') TT

  * use a custom field by extending models.ManyToManyField, overriding its isValidIDList method

      Ok, it is a blind test: I was looking for a way to access values from cuisines for a non saved instance of Restaurant and maybe something is lurking there. I'm a beginner.

  * accessing self's attributes for a non saved or retrieved instance of Restaurant

      # desesperately random commands (I tried more :):
     
      u = Restaurant()
      r = Restaurant.objects.all()[0]
     
      dir(u)
      # hey, a "cuisines" attribute, great!
      dir(u.cuisines)
      # TT, same error you encountered
      dir(r.cuisines)
      # fine
     
      u.clean_fields()
      # 'cusines' is not even mentionned in the error message
      r.clean_fields()
      # fine
     
      [f.verbose_name for f in r._meta.fields]
      # no cusines
     
      I believe that no control is enforced before saving, but if you try to create an new Restaurant and not provide a cuisine, the validation failed from the admin. How does it handle that? I'll try to find out. Is there a way to access data provided to 'cuisines' for an unsaved Restaurant?

Regards,

Reply all
Reply to author
Forward
0 new messages