Widget filling values in two fields - django

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?

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

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

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?

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!

Related

How to set Django forms with initial values from request.GET? (with mixed data types)

I have a django form that has both CharFields as well ChoiceFields. The form uses HTTP GET.
The issue I am having is that request.GET.dict() only contains one value for each key, regardless of if the data is a list or not. I discovered that I can use request.GET.getlist() to get the all the data but it will return a list even if the item is not a list. This is problematic because it causes the CharFields to have the value [u'']
What is the recommended way of solving the problem?
My current kludge is the following:
initial_dict = {k: v for k, v in request.GET.iterlists()}
clean_dict = {}
for key, value in initial_dict.iteritems():
if value[0] == '':
clean_dict[key] = ''
elif len(value) == 1:
clean_dict[key] = value[0]
else:
clean_dict[key] = value
SellerSearchForm(initial=clean_dict)
But I can't beleve that this is the best way to do this.
I myself had this issue as well as I wanted to be able to create links for (partial) prefilled forms. I ended up creating a Mixin class for my forms that overrides the get_initial_for_field() method (source).
class InitialFromGETMixin:
def __init__(self, *args, initial_from_get=False, **kwargs):
super(InitialFromGETMixin, self).__init__(*args, **kwargs)
self.initial_from_get = initial_from_get
def get_initial_for_field(self, field, field_name):
"""
Special implementation of initial gathering if initial values are given through request GET object
"""
if self.initial_from_get:
value = self.initial.get(field_name)
if value is not None:
return value[0] if len(value) == 1 else value
else:
return field.initial
return super(InitialFromGETMixin, self).get_initial_for_field(field, field_name)
This way at least the form is responsible for translating the initial keys to readable forms in a similar way it does with the data attribute. But other than that, there is no better alternative as the form class does not assume initial data to be supplied in the structure request.GET stores the information.

MultiWidget Break in Template

I have created a custom SelectTimeDateWidget that used the default SelectDateWidget and a custom SelectTimeWidget
class SelectDateTimeWidget(forms.MultiWidget):
supports_microseconds = False
def __init__(self, attrs=None, date_format=None, time_format=None):
widgets = (SelectDateWidget(empty_label=( "Year", "Month", "Day")),
SelectTimeWidget(use_seconds=False))
super(SelectDateTimeWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
value = to_current_timezone(value)
return [value.date(), value.time().replace(microsecond=0)]
return [None, None]
The widgets all work fine but because they are places next to one another in the form template it looks really clustered and not very user friendly. So I need a way to put a break tag and maybe a label tag in between them. I cant find anything on the web(Maybe I'm not wording it right)
This is the code within the form template and both widgets are in that one tag so i'm struggling to think of a way to break them up onto different lines, Any ideas?
<label> Start Time/Date:</label>
{{form.start}},
You can just overwrite format_output method from MultiWidget. It takes one argument, rendered_widgets, which is just a list of rendered sub-widgets. Default implementation just joins them, but you can do with them whatever you want. Example:
def format_output(self, rendered_widgets):
return '<br>'.join(rendered_widgets)
Or, if you want to write something between widgets in template, add this method to your MultiWidget implementation:
def subwidgets(self, name, value, attrs=None):
if self.is_localized:
for widget in self.widgets:
widget.is_localized = self.is_localized
# 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')
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 output
And you should be able to use {{ form.start.0 }}, {{ form.start.1 }} and so on in your template.

Django: How can I modify a form field's value before it's rendered but after the form has been initialized?

Given a form, I want to change a value on a field before it gets rendered. This is what I'm trying:
class RequiredFormSet(BaseFormSet):
def add_form(self):
tfc = self.total_form_count()
self.forms.append(self._construct_form(tfc))
if self.is_bound:
data = self.management_form.data.copy() # make data mutable
data[TOTAL_FORM_COUNT] = self.management_form.cleaned_data[TOTAL_FORM_COUNT] + 1
self.management_form.data = data
else:
self.extra += 1
I thought everything was stored in data, but I guess that data has been passed off to the individual fields (or widgets) already? So what property do I need to modify exactly?
I hope this helps:
This is the method which creates forms in a BaseFormSet:
def _construct_form(self, i, **kwargs):
"""
Instantiates and returns the i-th form instance in a formset.
"""
defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
if self.is_bound:
defaults['data'] = self.data
defaults['files'] = self.files
if self.initial:
try:
defaults['initial'] = self.initial[i]
except IndexError:
pass
# Allow extra forms to be empty.
if i >= self.initial_form_count():
defaults['empty_permitted'] = True
defaults.update(kwargs)
form = self.form(**defaults)
self.add_fields(form, i)
return form
As you can see there's an attribute called 'self.initial', this is passed as initial data to the new created form. If you want to use _construct_form to add a new form and set custom initial data you should modify 'self.initial' before calling _construct_form. Initial must be a dictionary where the key is the field name and the value the value you want for your field.

Django: Custom widget that can pre-fill from POST/GET data

Updated with my final solution, below.
I wrote a custom Django form widget to create a range query. It renders two input fields to define the min and max values for a query.
With well-crafted forms and widgets, fields can be filled with the values from the last query like so:
form = my_form(request.GET)
However, I cannot figure out a way to fill the values of those fields in my custom widget. Here is the widget code:
class MinMax(Widget):
input_type = None # Subclasses must define this.
def _format_value(self, value):
if self.is_localized:
return formats.localize_input(value)
return value
def render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_unicode(self._format_value(value))
return mark_safe(u'<input type="text" name="min-%s" /> to
<input type="text" name="max-%s" />' % (name, name) )
Probably because of the custom input field names, the values are not accessible. Is there a way to route them, or a way to rewrite the widget to include this useful functionality? One non-widget solution I can think of is some simple jquery logic, but that's less than optimal.
Here's the code I ended up using:
class MinMax(MultiWidget):
def __init__(self, attrs=None):
""" pass all these parameters to their respective widget constructors..."""
widgets = (forms.TextInput(attrs=attrs), forms.TextInput(attrs=attrs) )
super(MinMax, self).__init__(widgets, attrs)
def decompress(self, value):
return value or ''
def value_from_datadict(self, data, files, name):
value = ''
for key, value in data.items():
value += value
def format_output(self, rendered_widgets):
"""
Given a list of rendered widgets (as strings), it inserts stuff
between them.
Returns a Unicode string representing the HTML for the whole lot.
"""
rendered_widgets.insert(-1, ' to ')
return u''.join(rendered_widgets)
Note that these fields are returned as fieldname_0, fieldname_1 (and so on if you add additional widgets).

Different initial data for each form in a Django formset

Is it possible to prepopulate a formset with different data for each row? I'd like to put some information in hidden fields from a previous view.
According to the docs you can only set initial across the board.
If you made the same mistake as me, you've slightly mistaken the documentation.
When I first saw this example...
formset = ArticleFormSet(initial=[
{'title': 'Django is now open source',
'pub_date': datetime.date.today(),}
])
I assumed that each form is given the same set of initial data based on a dictionary.
However, if you look carefully you'll see that the formset is actually being passed a list of dictionaries.
In order to set different initial values for each form in a formset then, you just need to pass a list of dictionaries containing the different data.
Formset = formset_factory(SomeForm, extra=len(some_objects)
some_formset = FormSet(initial=[{'id': x.id} for x in some_objects])
You need to use the technique described in this post in order to be able to pass parameters in. Credit to that author for an excellent post. You achieve this in several parts:
A form aware it is going to pick up additional parameters
Example from the linked question:
def __init__(self, *args, **kwargs):
someeobject = kwargs.pop('someobject')
super(ServiceForm, self).__init__(*args, **kwargs)
self.fields["somefield"].queryset = ServiceOption.objects.filter(
somem2mrel=someobject)
Or you can replace the latter code with
self.fields["somefield"].initial = someobject
Directly, and it works.
A curried form initialisation setup:
formset = formset_factory(Someform, extra=3)
formset.form = staticmethod(curry(someform, somem2mrel=someobject))
That gets you to passing custom form parameters. Now what you need is:
A generator to acquire your different initial parameters
I'm using this:
def ItemGenerator(Item):
i = 0
while i < len(Item):
yield Item[i]
i += 1
Now, I can do this:
iterdefs = ItemGenerator(ListofItems) # pass the different parameters
# as an object here
formset.form = staticmethod(curry(someform, somem2mrel=iterdefs.next()))
Hey presto. Each evaluation of the form method is being evaluated in parts passing in an iterated parameter. We can iterate over what we like, so I'm using that fact to iterate over a set of objects and pass the value of each one in as a different initial parameter.
Building on Antony Vennard's answer, I am not sure what version of python/django he is using but I could not get the generator to work in the curry method either. I am currently on python2.7.3 and django1.5.1. Instead of using a custom Generator, I ended up using the built-in iter() on a list of things to create an iterator and passing the iterator itself in the curry method and calling next() on it in the Form __init__(). Here is my solution:
# Build the Formset:
my_iterator = iter(my_list_of_things) # Each list item will correspond to a form.
Formset = formset_factory(MyForm, extra=len(my_list_of_things))
Formset.form = staticmethod(curry(MyForm, item_iterator=my_iterator))
And in the form:
# forms.py
class MyForm(forms.Form):
def __init__(self, *args, **kwargs):
# Calling next() on the iterator/generator here:
list_item = kwargs.pop('item_iterator').next()
# Now you can assign whatever you passed in to an attribute
# on one of the form elements.
self.fields['my_field'].initial = list_item
Some Key things I found were that you need to either specify an 'extra' value in the formset_factory or use the initial kwarg on the formset to specify a list that corresponds to the list you pass to the iterator (In above example I pass the len() of the my_list_of_things list to 'extra' kwarg to formset_factory). This is necessary to actually create a number of forms in the formset.
I had this problem and I made a new widget:
from django.forms.widgets import Select
from django.utils.safestring import mark_safe
class PrepolutatedSelect(Select):
def render(self, name, value, attrs=None, choices=()):
if value is None: value = ''
if value == '':
value = int(name.split('-')[1])+1
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<select%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value])
if options:
output.append(options)
output.append('</select>')
return mark_safe(u'\n'.join(output))
Maybe this will work for you too.
formset = BookFormset(request.GET or None,initial=[{'formfield1': x.modelfield_name1,'formfield2':x.modelfield_name2} for x in model])
formfield1,formfield2 are the names of the formfields.
modelfield_name1,modelfield_name2 are the modal field names.
model is name of your modal class in models.py file.
BookFormset is the form or formset name which is defined in your forms.py file