How to render intermediate table inline *as* multiplechoice
-
06-06-2021 - |
سؤال
I have the following models,
### models.py
class Foo(models.Model):
name = models.CharField(max_length=200)
class Bar(models.Model):
foo = models.ForeignKey(Foo)
baz = models.ManyToManyField(Baz, through='Between')
class Baz(models.Model):
name = models.CharField(max_length=200)
class Between(models.Model):
foo = models.ForeignKey(Foo)
bar = models.ForeignKey(Bar)
CHOICES = (
('A', 'A'),
('B', 'B'),
('C', 'C'),
)
value = models.CharField(max_length=1, choices=CHOICES)
and I have the following forms,
### forms.py
class FooForm(forms.ModelForm):
class Meta:
model = Foo
class BarForm(forms.ModelForm):
baz = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple(),
queryset=Baz.objects.all())
class Meta:
model = Bar
exclude = ('foo',)
BarFormSet = inlineformset_factory(Foo, Bar, form=BarForm, can_delete=False)
Now, this works great in that I can render a single Foo
and I get a number of inline forms for Bar
. This renders is that the inline BarForm
renders all the options of Baz
as checkboxes.
What I would like is for each record of Baz
to be rendered as a set of radio buttons representing the possible choices for value
---along with a "N/A" choice---so that if A,B, or C is selected then the relationship to Baz
is implied. But by default there doesn't seem to be a nice way of doing this with completely re-implementing RadioSelect or implementing a completely new widget, but I would like to follow the path of least resistance.
Hopefully I am making things clear.
المحلول
My solution was to create SuperForm
which allows subforms. The total solution is not quite generic for everyone but I did my best here to take what I was using and make it a bit more general.
Here is models.py
:
from django.db import models
class Enemy(models.Model):
name = models.CharField(max_length=200)
def __unicode__(self):
return self.name
class Hero(models.Model):
name = models.CharField(max_length=200)
enemy = models.ManyToManyField(Enemy, through='Relationship', blank=False, null=False)
def __unicode__(self):
return self.name
class Relationship(models.Model):
hero = models.ForeignKey(Hero)
enemy = models.ForeignKey(Enemy)
ENEMY_TYPE_CHOICES = (
('G', 'Good'),
('B', 'Bad'),
('U', 'Ugly'),
)
enemy_type = models.CharField(max_length=1, choices=ENEMY_TYPE_CHOICES)
def __unicode__(self):
return u"{0} {1} {2}".format(self.hero, self.strength, self.relationship)
Here is what is in my forms.py
:
from models import *
from django import forms
from django.forms.formsets import formset_factory, BaseFormSet
from django.forms.models import inlineformset_factory, BaseModelFormSet
from django.utils.safestring import mark_safe
class SuperForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.formsets = []
super(SuperForm, self).__init__(*args, **kwargs)
@property
def is_bound(self):
return self._is_bound or \
any([subformset.is_bound for subformset in self.formsets])
@is_bound.setter
def is_bound(self, value):
self._is_bound = value
def has_changed(self):
return bool(self.changed_data) or \
any([subformset.has_changed() for subformset in self.formsets])
def update_empty_permitted(self, subformset):
empty_permitted = not self.has_changed()
for subformset in self.formsets:
for form in subformset:
form.empty_permitted = empty_permitted
def is_valid(self):
subforms_valid = True
for subformset in self.formsets:
self.update_empty_permitted(subformset)
sfserrors = [err for err in subformset.errors if err]
if bool(sfserrors):
subforms_valid = False
return subforms_valid and super(SuperForm, self).is_valid()
class RelationshipForm(forms.ModelForm):
enemy = forms.ModelChoiceField(queryset=Enemy.objects.all(),
widget=forms.HiddenInput(),
required=True)
enemy_type = forms.ChoiceField(label="", widget=forms.RadioSelect,
choices=Relationship.ENEMY_TYPE_CHOICES,
required=True)
def name(self):
pk = self['enemy'].value()
return self.fields['enemy'].queryset.get(pk=pk)
class Meta:
model = Relationship
exclude = ('hero', 'enemy_type',)
RelationshipFormSet = formset_factory(RelationshipForm, extra=0, can_delete=False)
class HeroForm(SuperForm):
def __init__(self, *args, **kwargs):
super(HeroForm, self).__init__(*args, **kwargs)
initial=[dict(enemy=enemy.pk) for enemy in Enemy.objects.all()]
self.relationship_formset = RelationshipFormSet(initial=initial, prefix=self.prefix, data=kwargs.get('data'))
self.formsets.append(self.relationship_formset)
class Meta:
model = Hero
exclude = ('enemy',)
HeroFormSet = formset_factory(HeroForm, extra=1, can_delete=False)
Here is the views.py
:
from django.views.generic import TemplateView
from django.views.generic.edit import FormMixin
from forms import *
class HeroView(FormMixin, TemplateView):
template_name = "formfun/hero.html"
form_class = HeroFormSet
def get_context_data(self, **kwargs):
context = super(HeroView, self).get_context_data(**kwargs)
context['formset'] = HeroFormSet()
return context
def form_valid(self, form):
return render_to_response({'form':form, 'valid': True})
def form_invalid(self, form):
return render_to_response({'form':form, 'valid': False})
And here is the template as I render it:
{% extends "base.html" %}
{% block title %}form fun{% endblock %}
{% block content %}
<form>
{% for form in formset %}
{{ form.as_p }}
{% for subform in form.relationship_formset %}
<label for="id_{{ subform.html_name }}_0" class="label50">{{ subform.name }}</label>
{{ subform.as_p }}
{% endfor %}
{% endfor %}
</form>
{% endblock content %}