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 }}
Related
I wanto to display the current user in the form before submitting.
views.py
class PostEncabezadoReporte(LoginRequiredMixin, CreateView):
login_url = '/login/'
redirect_field_name = 'redirect_to'
form_class = PostEncabezadoReporteForm
template_name = "crear_reporte.html"
def form_valid(self, form):
object = form.save(commit=False)
object.user = self.request.user
object.startweek, object.endweek = self.weekdatetimeconverter(
object.semana)
object.folio = self.getfolio(
object.user, object.semana, object.tipo_reporte)
self.validar_unico = self.reporte_unico(
object.user, object.semana, object.cliente)
if self.validar_unico == 0:
object.save()
else:
return self.form_invalid(form)
return super(PostEncabezadoReporte, self).form_valid(form)
forms.py
class PostEncabezadoReporteForm(forms.ModelForm):
class Meta:
model = EncabezadoReporte
fields = ('user', 'tipo_reporte', 'tipo_gasto', 'cliente',
'semana', 'folio')
widgets = {'semana': forms.DateInput(attrs={'type': 'week'}),
}
I alreayd tried to override the init in the form and is not working, I can select the user in the field but I want it to be displayed at init.
I have tried to override the init in the form but I had a problem, I was missing a line after the super, this is an example of another form init that I did:
def __init__(self, *args, **kwargs):
self.carro = kwargs.pop('encabezado')
super(AgregarGastoReporte, self).__init__(*args, **kwargs)
self.fields['encabezado'].initial = self.carro
I've created a simple app where logged in users can can submit a session for a conference, view the results of their submission, view a list of their submissions, and edit their submissions (they should not have access to other users' submissions). I'm using django's class-based views (CreateView, DetailView, ListView, UpdateView).
I'm struggling with the permissions however. All the views, except updateview, work but if I type in the url directly using a non-logged in username I can see their submissions.
I also suspect that the permissions is the same reason I can't get the updateview to work.
What am I missing? And is there a better way to avoid using usernames and slugs in the Url? I can't seem to find any examples or tips in how to do this type of thing. I'm a beginner so probably miss some understanding of the fundamentals here and there.
I've tried to understand how the User model works because there I did manage to find a way to create, view and edit user details in a protected way. I relied on function views there though and can't seem to apply that approach to the submission app.
models.py
class Hsession(models.Model):
submitter = models.ForeignKey(User, related_name="submittersessions", on_delete=models.CASCADE)
submission_date = models.DateTimeField(auto_now=True)
session_title = models.CharField("session title", max_length=40, default='')
session_description = models.TextField("session description", max_length=350, default='')
slug = models.SlugField(allow_unicode=True, unique=True)
def save(self, *args, **kwargs):
self.slug = slugify(self.session_title)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse("submission:detail-single", kwargs={"username": self.submitter.username, "slug": self.slug})
urls:
urlpatterns = [
path("", views.CreateSubmission.as_view(), name="create"),
path("by/<username>/<slug>",views.SubmissionDetail.as_view(),name="detail-single"),
path("by/<slug>/edit",views.EditSubmission.as_view(), name="edit"),
path("by/<username>/",views.SubmissionList.as_view(), name="list"),
]
views.py
class CreateSubmission(LoginRequiredMixin, generic.CreateView):
fields = ('session_title', 'session_description', 'subject_category')
model = models.Hsession
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.submitter = self.request.user
self.object.save()
return super().form_valid(form)
class SubmissionList(LoginRequiredMixin, generic.ListView):
model = models.Hsession
template_name = "submission/user_hsession_list.html"
def get_queryset(self):
try:
self.hsession_submitter = User.objects.prefetch_related("submittersessions").get(
username__iexact=self.kwargs.get("username")
)
except User.DoesNotExist:
raise Http404
else:
return self.hsession_submitter.submittersessions.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["hsession_submitter"] = self.hsession_submitter
return context
class SubmissionDetail(LoginRequiredMixin, generic.DetailView):
model = models.Hsession
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(
submitter__username__iexact=self.kwargs.get("username")
)
class EditSubmission(LoginRequiredMixin, generic.UpdateView):
model = models.Hsession
fields = ('session_title', 'session_description', 'subject_category')
template_name = 'submission/hsession_update.html'
success_url = 'submission/hsession_detail.html'
forms.py
class UserSubmissionForm(ModelForm):
class Meta:
model = Hsession
fields = ['session_title','session_description', 'subject_category']
class EditSubmissionForm(ModelForm):
class Meta:
model = Hsession
fields = ['session_title','session_description', 'subject_category']
You should use UserPassesTestMixin like this
from django.contrib.auth.mixins import UserPassesTestMixin
class EditSubmission(UserPassesTestMixin,LoginRequiredMixin, generic.UpdateView):
model = models.Hsession
fields = ('session_title', 'session_description', 'subject_category')
template_name = 'submission/hsession_update.html'
success_url = 'submission/hsession_detail.html'
def test_func(self):
//should return true if he have access
if self.request.user.is_authenticated:
slug = self.kwargs['slug']
obj = self.model.objects.get(slug=slug)
login_user = self.request.user
return login_user.pk == obj.submitter.pk
else:
return False
for more information UserPassesTestMixin
from django.contrib.auth.mixins import UserPassesTestMixin
class EditSubmission(LoginRequiredMixin, UserPassesTestMixin, generic.UpdateView):
model = models.Hsession
fields = ('session_title', 'session_description', 'subject_category')
template_name = 'submission/hsession_update.html'
success_url = 'submission/hsession_detail.html'
def test_func(self):
self.object = self.get_object()
if self.request.user == self.object.user:
return True
else:
return False
I'm using "UserPassesTestMixin" like this and it is working.
I tried this in my modelform:
class Ledgerform(forms.ModelForm):
class Meta:
model = ledger1
fields = ('name', 'group1_Name')
def __init__(self, User, Company, *args, **kwargs):
self.User = kwargs.pop('User', None)
self.Company = kwargs.pop('Company', None)
super(Ledgerform, self).__init__(*args, **kwargs)
self.fields['name'].widget.attrs = {'class': 'form-control',}
self.fields['group1_Name'].queryset = group1.objects.filter(User= self.User,Company = self.Company)
In my views.py I have done something like this:
class ledger1ListView(LoginRequiredMixin,ListView):
model = ledger1
paginate_by = 15
def get_queryset(self):
return ledger1.objects.filter(User=self.request.user, Company=self.kwargs['pk'])
class ledger1CreateView(LoginRequiredMixin,CreateView):
form_class = Ledgerform
def form_valid(self, form):
form.instance.User = self.request.user
c = company.objects.get(pk=self.kwargs['pk'])
form.instance.Company = c
return super(ledger1CreateView, self).form_valid(form)
I want to perform the the same query that I have passed in my ledger1ListView by using queryset in my modelform but my kwargs.pop is not returning the current user or the company...
This is my models.py:
class ledger1(models.Model):
User = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,null=True,blank=True)
Company = models.ForeignKey(company,on_delete=models.CASCADE,null=True,blank=True,related_name='Companys')
name = models.CharField(max_length=32)
group1_Name = models.ForeignKey(group1,on_delete=models.CASCADE,blank=True,null=True)
Do any one know what I am doing wrong in my code?
Thank you in advance
You can override the FormMixin.get_form_kwargs [Django-doc] in your view, that it constructs a dictionary with the parameters necessary to initialize the form, like:
class ledger1CreateView(LoginRequiredMixin,CreateView):
form_class = Ledgerform
def get_form_kwargs(self):
data = super(ledger1CreateView, self).get_form_kwargs()
data.update(
User=self.request.User,
Company=company.objects.get(pk=self.kwargs['pk'])
)
return data
The form_valid function is called after the form is constructed, validated and appears to be valid. Typically it is used to redirect the user to the "success page".
I want to limit the choices of a ManyToManyField to those matching a ForeignKey. The form displays properly, but upon saving results in an error Select a valid choice. <choice> is not one of the available choices.
Before I was trying to limit the queryset by passing a parameter in the view to the form, and then using that parameter to filter the queryset.
Models:
class VenueEventTimeslot(models.Model):
venue = models.ForeignKey(Venue)
name = models.CharField(max_length=255)
class VenueEvent(models.Model):
venue = models.ForeignKey(Venue)
event_timeslots = models.ManyToManyField(VenueEventTimeslot)
class VenueEventForm(ModelForm):
event_timeslots = ModelMultipleChoiceField(queryset=None, widget=CheckboxSelectMultiple())
def __init__(self, *args, **kwargs): # limit timeslots to those of the venue only
venue_obj = kwargs.pop('venue_obj',None)
super(VenueEventForm, self).__init__(*args,**kwargs)
self.fields['event_timeslots'].queryset=VenueEventTimeslot.objects.filter(venue=venue_obj)
class Meta:
model = VenueEvent
fields = ['event_timeslots']
Views:
#login_required
def calendar(request, pk):
venue = Venue.objects.get(pk = pk)
if request.method == "POST":
form = VenueEventForm(request.POST)
if form.is_valid():
# form stuff
else:
form = VenueEventForm(venue_obj = venue)
context = {'venue':venue, 'form':form}
return render(request, ... , context)
However, if I pass the queryset from the view, it works perfectly.
Models:
class VenueEventTimeslot(models.Model):
# same as above
class VenueEvent(models.Model):
# same as above
class VenueEventForm(ModelForm):
class Meta:
model = VenueEvent
fields = ['date','client_name','event_timeslots']
widgets = {
'date': SelectDateWidget(),
'event_timeslots': CheckboxSelectMultiple(),
}
Views:
#login_required
def calendar(request, pk):
venue = Venue.objects.get(pk = pk)
if request.method == "POST":
form = VenueEventForm(request.POST)
if form.is_valid():
# form stuff
else:
form = VenueEventForm()
form.fields['event_timeslots'].queryset=VenueEventTimeslot.objects.filter(venue=venue)
context = {'venue':venue, 'form':form}
return render(request, ..., context)
Would anyone be able to shed some light on this?
I just solved a problem similar to this yesterday which is right here, How To Exclude A Value In A ModelMultipleChoiceField?, but I think the issue with your init function is the way it is formatted. Instead of venue=venue_obj, you need to change it to pk=venue_obj because it appear you are getting the pk of venue in the view instead of the venue attribute of VenueEvent , and I reformatted your form a bit to make it look cleaner.
forms.py
class VenueEventForm(ModelForm):
def __init__(self, *args, **kwargs): # limit timeslots to those of the venue only
venue_obj = kwargs.pop('venue_obj')
super(VenueEventForm, self).__init__(*args,**kwargs)
self.fields['event_timeslots'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple(), queryset=VenueEventTimeslot.objects.filter(pk=venue_obj))
class Meta:
model = VenueEvent
fields = ['event_timeslots']
views.py
#login_required
def calendar(request, pk):
venue = Venue.objects.get(pk = pk)
if request.method == "POST":
form = VenueEventForm(request.POST, venue_obj=venue)
if form.is_valid():
# form stuff
else:
print VenueEventForm.errors
else:
form = VenueEventForm(venue_obj=venue)
context = {'venue':venue, 'form':form}
return render(request, ... , context)
#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)