Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Widget filling values in two fields

I know that if I need a custom "selector" for a field in django-admin I need to create a custom widget. But what if the widget have to produce two values, for example X and Y coordinates, how can I fill them in two different fields from the model?

like image 665
Ilian Iliev Avatar asked Aug 04 '10 13:08

Ilian Iliev


3 Answers

You can look at the implementation of the date-time field, that renders as 2 fields in the admin.

Going top-down,

the admin uses

class AdminSplitDateTime(forms.SplitDateTimeWidget):
    """
    A SplitDateTime Widget that has some admin-specific styling.
    """
    def __init__(self, attrs=None):
        widgets = [AdminDateWidget, AdminTimeWidget]
        # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
        # we want to define widgets.
        forms.MultiWidget.__init__(self, widgets, attrs)

    def format_output(self, rendered_widgets):
        return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \
            (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1]))

which in turn uses SplitDateTimeWidget:

class SplitDateTimeWidget(MultiWidget):
    """
    A Widget that splits datetime input into two <input type="text"> boxes.
    """
    date_format = DateInput.format
    time_format = TimeInput.format

    def __init__(self, attrs=None, date_format=None, time_format=None):
        if date_format:
            self.date_format = date_format
        if time_format:
            self.time_format = time_format
        widgets = (DateInput(attrs=attrs, format=self.date_format),
                   TimeInput(attrs=attrs, format=self.time_format))
        super(SplitDateTimeWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return [value.date(), value.time().replace(microsecond=0)]
        return [None, None]

Which in turn extends the MultiWidget defined in django.forms.widgets which you should also extend. It has many useful methods which you can override.

class MultiWidget(Widget):
"""
A widget that is composed of multiple widgets.

Its render() method is different than other widgets', because it has to
figure out how to split a single value for display in multiple widgets.
The ``value`` argument can be one of two things:

    * A list.
    * A normal value (e.g., a string) that has been "compressed" from
      a list of values.

In the second case -- i.e., if the value is NOT a list -- render() will
first "decompress" the value into a list before rendering it. It does so by
calling the decompress() method, which MultiWidget subclasses must
implement. This method takes a single "compressed" value and returns a
list.

When render() does its HTML rendering, each value in the list is rendered
with the corresponding widget -- the first value is rendered in the first
widget, the second value is rendered in the second widget, etc.

Subclasses may implement format_output(), which takes the list of rendered
widgets and returns a string of HTML that formats them any way you'd like.

You'll probably want to use this class with MultiValueField.
"""
def __init__(self, widgets, attrs=None):
    self.widgets = [isinstance(w, type) and w() or w for w in widgets]
    super(MultiWidget, self).__init__(attrs)

def render(self, name, value, attrs=None):
    # value is a list of values, each corresponding to a widget
    # in self.widgets.
    if not isinstance(value, list):
        value = self.decompress(value)
    output = []
    final_attrs = self.build_attrs(attrs)
    id_ = final_attrs.get('id', None)
    for i, widget in enumerate(self.widgets):
        try:
            widget_value = value[i]
        except IndexError:
            widget_value = None
        if id_:
            final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
        output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
    return mark_safe(self.format_output(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)

def value_from_datadict(self, data, files, name):
    return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]

def _has_changed(self, initial, data):
    if initial is None:
        initial = [u'' for x in range(0, len(data))]
    else:
        if not isinstance(initial, list):
            initial = self.decompress(initial)
    for widget, initial, data in zip(self.widgets, initial, data):
        if widget._has_changed(initial, data):
            return True
    return False

def format_output(self, rendered_widgets):
    """
    Given a list of rendered widgets (as strings), returns a Unicode string
    representing the HTML for the whole lot.

    This hook allows you to format the HTML design of the widgets, if
    needed.
    """
    return u''.join(rendered_widgets)

def decompress(self, value):
    """
    Returns a list of decompressed values for the given compressed value.
    The given value can be assumed to be valid, but not necessarily
    non-empty.
    """
    raise NotImplementedError('Subclasses must implement this method.')

def _get_media(self):
    "Media for a multiwidget is the combination of all media of the subwidgets"
    media = Media()
    for w in self.widgets:
        media = media + w.media
    return media
media = property(_get_media)

def __deepcopy__(self, memo):
    obj = super(MultiWidget, self).__deepcopy__(memo)
    obj.widgets = copy.deepcopy(self.widgets)
    return obj
like image 127
lprsd Avatar answered Nov 06 '22 07:11

lprsd


Jannis Leidel released a widget quite a long time ago. django-coordinatesfield As far as I remember, it took the coordinates from a map and passed it a single field and some javascript cut it into 2 coordinates for 2 fields.

Combined with a custom form it should work quite well

like image 31
vikingosegundo Avatar answered Nov 06 '22 08:11

vikingosegundo


Here's an example for a ModelForm: http://www.adamalton.co.uk/blog/displaying-django-genericforeignkey-as-single-form-field/

Add an extra form field to the form (for your single widget) and exclude the two 'real' fields, then override the init and save methods to do the extra logic that makes it work.

Also, same question: How to get a single widget to set 2 fields in Django?

like image 1
Anentropic Avatar answered Nov 06 '22 07:11

Anentropic