Login or Sign up

Wherein the inner workings of the DeathStarWidget are revealed

Posted by: skyl on Jan. 9, 2011

You may find that you want to use Django's forms.CheckboxSelectMultiple with a forms.ModelMultipleChoiceField. But, for each choice, you want to show a little table row of attrs for that choice (not just the __unicode__ for that model instance). Maybe you would like some actions for each of the model instances in the queryset of choices for the CheckboxSelectMultiple.

Hmm, how to do this? It seems that there must be an easy way. Below is a not so easy way to perform this presentational task. Does Django provide an easier way?

class DeathStarWidget(forms.widgets.SelectMultiple):
  '''like checkboxselectmultiple,

  allows user to specify:
    fields=(,) and the fields of the model will appear in the table.
    actions=[{'name': 'clone', 'url_method': 'clone_url', 'image_url': '/media/clone.png'},{...} ... ]
  checkboxes, field1, field2, action1, action2'''

  def __init__(self, *a, **kw):
      self.fields = kw.pop('fields', []) # list of attrs
      # actions is sequence of dicts /w keys 'image_url', 'name', 'url_method'
      self.actions = kw.pop('actions', [])
      self.rns = kw.pop('related_null_string', 'Null')
      super(DeathStarWidget, self).__init__(*a, **kw)

  def render(self, name, value, attrs=None, choices=()):
      from itertools import chain
      from django.forms.widgets import CheckboxInput
      from django.utils.encoding import force_unicode
      from django.utils.html import conditional_escape
      from django.utils.safestring import mark_safe

      if value is None: value = []
      has_id = attrs and 'id' in attrs
      final_attrs = self.build_attrs(attrs, name=name)
      output = []
      # Normalize to strings
      str_values = set([force_unicode(v) for v in value])
      for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
          # If an ID attribute was given, add a numeric index as a suffix,
          # so that the checkboxes don't all have the same ID attribute.
          if has_id:
              final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
              label_for = u' for="%s"' % final_attrs['id']
          else:
              label_for = ''

          cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
          option_value = force_unicode(option_value)
          rendered_cb = cb.render(name, option_value)
          option_label = conditional_escape(force_unicode(option_label))
          instance = self.choices.queryset.model.objects.get(id=option_value)
          rendered_fields = []
          for f in self.fields:
              try:
                  v = getattr(instance, f)
                  if hasattr(v, 'all'):
                      v = list(getattr(v, 'all')())
                      if v:
                          v = ', '.join([unicode(s) for s in v])
                      else:
                          v = self.rns # set to related_null_string
                  elif isinstance(v, datetime.datetime):
                      v = v.strftime('%b %d, %Y')

                  if isinstance(v, bool):
                      rendered_fields.append(
                          '<td class="%s"><span class="%s">%s</span></td>' % (
                              f, str(v).lower(), str(v).lower() )
                       )

                  else:
                      rendered_fields.append('<td class="%s">%s</td>' % (f, v))

              except AttributeError:
                  rendered_fields.append('<td class="%s">None</td>' % f)

          for a in self.actions:
              try:
                  if 'image_url' in a:
                      link = '<a href="%s"><img src="%s" alt="%s"/></a>' % (
                                  getattr(instance, a['url_method'])(),
                                  a['image_url'],
                                  a['name']
                      )

                  else:
                      link = '<a href="%s">%s</a>' % (
                                  getattr(instance, a['url_method'])(),
                                  a['name']
                      )

                  rendered_fields.append(
                      '<td class="%s">%s</td>' % (a['name'], link,)
                  )
              except AttributeError:
                  rendered_fields.append('<td class="%s">None</td>' % a['name'])

          output.append('<tr><td class="checkbox">%s</td>%s</tr>' % (rendered_cb, ''.join(rendered_fields)))

      #output.append(u'</table>')
      return mark_safe(u'\n'.join(output))

  def id_for_label(self, id_):
      # See the comment for RadioSelect.id_for_label()
      if id_:
          id_ += '_0'
      return id_
  id_for_label = classmethod(id_for_label)

The widget takes kwargs, fields and actions. fields should be an iterable of strings to access the fields/properties with getattr for the instances. actions expects a sequence of dictionaries with keys, 'name', 'url_method' and optionally, 'image_url'.

class BazFoosDeathStar(forms.ModelForm):
  """Select the Foos in style"""

  foos = forms.ModelMultipleChoiceField(
          queryset = Foo.objects.all(),
          widget=DeathStarWidget(
                  fields=('slug', 'countries', 'start_dt', 'some_bool'),
                  actions=[
                      {
                          'name': 'clone',
                          'url_method': 'clone_url',
                          'image_url': 'http://www.pointgphone.com/market/Death.Star.Clock_small.png',
                      }
                  ]
          ),
          required=False,
  )

  class Meta:
      model = Baz

Then, assuming in this example, Foo objects have a field, slug, a M2M, countries, a DateTimeField, start_dt, a BooleanField, some_bool. Foo instances also have a method, clone_url which returns a url for cloning this Foo.

The output then would look something like this:

<tr>
  <td class="checkbox">
    <div class="checker" id="uniform-id_foos_0">
      <span class="checked"><input checked="checked" type="checkbox" name="foos" value="5" id="id_foos_0" style="opacity: 0; "></span>
    </div>
  </td>
  <td class="slug">some</td>
  <td class="countries">Canada, United Kingdom, United States</td>
  <td class="start_dt">Jul 26, 2010</td>
  <td class="some_bool"><span class="true">true</span></td>
  <td class="clone">
    <a href="/portal/clone-foo/some/"><img src="http://www.pointgphone.com/market/Death.Star.Clock_small.png" alt="clone"></a>
  </td>
</tr>
<tr>
  <td class="checkbox">
    <div class="checker" id="uniform-id_contracts_1">
      <span><input type="checkbox" name="foos" value="27" id="id_foos_1" style="opacity: 0; "></span>
    </div>
  </td>
  <td class="slug">for-rilz</td>
  <td class="countries">France, Italy</td>
  <td class="start_dt">Jan 05, 2011</td>
  <td class="some_bool"><span class="true">true</span></td>
  <td class="clone">
    <a href="/portal/clone-foo/for-rilz/"><img src="http://www.pointgphone.com/market/Death.Star.Clock_small.png" alt="clone"></a>
  </td>
</tr>

You provide the the thead, the tbody wrapping and the table tag as well.

Comments on This Post:

Please Login (or Sign Up) to leave a comment