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.
Related
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.
I overrode the GET request function handler in this class based view. In my else statement, I need to pass, as context data, the form that the class naturally creates (if I had not overridden the GET function). How can I do that?
I did not create a form at forms.py to create a form for the Post model. I let the create class based view handle the form creation for me. So, how can I get this form and pass as context data.
The only way I can think of doing this is creating a function based view and avoid using this class based view in this circumstance.
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
fields = ["title", "content", "payment_option", "price"]
def get(self, request):
card_set = BillingProfile.objects.get(user=request.user).card_set.all()
if not card_set.exists():
# The user does NOT have an inserted payment method.
return redirect("/billing/payment-method?next=/post/new/")
else:
# The user DOES have an inserted payment method.
form = "???"
return render(request, "posting/post_form.html", {"form":form})
You could use the method the class provides, which is self.get_form().
But this actually wouldn't be the right thing to do. What your should really do is to delegate to the default implementation of get and let it do what it normally would.
if not card_set.exists():
# The user does NOT have an inserted payment method.
return redirect("/billing/payment-method?next=/post/new/")
else:
return super().get(request)
I have a view where the user should be able to update an instance of a model, and also update or create new instances of a model related to the first one. I tryied using formsets to do this, and it works perfeclty for creating new objects, but i'm not finding a way to show the objects that already have been created. My problem is that i don't know how to populate the formsets with the existing data, so that i can put it in the context
So this are my models:
class Order(Model):
...
class invoice(Model):
order = models.ForeignKey(Order)
...
And my view is something like this:
class OrderDetailView(UpdateView):
invoice_form_class = InvoiceForm
def get_context_data(self, **kwargs):
context = super(OrderDetailView, self).get_context_data(**kwargs)
if not 'invoice_formset' in context:
context['invoice_formset'] = formset_factory(self.invoice_form_class, extra=3, can_delete=True, formset=BaseFormSet)
return context
There's probably an easy way to do this, but i'm not finding it anywhere
EDIT:
Thanks to #mariodev, i've learned about the inline_formsetfactory, and i'm using it. Now i can fill the formsets with the existing data, and i can create and alter existing ones, but when i try to delete them, nothing happens.
So now i'm defining this formset:
InvoiceFormset = inlineformset_factory(Order, Invoice, fields=('code',), can_delete=True, extra=0)
and my view looks like:
class OrderDetailView(UpdateView):
invoice_form_class = InvoiceForm
def get_context_data(self, **kwargs):
context = super(OrderDetailView, self).get_context_data(**kwargs)
if not 'invoice_formset' in context:
context['invoice_formset'] = InvoiceFormset(instance=self.get_object())
return context
def post(self, *args, **kwargs):
data = self.request.POST
order = self.get_object()
form = self.form_class(data)
invoice_formset = InvoiceFormset(data, instance=order)
if form.is_valid() and invoice_formset.is_valid():
self.object = form.save(order)
for f in invoice_formset:
f.save(self.object)
return HttpResponseRedirect(reverse('order_detail', kwargs={'order_id': self.get_object().order_id}))
I could add some extra lines in the post() to check if i have to delete the form, but it doesn't seem right for me to do it in the view. Is there something else i'm missing?
EDIT AGAIN:
Ended up finding this link which fix exactly this last problem i was having, so now it's all good!
I think it's better use normal function based views (FBV) for this. Understand what's going on first and then gradually move to CBV if you really need to.
This will help you with FBV:
http://catherinetenajeros.blogspot.com/2013/03/inline-formset-saving-and-updating-two.html
This may help you with CBV:
django class-based views with inline model-form or formset
I want to use a Django (1.4) modelformset in which, when the formset is loaded the forms will be arranged by a exam_date field I have in my model. To do this I've created the following simple BaseModelFormSet
class BaseExamDateFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(BaseExamDateFormSet, self).__init__(*args, **kwargs)
self.queryset = models.ExamDate.objects.all().order_by('exam_date')
As you can see I've changed the queryset as proposed in the
django docs (later I'll also change its clean method but it doesn't matter right now). After that I am using it in my view to create the formset:
ExamDateFormSet = modelformset_factory(
models.ExamDate,
exclude =('authority', ),
can_delete=True,
extra=1,
formset = forms.BaseExamDateFormSet
)
...
formset = ExamDateFormSet()
My problem is that when the formset is rendered the data in the forms is always in the same ordering (probaby by id) regardless of the value of the order_by attribute :(
Should I try setting a default order by in my ExamDate model ? I really don't like that solution though :(
TIA !
After some more searching I found a solution !
I ignored the queryset attribute and added the following method to the BaseExamDateFormSet:
def get_queryset(self):
return models.ExamDate.objects.all().order_by('exam_date')
However I don't know yet why the queryset attribute isn't working.
Easier still, in your above code you could erase your formset= argument in modelformset_factory call and instantiate your formset as:
formset = ExamDateFormSet(queryset=models.ExamDate.objects.order_by('exam_date'))
I have what I think should be a simple problem. I have an inline model formset, and I'd like to make a select field have a default selected value of the currently logged in user. In the view, I'm using Django's Authentication middleware, so getting the user is a simple matter of accessing request.user.
What I haven't been able to figure out, though, is how to set that user as the default selected value in a select box (ModelChoiceField) containing a list of users. Can anyone help me with this?
This does the trick. It works by setting the initial values of all "extra" forms.
formset = MyFormset(instance=myinstance)
user = request.user
for form in formset.forms:
if 'user' not in form.initial:
form.initial['user'] = user.pk
I'm not sure how to handle this in inline formsets, but the following approach will work for normal Forms and ModelForms:
You can't set this as part of the model definition, but you can set it during the form initialization:
def __init__(self, logged_in_user, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.fields['my_user_field'].initial = logged_in_user
...
form = MyForm(request.user)
I'm using Rune Kaagaard's idea above, except I noticed that formsets provide an extra_forms property: django.forms.formsets code
#property
def extra_forms(self):
"""Return a list of all the extra forms in this formset."""
return self.forms[self.initial_form_count():]
So, sticking with the example above:
formset = MyFormset(instance=myinstance)
user = request.user
for form in formset.extra_forms:
form.initial['user'] = user.pk
Saves having to test any initial forms, just provide default for extra forms.