2012-12-16 8 views
8

Jak pogrupować pola wyboru utworzone przez CheckboxSelectMultiple przez podobny model?Zgrupowane CheckboxSelectMultiple w szablonie Django

Najlepiej widać to na przykładzie.

models.py:

class FeatureCategory(models.Model): 
    name = models.CharField(max_length=30) 

class Feature(models.Model): 
    name = models.CharField(max_length=30) 
    category = models.ForeignKey(FeatureCategory) 

class Widget(models.Model): 
    name = models.CharField(max_length=30) 
    features = models.ManyToManyField(Feature, blank=True) 

forms.py:

class WidgetForm(forms.ModelForm): 
    features = forms.ModelMultipleChoiceField(
     queryset=Feature.objects.all(), 
     widget=forms.CheckboxSelectMultiple, 
     required=False 
    ) 
    class Meta: 
     model = Widget 

views.py:

def edit_widget(request): 
    form = WidgetForm() 
    return render(request, 'template.html', {'form': form}) 

template.html:

{{ form.as_p }} 

Powyższe daje następujący wynik:

[] Widget 1 
[] Widget 2 
[] Widget 3 
[] Widget 1 
[] Widget 2 

Co chciałbym się do pola wyboru funkcji, aby być pogrupowane według kategorii funkcji (na podstawie ForeignKey):

Category 1: 
    [] Widget 1 
    [] Widget 2 
    [] Widget 3 

Category 2: 
    [] Widget 1 
    [] Widget 2 

Jak mogę to osiągnąć? Próbowałem użyć znacznika szablonu {% regroup %} bez rezultatu.

Każda rada bardzo cenna.

Dzięki.

Odpowiedz

14

Musisz napisać niestandardowy widget CheckboxSelectMultiple. Używając snippet próbowałem uczynić pole CheckboxSelectMultiple iterowalne, dodając category_name jako atrybut w polu attrs. Tak, abym mógł później użyć tagu regroup w szablonie.

Poniższy kod jest modyfikowany z fragmentu w zależności od potrzeb, oczywiście kod ten może być bardziej przejrzysty i bardziej ogólny, ale w tym momencie nie jest generyczny.

forms.py

from django import forms 
from django.forms import Widget 
from django.forms.widgets import SubWidget 
from django.forms.util import flatatt 
from django.utils.html import conditional_escape 
from django.utils.encoding import StrAndUnicode, force_unicode 
from django.utils.safestring import mark_safe 

from itertools import chain 
import ast 

from mysite.models import Widget as wid # your model name is conflicted with django.forms.Widget 
from mysite.models import Feature 

class CheckboxInput(SubWidget): 
    """ 
    An object used by CheckboxRenderer that represents a single 
    <input type='checkbox'>. 
    """ 
    def __init__(self, name, value, attrs, choice, index): 
     self.name, self.value = name, value 
     self.attrs = attrs 
     self.choice_value = force_unicode(choice[1]) 
     self.choice_label = force_unicode(choice[2]) 

     self.attrs.update({'cat_name': choice[0]}) 

     self.index = index 

    def __unicode__(self): 
     return self.render() 

    def render(self, name=None, value=None, attrs=None, choices=()): 
     name = name or self.name 
     value = value or self.value 
     attrs = attrs or self.attrs 

     if 'id' in self.attrs: 
      label_for = ' for="%s_%s"' % (self.attrs['id'], self.index) 
     else: 
      label_for = '' 
     choice_label = conditional_escape(force_unicode(self.choice_label)) 
     return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), choice_label)) 

    def is_checked(self): 
     return self.choice_value in self.value 

    def tag(self): 
     if 'id' in self.attrs: 
      self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) 
     final_attrs = dict(self.attrs, type='checkbox', name=self.name, value=self.choice_value) 
     if self.is_checked(): 
      final_attrs['checked'] = 'checked' 
     return mark_safe(u'<input%s />' % flatatt(final_attrs)) 

class CheckboxRenderer(StrAndUnicode): 
    def __init__(self, name, value, attrs, choices): 
     self.name, self.value, self.attrs = name, value, attrs 
     self.choices = choices 

    def __iter__(self): 
     for i, choice in enumerate(self.choices): 
      yield CheckboxInput(self.name, self.value, self.attrs.copy(), choice, i) 

    def __getitem__(self, idx): 
     choice = self.choices[idx] # Let the IndexError propogate 
     return CheckboxInput(self.name, self.value, self.attrs.copy(), choice, idx) 

    def __unicode__(self): 
     return self.render() 

    def render(self): 
     """Outputs a <ul> for this set of checkbox fields.""" 
     return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' 
       % force_unicode(w) for w in self])) 

class CheckboxSelectMultipleIter(forms.CheckboxSelectMultiple): 
    """ 
    Checkbox multi select field that enables iteration of each checkbox 
    Similar to django.forms.widgets.RadioSelect 
    """ 
    renderer = CheckboxRenderer 

    def __init__(self, *args, **kwargs): 
     # Override the default renderer if we were passed one. 
     renderer = kwargs.pop('renderer', None) 
     if renderer: 
      self.renderer = renderer 
     super(CheckboxSelectMultipleIter, self).__init__(*args, **kwargs) 

    def subwidgets(self, name, value, attrs=None, choices=()): 
     for widget in self.get_renderer(name, value, attrs, choices): 
      yield widget 

    def get_renderer(self, name, value, attrs=None, choices=()): 
     """Returns an instance of the renderer.""" 

     choices_ = [ast.literal_eval(i[1]).iteritems() for i in self.choices] 
     choices_ = [(a[1], b[1], c[1]) for a, b, c in choices_] 

     if value is None: value = '' 
     str_values = set([force_unicode(v) for v in value]) # Normalize to string. 
     if attrs is None: 
      attrs = {} 
     if 'id' not in attrs: 
      attrs['id'] = name 
     final_attrs = self.build_attrs(attrs) 
     choices = list(chain(choices_, choices)) 
     return self.renderer(name, str_values, final_attrs, choices) 

    def render(self, name, value, attrs=None, choices=()): 
     return self.get_renderer(name, value, attrs, choices).render() 

    def id_for_label(self, id_): 
     if id_: 
      id_ += '_0' 
     return id_ 

class WidgetForm(forms.ModelForm): 
    features = forms.ModelMultipleChoiceField(
     queryset=Feature.objects.all().values('id', 'name', 'category__name'), 
     widget=CheckboxSelectMultipleIter, 
     required=False 
    ) 
    class Meta: 
     model = wid 

Następnie w szablonie:

{% for field in form %} 
{% if field.name == 'features' %} 
    {% regroup field by attrs.cat_name as list %} 

    <ul> 
    {% for el in list %} 
     <li>{{el.grouper}} 
     <ul> 
      {% for e in el.list %} 
       {{e}} <br /> 
      {% endfor %} 
     </ul> 
     </li> 
    {% endfor %} 
    </ul> 
{% else %} 
    {{field.label}}: {{field}} 
{% endif %} 

{% endfor %} 

Wyniki: dodałem kraje nazwę w kategorii stole i miast nazwa funkcje stołu tak w szablonie udało mi się przegrupować miasta (funkcje) według kraju (kategoria)

enter image description here

+0

Miałem nadzieję, że będzie to wbudowany sposób, ale to dokładnie to, czego szukałem. Dziękuję Ci. – gjb

+0

Twoje powitanie :) –

+0

Czy ten kod nadal działa? Po prostu próbowałem go użyć i zagubiłem się w komunikatach o błędach związanych z ast. – stickwithjosh