Django Custom Forms in Formsets using CBV - django

I want to create a new site and add corresponding publications at the same time. I have to use a custom made form for the "site" due to the large dataset linked to it through the "municipality" foreign key.
I have these models:
class site(models.Model):
sid = models.AutoField(primary_key=True)
site_name = models.CharField(max_length=250)
site_notes = models.CharField(max_length=2500, blank=True, null=True)
municipality = models.ForeignKey('local_administrative_unit', on_delete=models.PROTECT)
geom = models.PointField(srid=4326)
def __str__(self):
return '{}, {} ({})'.format(self.sid, self.site_name, self.municipality)
lass cit_site(models.Model):
cit_site_id = models.AutoField(primary_key=True)
publication = models.ForeignKey('publication', on_delete=models.PROTECT)
site = models.ForeignKey('site', on_delete=models.PROTECT)
first_page = models.IntegerField(null=True, blank=True)
last_page = models.IntegerField(null=True, blank=True)
notes = models.CharField(max_length=500, null=True, blank=True)
def __str__(self):
return '{}: {} - {}'.format(self.publication.pub_id, self.first_page, self.last_page)
The site form at the moment just adds a site through a class based view. Because of the large dataset of municipalities, loading the form would take forever and it wouldn't be very handy to actually choose the right municipality (16k+ records in this table atm), so i made this custom form:
class NewSiteForm(DynamicFormMixin, forms.Form):
def land_choices(form):
country = form['country'].value()
return models.administrative_level_4.objects.filter(adm_level_5=country)
def land_initial(form):
country = form['country'].value()
return models.administrative_level_4.objects.filter(adm_level_5=country).first()
def district_choices(form):
land = form['land'].value()
return models.administrative_level_3.objects.filter(adm_level_4=land)
def district_inital(form):
land = form['land'].value()
return models.administrative_level_3.objects.filter(adm_level_4=land).first()
def town_choices(form):
district = form['district'].value()
return models.administrative_level_2.objects.filter(adm_level_3=district)
def town_initial(form):
district = form['district'].value()
return models.administrative_level_2.objects.filter(adm_level_3=district).first()
def municipality_choices(form):
town = form['town'].value()
return models.local_administrative_unit.objects.filter(adm_level_2=town)
def municipality_initial(form):
town = form['town'].value()
return models.local_administrative_unit.objects.filter(adm_level_2=town).first()
country = forms.ModelChoiceField(
queryset=models.administrative_level_5.objects.all(), empty_label='Select a country...'
)
land = DynamicField(
forms.ModelChoiceField,
queryset=land_choices,
initial=land_initial
)
district = DynamicField(
forms.ModelChoiceField,
queryset=district_choices,
initial=district_inital
)
town = DynamicField(
forms.ModelChoiceField,
queryset=town_choices,
initial=town_initial
)
siteMunicipality = DynamicField(
forms.ModelChoiceField,
queryset=municipality_choices,
initial=municipality_initial
)
siteNotes = forms.CharField(
required=False,
widget=forms.Textarea
)
siteName = forms.CharField()
It uses some htmx to fill the cascading dropdowns which makes it load much faster. The view looks like this:
class NewSiteView(FormMixin, View):
form_class = forms.NewSiteForm
template_name = 'datamanager/newsiteCascade.html'
success_url = 'datamanager/newsiteCascade.html'
def get(self, request, *args, **kwargs):
form = self.form_class()
return render(request, self.template_name, {'form':form})
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
pol = models.local_administrative_unit.objects.values('geom').filter(lau_id=form.cleaned_data['siteMunicipality'].lau_id)[0]['geom']
cent_point = pol.centroid
geom = cent_point.wkt
municipality = form.cleaned_data['siteMunicipality']
site_name = form.cleaned_data['siteName']
site_notes = form.cleaned_data['siteNotes']
new_site = models.site(site_name = site_name, site_notes=site_notes, municipality=municipality, geom=geom)
new_site.save()
if 'Save' in request.POST:
return HttpResponseRedirect(reverse_lazy('data_manager:all_sites'))
elif 'SaveAnother' in request.POST:
return HttpResponseRedirect(reverse_lazy('data_manager:new_site'))
###############################################################
## HTMX Queries
###############################################################
def lands(request):
form = forms.NewSiteForm(request.GET)
return HttpResponse(form['land'])
def districts(request):
form = forms.NewSiteForm(request.GET)
return HttpResponse(form['district'])
def towns(request):
form = forms.NewSiteForm(request.GET)
return HttpResponse(form['town'])
def siteMunicipalities(request):
form = forms.NewSiteForm(request.GET)
return HttpResponse(form['siteMunicipality'])
However at the moment I have to add literature to those sites from another form (just a modelform from the cit_site model shown above) after creating the site first. I want to have this all done in once so I can create a site, add the citations and save it all together. I also want to keep using the cascading dropdowns from the form above to avoid the loading problem.
As far as I understand by now I need to use some kind of formset which holds both forms. But everything I found so far was using modelforms which wouldn't be useful in my case (no custom form). Also, the data for the parent form seems to already need to exist when the formset is rendered.
So I need some help how to approach this problem (maybe its two problems in one?). Thx in advance.

Related

Django - Object list inside object list

I'm using Django to render a list of tube numbers with their attached ingredient. When clicking on a tube number it renders the list of all the ingredients. Then if I click on an ingredient I want it to replace the saved ingredient for this tube.
But I don't know how to proceed, if I should do a sub-app for ingredients in my tube app, or if their is a way to keep the tube id.
I tried many things, here is my actual views :
def tube_view(request):
tubes = Tube.objects.all()
return render(request, 'tube_ing.html', {'tubes': tubes})
def tube_detail_view(request, my_id):
obj = get_object_or_404(Tube, id=my_id)
request.session['my_id'] = my_id
ings = Ing.objects.all()
return render(request, "tube_detail.html", {"obj": obj, "ings": ings)
def tube_choice(request, id_ing):
obj = get_object_or_404(Ing, id=id_ing)
request.session['id_ing'] = id_ing
data = {'qty': 100}
form = TubeForm(data)
return render(request, "tube_choice.html", {"obj": obj, 'form': form})
def tube_form(request):
if request.method == 'POST':
form = TubeForm(request.POST)
if form.is_valid():
id_ing = request.session['id_ing']
form.cleaned_data['name_ing'] = my_id
t = Tube(name_ing_tube=name_ing_ing)
t.save()
else:
return HttpResponseRedirect('/tube_ing/')
else:
return HttpResponseRedirect('/tube_ing/')
My models :
class Tube(models.Model):
tube_number = models.IntegerField()
name_ing = models.CharField(null=False, blank=True, max_length=255, default='')
qty = models.IntegerField(default=0)
def get_absolute_url(self):
return reverse('tube_ing:tube_detail', kwargs={"my_id": self.id})
class Ing(models.Model):
name_ing = models.CharField(null=False, blank=True, max_length=255, default='')
def get_absolute_url(self):
return reverse('tube_ing:tube_choice', kwargs={"id_ing": self.id})
And my url patterns :
path('', views.tube_view, name='tube_view'),
path('<int:my_id>/', views.tube_detail_view, name='tube_detail'),
path('ing/<int:id_ing>/', views.tube_choice, name='tube_choice'),
path('ing/tube_form/', views.tube_form, name='tube_form')
So I don't know what to put in my form and how to proceed to keep the tube's info.
Thanks a lot to the ones who will take some time to help me on this.
if [there's] a way to keep the tube id[?]
Yes, in your tube_form function try accessing request.session['my_id'], the tube ID was established from the the tube_detail_view.

Setting form fields in django class based generic view CreateView

I'm using django's CreateView to add images to a book. I pass the book's id to the class based view as a parameter in the url. Form fields such as book and language are not rendered on the template, rather they're obtained with the help of the book's id.
# views.py
class PictureCreateView(CreateView):
model = Upload
fields = "__all__"
book_id = None
def get_initial(self):
initial = super(PictureCreateView, self).get_initial()
initial = initial.copy()
self.book_id = self.kwargs['book_id']
book = Book.objects.get(id=self.book_id)
initial['book'] = book
initial['language'] = language
initial['uploader'] = self.request.user
return initial
# set book_id so it used in the template
def get_context_data(self, **kwargs):
context = super(PictureCreateView, self).get_context_data(**kwargs)
context['book_id'] = self.book_id
return context
def form_valid(self, form, **kwargs):
print('Form is valid')
self.object = form.save()
files = [serialize(self.object)]
data = {'files': files}
response = JSONResponse(data, mimetype=response_mimetype(self.request))
response['Content-Disposition'] = 'inline; filename=files.json'
return super(PictureCreateView, self).form_valid(form)
def form_invalid(self, form):
print('Form invalid!')
print(form.errors)
data = json.dumps(form.errors)
return HttpResponse(content=data, status=400, content_type='application/json')
# models.py
class Upload(models.Model):
image = models.ImageField(upload_to=get_upload_path, help_text='Image to process')
uploader = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, related_name='uploader')
language = models.ForeignKey(Language, models.CASCADE)
book = models.ForeignKey(Book, models.CASCADE)
The problem is that I get an error saying the form is invalid, and the fields uploader, book and language are required. How do I resolve this?
The initial data is used to display the defaults when the form is initially displayed. It isn't used when those values are missing from the submitted form data. If fields like book and uploader are set from the URL or logged-in user, then you should leave them out of the form completely, instead of setting them in the initial data. You can then set the values on the instance in the form_valid method before the form is saved.
from django.contrib.auth.mixins import LoginRequiredMixin
class PictureCreateView(LoginRequiredMixin, CreateView):
model = Upload
fields = ['other_field1', 'other_field2', ...] # leave out book, language and uploader
def form_valid(self, form):
self.book_id = self.kwargs['book_id']
book = Book.objects.get(id=self.book_id)
form.instance.book = book
form.instance.language = ????
form.instance.uploader = self.request.user
return super(
The LoginRequiredMixin makes sure that only logged-in users can access the view.
You may want to use get_object_or_404 to handle the case where book_id refers to a book that does not exist.
One thought, initial doesn't fill the model for submission. You need to do that in init
def __init__(self):
super(PictureCreateView, self).__init__()
self.fields['book'] = self.initial['book']
self.fields['uploader'] = self.initial['uploader']
self.fields['language'] = self.initial['book']
Or, if you don't want to set the fields, make sure they are optional in your original model:
class Upload(models.Model):
uploader = models.ForeignKey('uploader', on_delete=models.CASCADE, null=True, blank=True)
book = models.ForeignKey('book', on_delete=models.CASCADE, null=True, blank=True)
language = models.ForeignKey('language', on_delete=models.CASCADE, null=True, blank=True)

How to change the rendered field in Django's ModelForm queryset?

I want to change the rendered field shown in a model form choicefield, based on some user selected feature, which is language in my case.
I've got a two models. Of the two, the 'Vastausvaihtoehto' model saves an answer in both english and finnish, saving it to the database. It also returns the finnish answer by default, because that's how I've defined the unicode function:
Model
class Vastausvaihtoehto(models.Model):
...
vastaus_fi = models.CharField(
verbose_name=_(u'Vastaus'),
max_length=256,
null=True,
blank=True,
)
vastaus_en = models.CharField(
verbose_name=_(u'Vastaus_en'),
max_length=256,
null=True,
blank=True,
)
...
def __unicode__(self):
return u'%s' % (self.vastaus_fi)
class Valinta(models.Model):
organisaatio = models.ForeignKey(
Organisaatio,
related_name=_(u'valinta'),
null=True,
blank=True,
on_delete=models.CASCADE,
)
kysymys = models.ForeignKey(
Kysymysvaihtoehto,
related_name=_(u'valinta'),
null=True,
blank=True,
)
vastausvaihtoehto = models.ForeignKey(
Vastausvaihtoehto,
related_name=_(u'valinta'),
null=True,
blank=True,
)
def __unicode__(self):
return u'%s' % (self.kysymys)
I also have a ModelForm, that I use to select the correct choices
Form
class ValintaForm(ModelForm):
class Meta:
model = Valinta
fields = '__all__'
widgets = {
'organisaatio':forms.HiddenInput(),
'kysymys':forms.HiddenInput(),
'vastausvaihtoehto':forms.RadioSelect(),
}
And here's my view:
View
class kysymys(View):
template_name = 'mytemplate.html'
success_url = 'something'
def get(self, request, pk, question_id, *args, **kwargs):
kysymys = Kysymysvaihtoehto.objects.get(kysymys_id=int(question_id))
vastausvaihtoehdot = Vastausvaihtoehto.objects.filter(kysymysvaihtoehto=kysymys)
if request.LANGUAGE_CODE == 'fi':
# What do I put here?
else:
# What do I put in here?
form = ValintaForm()
form.fields['vastausvaihtoehto'].queryset = vastausvaihtoehdot
form.fields['vastausvaihtoehto'].empty_label = None
return render(request, self.template_name, {
'form':form,
'kysymys':kysymys,
"pk":pk,
"question_id":question_id,
})
I've tried to query just some certain values using values and values_list, and set them as the ModelForm queryset:
#Like so:
answers_en = Vastausvaihtoehto.objects.filter(kysymysvaihtoehto=kysymys).values_list('pk','vastaus_en')
form.fields['vastausvaihtoehto'].queryset = answers_en
But that does not render the form correctly. Should I add a helper method to the 'Vastausvaihtoehto' model, which returns the english name when called?
I know it's possible to circumvent this by just not using ModelForms, but is there a way to do this while using a ModelForm?
Define your ModelForm with an __init__ method which will accept language and question_id as keyword arguments.
class ValintaForm(ModelForm):
class Meta:
model = Valinta
fields = '__all__'
widgets = {
'organisaatio':forms.HiddenInput(),
'kysymys':forms.HiddenInput(),
'vastausvaihtoehto':forms.RadioSelect(),
}
def __init__(self, *args, **kwargs):
language = kwargs.pop('language', None)
question_id = kwargs.pop('question_id')
super(ValintaForm, self).__init__(*args, **kwargs)
if language == "fi":
kysymys = Kysymysvaihtoehto.objects.get(kysymys_id=int(question_id))
vastausvaihtoehdot = Vastausvaihtoehto.objects.filter(kysymysvaihtoehto=kysymys)
self.fields['vastausvaihtoehto'].queryset = vastausvaihtoehdot
else:
# put your other conditions here
pass
In your views, when you initialize your form, pass the keyword arguments
form = ValintaForm(language=request.LANGUAGE_CODE, question_id=question_id)
Or if you think it is better, you can pass the whole queryset to the forms.
def __init__(self, *args, **kwargs):
qs = kwargs.pop('qs')
super(ValintaForm, self).__init__(*args, **kwargs)
self.fields['vastausvaihtoehto'].queryset = qs
Pass the query set when you initialize form
form = ValintaForm(qs=vastausvaihtoehdot)

Filter select field in ModelForm by currently logged in user

I'm trying to display a form (ModelForm) with a select field filtered by currently logged in user. The select field in this case contains a list of categories. I want to display only the categories which "belong" to the currently logged in user. The category field is a foreign key to the IngredienceCategory model.
Here is what I've come up with so far but it's giving me an error (unexpected keyword queryset). Any ideas what I'm doing wrong?
# models.py
class IngredienceCategory(models.Model):
name = models.CharField(max_length=30)
user = models.ForeignKey(User, null=True, blank=True)
class Meta:
verbose_name_plural = "Ingredience Categories"
def __unicode__(self):
return self.name
class Ingredience(models.Model):
name = models.CharField(max_length=30)
user = models.ForeignKey(User, null=True, blank=True)
category = models.ForeignKey(IngredienceCategory, null=True, blank=True)
class Meta:
verbose_name_plural = "Ingredients"
def __unicode__(self):
return self.name
class IngredienceForm(ModelForm):
class Meta:
model = Ingredience
fields = ('name', 'category')
# views.py
def home(request):
if request.user.is_authenticated():
username = request.user.username
email = request.user.email
foods = Food.objects.filter(user=request.user).order_by('name')
ingredients = Ingredience.objects.filter(user=request.user).order_by('name')
ingrcat = IngredienceCategory.objects.filter(user=request.user)
if request.method == 'POST':
form = IngredienceForm(request.POST)
if form.is_valid():
# Create an instance of Ingredience without saving to the database
ingredience = form.save(commit=False)
ingredience.user = request.user
ingredience.save()
else:
# How to display form with 'category' select list filtered by current user?
form = IngredienceForm(queryset=IngredienceCategory.objects.filter(user=request.user))
context = {}
for i in ingredients:
context[i.category.name.lower()] = context.get(i.category.name.lower(), []) + [i]
context2 = {'username': username, 'email': email, 'foods': foods, 'ingrcat': ingrcat, 'form': form,}
context = dict(context.items() + context2.items())
else:
context = {}
return render_to_response('home.html', context, context_instance=RequestContext(request))
That's happening because ModelForm does not take a queryset keyword.
You can probably achieve this by setting the queryset on the view:
form = IngredienceForm()
form.fields["category"].queryset =
IngredienceCategory.objects.filter(user=request.user)
See related question here.
Here i have another suggestion to solve the problem. You can pass request object in your form object inside view.
In view.py just pass the request object.
form = IngredienceForm(request)
In your forms.py __init__ function also add request object
from models import IngredienceCategory as IC
class IngredienceForm(ModelForm):
class Meta:
model = Ingredience
fields = ('name', 'category')
def __init__(self, request, *args, **kwargs):
super(IngredienceForm, self).__init__(*args, **kwargs)
self.fields['name'].queryset = IC.objects.filter(user=request.user)
This filter always will be applied whenever you initialize your form .

saving django ManyToMany not valid

I have a form from my model that needs to be validated and saved making use of ManyToMany Fields.
Everytime I try and save it, I get thrown back to the page, just saying this field is required
My models.py
class HuntingReport(models.Model):
user = models.ForeignKey(User, related_name='User')
outfitter = models.ForeignKey(User, related_name='Outfitter', null=True, blank=True)
date_travel_started = models.DateField(blank=True, null=True)
date_travel_ended = models.DateField(null=True, blank=True)
title = models.CharField(max_length=50)
report = models.TextField()
wish_list = models.ManyToManyField(Specie)
bag_list = models.ManyToManyField(Trophies)
def __unicode__(self):
return self.title
My forms.py looks as follows
class HuntingReportForm(ModelForm):
date_travel_started = forms.DateField(widget=extras.SelectDateWidget(years=range(1970,2010)))
date_travel_ended = forms.DateField(widget=extras.SelectDateWidget(years=range(1970,2010)))
wish_list = forms.ModelMultipleChoiceField(queryset=Specie.objects.all(), widget=FilteredSelectMultiple("verbose name", is_stacked=False))
bag_list = forms.ModelMultipleChoiceField(queryset=Trophies.objects.all(), widget=FilteredSelectMultiple("verbose name", is_stacked=False))
class Meta:
model = HuntingReport
exclude = ['user']
def __init__(self, user, *args, **kwargs):
super(HuntingReportForm, self).__init__(*args, **kwargs)
users = User.objects.filter(userprofile__outfitter=True)
self.fields['outfitter'].choices = [('', '')] + [(user.pk, user.get_full_name()) for user in users]
my views.py
def create(request, template_name='reports/new.html'):
if request.method == 'POST':
form = HuntingReportForm(request.POST, request.FILES)
if form.is_valid():
newform = form.save(commit=False)
newform.user = request.user
newform.save_m2m()
return HttpResponseRedirect('/hunting-reports/')
else:
form = HuntingReportForm(request.user)
context = { 'form':form, }
return render_to_response(template_name, context,
context_instance=RequestContext(request))
Did you try passing blank=True for model field's constructor, or required=False for the ModelMultipleChoiceField's constructor?
I know that blank=True solves the problem for the form in the admin panel, but I don't know how it gets mapped to the ModelForm's fields. I'm assuming that it gets mapped to required property.