When saving the formset, it is not validating and respecting the model fields that I have made unique and therefore renders an Integrity Error should I duplicate fields deliberately to test it.
My guess is that the formsets themselves validate correctly, but as the unique field is something I have to assign during the save(commit=False) process it never gets validated. Does that make sense?
Is there something I am missing please?
My code:
class ClientCreate(LoginRequiredMixin, FormView):
def dispatch(self, *args, **kwargs):
self.case = Case.objects.get(pk=kwargs['case_pk'])
self.num_clients = self.case.number_clients
return super().dispatch(*args, **kwargs)
template_name = 'clients/client_form.html'
form_class = modelformset_factory(Client, ClientForm,
min_num=2, max_num=2, extra=0,
validate_max=True, validate_min=True,
can_delete=False)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["queryset"] = Client.objects.none()
return kwargs
def form_valid(self, form_class):
form_class.save()
return super().form_valid(form)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if self.request.POST:
ctx['inlines'] = self.form_class(self.request.POST)
else:
ctx['inlines'] = self.form_class()
return ctx
def get_success_url(self):
return reverse('client-list',
kwargs={'case_pk': self.kwargs['case_pk']})
I appreciate that formview is really supposed to be used for single form saving, but this does actually work correctly when not having duplicate unique items.
Many thanks
EDIT:::
This is my function based version that also suffers from the same issue:
#login_required
def client(request, case_pk):
template_name = 'clients/client_form.html'
case = get_object_or_404(Case,
pk=case_pk, adviser__company__account=request.user
)
formset_class = modelformset_factory(Client, ClientForm,
min_num=case.number_clients,
max_num=case.number_clients, extra=0,
validate_max=True, validate_min=True,
can_delete=False)
formset = formset_class(request.POST or None)
if request.method == 'POST':
# check all formsets valid
if all(form.is_valid() for form in formset):
for f in formset:
if f.is_valid():
form = f.save(commit=False)
form.case = case
f.save()
return HttpResponseRedirect(reverse('client-create',
kwargs={'case_pk': case_pk}))
return render(request, template_name, {
'inlines': formset,
'case': case,
'breadcrumbs': 'Family & Dependants'
})
Client Form:
class ClientForm(ModelForm):
class Meta:
model = Client
fields = ['prefix', 'first_name', 'middle_names', 'last_name',
'gender', 'date_of_birth', 'residence', 'address_1',
'address_2', 'address_3', 'city', 'postcode', 'telephone',
'marital_status', 'widowed_date_of_death',
'have_will', 'why_changing', 'existing_poa', 'dependant', ]
Client Model is large so this is the unique clause:
class Meta:
unique_together = ('case', 'first_name', 'last_name',
'date_of_birth', )
modelformset_factory appears to not respect database level constraints. Therefore, the check has to be done before saving the form.
They way to do this is by overriding the BaseModelFormSet class.
Solution here: Save multiple objects with a unique attribute using Django formset
Related
I'm trying to pass some variables into a model formset. I've looked at the official django documentation and they talk about doing this with a regular, non-model formset. I can't get this to work for a modelformset though.
views.py
EmployeeTeamFormset = modelformset_factory(EmployeeTeamMembership, form=EmployeeTeamForm(), extra=0, max_num=10, can_delete=True)
formset = EmployeeTeamFormset(request.POST or None, queryset=EmployeeTeamMembership.objects.filter(employee__user=user, team=team), kwargs={'user': user, 'team': team})
forms.py
class EmployeeTeamForm(forms.ModelForm):
class Meta:
model = EmployeeTeamMembership
fields = ('employee', 'team_lead',)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
self.team = kwargs.pop('team', None)
I get the following error:
__init__() got an unexpected keyword argument 'kwargs'
Thanks for your help!
Couple of typos in my code. This works:
EmployeeTeamFormset = modelformset_factory(EmployeeTeamMembership, form=EmployeeTeamForm, extra=0, max_num=10, can_delete=True)
formset = EmployeeTeamFormset(request.POST or None, queryset=EmployeeTeamMembership.objects.filter(employee__user=user, team=team), form_kwargs={'user': user, 'team': team})
Hi (sorry for my bad english), i'm trying to show a form using a number input instead a select in a FK Relation on django model, but i cant make it.
The thing is that i need to fill the ID of the product manually writing the id in a input box, but django automatically makes the select form with all the products!
this is what i have in forms.py
class PurchaseForm(forms.ModelForm):
class Meta:
model = Purchase
fields = [
'date',
'supplier',
'warehouse',
]
widgets = {
'date': forms.DateInput(attrs={
'type': 'date',
}),
}
class PurchaseItemForm(forms.ModelForm):
class Meta:
model = PurchaseItem
fields = [
'product',
'quantity',
'net_purchase_price',
'sell_price',
]
widgets = {
'product': forms.NumberInput(),
}
PurchaseFormSet = inlineformset_factory(
Purchase,
PurchaseItem,
fields='__all__',
extra = 10,
)
this is my view
class PruchaseCreateView(CreateView):
template_name = 'purchases/create_purchase.html'
form_class = PurchaseForm
def get_context_data(self, **kwargs):
context = super(PruchaseCreateView, self).get_context_data(**kwargs)
if self.request.POST:
context['formset'] = PurchaseFormSet(self.request.POST)
else:
context['formset'] = PurchaseFormSet()
return context
def form_valid(self, form):
context = self.get_context_data()
formset = context['formset']
if formset.is_valid():
self.object = form.save()
formset.instance = self.object
formset.save()
return redirect(self.object.get_absolute_url())
else:
return self.render_to_response(self.get_context_data(form=form))
i try with the widget NumberInput but nothing!!! any idea?
You need to include the custom form when you call inlineformset_factory:
PurchaseFormSet = inlineformset_factory(
Purchase,
PurchaseItem,
form=PurchaseItemForm
fields='__all__',
extra = 10,
)
Using inlineformset_factory I am able to add / remove phone numbers related to a single customer. Only problem is, I want to require at least 1 valid phone number for each customer.
Here is some demo code:
Models:
class Customer( models.Model ):
name = models.CharField( max_length=255 )
class PhoneNumber( models.Model ):
customer = models.ForeignKey( Customer )
number = models.CharField( max_length=10 )
Forms:
class CustomerForm( ModelForm ):
class Meta:
model = Customer
fields = ['name']
class PhoneNumberForm( ModelForm ):
class Meta:
model = PhoneNumber
fields = ['number']
Ok, so that's pretty straight forward.
Then in my view:
class Create( View ):
template_name = 'path_to_template'
CustomerForm = forms.CustomerForm
PhoneNumberFormSet = inlineformset_factory (
parent_model = Customer,
model = PhoneNumber,
form = PhoneNumberForm,
extra = 1,
)
def get(self, request):
# Return empty forms
context = {
'customer_form': self.CustomerForm,
'phone_number_formset': self.PhoneNumberFormSet
}
render( request, self.template_name, context)
def post(self, request):
this_customer_form = self.CustomerForm( request.POST )
if this_customer_form.is_valid():
new_customer.save(commit=False)
this_phone_number_formset = self.PhoneNumberFormSet(request.POST, instance=new_customer)
if this_phone_number_formset.is_valid():
new_customer.save()
this_phone_number_formset.save()
return HttpResponseRedirect(reverse_lazy('customer-detail', kwargs={'pk': new_customer.pk}))
# Something is not right, show the forms again
this_phone_number_formset = self.PhoneNumberFormSet(request.POST)
context = {
'customer_form': this_customer_form,
'phone_number_formset': this_phone_number_formset
}
render( request, self.template_name, context)
You get the point I think. Same thing for the Edit/Update view of the customer. Only then the forms are prepopulated.
At this point all I need is a way to require at least 1 valid PhoneNumber per Customer.
I found something like:
class RequiredFormSet(BaseFormSet):
def __init__(self, *args, **kwargs):
super(RequiredFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
from https://stackoverflow.com/questions/2406537/django-formsets-make-first-required
but it doesnt seem to work when I apply this on a BaseInlineFormSet class.
Django 1.7 seems to answer my wishes, but not for a InlineModelFormSet so far..
Any ideas?
If you just want to set the minimum or maximum, you can set them directly in inlineformset_factory, here's my code for minimum of one entry
from django.forms import inlineformset_factory
SubUnitFormSet = inlineformset_factory(
Unit, SubUnit, form=SubUnitForm, min_num=1, validate_min=True, extra=0)
You need to properly handle this in your view. I'm using CBV and this is my code for your reference
class UnitCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
permission_required = "core.add_unit"
model = Unit
form_class = UnitForm
template_name = 'core/basic-info/unit_form.html'
success_url = reverse_lazy('core:units')
success_message = _("%(code)s was added successfully")
def get_context_data(self, **kwargs):
data = super(UnitCreateView, self).get_context_data(**kwargs)
if self.request.POST:
data['subunits'] = SubUnitFormSet(self.request.POST, )
else:
data['subunits'] = SubUnitFormSet()
return data
def form_valid(self, form):
context = self.get_context_data()
subunits = context['subunits']
with transaction.atomic():
if subunits.is_valid():
self.object = form.save()
subunits.instance = self.object
subunits.save()
else:
return self.render_to_response(self.get_context_data(form=form))
return super(UnitCreateView, self).form_valid(form)
Thank you kezabella ( django irc ).
Seems I found a solution by subclassing BaseInlineFormset:
class RequiredFormSet(BaseInlineFormSet):
def clean(self):
for form in self.initial_forms:
if not form.is_valid() or not (self.can_delete and form.cleaned_data.get('DELETE')):
return
for form in self.extra_forms:
if form.has_changed():
return
raise ValidationError("No initial or changed extra forms")
Btw, these validation errors do not show up in {{ formset.error }} but in:
{{ formset.non_form_errors }}
My admin looks like this (with no exclude variable):
class MovieAdmin(models.ModelAdmin)
fields = ('name', 'slug', 'imdb_link', 'start', 'finish', 'added_by')
list_display = ('name', 'finish', 'added_by')
list_filter = ('finish',)
ordering = ('-finish',)
prepopulated_fields = {'slug': ('name',)}
form = MovieAdminForm
def get_form(self, request, obj=None, **kwargs):
form = super(MovieAdmin, self).get_form(request, obj, **kwargs)
form.current_user = request.user
return form
admin.site.register(Movie, MovieAdmin)
The form:
class MovieAdminForm(forms.ModelForm):
class Meta:
model = Movie
def save(self, commit=False):
instance = super(MovieAdminForm, self).save(commit=commit)
if not instance.pk and not self.current_user.is_superuser:
if not self.current_user.profile.is_manager:
instance.added_by = self.current_user.profile
instance.save()
return instance
I'm trying to remove the added_by field for users since I'd prefer to populate that from the session. I've tried methods from the following:
Django admin - remove field if editing an object
Remove fields from ModelForm
http://www.mdgart.com/2010/04/08/django-admin-how-to-hide-fields-in-a-form-for-certain-users-that-are-not-superusers/
However with each one I get: KeyError while rendering: Key 'added_by' not found in Form. It seems I need to remove the field earlier in the form rendering process but I'm stuck on where to do this.
So how can I exclude the added_by field for normal users?
You're probably getting that error when list_display is evaluated. You can't show a field that's excluded. The version with added_by removed also needs a corresponding list_display.
def get_form(self, request, obj=None, **kwargs):
current_user = request.user
if not current_user.profile.is_manager:
self.exclude = ('added_by',)
self.list_display = ('name', 'finish')
form = super(MovieAdmin, self).get_form(request, obj, **kwargs)
form.current_user = current_user
return form
#login_required
def post_review(request):
if request.method == 'POST':
formset = ReviewForm(request.POST)
if formset.is_valid():
formset.save(commit=False)
#formset.author = User.objects.get(pk=int(request.user.id))
formset.pub_date = datetime.datetime.now
formset.save()
return HttpResponseRedirect(reverse(review_index))
else:
formset = ReviewForm()
return render_to_response("review/post_review.html",
{"formset": formset}, context_instance=RequestContext(request),
)
I have this view, I want to auto set the current logged-in user in my review form author field. But I dont know how. Any ideas/hint pls?
Below is my form:
class ReviewForm(ModelForm):
class Meta:
model = Review
fields = ('title','category', 'body', )
widgets = {
'body': Textarea(attrs={'cols': 60, 'rows': 20}),
}
I've always done this by accepting a new kwarg in my form's __init__, and saving the value until save-time.
class ReviewForm(ModelForm):
class Meta:
model = Review
fields = ('title','category', 'body', )
widgets = {
'body': Textarea(attrs={'cols': 60, 'rows': 20}),
}
def __init__(self, *args, **kwargs):
self._user = kwargs.pop('user')
super(ReviewForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
inst = super(ReviewForm, self).save(commit=False)
inst.author = self._user
if commit:
inst.save()
self.save_m2m()
return inst
And then in my view:
def post_review(request):
# ... snip ...
if request.method == 'POST'
form = ReviewForm(request.POST, user=request.user)
if form.is_valid():
form.save()
return HttpResponseRedirect('/thanks/') #or whatever the url
else:
# Don't forget to add user argument
form = ReviewForm(user=request.user)
# ... snip ...
If Review.author isn't a required field, you can add a second value to the kwargs.pop call to set a default, like None. Otherwise, if the user kwarg isn't provided, it'll raise an error, effectively making it a required argument.
As an alternative solution, in Django 2+ using a form view - such as a CreateView or FormView, I can simply pass the self.request.user to my pre-saved form model:
class AppCreateView(CreateView):
model = models.App
fields = ['name']
success_url = '/thanks/'
def form_valid(self, form):
app_model = form.save(commit=False)
app_model.author = self.request.user
# app_model.user = User.objects.get(user=self.request.user) # Or explicit model
app_model.save()
return super().form_valid(form)
I agree the class based view is not important here. The important line is app_model.author = self.request.user.
The model is not special:
from django.db import models
from django.contrib.auth.models import User
class App(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255, help_text="Arbitrary name")
created = models.DateTimeField(auto_now_add=True, max_length=255)
I have a formset mixin which lets you pass extra arguments to the generated forms.
Just add the mixin as the first base class, set a dictionary named "form_kwargs" as a class attribute to describe the
arguments to pass.
from django.forms.formsets import BaseFormSet
class BaseKwargsFormSet(BaseFormSet):
"""
A formset mix-in to allow keyword arguments to be passed to constructed forms
For model_formsets, derive from this model *first* because django's formsets
can't grok the extra arguments.
To use, specify a dictionary with the kwargs & default values as an attribute
named "form_kwargs" on the formset base class.
example:
class BaseUserModelFormset (BaseKwargsFormSet, BaseModelFormSet):
form_kwargs = { 'user': None }
UserFormset = modelformset_factory (usermodel, form=userform,
formset=BaseUserModelFormset)
formset = UserFormset (request.POST or None, user=request.user)
"""
def __init__(self, *args, **kwargs):
form_kwargs = getattr(self, 'form_kwargs', {})
self.form_kwargs = dict((k, kwargs.pop(k, v)) for k, v in form_kwargs.items())
super(BaseKwargsFormSet, self).__init__(*args, **kwargs)
def _construct_form(self, index, **kwargs):
kwargs.update(**self.form_kwargs)
return super(BaseKwargsFormSet, self)._construct_form(index, **kwargs)