I have a many-to-many relationship with a custom pivot table (through argument) to have additional fields. In my form I want to display the many-to-many relationship using checkboxes. And for each ckechbox I want to be able to set the quantity
parameter.
The model:
class Match(models.Model):
# ...Some additional fields...
bonus = models.ManyToManyField(Bonus_type, blank=True, through='Match_bonus')
class Match_bonus(models.Model):
match = models.ForeignKey(Match, on_delete=models.CASCADE)
bonus = models.ForeignKey(Bonus_type, on_delete=models.CASCADE)
quantity = models.IntegerField(blank=True, default=1, validators=[MinValueValidator(0)])
The form:
class MatchForm(forms.ModelForm):
bonus_quantity = forms.IntegerField(validators=[MinValueValidator(0)]) # this is actually wrong because I need one of this field for each option of the ModelMultipleChoiceField
bonus = forms.ModelMultipleChoiceField(queryset=None, widget=forms.CheckboxSelectMultiple)
def __init__(self, *args, tournament_type, **kwargs):
super(MatchForm, self).__init__(*args, **kwargs)
self.fields["bonus"].queryset = Bonus_type.objects.filter(tournament_type=tournament_type)
class Meta:
model = Match
exclude = ()
In my view, I display the form in a formset using inlineformset_factory
:
matchFormset = inlineformset_factory(Menu, Match, MatchForm, exclude=['weight_player_1,weight_player_2'], extra=extra_form)
formset = matchFormset(instance=menu, form_kwargs={'tournament_type': tournament.tournament_type})
Any hint on how to perform that?
Thank you
How to perform what? It looks like you’re off to a good start here, I’m not sure I understand where you’re getting stuck.
If it’s with:
Then I’d be thinking of using an formset here based on the “through” model. The form would be the quantity box and a checkbox for the Bonus_type field. I’d generate one instance of the form for each instance of Bonus_type.
The save method on that form would delete any relationships for which the checkbox is unchecked, and update or add any relationship where it is checked. (Actually, I’d be tempted to just delete all existing instances and insert a new set of instances based on which fields were checked. It’s likely to be faster than doing all the comparisons.)
Thanks for your reply!
I’ll try to be clearer.
In my view, I generate a formset using the MatchForm model form. Each match instance is then a form. In the Match model, I have a many to many relationship to a bonus type, and in the form to edit the match, I want the user to be able to select one or more bonuses (with checkboxes) and also the quantity of each bonus (the quantity field of the Match_bonus).
The picture below shows one form of the formset. Basically for option 1 and option 2 I would like to have a field to set the associated quantity.
So the questions boils down to: How to have fields in the MatchForm model form to change the quantity field of each option of the many to many relationship.
So yes, my previous reply applies. For each instance of the Match form, you would include a formset for the Match_bonus form instead of supplying a field for the bonus field alone. (You’ll also need to manage custom prefixes for each instance of that formset for each of the Match forms.)
Ok, thank you, that’s helpful.
So to summarize (sorry, that’s my first Django app)…
- In my MatchForm, I exclude the bonus field.
- In my view, I create as many Match bonus formset as I have of match instances
But then, a followup question: How to support the dynamic addition of forms on client’s side? In JS I use the “empty form” of the match formset to duplicate it and add forms to the Match formset, but in this case I would need to create a whole new formset, right? How to do that?
That’s one way to do it.
You could also add the creation of the match bonus formset into the init method of your match form. That way, each time you create a new instance of the match form, it will be built with the new instance of the match bonus formset.
Just remember that you need to use a custom prefix with the match bonus formset so that Django can properly bind the form values to the correct instance of the formset.
I come back to you because I don’t understand how I can create the formset in the __init__
function and access it in the view and the template later.
This is how I implemented the formset (I’ll deal with the prefix later).
Could you write a small squeleton for my view and my template on how to use the formset?
class MatchForm(forms.ModelForm):
# bonus_quantity = forms.IntegerField(validators=[MinValueValidator(0)])
# bonus = forms.ModelMultipleChoiceField(queryset=None, widget=forms.CheckboxSelectMultiple)
def __init__(self, *args, tournament_type, **kwargs):
super(MatchForm, self).__init__(*args, **kwargs)
match_formset = modelformset_factory(model=Match_bonus, form=Match_bonus_form, extra=0)
self.match_formset = match_formset(queryset=Match_bonus.objects.filter(bonus__tournament_type=tournament_type))
class Meta:
model = Match
exclude = ['bonus', 'winner']
class Match_bonus_form(forms.ModelForm):
class Meta:
model = Match_bonus
exclude = ['match']
In the view, I create the Match formset as follows:
matchFormset = inlineformset_factory(Menu, Match, MatchForm, extra=extra_form)
formset = matchFormset(instance=menu, form_kwargs={'tournament_type': tournament.tournament_type})
Thank you
Unfortunately I don’t have any samples I can supply, just some more notes that I hope will help.
-
Suggestion - since the output of modelformset_factory
is a Class, your first identifier you’re creating could be MatchFormset instead of match_formset. (It’ll help avoid confusion).
- Then, your second call becomes match_formset = MatchFormset(…)
-
You need to add that formset instance as a field within the form:
Let’s say that you’ve named that field match_formset
. Since that variable is now another field in the form, it would be rendered in your form as form.match_formset. How that formset gets rendered depends upon the formset and the underlying form being replicated.
I followed your recommendations and I implemented the field as
MatchFormset = modelformset_factory(model=Match_bonus, form=Match_bonus_form, extra=0)
self.fields["match_formset"] = MatchFormset(queryset=Match_bonus.objects.all())
But when I render the form using {{form.match_formset}}
I get
Match_bonusFormFormSet' object has no attribute 'get_bound_field
which sort of makes sense to me as the match_formset field is not really a field…
If we can manually add fields in the __init__
function, why not adding one quantity field per bonus option and take care of them in the save
function?
You are correct and was right the first time, I was getting myself confused with something else.
You don’t assign the formset as a field, you do assign it as a member variable in the form.
e.g. self.match_formset = MatchFormset(...)
It should still be rendered at {{ form.match_formset)}
- see the third example in Using a formset in views and templates. In your case, you’re already supplying the form in the context being rendered, so you don’t need to add an extra reference there.
One thing I’m not clear on, and wasn’t clear to me from the screen shot you posted - is your page only going to have one “Match”, or is your page a formset of Matches, each one needing to contain a formset of MatchBonuses?
The second option, this looks like that:
My page consists in a formset of matches and for each match, there will be a formset for the bonuses.
I checked a bit more and indeed the match_formset is render as planned, the issue was that there was no forms in the formset. I have to fix that. I also have a button to add more matches.
At the end, for each match, I’d like to see that. The field next to each option is to set the quantity
My previous question remains (I am trying to learn the best practice and therefore understand why this is not a good option):
If we can manually add fields in the __init__
function, why not adding one quantity field per bonus option and take care of them in the save
function?
We could for instance have a loop in the __init__
function of the match_formset
to create as many quantity
fields as bonus options. In the save function of that form, we would take care of the additional fields to update the relationships.
Yes, you can do this - in which case you’re effectively replicating a feature that Django provides. What you’re describing is effectively what the formset is going to do - without some features that you’re (probably) never going to need in this specific case. It’s quite possible that your solution may even require fewer lines of code - but I still wouldn’t recommend it.
<opinion>
It really comes down to a “mindset” decision. For us, we made a conscious decision - if we need to do “X”, and Django provides a feature that does “X”, we use Django’s feature rather than coding our own solution. Tactically, it makes some decisions “sub optimal”. Strategically, it makes sense for us. Our code is more uniform across the entirety of the code base - in theory, making it easier for someone to step in to help. We look at it, not from the perspective of “what we think is the easiest way right now”, but “what we think will be the easiest to maintain in the future.” Part of that is ensuring that there is some uniformity of structure and design across the entirety of the application.
</opinion>
Understood, thanks for taking the time to explain.
So if I come back to my problem, let’s try to have a roadmap before I start coding:
- Each bonus option will have its own form in the formset, displayed as a checkbox
- As I want the foreign key to be implemented as a checkbox (and not a select), I would need to add extra parameters to the
Match_bonus_form
constructor, telling it which option to display. Correct?
- This implies that I write a subclass of the BaseModelFormset and override one of its methods (
_construct_form
?) to provide the form constructor with the additional parameter mentioned at step 2. For this I think of looping over the results of a queryset on the child table bonus_type
As an example the query returning the right bonus_options is Bonus_type.objects.filter(tournament_type=tournament_type)
Is that the good way to approach this?
I thought the idea was to create the form as a combination of checkbox and numerical entry field? (Making the form two fields, not one)
Kinda?
Since you want one checkbox for every Bonus_type, and not just the ones currently assigned you could do something like:
MatchBonusFormset = formset_factory(BonusForm, extra=0)
queryset = BonusType.objects.all().values('id', 'bonus_name')
bonus_formset = MatchBonusFormset(initial=queryset)
where your BonusForm may look something like:
BonusForm(Form):
id = IntegerField(widget=HiddenInput)
bonus_name = BooleanField(required=False)
count = IntegerField(initial=0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['bonus_name'].label = kwargs['initial']['bonus_name']
Now, this becomes a little more complex if you need to populate the count field based upon existing relationships, but that’s just a matter of creating the proper queryset based on parameters within the forms being built.
Thanks a lot
It works. I adapted it to my case.
I am now focusing on the formset processing.
What would you suggest, creating a save function in the formset to handle the data?
I have the feeling that processing the data in the view is not a good idea wrt to the DRY concept.
In that function I would use the id field to change the corresponding entry in the “through” table.
I would then call this save function from the save function of the “parent” formset
Generally I would say yes to that. But that’s actually one of those areas where I don’t hold any really strong opinion. My typical answer to a question like that is that I would use whatever convention, standard, or practice that exists in the rest of the code.