In the previous version of my app, I had a many-to-many relationship between Account and Club. In my AccountForm I used "club = forms.MultipleChoiceField(widget=CheckboxSelectMultiple)" to enable the user to select from the full listing of clubs.
▢ Football
▢ Hockey
▢ Tennis
▢ Swimming
However, I now need to include an optional field where they can include their membership reference number if they have it. So something like
▢ Football ________
▢ Hockey ________
▢ Tennis ________
▢ Swimming ________
I realise that I have to use a through
model, but am now struggling to replicate the multiple choice style layout I had before.
a) I presume that I need to use an inline formset but based on the through table, so somehow I need to get a formset factory to create forms for each of the clubs. I'm not sure how to do that. Clues?
b) Include a checkbox to reflect membership of that club. So presumably a boolean field with a hidden field indicating the id of the club and then some custom work clean and save functions.
Does this seem right, or is there a simpler way?
class Account(models.Model):
name = models.CharField(max_length=20)
address_street01 = models.CharField(max_length=50)
address_pc = models.CharField(max_length=10)
address_city = models.CharField(max_length=50)
class Club(models.Model):
name = models.CharField(max_length=30, unique=True)
class Membership(models.Model):
club = models.ForeignKey(Club)
account = models.ForeignKey(Account)
membership_ref = models.CharField(max_length=50, blank=True)
ModelChoiceField , which is a ChoiceField whose choices are a model QuerySet .
If the model field has blank=True, then required is set to False on the form field. Otherwise, required=True. Don't forget to reset and sync DB again after changing this.
The simplest way is by using the field option blank=True (docs.djangoproject.com/en/dev/ref/models/fields/#blank).
We are using ModelFormSetView
from django-extra-views for a similar use case. It is not backed by a through
model but by a table with a many-to-one relationship where the many relations with all their attributes are displayed as part of the detail view of the main model that is related via ForeignKey.
It would work for a through
Model as well by just giving the through
Model as the model attribute of the ModelFormSetView
. When saving or even before, via get_extra_form_kwargs
you would have to set the reference to the main model instance that defines the m2m field.
The tricky thing with the regular django FormSets is (to me) that it's mostly for creating new objects while we only needed to display existing objects and modify them. Basically we needed repeating forms populated with initial data that are all saved at once. It's also possible to delete them.
# You could additionally try to inherit from SingleObjectMixin
# if you override the methods that refer to cls.model
class ImportMatchView(ImportSessionMixin, ModelFormSetView):
template_name = 'import_match.html'
model = Entry # this is your through model class
form_class = EntryForm
can_delete = True
def get_success_url(self):
return self.get_main_object().get_absolute_url()
def get_factory_kwargs(self):
kwargs = super().get_factory_kwargs()
num = len(self.get_match_result())
kwargs['extra'] = num # this controls how many forms are generated
kwargs['max_num'] = num # no empty forms!
return kwargs
def get_initial(self):
# override this if you have to previous m2m relations for
# this main object
# this is a dictionary with the attributes required to prefill
# new instances of the through model
return self.get_match_result() # this fetches data from the session
def get_extra_form_kwargs(self):
# you could add the instance of the m2m main model here and
# handle it in your custom form.save method
return {'user': self.request.user}
def get_queryset(self):
# return none() if you have implemented get_initial()
return Entry.objects.none()
# return existing m2m relations if they exist
# main_id = self.get_object().pk # SingleObjectMixin or alike
# return Entry.objects.filter(main=main_id)
def formset_valid(self, formset):
# just some example code of what you could do
main = self.get_main_object()
response = super().formset_valid(formset)
main_attr_list = filter(None, [form.cleaned_data.get('entry_attr') for form in formset.forms])
main.main_attr = sum(main_attr_list)
main.save()
return response
A regular Django ModelForm for your through
model. Just like with the user here, provide the reference to the instance of the model defining the m2m field so that you can assign it before saving.
def __init__(self, *args, user=None, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
def save(self, commit=True):
self.instance.owner = self.user
return super().save(commit)
<form id="the-matching" method="POST"
action="{{ save_url }}" data-session-url="{{ session_url }}">
{% csrf_token %}
{{ formset.management_form }}
<ul class="match__matches">
{% for form in formset %}
{% include 'import_match__match.html' %}
{% endfor %}
</ul>
</form>
In each form (inside import_match__match.html
), you iterate over the fields the usual django way. Here an example for hidden fields:
{% for field in form %}
{% if field.value %}
<input type="hidden" name="{{ form.prefix }}-{{ field.name }}" value="{{ field.value }}"/>
{% endif %}
{% endfor %}
Handling the form for the main object:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With