Key/value pairs in custom widget - python

So let's say I have jQuery convert a simple text input from this
<input class='employee_list' name='requestor' type='text' />
to this
<div name='requestor' class='ajax_picker'>
<input class='search_box' class='employee_list' name='requestor_text' type='text'/>
<input class='id' name='requestor' type='hidden' value='' />
<div class='results'></div>
</div>
And load the hidden input value with AJAX + JSON. Problem is, if form.is_valid() is not True, then how can I create a custom Widget that will render both values? Thinking I could do two different fields, but that seems ugly. I could also customize the form rendering, but that's even worse. Maybe the form can pass all of the POST data to the widget, but can't seem to figure out how to get that to work.
There must be an elegant way to achieve this!
from django import forms
class AjaxPickerWidget(forms.TextInput):
def render(self, name, value, attrs=None):
# ... now what?
return super(AjaxPickerWidget, self).render(name, value, attrs=attrs)
My Solution
Thanks for your help, I took a variant approach. Since I am using this strictly for Models (hence needing a key/value pair) I made the widget interact directly with the model and create a data-id attribute that the jQuery would catch to move to the hidden field.
from django import forms
class AjaxPickerModelWidget(forms.TextInput):
def __init__(self, css_class, queryset, id_name, value_name, attrs={}):
attrs['class'] = css_class
self.queryset = queryset
self.id_name = id_name
self.value_name = value_name
super(AjaxPickerModelWidget, self).__init__(attrs=attrs)
def render(self, name, value, attrs={}):
try:
instance = self.queryset.get(**{self.id_name: value})
attrs['data-id'] = value
value = getattr(instance, self.value_name)
except:
value = ''
return super(AjaxPickerModelWidget, self).render(name, value,
attrs=attrs)

Well, I don't know if this qualifies as elegant, but how about something like this. Here I'm storing the search string as a property on the widget, and setting the property from the POST data in the form constructor. I'm also using name="foo_picker_text" to indicate the search input for the corresponding input named "foo".
class AjaxPickerWidget(HiddenInput):
search_text_suffix = "_picker_text"
def __init__(self, **kwargs):
super(AjaxPickerWidget, self).__init__(**kwargs)
self.search_text = None
def render(self, name, value, attrs=None):
start = '<div name="%s" class="ajax_picker">' % (name) + \
'<input class="search_box" name="%s%s" ' % (name, self.search_text_suffix)+ \
'type="text" value="%s" />' % (self.search_text or "")
end = super(AjaxPickerWidget, self).render(name, value, attrs=attrs) + '</div>'
return '%s%s' % (start, end)
class MyForm(Form):
requestor = CharField(widget = AjaxPickerWidget())
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
# find picker text in POST data and tell the corresponding widget about it
for name in self.data:
if name.endswith(AjaxPickerWidget.search_text_suffix):
field_name = name.split(AjaxPickerWidget.search_text_suffix)[0]
self.fields[field_name].widget.search_text = self.data[name]
You might have to tweak it a bit to get attributes to show up where you want them, etc. but hopefully this basically does what you want?

You could instead of calling is_valid() on your form create a custom function
from django import forms
class AjaxPickerWidget(forms.TextInput):
def render(self, name, value, attrs=None):
return super(AjaxPickerWidget, self).render(name, value, attrs=attrs)
def validate(self):
values = self.cleaned_data.items():
# Modify the values to fit the original form (delete, change etc)
return whateversleft.is_valid() # Perhaps with some additional checks

Related

Exception on custom form field not being propagated and form reposting for use of tupple

I'm fairly new to python(3.5)/django(1.10), and I've run into the following problem:
I am using Django's generic CreateView to create a model and respective children model.
My objective is to save information about a purchase. A purchase is composed of a Bill and one or many Receipt. To accomplish this, I've created a custom Form (BillForm) with a custom field where user can input coma separated values that will be used to create receipts.
Here is what I have:
models.py
class Bill(models.Model):
""" A given bill whenever an item is purchased."""
number = models.CharField(_('Number'), max_length=20)
purchase_date = models.DateTimeField(_('Purchase Date'))
company = models.ForeignKey(Company, verbose_name=_('Company'),
on_delete=models.DO_NOTHING)
...
def _get_receipts(self):
''' returns all receipts that related to a bill '''
return Receipt.objects.filter(bill = self)
receipts = property(_get_receipts)
class Receipt(models.Model):
""" A receipt confirming a product is in our posession """
number = models.CharField(_('Number'), max_length=20)
bill = models.ForeignKey(Bill, verbose_name=_('Bill'),
on_delete=models.DO_NOTHING)
urls.py:
url(_(r'^create_purchase/$'), views.ObjectCreateView.as_view(
model=Bill,
form_class=BillForm
),
name="create_purchase"),
forms.py:
class MultipleReceiptsField(forms.Field):
''' A custom field to store a list of coma separated receipts '''
def to_python(self, value):
''' Normalize data to a set of receipts numbers '''
if not value:
return set()
return set(v.strip() for v in value.split(',') if v.strip())
def validate(self, value):
''' check if the values passed are less than 20 char long (limit for
model). '''
super(MultipleReceiptsField, self).validate(value)
# had made the following a tuple, then updated to list and worked
invalid_receipts = [r for r in value if len(r) > 20]
if invalid_receipts:
# EXCEPTION THROWN HERE BUT NOT PROPAGATED
raise ValidationError(
[ValidationError(_("Length of receipt %(r) is too large (max 20 characters)"),
code=INVALID_PARAMS, params={'r':int(r)}) # it was params={r:r} should have used 'r'
for r in invalid_receipts]
)
class BillForm(forms.ModelForm):
''' A form used to create a Purchase Bill. '''
receipts = MultipleReceiptsField(label=_("Receipts"), widget=forms.Textarea)
def __init__(self, *args, **kwargs):
''' constructor used to filter the companies. '''
# executes
super(BillForm, self).__init__(*args, **kwargs)
self.fields['company'].queryset =\ Company.objects.filter(is_provider=True).filter(is_active=True)
def save(self, commit=True):
''' createe receipts when saving a bll '''
# save logic | NEVER EXECUTES...
# for each elementt in receipts, create a receipt object and
# set its bill value to the bill that we just created
bill = super(BillForm, self).save(commit=False)
receipts_cd = self.cleaned_data["receipts"]
...
return bill
class Meta:
model = Bill
fields = ('company', "number", "currency", "price", "receipts")
view.py:
class ObjectCreateView(IsInPurchaseGroup, CreateView):
model = None
form_class = None
#fields = "__all__"
template_name = "purchases/object_form.html"
def get_context_data(self, **kwargs):
context = super(ObjectCreateView, self).get_context_data(**kwargs)
context["title"] = str(ACTIONS[CREATE_ACTION]) + " " +\
str(self.model._meta.verbose_name)
context["button"] = ACTIONS[CREATE_ACTION]
return context
def get_success_url(self):
next_url, kwargs = get_next_url(self.request)
if not next_url:
# works because the pluralization of both models end with s
next_url =\
"purchases:{}s".format((str(self.model).split(".")[-1])[:-2])
# if i am creating a bill, then i must navigate to
# create_purchased_products in the warehouse department.
if self.model == Bill:
next_url = "warehouse:create_purchased_products"
kwargs = {"receipts" : self.object.receipts}
return reverse_lazy(next_url, kwargs=kwargs)
object_form.html
{% extends "purchases/base.html" %}
{% load i18n %}
{% block body %}
<h3>{{ title }}</h3>
{% if error_message %}
<p><strong>{{ error_message }}</strong></p>
{% endif %}
<form action="" method="post">{% csrf_token %}
<table>{{ form.as_table }}</table>
<div class="buttons">
<input type="Submit" value="{{ button }}"/>
</div>
</form>
{% endblock %}
The problem:
whenever a form_class is given on the url, the following functions don't get executed:
BillForm.save()
ObjectCreateView.get_success_url()
If I omit the form_class on the url and set variable fields = "__all__" (simply creating an object of my Bill model), then the method ObjectCreateView.get_success_url() gets called.
My question
Why the BillForm.save() nor the ObjectCreateView.get_success_url() don't get executed? I know there has to be something wrong with the BillForm, but I can't seem to understand what...
Any help is greatly appreciated.
Update
Uploaded object_form.html and update forms.py
There is no exception being thrown. After clicking on Submit, a POST gets sent to the server. On the client (the browser), no changes happen (form remains with already entered data).
Found my errors; but I'm still unsure why it happens.
I had some exceptions being thrown in MultipleReceiptsField.validate(), but they didn't get propagated (when I submited the form, I didn't get exception, form just kept on posting).
After exception was fixed (saw that when placing invalid input will raise a validation error) and tried to submit with correct data, form kept on reposting.
Then I updated the invalid_receipts variable from being a tuple to a list and it began working
Am I missing some subtle difference between tuple and lists?
forms.py - not working:
class MultipleReceiptsField(forms.Field):
''' A custom field to store a list of coma separated receipts '''
def to_python(self, value):
''' Normalize data to a set of receipts numbers '''
if not value:
return set()
return set(v.strip() for v in value.split(',') if v.strip())
def validate(self, value):
''' check if the values passed are less than 20 char long (limit for
model). '''
super(MultipleReceiptsField, self).validate(value)
invalid_receipts = (r for r in value if len(r) > 20)
if invalid_receipts:
raise ValidationError(
[ValidationError(_("Length of recipt %(r) is too large (max 20 characters)"),
code=INVALID_PARAMS, params={'r':int(r)})
for r in invalid_receipts]
)
forms.py - working:
lass MultipleReceiptsField(forms.Field):
''' A custom field to store a list of coma separated receipts '''
def to_python(self, value):
''' Normalize data to a set of receipts numbers '''
if not value:
return set()
return set(v.strip() for v in value.split(',') if v.strip())
def validate(self, value):
''' check if the values passed are less than 20 char long (limit for
model). '''
super(MultipleReceiptsField, self).validate(value)
invalid_receipts = [r for r in value if len(r) > 20]
if invalid_receipts:
raise ValidationError(
[ValidationError(_("Length of receipt %(r) is too large (max 20 characters)"),
code=INVALID_PARAMS, params={'r':int(r)})
for r in invalid_receipts]
)
I think problem in method save. Method save return instance of object. If cant save it return None. Your method return None all time. Create view check result of form save after call other methods. If it see none poccess aborted. You need return some instance or call super.save like this:
def save(self, commit=True):
inst = super(BillForm, self).save(commit=False)
#addition logic, all what you need.
return inst
Or you need override valid method on your view class, without call super().valid() and override save method in form.
Sorry, my english very bad.

How to use "Readonly Field" outside Admin

I have a form that I need to show my project outside the area of administration, some fields can not be edited but can see them.
To do this would be great "AdminReadonlyField" found in "django.contrib.admin.helpers" The problem is that you can not do.
I have tried to create some widget that can replace this complex class, but I can not get it to work properly with DateTiemField fields.
class UserUpdateForm(forms.ModelForm):
"""
We need field "date_joined" can not be edited but can see them
"""
class Meta:
model = User
fields = ('first_name', 'last_name',
'email', 'date_joined', 'slug')
def __init__(self, user, *args, **kwargs):
kwargs['instance'] = user
super(UserUpdateForm, self).__init__(*args, **kwargs)
self.fields['date_joined'].widget = widgets.CMDateTimeText()
def clean_date_joined(self):
instance = getattr(self, 'instance', None)
if instance and instance.pk:
return instance.date_joined
else:
return self.cleaned_data['date_joined']
My code, something is not right.
class CMDateTimeText(DateTimeBaseInput):
"""
A SplitDateTime Widget that has some admin-specific styling.
Hereda Field and Widget.
"""
format_key = 'DATETIME_FORMAT'
def __init__(self, attrs=None, format=None):
# Use slightly better defaults than HTML's 20x2 box
default_attrs = {'class': 'date_id'}
if attrs:
default_attrs.update(attrs)
super(CMDateTimeText, self).__init__(attrs=default_attrs, format=format)
def render(self, name, value, attrs=None):
if value is None:
value = ''
value = self._format_value(value)
final_attrs = self.build_attrs(attrs, name=name)
return format_html('<p{}>{}</p>', flatatt(final_attrs), conditional_escape(value))
Result image:
any idea how to do "AdminReadonlyField"" any view or form?
So after hours of looking for various solutions, I found out how to do it the Django way.
Simply add the attribute disabled to the field in the form (not the widget!):
# in __init__() with crispy-forms for instance
self.fields['field'].disabled = True
# as form field
field = forms.CharField(disabled=True)
And it works... Django is taking care of not saving the field, if some hacker tampered with it although it's disabled. Only works with Django 1.9+.

Making initialized foreign key field as read-only in the form

I have a foreign key field in my form, initialized with:
form=myform(intial= {'val':abc.objects.get(pk=id)})
I have tried the following set of codes individually to make the field "val" as a read only value.
form.fields['val'].widget = forms.HiddenInput()
form.fields['val'].widget.attrs['readonly'] = True
form.fields['val'].widget.attrs['disabled'] = True
form.fields['val'].widget.attrs['disabled'] = 'disabled'
form.fields['val'].widget.attrs['disabled'] = False
form.fields['val'].widget.attrs['display_only'] = True
form.fields['val'].widget.attrs['editable'] = False
Only HiddenInput worked but it didn't show the field in the form, while disabled showed an error. readonly worked on all fields except the foreign key field.
I did this by overriding the Select widgets render method, and changing its output.
class ReadOnlySelect(Select):
"""
This should replace the Select widget with a disabled text widget displaying the value,
and hidden field with the actual id
"""
def render(self, name, value, attrs=None, choices=()):
final_attrs = self.build_attrs(attrs, name=name)
display = "None"
for option_value, option_label in chain(self.choices, choices):
if str(option_value) == (value) :
display = option_label
output = format_html('<input type=text value="%s" disabled="disabled" ><input type="hidden" value="%s" %s> ' % (display, value, flatatt(final_attrs)))
return mark_safe(output)
fuller example on Django snippets.
https://djangosnippets.org/snippets/10436/
I have similar problem and I decided to use different approach. Firstly I define my own class for form, for example this way:
class MyForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
class Meta:
model = YourModel
exclude = ('foreign_key_field')
And then in models myModel did sth like this:
foreign_key_field = models.ForeignKey(KeyField)
def set_values(self, *args, **kwargs):
foreign_key_field = kwargs.pop('foreign_key_field')
I define function to set fields values that are read only. I call this method
after creating form object in views.py. (after submiting form)
And at last views.py:
if form.is_valid():
new_instance = form.save(commit=False)
new_instance.set_values(
foreign_key_field='YOUR VALUE',
)
new_instance.save()
I exclude this fields from form but after creating instance I am setting values. After setting values I save to db.

WTForms - display property value instead of HTML field

I want to re-use a template I have with my WTForms form:
<th>${form.name.label}</th>
<td>${form.name()}</td>
...
However, on my edit page, I want the input fields to display as normal (TextField, SelectField, etc.), while on my view page, I want to just display the value of the property, not the input field with the value.
Edit page:
<th>Name:</th>
<td><input type="text" value="Current Name" name="name" id="name"/></td>
View page:
<th>Name:</th>
<td>Current Name</td>
I know I can access a field's value via form.name.data, but is there any way I can keep the same template with form.name() being called and somehow toggle whether that outputs <input type="text"... or Current Name?
I created a custom widget:
from wtforms.fields import Field
class PlainTextWidget(object):
def __call__(self, field, **kwargs):
return field.data if field.data else ''
Then, for my view page, I added the following:
form = MyForm(obj=myDataRow)
fields = [val for val in form._fields]
for fieldName in fields:
fieldProp = getattr(form, fieldName)
setattr(fieldProp, 'widget', PlainTextWidget())
Sarah's answer above led me to the solution to a related problem: What if you want some of your fields to be read only? In that case, instead of doing run-time surgery on the form object, you could define a new ROTextField variant (for example), that always renders to the pure value. For example:
from wtforms.widgets import Input
from wtforms.fields import StringField
class TextOutput(Input):
def __call__(self, field, **kwargs):
return kwargs.get('value', field._value())
class ROTextField(StringField):
widget = TextOutput()
Now define your field with ReadOnly attributes:
class UserPrefs(Form):
name = ROTextField('name', default='Jon')
# ...
Thinking about this problem helped me better understand how WTForms work. Leaving this here in case this might help someone else work through related issues.
Based on Sarah's answer and code found in WTForms-Components I use the following to quickly turn all a form's fields into read-only and disabled fields.
Suppose we have a ProfileForm defined as follows:
class ProfileEditForm(Form):
title = StringField("Title", validators=[validators.required("Please enter your title.")])
first_name = StringField("First Name", validators=[validators.required("Please enter your first name.")])
middle_name = StringField("Middle Name")
last_name = StringField("Last Name", validators=[validators.required("Please enter your last name.")])
organisation = StringField("Company Name", validators=[validators.required("Please enter your company name.")])
organisation_website = StringField("Company Website")
# more fields ...
Define the following class (based on ReadOnlyWidgetProxy from WTForms-Components):
class ReadOnlyAndDisabledWidgetProxy(object):
def __init__(self, widget):
self.widget = widget
def __getattr__(self, name):
return getattr(self.widget, name)
def __call__(self, field, **kwargs):
kwargs.setdefault('readonly', True)
kwargs.setdefault('disabled', True)
return self.widget(field, **kwargs)
Now inherit from ProfileForm as follows:
class ReadOnlyProfileForm(ProfileForm):
def __init__(self, *args, **kwargs):
super(ReadOnlyProfileForm, self).__init__(*args, **kwargs)
for field_name in self._fields:
field_property = getattr(self, field_name)
field_property.widget = ReadOnlyAndDisabledWidgetProxy(field_property.widget)

Making a Django form class with a dynamic number of fields

I'm working on something like an online store. I'm making a form in which the customer buys an item, and she can choose how many of these item she would like to buy. But, on every item that she buys she needs to choose what its color would be. So there's a non-constant number of fields: If the customer buys 3 items, she should get 3 <select> boxes for choosing a color, if she buys 7 items, she should get 7 such <select> boxes.
I'll make the HTML form fields appear and disappear using JavaScript. But how do I deal with this on my Django form class? I see that form fields are class attributes, so I don't know how to deal with the fact that some form instance should have 3 color fields and some 7.
Any clue?
Jacob Kaplan-Moss has an extensive writeup on dynamic form fields:
http://jacobian.org/writing/dynamic-form-generation/
Essentially, you add more items to the form's self.fields dictionary during instantiation.
Here's another option: how about a formset?
Since your fields are all the same, that's precisely what formsets are used for.
The django admin uses FormSets + a bit of javascript to add arbitrary length inlines.
class ColorForm(forms.Form):
color = forms.ChoiceField(choices=(('blue', 'Blue'), ('red', 'Red')))
ColorFormSet = formset_factory(ColorForm, extra=0)
# we'll dynamically create the elements, no need for any forms
def myview(request):
if request.method == "POST":
formset = ColorFormSet(request.POST)
for form in formset.forms:
print "You've picked {0}".format(form.cleaned_data['color'])
else:
formset = ColorFormSet()
return render(request, 'template', {'formset': formset}))
JavaScript
<script>
$(function() {
// this is on click event just to demo.
// You would probably run this at page load or quantity change.
$("#generate_forms").click(function() {
// update total form count
quantity = $("[name=quantity]").val();
$("[name=form-TOTAL_FORMS]").val(quantity);
// copy the template and replace prefixes with the correct index
for (i=0;i<quantity;i++) {
// Note: Must use global replace here
html = $("#form_template").clone().html().replace(/__prefix_/g', i);
$("#forms").append(html);
};
})
})
</script>
Template
<form method="post">
{{ formset.management_form }}
<div style="display:none;" id="form_template">
{{ formset.empty_form.as_p }}
</div><!-- stores empty form for javascript -->
<div id="forms"></div><!-- where the generated forms go -->
</form>
<input type="text" name="quantity" value="6" />
<input type="submit" id="generate_forms" value="Generate Forms" />
you can do it like
def __init__(self, n, *args, **kwargs):
super(your_form, self).__init__(*args, **kwargs)
for i in range(0, n):
self.fields["field_name %d" % i] = forms.CharField()
and when you create form instance, you just do
forms = your_form(n)
it's just the basic idea, you can change the code to whatever your want. :D
The way I would do it is the following:
Create an "empty" class that inherits from froms.Form, like this:
class ItemsForm(forms.Form):
pass
Construct a dictionary of forms objects being the actual forms, whose composition would be dependent on the context (e.g. you can import them from an external module). For example:
new_fields = {
'milk' : forms.IntegerField(),
'butter': forms.IntegerField(),
'honey' : forms.IntegerField(),
'eggs' : forms.IntegerField()}
In views, you can use python native "type" function to dynamically generate a Form class with variable number of fields.
DynamicItemsForm = type('DynamicItemsForm', (ItemsForm,), new_fields)
Pass the content to the form and render it in the template:
Form = DynamicItemsForm(content)
context['my_form'] = Form
return render(request, "demo/dynamic.html", context)
The "content" is a dictionary of field values (e.g. even request.POST would do).
You can see my whole example explained here.
Another approach: Rather than breaking the normal field initialization flow, we can override fields with a mixin, return an OrderedDict of dynamic fields in generate_dynamic_fields which will be added whenever its set.
from collections import OrderedDict
class DynamicFormMixin:
_fields: OrderedDict = None
#property
def fields(self):
return self._fields
#fields.setter
def fields(self, value):
self._fields = value
self._fields.update(self.generate_dynamic_fields())
def generate_dynamic_fields(self):
return OrderedDict()
A simple example:
class ExampleForm(DynamicFormMixin, forms.Form):
instance = None
def __init__(self, instance = None, data=None, files=None, auto_id='id_%s', prefix=None, initial=None,
error_class=ErrorList, label_suffix=None, empty_permitted=False, field_order=None,
use_required_attribute=None, renderer=None):
self.instance = instance
super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, field_order,
use_required_attribute, renderer)
def generate_dynamic_fields(self):
dynamic_fields = OrderedDict()
instance = self.instance
dynamic_fields["dynamic_choices"] = forms.ChoiceField(label=_("Number of choices"),
choices=[(str(x), str(x)) for x in range(1, instance.number_of_choices + 1)],
initial=instance.initial_choice)
return dynamic_fields

Categories