My Issue
I'm writing a Django application where I want users to input addresses via a form. I want users to be able to add additional address fields if they want to enter more than the default number of addresses (3). I have been struggling to figure out how to accomplish this.
First Attempt
The first way I tried was simply using JavaScript to add fields on the click of a button, but when I do this the data from the added field is not submitted. I get the data from the three fields hard-coded into the form class, but the additional fields are not submitted, I assume because they are not part of the form class that I created.
Current Attempt
Since I couldn't use data from fields added using JavaScript, I'm now trying to create a dynamically-sized form in Python. My general thinking is to pass the number of location fields to the class and then use that number to create the form. I know my looping mechanism for creating the form itself works because I tested it with hard-coded numbers. However, I did that without overriding the __init__ function. To pass a number to the form, I need to override this function. When I do this, it seems that no form object is created and I have no idea why. Here is a sample of my code:
forms.py
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Row, Column, ButtonHolder, Button
class EnterLocationsForm(forms.Form):
def __init__(self, *args, **kwargs):
num_locations = kwargs.pop('num_locations')
super().__init__(*args, **kwargs)
for i in range(1, num_locations + 1):
name = 'location{}'.format(i)
exec("{} = {}".format(
name,
"forms.CharField(widget=forms.TextInput(
attrs={'placeholder': 'Address {}'.format(i)}))")
)
helper = FormHelper()
helper.form_class = 'form-horizontal'
helper.form_method = 'POST'
helper.form_show_labels = False
helper.layout = Layout()
for i in range(1, num_locations + 1):
helper.layout.append(Row(
Column('location{}'.format(i), css_class='form-group'),
css_class='form-row'
),)
helper.layout.append(ButtonHolder(
Button('submit', 'Go', css_class='btn btn-primary'),
Button('add', '+', css_class="btn btn-success"),
css_id="button-row",
))
views.py
from .forms import EnterLocationsForm
location_count = 3
def index(request):
if request.method == 'POST':
form = EnterLocationsForm(request.POST, num_locations=location_count)
...
else:
form = EnterLocationsForm(num_locations=location_count)
# if I write print(form) here, nothing is printed
return render(request, 'index.html', {'form': form})
Any tips on why this isn't creating a form object would be much appreciated! If there's a way to make my first approach work that would be great too.
Related
In my Django form, I have one select field that needs to be populated dynamically based on some information about the current user. I am able to get the field set up correctly and rendered in the form - however, I'm getting an error when submitting the form because it's getting hung up on some of the logic that I have in the form's __init__ method that only makes sense in the context of generating the form in the first place. I'm super-new to Django, and I'm not quite familiar with the design principles for this sort of situation.
In my app's admin.py, I have a method that's used for creating a custom view for a data export form - the relevant parts of it are set up like so...
# admin.py
from organizations.models import Organization
from .forms import ExportForm
class SomeModelAdmin(SimpleHistoryAdmin, SoftDeletionModelAdmin):
def export_view(self, request):
authorized_orgs_queryset = Organization.objects.viewable_for_user(request.user).all()
authorized_orgs = [{'id': org.id, 'name': org.name} for org in authorized_orgs_queryset]
context = dict(
self.admin_site.each_context(request),
form = ExportForm({'authorized_orgs': authorized_orgs}),
)
if request.method == 'POST':
form = ExportForm(request.POST)
if form.is_valid():
# do some stuff with the form.cleaned_data and return a .csv file as a response
return response
return TemplateResponse(request, 'export.html', context)
So the current user may be authorized to export data for multiple organizations, and in the form I'd like to present the user with a select element populated with these organizations.
The ExportForm has a number of "fixed" fields that are always the same, and just the one dynamic select element, which is populated by the authorized_orgs arg that I pass to it - it's defined as...
# forms.py
from django import forms
min_year = 1950
export_formats = [
'csv',
'xls',
'xlsx',
'ods',
'json',
]
class ExportForm(forms.Form):
current_year = datetime.datetime.now().year
export_format = forms.ChoiceField(required=True, label='Format', choices=export_format_choices)
apply_date_range = forms.BooleanField(required=False)
year_from = forms.IntegerField(required=False, disabled=True, min_value=min_year, max_value=current_year, initial=current_year)
year_through = forms.IntegerField(required=False, disabled=True, min_value=min_year, max_value=current_year, initial=current_year)
def __init__(self, *args, **kwargs):
super(ExportForm, self).__init__(*args, **kwargs)
authorized_orgs_choices = [(org['id'], org['name']) for org in args[0]['authorized_orgs']]
self.fields['authorized_org'] = forms.ChoiceField(required=False, label='Choose an authorized organization', choices=authorized_orgs_choices)
When I render the form, all is well. However, form submission is where things go awry. Submitting the form produces the error
File "/code/observations/forms.py", line 28, in __init__
authorized_orgs_choices = [(org['id'], org['name']) for org in args[0]['authorized_orgs']]
File "/usr/local/lib/python3.7/site-packages/django/utils/datastructures.py", line 78, in __getitem__
raise MultiValueDictKeyError(key)
django.utils.datastructures.MultiValueDictKeyError: 'authorized_orgs'
Now, I do understand why this is happening - the __init__ is getting the values from the submitted form as its args, which are different from what I've supplied when setting up the form in the first place.
What I don't know is how this sort of thing should typically be handled in Django... how do I make it so that this dynamic field is created correctly when defining the form to be rendered, and that the data is available to me in form.cleaned_data when it's submitted?
Thanks very much for any insight and help.
Aha - found an answer here - django forms post request raising an error on __init__ method
I needed to make sure to pass those values again when handling the form's POST - I also reworked it a bit to make use of kwargs -
# admin.py
if request.method == 'POST':
form = ExportForm(request.POST, authorized_orgs=authorized_orgs)
# forms.py
def __init__(self, *args, **kwargs):
super(ExportForm, self).__init__(*args)
authorized_orgs_choices = [(org['id'], org['name']) for org in kwargs['authorized_orgs']]
self.fields['authorized_org'] = forms.ChoiceField(required=False, label='Choose an authorized organization', choices=authorized_orgs_choices)
I have two forms named GoodAtForm and PaidForForm. What these do is as follows...
GoodAtForm Takes an input from a list in request.session['love'] and presents it to the user.
Then user is presented with a CheckboXSelectMultiple fields so that users can select.
After The form is submitted in the view, the user choices are then stored inside another list request.session['good'].
4.Another Form named PaidForForm uses that list for further asking of questions from users using CheckBocSelectMultiple and the selections are from the list ```request.session['good'].
My problem is that I am unable to access output data inside the Forms to provide it to view.
Input is working fine when initialised. My forms renders Check Boxes from the given LOVE list but the problem is that Form is not providing output. It says
form = GoodAtForm(request.POST)
input_list = request.session['love']
'QueryDict' object has no attribute 'session'
This is my GoodAtForm
class GoodAtForm(forms.Form):
def __init__(self, request, *args, **kwargs):
super(GoodAtForm, self).__init__(*args, **kwargs)
input_list = request.session['love']
self.fields['good'] = forms.MultipleChoiceField(
label="Select Things You are Good At",
choices=[(c, c) for c in input_list],
widget=forms.CheckboxSelectMultiple
)
View For the GoodAtForm
def show_good_at(request):
if request.method == 'POST':
form = GoodAtForm(request.POST) #it is showing problem here. Throws an exception here
if form.is_valid():
if not request.session.get('good'):
request.session['good'] = []
request.session['good'] = form.cleaned_data['good']
return redirect('paid_for')
else:
form = GoodAtForm(request=request) #rendering form as usual from the list 'love'
return render(request, 'good_at_form.html', {'form':form})
Usually the first "positional" argument passed to a Django form is the request data, you've defined request as the first argument to your form class but are passing request.POST in your view
You either need to pass request as the first argument every time that you instantiate your form
form = GoodForm(request, request.POST)
or change request to be a keyword argument
class GoodAtForm(forms.Form):
def __init__(self, *args, request=None, **kwargs):
super().__init__(*args, **kwargs)
...
form = GoodForm(request.POST, request=request)
I have my Order model which has a FK to Item model. I customized the Order create method to create the item which is passed in from the POST request. I want to customize it to allow the POST request to pass the Item pk instead of the name. so if the pk exists, I just use it and no need to create a new item.
However, when I look at the validated_data values in the create(), the id field doesn't exist.
class Item(models.Model):
name = models.CharField(max_length=255,blank=False)
class Order(models.Model):
user = models.OneToOneField(User,blank=True)
item = models.OneToOneField(Item,blank=True)
I want the POST body to be
{
"name":"iPhone"
}
or
{
"id":15
}
I tried to implement this kind of behavior myself, and didn't find a satisfying way in terms of quality. I think it's error prone to create a new object without having the user confirming it, because the user could type in "iphone" or "iPhon" instead of "iPhone" for example, and it could create a duplicate item for the iphone.
Instead, I recommend to have two form fields:
a select field for the item name,
a text input to create an item that's not in the list.
Then, it's easy to handle in the form class.
Or, with autocompletion:
user types in an Item name in an autocompletion field,
if it doesn't find anything, the autocomplete box proposes to create the "iPhon" Item,
then the user can realize they have a typo,
or click to create the "iPhone" item in which case it's easy to trigger an ajax request on a dedicated view which would respond with the new pk, to set in the form field
the form view can behave normally, no need to add confusing code.
This is how the example "creating choices on the fly" is demonstrated in django-autocomplete-light.
You may need to use Django Forms.
from django import forms
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from .models import Order, Item
from django.shortcuts import render
class OrderForm(forms.Form):
item_name = forms.CharField(required=False)
item = forms.ModelChoiceField(queryset=Item.objects.none(), required=False)
def __init__(self, *args, **kwargs):
super(OrderForm, self).__init__(*args, **kwargs)
self.fields['item'].queryset = Item.objects.all()
def clean_item(self):
item_name = self.cleaned_data['item_name']
item = self.cleaned_data['item']
if item:
return item
if not item_name:
raise ValidationError('New item name or existing one is required.')
return Item.objects.create(name=item_name)
#login_required
def save(request):
if request.method == 'GET':
return render(request, 'template.html', {'form': OrderForm()})
user = request.user
form = OrderForm(request.POST)
if form.is_valid():
order = Order.objects.create(user=user, item=form.cleaned_data['item'])
return render(request, 'template.html', {'form': OrderForm(), 'order': order, 'success': True})
else:
return render(request, 'template.html', {'form': OrderForm(), 'error': True})
I have the following view in my django app.
def edit(request, collection_id):
collection = get_object_or_404(Collection, pk=collection_id)
form = CollectionForm(instance=collection)
if request.method == 'POST':
if 'comicrequest' in request.POST:
c = SubmissionLog(name=request.POST['newtitle'], sub_date=datetime.now())
c.save()
else:
form = CollectionForm(request.POST, instance=collection)
if form.is_valid():
update_collection = form.save()
return redirect('viewer:viewer', collection_id=update_collection.id)
return render(request, 'viewer/edit.html', {'form': form})
It displays a form that allows you to edit a collection of images. The footer of my html contains a form that allows you to request a new image source from the admin. It submits to a different data model than the CollectionForm. Since this is in the footer of every view, I want to extract lines 5-7 of the code and turn it into a decorator. Is this possible and if so how might I go about doing that?
I would make a new view to handle the post of the form. And then stick a blank form instance in a context processor or something, so you can print it out on every page.
If you do want to make a decorator, i would suggest using class based views. That way, you could easily make a base view class that handles the form, and every other view could extend that.
EDIT:
Here's the docs on class based views: https://docs.djangoproject.com/en/dev/topics/class-based-views/intro/
Note, I would still recommend having a separate view for the form POST, but here's what your solution might look like with class based views:
class SubmissionLogFormMixin(object):
def get_context_data(self, **kwargs):
context = super(SubmissionLogFormMixin, self).get_context_data(**kwargs)
# since there could be another form on the page, you need a unique prefix
context['footer_form'] = SubmissionLogForm(self.request.POST or None, prefix='footer_')
return context
def post(self, request, *args, **kwargs):
footer_form = SubmissionLogForm(request.POST, prefix='footer_')
if footer_form.is_valid():
c = footer_form.save(commit=False)
c.sub_date=datetime.now()
c.save()
return super(SubmissionLogFormMixin, self).post(request, *args, **kwargs)
class EditView(SubmissionLogFormMixin, UpdateView):
form_class = CollectionForm
model = Collection
# you can use SubmissionLogFormMixin on any other view as well.
Note, that was very rough. Not sure if it will work perfectly. But that should give you an idea.
I am using inlineformset_factory to create fields for a many to many relationship between Clients and Sessions, with an intermediary Attendance model.
I have the following in my views file:
AttendanceFormset = inlineformset_factory(
Session,
Attendance,
formset=BaseAttendanceFormSet,
exclude=('user'),
extra=1,
max_num=10,
)
session = Session(user=request.user)
formset = AttendanceFormset(request.POST, instance=session)
And, as I needed to override one of the form fields, I added the following to the formset base class:
class BaseAttendanceFormSet(BaseFormSet):
def add_fields(self, form, index):
super(BaseAttendanceFormSet, self).add_fields(form, index)
form.fields['client'] = forms.ModelChoiceField(
queryset=Client.objects.filter(user=2))
Now, the form works correctly, but I need to pass a value into the formset so that I can filter the clients displayed based the current user rather than just using the id 2.
Can anyone help?
Any advice appreciated.
Thanks.
EDIT
For anyone reading, this is what worked for me:
def get_field_qs(field, **kwargs):
if field.name == 'client':
return forms.ModelChoiceField(queryset=Client.objects.filter(user=request.user))
return field.formfield(**kwargs)
How about utilizing the inlineformset_factory's formfield_callback param instead of providing a formset ? Provide a callable which in turns returns the field which should be used in the form.
Form fields callback gets as 1st parameter the field, and **kwargs for optional params (e.g: widget).
For example (using request.user for the filter, replace with another if needed:
def my_view(request):
#some setup code here
def get_field_qs(field, **kwargs):
formfield = field.formfield(**kwargs)
if field.name == 'client':
formfield.queryset = formfield.queryset.filter(user=request.user)
return formfield
AttendanceFormset = inlineformset_factory(
...
formfield_callback=get_field_qs
...
)
formset = AttendanceFormset(request.POST, instance=session)
To better understand it, see the usage of formfield_callback in Django's FormSet code.