I posted this question on the django-users list, but haven't had a reply there yet.
I have models that look something like this:
class ProductGroup(models.Model):
name = models.CharField(max_length=10, primary_key=True)
def __unicode__(self): return self.name
class ProductRun(models.Model):
date = models.DateField(primary_key=True)
def __unicode__(self): return self.date.isoformat()
class CatalogItem(models.Model):
cid = models.CharField(max_length=25, primary_key=True)
group = models.ForeignKey(ProductGroup)
run = models.ForeignKey(ProductRun)
pnumber = models.IntegerField()
def __unicode__(self): return self.cid
class Meta:
unique_together = ('group', 'run', 'pnumber')
class Transaction(models.Model):
timestamp = models.DateTimeField()
user = models.ForeignKey(User)
item = models.ForeignKey(CatalogItem)
quantity = models.IntegerField()
price = models.FloatField()
Let's say there are about 10 ProductGroups and 10-20 relevant
ProductRuns at any given time. Each group has 20-200 distinct
product numbers (pnumber), so there are at least a few thousand
CatalogItems.
I am working on formsets for the Transaction model. Instead of a
single select menu with the several thousand CatalogItems for the
ForeignKey field, I want to substitute three drop-down menus, for
group, run, and pnumber, which uniquely identify the CatalogItem.
I'd also like to limit the choices in the second two drop-downs to
those runs and pnumbers which are available for the currently
selected product group (I can update them via AJAX if the user
changes the product group, but it's important that the initial page
load as described without relying on AJAX).
What's the best way to do this?
As a point of departure, here's what I've tried/considered so far:
My first approach was to exclude the item foreign key field from the
form, add the substitute dropdowns by overriding the add_fields
method of the formset, and then extract the data and populate the
fields manually on the model instances before saving them. It's
straightforward and pretty simple, but it's not very reusable and I
don't think it is the right way to do this.
My second approach was to create a new field which inherits both
MultiValueField and ModelChoiceField, and a corresponding
MultiWidget subclass. This seems like the right approach. As
Malcolm Tredinnick put it in
a django-users discussion,
"the 'smarts' of a field lie in the Field class."
The problem I'm having is when/where to fetch the lists of choices
from the db. The code I have now does it in the Field's __init__,
but that means I have to know which ProductGroup I'm dealing with
before I can even define the Form class, since I have to instantiate the
Field when I define the form. So I have a factory
function which I call at the last minute from my view--after I know
what CatalogItems I have and which product group they're in--to
create form/formset classes and instantiate them. It works, but I
wonder if there's a better way. After all, the field should be
able to determine the correct choices much later on, once it knows
its current value.
Another problem is that my implementation limits the entire formset
to transactions relating to (CatalogItems from) a single
ProductGroup.
A third possibility I'm entertaining is to put it all in the Widget
class. Once I have the related model instance, or the cid, or
whatever the widget is given, I can get the ProductGroup and
construct the drop-downs. This would solve the issues with my
second approach, but doesn't seem like the right approach.
One way of setting field choices of a form in a formset is in the form's __init__ method by overwriting the self.fields['field_name'].choices, but since a more dynamic approach is desired, here is what works in a view:
from django.forms.models import modelformset_factory
user_choices = [(1, 'something'), (2, 'something_else')] # some basic choices
PurchaserChoiceFormSet = modelformset_factory(PurchaserChoice, form=PurchaserChoiceForm, extra=5, max_num=5)
my_formset = PurchaserChoiceFormSet(self.request.POST or None, queryset=worksheet_choices)
# and now for the magical for loop
for choice_form in my_formset:
choice_form.fields['model'].choices = user_choices
I wasn't able to find the answer for this but tried it out and it works in Django 1.6.5. I figured it out since formsets and for loops seem to go so well together :)
I ended up sticking with the second approach, but I'm convinced now that it was the Short Way That Was Very Long. I had to dig around a bit in the ModelForm and FormField innards, and IMO the complexity outweighs the minimal benefits.
What I wrote in the question about the first approach, "It's straightforward and pretty simple," should have been the tip-off.
Related
Forms can be complicated in Django. Formsets can make you want to quit Django. I'm at that point.
What are the different use cases and considerations of which one(s) to use?
I'm looking for some better guidance as to when to use each factory, as they seem to depend on when you know what type of form, fields, and whether or not you are creating, editing, or deleting (individual forms entirely or the parent model altogether). I have read many walkthroughs, but am struggling to see the larger picture, especially as I am attempting to move from function based views to Class Based Views.
Below are some pseudo code with assumptions/restrictions to help you help me understand the differences. It may help to provide psuedocode, such as what kind of Form (ModelForm or regular) goes with the Formset, or what should be popped from the form, given this seems to be a trend for creating forms with relations.
Assuming you have some models:
class Dish(models.Model):
name = models.CharField(max_length=50)
class Meal(models.Model):
name = models.CharField(max_length=50)
dishes = models.ManyToManyField(Dish,
# through='OPTIIONALMealDishIntermediaryClassTable',
related_name="meal")
class Reservation(models.Model):
date = models.DateTimeField()
greeting = models.CharField(max_length=255)
meal = models.OneToOneField(Meal, on_delete=models.CASCADE)
class MealPhotos(models.Model):
photo = models.OneToOneField(Photo, on_delete=models.CASCADE, related_name='mealPhoto')
meal = models.ForeignKey(Meal, on_delete=models.CASCADE)
# optional, so a photo can be attached to a dish if the picture is just of the dish
dish = models.ForeignKey(Dish, blank=True, null=True, on_delete=models.CASCADE)
And you want to create a new Meal, you want to send a Reservation at the same time:
which factory would you use?
does it depend on if the forms are all ModelForms? (meaning how would you handle assignming the Meal its Reservation)
assuming:
at this stage you know which Meal it is, but you still have to make a Reservation at the same time/same view.
you don't know which Dishes you are going to cook, since the reservation will tell you.
MealPhotos won't exist yet since the meal isn't prepared yet.
You want to create the meal and the reservation on the same form/screen
Then later, you want to add some dishes, based on what the Reservation says:
which factory would you use?
does it depend on if the forms are all ModelForms?
assuming:
at this stage you know which Meal it is, and you have a Reservation
you are going to assign dishes to the meal based on the Reservation, and you have enough information to do so, and can use a ModelForm easily, but not required
Later, the person eating the dish wants to take some photos, and you don't know how many they will take
which factory would you use?
does it depend on if the forms are all ModelForms?
assuming:
we will require them to take at least two
we have access to the Meal, Reservation, and Dishes
a photo could optionally be assigned to a Dish
The difference between the 3 formset factories is basically:
formset_factory lets you render a bunch of forms together, but these forms are NOT necessarily related to a particular database models (this is not what you need, since you have models for everything)
modelformset_factory lets you create/edit a bunch of Django model objects together, for example, if you were managing the "menu" you could use a modelformset_factory(Dish, fields=('name'))
inlineformset_factory lets you manage a bunch of Django model objects that are all related to a single instance of another model. For example, if you wanted to manage all of the MealPhotos that were taken at a particular Meal, this is what you would use
To answer your specific scenarios:
If you wanted a page that created a single Meal and a single Reservation at the same time, without assigning any Dishes yet, you don't need any formsets. You could just use a ModelForm for Meal and a ModelForm for Reservation.
Later on, if you want to attach multiple Dishes to the Meal, you would use an inlineformset_factory(Meal, Dish) to edit multiple Dishes belonging to a single Meal
Since we are using an inlineformset_factory, we have to create the Meal instance in the view that renders the form. Something like this:
DishFormSet = inlineformset_factory(Meal, Dish)
bday_dinner = Meal.objects.create(name='30th Birthday dinner')
formset = DishFormSet(instance=bday_dinner)
For someone uploading photos of the Meal, you would use:
PhotosFormSet = inlineformset_factory(Meal, MealPhotos)
bday_dinner = Meal.objects.get(name='30th Birthday dinner')
formset = PhotosFormSet(instance=bday_dinner)
This tells Django that all the photos submitted are all linked to that one Meal, but allows the possibility of assigning each photo to a different Dish (via a dropdown in the form).
Note: In the first scenario, I haven't tested whether you the use of a ManyToManyField as Meal.dishes is supported by the formset factory. If it isn't, then you could simply use a ModelFormset(Dish) and after those are created, link them to the Meal in the Django view that process the form submission.
I have a model in my app that contains only one field - a ModelChoiceField - that I would like to be associated with user profiles (users being able to have multiple choices associated with them). These choices won't change while running, and I will eventually use them to filter the users by their choice. Here's what the two models look like.
...
class Choice(models.Model):
Choice1 = 0
Choice2 = 1
Choice3 = 2
Choices_Available = (
(Choice1, 'Choice1'),
(Choice2, 'Choice2'),
(Choice3, 'Choice3'),
)
choice = models.IntegerField(choices=Choices_Available, null=True)
def __iter__(self):
return iter(self.Choices_Available)
def __str__(self):
return self.Choices_Available[self.choice]
class Profile(models.Model):
choice = models.ManyToManyField(Choice)
The problem I have is trying to actually select a choice and associate that choice with the user. In the user form that would actually do the selecting, in most examples I have seen, a MultipleChoiceField with the available choice objects filtered:
...
class EditProfile(forms.ModelForm):
choice = forms.MultipleChoiceField(choices=Choice.objects.all(), widget=CheckboxSelectMultiple)
class Meta:
model = Profile
fields = ['choice']
That particular setup currently means nothing shows up because there are no Choice objects.
I think I would fix that by creating a choice object for each available choice. Then when a user requests that page with the form, they see the available choices. In the end, all users would be able to be associated with however many choice objects there are (in this example 3), and there would only be that many objects stored in the database minimizing space used.
To do that I need to be able to create Choice objects. My question boils down to: how do I create Choice objects with a choice selected (there currently is only the default init method). Also, is this a reasonable way of going about this feature?
Thanks
For anyone wondering the same thing, I'll tell you what I've found out.
To instantiate an object specifying the choice:
Choice.objects.create(choice=0)
Of course change the number corresponding to the choice, which can be done programmatically.
Also, I had a mistake in the form. MultipleChoiceField is for choosing from a set of options. ModelChoiceField is very similar, but it takes in a queryset of objects and allows you to chose from those objects. This is the functionality you might want for a many to many field like in my example.
[This is a refactor of a previous question, and I hope this question is more clear.]
I have a an activity model that holds a number and is dependent on a person and a category.
class Activity( models.Model ):
person = models.ForeignKey( Person ) # approx 80 people
category = models.ForeignKey( Category ) # approx 6 categories
value = models.FloatField( )
The users would like to have a table with category across the top and person down the side and then a value field in each cell if the table.
Currently my form is:
class MonthlyActivityForm(forms.Form):
def __init__(self, *args, **kwargs ):
affiliations = kwargs.pop('affiliations')
point_categories = kwargs.pop('point_categories')
super(MonthlyActivityForm, self).__init__(*args, **kwargs)
for pc in point_categories:
for a in affiliations:
self.fields['pc-%d-pe-%d' % (pc.id, a.person.id)] = forms.FloatField()
I can't see how to do a table of form fields (nicely) in regular Django. I think it comes down to the form building which will affect the efficiency of the template and view processing.
First pass thought is to:
1) create a FloatField for each person,category create a template to create a table as described above (this part seems harder than it should be)
2) in the view, on a valid form run through each FloatField in the returned form and save any changes
So, I suppose there are two questions:
1) is there a more efficient method to create a table of FloatFields other than trying to figure out looping in the template to create a table with FloatFields in the td's?
2) how can I store the person.id and category.id in the FloatField such that when I go back to the view with a valid form then I can create/update the proper Activity entry? (or can I just grab it from the form key for each FloatField?)
I am reasonably new to Django, but it seems like it should be slightly easier. I have to be missing something here.
You could make this much simpler by using formsets. In this case I would probably have one form per user, containing all the category fields created dynamically, but the formset would take care of instantiating one of these forms per user.
In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:
class Bet(models.Model):
...
participants = models.ManyToManyField(User)
User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.
I'm using a ModelForm for my NewBetForm:
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
Notice the redefined widget for the participants field which makes sure you can only choose one participant.
However, this gives me a validation error:
Enter a list of values.
I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.
I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.
Can anyone tell me what the problem is exactly and if there is a good way to solve it?
Thanks in advance!
Edit
As suggested in the comments: the (relevant) code of the view.
def new_bet(request):
if request.method == 'POST':
form = NewBetForm(request.POST)
if form.is_valid():
form.save(request.user)
... # success message and redirect
else:
form = NewBetForm()
return render(request, 'bets/new.html', {'form': form})
After digging in the Django code, I can answer my own question.
The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.
To solutions I could think of:
Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.
The seconds solution seems to be less verbose, so that's what I will be going with.
I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.
In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.
forms.py
from django.utils.datastructures import MultiValueDict, MergeDict
class M2MSelect(forms.Select):
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
class WindowsSubnetForm(forms.ModelForm):
port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
class Meta:
model = Subnet
The problem is that ManyToMany is the wrong data type for this relationship.
In a sense, the bet itself is the many-to-many relationship. It makes no sense to have the participants as a manytomanyfield. What you need is two ForeignKeys, both to User: one for the creator, one for the other user ('acceptor'?)
You can modify the submitted value before (during) validation in Form.clean_field_name. You could use this method to wrap the select's single value in a list.
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
def clean_participants(self):
data = self.cleaned_data['participants']
return [data]
I'm actually just guessing what the value proivded by the select looks like, so this might need a bit of tweaking, but I think it will work.
Here are the docs.
Inspired by #Ryan Currah I found this to be working out of the box:
class M2MSelect(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
return rendered.replace(u'multiple="multiple"', u'')
The first one of the many to many is displayed and when saved only the selected value is left.
I found an easyer way to do this inspired by #Ryan Currah:
You just have to override "allow_multiple_selected" attribut from SelectMultiple class
class M2MSelect(forms.SelectMultiple):
allow_multiple_selected = False
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())
Suppose I have following models:
class Thing(models.Model):
name = models.CharField(max_length=100)
ratings = models.ManyToManyField('auth.User', through='Rating')
class Rating(models.Model):
user = models.ForeignKey('auth.User')
thing = models.ForeignKey('Thing')
rating = models.IntegerField()
So I have a lot of things, and every user can rate every thing. I also have a view showing a list of all things (and they are huge in numbers) with a rating that user assigned to each of them. I need a way to retreive all the data from database: Thing objects with additional field user_rating taken from at most one (because we have a fixed User) related Rating object.
Trivial solution looks like that:
things = Thing.objects.all()
for thing in things:
try:
thing.user_rating = thing.ratings.objects.get(user=request.user).rating
except Rating.DoesNotExist:
thing.user_rating = None
But the flaw of this approach is obvious: if we have 500 things, we'll do 501 requests to database. Per one page. Per user. And this is the most viewed page of the site. This task is easily solvable with SQL JOINs but in practice I have more complicated schema and I will certainly benefit from Django model framework. So the question is: is it possible to do this Django-way? It would be really strange if it isn't, considering that such tasks are very common.
As I understood, neither annotate(), nor select_related() will help me here.
I guess you should try this:
https://docs.djangoproject.com/en/1.3/ref/models/querysets/#extra
Example
result = Thing.objects.all().extra(select={'rating': 'select rating from ratings where thing_id = id'})
Your result set gets a new field 'rating' for each 'thing' object.
I use this approach in one of my recent projects. It produces one complex query instead of n+1 queries.
Hope this helps :)
Since you are planning to display everything in one page. I can think of this approach. You can give this a try:
Get all the ratings given by the current user and Get all the Things.
Now try to create a dictionary like this:
thing_dict = {}
for thing in Thing.objects.all():
thing_dict[thing] = None
for rating in Rating.objects.filter(user = request.user):
thing_dict[rating.thing] = rating
Now thing_dict contains all the entries of model Thing as keys and has its rating as its value.
May not be the best way. I am keen on seeing what others answer.