Django - unable to handle form submission with dynamic field - python

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)

Related

Django: tie two redirects to a single Update button depending on where the user came from

So, i have a rather usual "update item" page that is a class-based view which inherits UpdateView. (in views.py it looks like "class ItemUpdateView(UpdateView) and it has method get_success_url(self) defined which contains the redirect url where user will be taken after clicking "Update" button.
My problem is that in my application, there are two different pages that could lead me to this "Update item" page, and depending on the page that user comes from - i want to take the user back to either pageA or pageB upon the successful update of the item.
I wasn't able to find the best-practices of how to handle this anywhere on the web, so - would really appreciate the help.
My guess is that I need to create an additional parameter that will be a part of the url and will contain A or B depending on the pageA or pageB that user came from, i.e. the url itself would be something like '/itemUpdate/int:pk/sourcepage' => '/itemUpdate/45/A'. Does that sound like a correct aproach or is there a better way?
There is a better way that you can check Meta dictionary in request:
write in your views file:
class ItemUpdateView(UpdateView):
previous_url = ''
form_class = UpdateItem
def get(self, request, *args, **kwargs):
self.previous_url = request.META.get('HTTP_REFERER')
print(self.previous_url)
return super().get(request, *args, **kwargs)
def get_initial(self):
initial = super().get_initial()
initial['success_url'] = self.previous_url
return initial
def form_valid(self, form):
self.success_url = form.cleaned_data['success_url']
print(self.success_url)
return super().form_valid(form)
# also you can use get_success_url instead of form_valid()
# def get_success_url(self):
# return super().get_form().cleaned_data['success_url']
and then write a hidden field in your form and name it success_url
class UpdateItem(forms.ModelForm):
success_url = forms.URLField(widget=forms.HiddenInput)
class Meta:
model=Item
fields=['itemName','quantity']
Note you can not use instance in order to get success_url field, because this field belong to form nor your model instance !
refer to documentions

Django Crispy Form class not creating an object

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.

Error while accessing request.session['key'] inside forms. [using CheckboxSelectMultiple]

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)

Include the pk or models fields when creating the object in POST

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})

How do I save post data using a decorator in Django

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.

Categories