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?
-
can you explain your question in details.Is there any relation between x & y – ha22109 Aug 08 '10 at 22:12
-
X and Y are coordinates taken from a map and I want to store them in separate fields of the model. Is there a way to achieve this with the widget, or I have to modify the save function of the model? – Ilian Iliev Aug 09 '10 at 07:24
-
Similar Questions: http://stackoverflow.com/questions/2856790/ http://stackoverflow.com/questions/9333406/ – Jelko Aug 21 '13 at 14:54
5 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

- 84,407
- 47
- 135
- 168
-
I am not finished building my widget but from the current progress I think that this is the solution I was looking for. It is strange that I was unable to find any info about multiwidget in the official django documentation so if you know some please provide it. – Ilian Iliev Aug 16 '10 at 07:04
-
I found that multiwidget is just a way to handle multiple widgets from different and then bind the data together in single field. So it now looks more like the JS wasy that vikingosegundo offered is the right one – Ilian Iliev Aug 18 '10 at 08:12
-
8solves the opposite problem to the question asked, this is how to do single field -> two widgets – Anentropic Apr 21 '11 at 15:14
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

- 52,040
- 14
- 137
- 178
-
2@downvoter: please explain why you found this answer down-vote worthy – vikingosegundo Mar 14 '11 at 22:38
You can make the widget render two (hidden) html inputs, whose names relate to the model's fields that need to be filled and assign the necessary values via javascript to them!

- 49,468
- 20
- 120
- 148
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?

- 1
- 1

- 32,188
- 12
- 99
- 147
I spent some time trying to bend django to my will on this and realised I was overthinking the problem. The issue really isn't setting two fields with one widget, it's just being able to set two fields on a form with a map on the same page. Then the problem is quite simple. Say you have a form derived from a model that has two fields 'latitude' and 'longitude'. Now all you need to do is put a map on the template page that sets those fields, and maybe displays a a marker. For example when you click the map. There are a few ways to do this but one would be:
{% block head %}
<link href="{% static 'css/map.css' %}" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
{% endblock %}
{% block content %}
{{ form.media }}
<div id='map' style='height: 400px; width: 600px;'></div>
<div class="row">
<div class="col">
<div id='map' style='height: 400px; width: 600px;'></div>
<form method="post">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
<script>
//Get lat lon elements
var lat_element = document.getElementsByName("latitude")[0]
var lon_element = document.getElementsByName("longitude")[0]
// Create a marker that will be moved to the current location
var marker = L.marker([0.0, 0.0])
// Function that will set the location
function onMapClick(e) {
marker.setLatLng(e.latlng).addTo(map);
// Set the form element values with the new pos
lat_element.value = e.latlng['lat'].toFixed(6).toString();
lon_element.value = e.latlng['lng'].toFixed(6).toString();
}
//Get existing lat lon
var lat = parseFloat(lat_element.value);
var lon = parseFloat(lon_element.value);
// initialize the map
var map = L.map('map').setView([0.0, 0.0], 1).on('click', onMapClick);
// load a tile layer
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
if (!isNaN(lat)) {
marker.setLatLng([lat, lon]).addTo(map);
map.setView([lat, lon], 12)
}
</script>
{% endblock content %}
Of course that's a very simple layout and implementation but I think that once you see this doesn't need to be a django problem at all but a more general front-end design problem, it becomes clear there are a million ways to do this. I haven't put any django code in the answer for that reason. Personally I'm using crispy forms, which comes with a handy Layout
class and I'll be putting the map div element in that Layout
rather than in the template. The possibilities are endless.
It seems to me django widgets and forms are built with the idea that one widget == one field and trying to bend it away from that could be endlessly more complicated than the above solution.

- 172
- 9