Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Many-to-Many Multiplechoice form with optional information

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)
like image 729
alj Avatar asked Dec 09 '16 08:12

alj


People also ask

What is ModelChoiceField?

ModelChoiceField , which is a ChoiceField whose choices are a model QuerySet .

How do I make a field optional in Django?

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.

How do you make a field not required in Django?

The simplest way is by using the field option blank=True (docs.djangoproject.com/en/dev/ref/models/fields/#blank).


Video Answer


1 Answers

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.

View

# 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

Form

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)

Template

<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:

  • you can either create two views and submit to both of them via JS after one "save" button was clicked.
  • or you can submit to one view (like the above) and create the form for the main object explicitly in get() and post() and then save it when formset_valid is called.
  • you might also try implementing both ModelFormsetView and FormView and override all relevant methods to handle both form instances (the formset instance and the main form).
like image 135
Risadinha Avatar answered Oct 02 '22 09:10

Risadinha