Is it possible to created named choice groups in a Django select (dropdown) widget, when that widget is on a form that is auto-generated from a data Model? Can I create the widget on the left-side picture below?
My first experiment in creating a form with named groups, was done manually, like this:
class GroupMenuOrderForm(forms.Form):
food_list = [(1, 'burger'), (2, 'pizza'), (3, 'taco'),]
drink_list = [(4, 'coke'), (5, 'pepsi'), (6, 'root beer'),]
item_list = ( ('food', tuple(food_list)), ('drinks', tuple(drink_list)),)
itemsField = forms.ChoiceField(choices = tuple(item_list))
def GroupMenuOrder(request):
theForm = GroupMenuOrderForm()
return render_to_response(menu_template, {'form': theForm,})
# generates the widget in left-side picture
And it worked nicely, creating the dropdown widget on the left, with named groups.
I then created a data Model that had basically the same structure, and used Django's ability to auto-generate forms from Models. It worked - in the sense that it showed all of the options. But the options were not in named groups, and so far, I haven't figured out how to do so - if it's even possible.
I have found several questions, where the answer was, “create a form constructor and do any special processing there”. But It seems like the forms.ChoiceField requires a tuple for named groups, and I’m not sure how to convert a tuple to a QuerySet (which is probably impossible anyway, if I understand QuerySets correctly as being pointer to the data, not the actual data).
The code I used for the data Model is:
class ItemDesc(models.Model):
''' one of "food", "drink", where ID of “food” = 1, “drink” = 2 '''
desc = models.CharField(max_length=10, unique=True)
def __unicode__(self):
return self.desc
class MenuItem(models.Model):
''' one of ("burger", 1), ("pizza", 1), ("taco", 1),
("coke", 2), ("pepsi", 2), ("root beer", 2) '''
name = models.CharField(max_length=50, unique=True)
itemDesc = models.ForeignKey(ItemDesc)
def __unicode__(self):
return self.name
class PatronOrder(models.Model):
itemWanted = models.ForeignKey(MenuItem)
class ListMenuOrderForm(forms.ModelForm):
class Meta:
model = PatronOrder
def ListMenuOrder(request):
theForm = ListMenuOrderForm()
return render_to_response(menu_template, {'form': theForm,})
# generates the widget in right-side picture
I'll change the data model, if need be, but this seemed like a straightforward structure. Maybe too many ForeignKeys? Collapse the data and accept denormalization? :) Or is there some way to convert a tuple to a QuerySet, or something acceptable to a ModelChoiceField?
Update: final code, based on meshantz' answer:
class FooIterator(forms.models.ModelChoiceIterator):
def __init__(self, *args, **kwargs):
super(forms.models.ModelChoiceIterator, self).__init__(*args, **kwargs)
def __iter__(self):
yield ('food', [(1L, u'burger'), (2L, u'pizza')])
yield ('drinks', [(3L, u'coke'), (4L, u'pepsi')])
class ListMenuOrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ListMenuOrderForm, self).__init__(*args, **kwargs)
self.fields['itemWanted'].choices = FooIterator()
class Meta:
model = PatronOrder
(Of course the actual code, I'll have something pull the item data from the database.)
The biggest change from the djangosnippet he linked, appears to be that Django has incorporated some of the code, making it possible to directly assign an Iterator to choices, rather than having to override the entire class. Which is very nice.
After a quick look at the ModelChoiceField code in django.forms.models, I'd say try extending that class and override its choice property.
Set up the property to return a custom iterator, based on the orignial ModelChoiceIterator in the same module (which returns the tuple you're having trouble with) - a new GroupedModelChoiceIterator or some such.
I'm going to have to leave the figuring out of exactly how to write that iterator to you, but my guess is you just need to get it returning a tuple of tuples in a custom manner, instead of the default setup.
Happy to reply to comments, as I'm pretty sure this answer needs a little fine tuning :)
EDIT BELOW
Just had a thought and checked djangosnippets, turns out someone's done just this:
ModelChoiceField with optiongroups. It's a year old, so it might need some tweaks to work with the latest django, but it's exactly what I was thinking.
Here's what worked for me, not extending any of the current django classes:
I have a list of types of organism, given the different Kingdoms as the optgroup. In a form OrganismForm, you can select the organism from a drop-down select box, and they are ordered by the optgroup of the Kingdom, and then all of the organisms from that kingdom. Like so:
[----------------|V]
|Plantae |
| Angiosperm |
| Conifer |
|Animalia |
| Mammal |
| Amphibian |
| Marsupial |
|Fungi |
| Zygomycota |
| Ascomycota |
| Basidiomycota |
| Deuteromycota |
|... |
|________________|
models.py
from django.models import Model
class Kingdom(Model):
name = models.CharField(max_length=16)
class Organism(Model):
kingdom = models.ForeignKeyField(Kingdom)
name = models.CharField(max_length=64)
forms.py:
from models import Kingdom, Organism
class OrganismForm(forms.ModelForm):
organism = forms.ModelChoiceField(
queryset=Organism.objects.all().order_by('kingdom__name', 'name')
)
class Meta:
model = Organism
views.py:
from models import Organism, Kingdom
from forms import OrganismForm
form = OrganismForm()
form.fields['organism'].choices = list()
# Now loop the kingdoms, to get all organisms in each.
for k in Kingdom.objects.all():
# Append the tuple of OptGroup Name, Organism.
form.fields['organism'].choices = form.fields['organism'].choices.append(
(
k.name, # First tuple part is the optgroup name/label
list( # Second tuple part is a list of tuples for each option.
(o.id, o.name) for o in Organism.objects.filter(kingdom=k).order_by('name')
# Each option itself is a tuple of id and name for the label.
)
)
)
You don't need custom iterators. You're gonna need to support that code. Just pass the right choices:
from django import forms
from django.db.models import Prefetch
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = [...]
def __init__(self, *args, **kwargs):
super(ProductForm, self).__init__(*args, **kwargs)
cats = Category.objects \
.filter(category__isnull=True) \
.order_by('order') \
.prefetch_related(Prefetch('subcategories',
queryset=Category.objects.order_by('order')))
self.fields['subcategory'].choices = \
[("", self.fields['subcategory'].empty_label)] \
+ [(c.name, [
(self.fields['subcategory'].prepare_value(sc),
self.fields['subcategory'].label_from_instance(sc))
for sc in c.subcategories.all()
]) for c in cats]
Here,
class Category(models.Model):
category = models.ForeignKey('self', null=True, on_delete=models.CASCADE,
related_name='subcategories', related_query_name='subcategory')
class Product(models.Model):
subcategory = models.ForeignKey(Category, on_delete=models.CASCADE,
related_name='products', related_query_name='product')
This same technique can be used to customize a Django admin form. Although, Meta class is not needed in this case.
Related
I have to models which are connected by a M2M-Field realized by another Class ComponentInModule, so that I can add there the extra information, how often a component is in the module.
class Module(models.Model):
...
component = models.ManyToManyField(Component, through="ComponentInModule")
class Component(models.Model):
...
class ComponentInModule(models.Model):
module = models.ForeignKey(InfrastructureModule, on_delete=models.CASCADE)
component = models.ForeignKey(InfrastructureComponent, on_delete=models.CASCADE)
amount = models.IntegerField(default=1)
Now I am trying to load a Module as a form with its corresponding Components as a formset.
class ComponentForm(ModelForm):
amount = IntegerField()
module = InfrastructureModule.objects.get(id=x)
ComponentFormSet = modelformset_factory(Component, form=ComponentForm, extra=0)
component_formset = ComponentFormSet(queryset=module.get_components())
As you can see my ComponentForm has the extra field for the amount. The question now is, how can I pass the value of amount to the Formset on creation, so that all forms are initialized with the right value? With a single Form it's no problem, because I can just pass the value to the __init__ function of the form and put it into the amount field self.fields["amount"].initial = amount. I tried passing a list of values to the formset with form_kwargs, but then I got the problem, that in the __init__function I dont know which of the values in the list is the right one right now.
Is there any way to do this using formsets? Or is there some other option I am missing how you can include the extra fields from a M2M-relation in a ModelForm?
So I worked it out. I made a custom BaseModelFormSet class:
class BaseCompFormset(BaseModelFormSet):
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
amount = kwargs["amount"][index]
return {"amount": amount}
Adjusted the __init__ function of the form:
def __init__(self, *args, **kwargs):
amount = kwargs.pop("amount")
super(ComponentForm, self).__init__(*args, **kwargs)
if self.instance:
self.fields["amount"].initial = amount
And used those to create my modelformset_factory:
amounts = [x.amount for x in module.get_components_in_module()]
ComponentFormSet = modelformset_factory(Component, formset=BaseCompFormset, form=ComponentForm, extra=0)
component_formset = ComponentFormSet(queryset=module.get_components(), form_kwargs={'amount':amounts})
And now succesfully got the forms of the formset with the right initial value for amount!
I have two models: City, and its alias CityAlias. The CityAlias model contains all the names in the City, plus the aliases. What I want is that whenever City is searched by name, the CityAlias model should be queried. This is what I've come up with:
class CityQuerySet(models.QuerySet):
""" If City is searched by name, search it in CityAlias """
def _search_name_in_alias(self, args, kwargs):
for q in args:
if not isinstance(q, models.Q): continue
for i, child in enumerate(q.children):
# q.children is a list of tuples of queries:
# [('name__iexact', 'calcutta'), ('state__icontains', 'bengal')]
if child[0].startswith('name'):
q.children[i] = ('aliases__%s' % child[0], child[1])
for filter_name in kwargs:
if filter_name.startswith('name'):
kwargs['aliases__%s' % filter_name] = kwargs.pop(filter_name)
def _filter_or_exclude(self, negate, *args, **kwargs):
# handles 'get', 'filter' and 'exclude' methods
self._search_name_in_alias(args=args, kwargs=kwargs)
return super(CityQuerySet, self)._filter_or_exclude(negate, *args, **kwargs)
class City(models.Model):
name = models.CharField(max_length=255, db_index=True)
state = models.ForeignKey(State, related_name='cities')
objects = CityQuerySet.as_manager()
class CityAlias(models.Model):
name = models.CharField(max_length=255, db_index=True)
city = models.ForeignKey(City, related_name='aliases')
Example: Kolkata will have an entry in City model, and it will have two entries in the CityAlias model: Kolkata and Calcutta. The above QuerySet allows to use lookups on the name field.
So the following two queries will return the same entry:
City.objects.get(name='Kolkata') # <City: Kolkata>
City.objects.get(name__iexact='calcutta') # <City: Kolkata>
So far so good. But the problem arises when City is a ForeignKey in some other model:
class Trip(models.Model):
destination = models.ForeignKey(City)
# some other fields....
Trip.objects.filter(destination__name='Kolkata').count() # some non-zero number
Trip.objects.filter(destination__name='Calcutta').count() # always returns zero
Django internally handles these joins differently, and doesn't call the get_queryset method of City's manager. The alternative is to call the above query as following:
Trip.objects.filter(destination=City.objects.get(name='Calcutta'))
My question is that can I do something, so that however the City model is searched by name, it always searches in the CityAlias table instead?
Or is there another better way to implement the functionality I require?
I think it is better (and more pythonic) to be explicit in what you ask for throughout instead of trying to do magic in the Manager and thus:
City.objects.get(aliases__name__iexact='calcutta') # side note: this can return many (same in original) so you need to catch that
And:
Trip.objects.filter(destination__aliases__name='Calcutta').count()
I was trying to use Custom Lookups but apparently you cannot add a table to the join list. (Well, you could add an extra({"table": ...}) in the model's manager but it's not an elegant solution).
So I'd propose you:
1) Keep always your 'main/preferred' name city also as a CityAlias. So the metadata of the city will be in City... but all the naming information will be in CityAlias. (and maybe change the names)
In this way all look-ups will happen in that table. You could have a boolean to mark which instance is the original/preferred.
class City(models.Model):
state = models.ForeignKey(State, related_name='cities')
[...]
class CityAlias(models.Model):
city = models.ForeignKey(City, related_name='aliases')
name = models.CharField(max_length=255, db_index=True)
2) If you are thinking about translations... Have you thought about django-modeltranslation app?
In this case, it would create a field for each language and it would be always better than having a join.
3) Or, if you are using PostgreSQL, and you are thinking about "different translations for the same city-name" (and I'm thinking with transliterations from Greek or Russian language), maybe you could use PostgreSQL dictionaries, trigrams with similarities, etc. Or even in this case, the 1st approach.
Speaking of keeping it simple. Why not just give the City model a char field 'CityAlias' that contains the string? If I understand your question correctly, this is the most simple solution if you only need one alias per city. It just looks to me as though you are complicating a simple problem.
class City(models.Model):
name = models.CharField(max_length=255, db_index=True)
state = models.ForeignKey(State, related_name='cities')
alias = models.CharField(max_length=255)
c = City.objects.get(alias='Kolkata')
>>>c.name
Calcutta
>>>c.alias
Kolkata
I have a Django model called CarAd which is implemented as follows:
class CarAd(models.Model):
MAKE = (
(0, 'Acura'), (1, 'Alpha Romeo'), (2, 'Aston'), (3, 'Toyota'), (4, 'Honda')
)
make = models.IntegerField(choices=MAKE)
I want to implement it such that each make has a subset of car models (not Django models). For example, Toyota has a subset with elements 'Corolla', 'Prius', and 'Camry'. I want to provide the list of this subset based on what user selects as the make of the car.
For example, if he selects, Toyota, he should be provided with 'Corolla', 'Prius', and 'Camry'. If he selects Honda, he should be provided with 'Civic', 'City', 'Accord' and so on. How am I supposed to acheive this in the best possible manner without using any separate model(s)/Foreign keys? Note that these lists are static.
class CarAd(models.Model):
MAKE_HONDA = 0
MAKE_TOYOTA = 1
MAKES = ((MAKE_HONDA, 'Honda'),
(MAKE_TOYOTA, 'Toyota'),)
MODELS = { MAKE_HONDA: ('Civic', 'City',),
MAKE_TOYOTA: ('Corolla', 'Prius',)
}
make = models.IntegerField(choices=MAKES)
# Django Form
class CarAdForm(forms.Form):
make = forms.ChoiceField(choices=CarAd.MAKES)
def __init__(self, *args, **kwargs):
super(forms.Form, self).__init__(*args, **kwargs)
# Car make
make = args[0]['make']
models = CarAd.MODELS[make]
self.fields['models'] = forms.ChoiceField(choices=models)
Let's say I have the following models:
class House(models.Model):
address = models.CharField(max_length=255)
class Person(models.Model):
name = models.CharField(max_length=50)
home = models.ForeignKey(House, null=True, related_name='tenants')
class Car(models.Model):
make = models.CharField(max_length=50)
owner = models.ForeignKey(Person)
Let's say I have a need (strange one, albeit) to get:
list of people who live in a house or are named 'John'
list of cars of the above people
I would like to have two functions:
get_tenants_or_johns(house)
get_cars_of_tenants_or_johns(house)
I could define them as:
from django.db.models.query_utils import Q
def get_cars_of_tenants_or_johns(house):
is_john = Q(owner__in=Person.objects.filter(name='John'))
is_tenant = Q(owner__in=house.tenants.all())
return Car.filter(is_john | is_tenant)
def get_tenants_or_johns(house):
johns = Person.objects.filter(name='John')
tenants = house.tenants.all()
return set(johns) | set(tenants)
The problem is that the logic is repeated in the above examples. If I could get get_tenants_or_johns(house) to return a queryset I could define get_cars_of_tenants_or_johns(house) as:
def get_cars_of_tenants_or_johns(house):
return Car.objects.filter(owner__in=get_tenants_or_johns(house))
In order to do that, get_tenants_or_johns(house) would need to return a union of querysets, without turning them into other collections.
I cannot figure out how to implement get_tenants_or_johns(house) so that it would return a queryset containing a SQL UNION. Is there a way to do that? If not, is there an alternate way to achieve what I am trying to do?
The | operator on two querysets will return a new queryset representing a union.
The function will need to change to (got rid of set() wrappers):
def get_tenants_or_johns(house):
johns = Person.objects.filter(name='John')
tenants = house.tenants.all()
return johns | tenants
and everything will work exactly like needed.
You mention users who live in a house, but have no mention of your User model.
I think you really need to take a long look at the structure of your application - there are probably much easier ways to accomplish your goal.
But to answer your question let's set up three helper functions. Since, as I mentioned above, you haven't outlined what you want to do with the User class - I've assumed that the house that will be passed to these functions is an address:
helpers.py
def get_johns(house):
is_john = Person.objects.filter(name='John')
return is_john
def get_cars_of_tenants(house):
cars = Car.objects.filter(owner__home__address=house)
return cars
def get_tenants(house):
tenants = Person.objects.filter(home__address=house)
return tenants
Now you could create a view for each of your combination queries:
views.py:
import helpers.py
from itertools import chain
def get_cars_of_tenants_or_johns(request, house):
results = list(chain(get_cars_of_tenants(house), get_johns(house)))
return render_to_response('cars_or_johns.html', {"results": results,})
def get_tenants_or_johns(request, house):
results = list(chain(get_tenants(house), get_johns(house)))
return render_to_response('tenants_or_johns.html', {"results": results,})
And this can go on for all of the various combinations. What is returned is results which is a list of all of the matches that you can iterate over.
I am working on a catalog of classified grouped by categories.
However when I submit my form, I get the following error message:
Caught ValueError while rendering: Cannot assign "u'9'": "Classified.category" must be a "Category" instance.
I believe this is because Django expects a Category objects instead of a simple integer which corresponds to the chosen Category ID.
Here is how I wrote the system:
A classified is linked to one category.
The category system is hierarchical with a parent category and a list of child categories.
For example I have something like this:
Electronics
|-- IPad
|-- IPods
|-- ...
So I have the following models:
class Category(BaseModel):
# [...]
name = models.CharField(u'Name', max_length=50)
slug = AutoSlugField(populate_from='name', slugify=slugify, unique=True,
unique_with='name', max_length=255, default='')
parent = models.IntegerField(u'parent', max_length=10, null=False,
default=0)
objects = CategoryManager()
# [...]
class Classified(BaseModel):
# [...]
category = models.ForeignKey(Category, related_name='classifieds')
I created the following manager:
class CategoryManager(Manager):
def categoryTree(self):
tree = self.raw("SELECT"
" P.id, P.name parent_name, P.slug parent_slug, P.id parent_id,"
" C.name child_name, C.slug child_slug, C.id child_id"
" FROM classified_category C"
" LEFT JOIN classified_category P ON P.id = C.parent"
" WHERE C.parent <> 0"
" ORDER BY P.name, C.name;")
categoryTree = []
current_parent_id = tree[0].parent_id
current_parent_name = tree[0].parent_name
option_list = []
for c in tree:
if current_parent_id != c.parent_id:
categoryTree.append((current_parent_name, option_list))
option_list = []
current_parent_id = c.parent_id
current_parent_name = c.parent_name
option_list.append((c.child_id, c.child_name))
categoryTree.append((current_parent_name, option_list))
return category
And my Django form contains the following:
class ClassifiedForm(forms.ModelForm):
# [...]
category = forms.ChoiceField(label=u'Category', required=True,
choices=Category.objects.categoryTree(), widget=forms.Select())
# [...]
If I use category = forms.ModelChoiceField(Category.objects.all()) everything works fine but I need to control how the <select> field is displayed with a list of <optgroup>. This is why use categoryTree()
But unfortunately using CategoryManager.categoryTree() breaks my form validation and I do not know how to fix my problem.
If I could be pointed to where I was wrong and how I can fix this, that would be awesome.
Thanks in advance for your help.
Quick solution is to save category manually
class ClassifiedForm(forms.ModelForm):
# [...]
category = forms.ChoiceField(label=u'Category', required=True,
choices=Category.objects.categoryTree(), widget=forms.Select())
class Meta:
exclude=('category',)
def save(self):
classified = super(ClassifiedForm, self).save(commit=False)
classified.category = Category.objects.get(id=self.cleaned_data['category'])
classified.save()
return classified
You can and should still use a ModelChoiceField. The list of choices can be modified in the init method of the form class - i.e.
class ClassifiedForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ClassifiedForm, self).__init__(*args, **kwargs)
# Set the queryset for validation purposes.
# May not be necessary if categoryTree contains all categories
self.fields['category'].queryset = Category.objects.categoryTreeObjects()
# Set the choices
self.fields['category'].choices = Category.objects.categoryTree()
Also, you should look carefully at the django-mptt package. It looks like you may be reinventing the wheel here.