How to save multiple dynamically created forms in django - django

I am trying to create a page where various data corresponding to mutliple models can be input by the user, and to have an option to dynamically add additional forms. I have been attempting to use htmx for this and am able to dynamically add forms, however when I save it is only the last entered form that is saved. I haven't used formsets as this wont integrate well with htmx https://justdjango.com/blog/dynamic-forms-in-django-htmx#django-formsets-vs-htmx.
Code below any suggestion as to how to get all the dynamically created forms to be save would be most appreciated!
models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=50)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
forms.py
from django import forms
from .models import Book, Author
from crispy_forms.helper import FormHelper
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
fields = ['name']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-CaseForm'
self.helper.form_class = 'blueForms'
self.helper.form_method = 'post'
self.helper.form_tag = False
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ('title',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_tag = False
self.helper.form_show_labels = False
views.py
class create_book(TemplateView):
template_name = 'create_book.html'
def get(self, *args, **kwargs):
author_form = AuthorForm
book_form = BookForm
return self.render_to_response(
{'book_form': BookForm,
"author_form": AuthorForm,}
)
def post(self, *args, **kwargs):
author_form = AuthorForm(data=self.request.POST)
book_form = BookForm(data=self.request.POST)
if author_form.is_valid():
author = author_form.save()
if book_form.is_valid():
book = book_form.save(commit=False)
book.author = author
book.save()
def create_book_form(request):
form = BookForm()
context = {
"form": form
}
return render(request, "partials/book_form.html", context)
urls.py
urlpatterns = [
path('create_book/', create_book.as_view(), name='create-book'),
path('htmx/create-book-form/', create_book_form, name='create-book-form'),]
create_book.html
{% extends "base.html" %}
{% block content %}
{% load crispy_forms_tags %}
<form class="blueForms" id="id-CaseForm" method="post" >
{% crispy author_form %}
{% crispy book_form %}
<button type="button" hx-get="{% url 'create-book-form' %}" hx-target="#bookforms" hx-swap="beforeend">
Add book form
</button>
<div id="bookforms"></div>
</form>
partials/book_form.html
{% load crispy_forms_tags %}
<div hx-target="this" hx-swap="outerHTML">
{% csrf_token %}
{% crispy form %}
{{ form.management_form }}
</div>

A quick Google finds me this module:
https://github.com/adamchainz/django-htmx
It seems someone may have already done this for you my friend :)

Related

Using formsets for my fileupload does not work when doing an update class based view only on create in django

I have used as many examples online as I could cobble together in an attempt to get my two simple models to have the ability to do an inline formset allowing me to add many files to a technial drawing.
This is not working, I can only add one file for the Create and the update only updates the Technical_Entry model, not a file...which in of itself acts funny. The UI ona create shows one spot to add a file, then save the entire record and its child record. That works.
The update, the UI shows the file that was saved earlier..(great!) but then has two more 'choose file' slots (random?) and adding a file to those does nothing when the save is clicked. It doesn't remove the file previously added in the create, but it also
does not save the new file added to the now three slots (one used, two free). So update does not work for the files for some reason.
class Technical_Entry(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
ema = models.ForeignKey(EMA, on_delete=models.CASCADE)
system = models.ForeignKey('System', on_delete=models.CASCADE) # are SYSTEMS RELATED TO SUBSYSTEMS OR JUST TWO GROUPS?
sub_system = models.ForeignKey(SubSystem, on_delete=models.CASCADE)
drawing_number = models.CharField(max_length=200)
drawing_title = models.CharField(max_length=255)
engineer = models.CharField(max_length=200)
vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE)
date_drawn = models.DateField()
ab = models.BooleanField()
class Technical_Entry_Files(models.Model):
tech_entry = models.ForeignKey(Technical_Entry, on_delete=models.CASCADE)
file = models.FileField(upload_to='techdb/files/')
def __str__(self):
return self.tech_entry.drawing_number
To upload using a formset. While the page 'displays' mostly correctly, it flat out does not create the record in the Technical_Entry_Files model.
Relevant forms.py:
class FileUploadForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(FileUploadForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-lg-4'
self.helper.field_class = 'col-lg-8'
class Meta:
model = Technical_Entry_Files
fields = ('file',)
TechFileFormSet = inlineformset_factory(Technical_Entry, Technical_Entry_Files, form=FileUploadForm, extra=1, max_num=15)
class Technical_EntryForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(Technical_EntryForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-lg-4'
self.helper.field_class = 'col-lg-8'
self.helper.add_input(Submit('submit', 'Submit'))
class Meta:
model = Technical_Entry
fields = ('category', 'ema', 'system', 'sub_system', 'drawing_number', 'drawing_title', 'engineer', 'vendor', 'date_drawn', 'ab')
widgets = {
'date_drawn':DateInput(attrs={
'class':'datepicker form-control',
'id' : 'datetimepicker2',
'tabindex' : '1',
'placeholder' : 'MM/DD/YYYY hh:mm',
'autocomplete':'off',
}, format='%m/%d/%Y'),
'system' : Select(attrs={'tabindex':'2'}),
}
Relevant views.py:
class TechEntryCreateView(LoginRequiredMixin, CreateView):
print ("we are here")
model = Technical_Entry
form_class = Technical_EntryForm
template_name = 'techdb/tech_entry_form.html'
print(template_name)
success_url = '/techentry_add'
def get_context_data(self, **kwargs):
data = super(TechEntryCreateView, self).get_context_data(**kwargs)
if self.request.POST:
data['file_upload'] = TechFileFormSet(self.request.POST, self.request.FILES)
else:
data['file_upload'] = TechFileFormSet()
return data
def form_valid(self, form):
context =self.get_context_data()
file_upload = context['file_upload']
with transaction.atomic():
self.object = form.save()
if file_upload.is_valid():
file_upload.instance =self.object
file_upload.save()
return super(TechEntryCreateView, self).form_valid(form)
class TechEntryUpdateView(LoginRequiredMixin, UpdateView):
model = Technical_Entry
form_class = Technical_EntryForm
template_name = 'techdb/tech_entry_form.html'
success_url = '/'
def get_context_data(self, **kwargs):
context = super(TechEntryUpdateView, self).get_context_data(**kwargs)
if self.request.POST:
context["file_upload"] = TechFileFormSet(self.request.POST, self.request.FILES,instance=self.object)
else:
context["file_upload"] = TechFileFormSet(instance=self.object)
return context
def form_valid(self, form):
context = self.get_context_data()
file_upload = context["file_upload"]
self.object = form.save()
if file_upload.is_valid():
file_upload.instance =self.object
file_upload.save()
return super(TechEntryUpdateView, self).form_valid(form)
and the tech_entry_form.html:
{% extends 'base.html' %}
{% load static %}
{% block page-js %}
<script>
$('.link-formset').formset({
addText: 'add file',
deleteText: 'remove',
});
</script>
{% endblock %}
{% block content %}
<main role="main" class="container">
<div class="starter-template">
<h1>New Tech Entry</h1>
</div>
<h2> Details of Technical Entry </h2>
<div class="row">
<div class="col-sm">
<form action="" method="post" enctype="multipart/form-data">{% csrf_token %}
{{ form.as_p }}
<h2> Files </h2>
{{ file_upload.management_form }}
{% for upload_form in file_upload.forms %}
<div class="link-formset">
{{ upload_form.file }}
</div>
{% endfor %}
<input type="submit" value="Save"/>back to the list
</form>
</div>
</div>
</div>
</main><!-- /.container -->
{% endblock %}
And what the UI looks like on edit...
class TechEntryUpdateView(LoginRequiredMixin, UpdateView):
model = Technical_Entry
form_class = Technical_EntryForm
template_name = 'techdb/tech_entry_form.html'
success_url = '/'
#log_entry_class = Technical_EntryForm(Technical_Entry) #removed
def get_context_data(self, **kwargs):
context = super(TechEntryUpdateView, self).get_context_data(**kwargs)
#self.object = self.get_object() #removed
if self.request.POST:
context["file_upload"] = TechFileFormSet(self.request.POST, self.request.FILES,instance=self.object)
else:
context["file_upload"] = TechFileFormSet(instance=self.object)
return context
def form_valid(self, form):
context = self.get_context_data()
file_upload = context["file_upload"]
self.object = form.save()
if file_upload.is_valid():
file_upload.instance =self.object
file_upload.save()
#return super().form_valid(form)
return super(TechEntryUpdateView, self).form_valid(form) #replaced old one
UPDATE
1. in order to be able to add mulitple files when creating,
TechFileFormSet = inlineformset_factory(Technical_Entry, Technical_Entry_Files, form=FileUploadForm, extra=4, max_num=4)
# note I changed to extra=4, if you always want to have only 4 files, then also change to max_num=4
2. When update, the reason why the update is not working even after modifying the views was because you were not passing hidden fields. You were not passing the ids of the files, therefore, your formsets were not passing .is_valid(), hence, no update. Add for loop about hidden fields like below.
{{ file_upload.management_form }}
{% for upload_form in file_upload.forms %}
{% for hidden in upload_form.hidden_fields %}
{{hidden}}
{% endfor %}
<div class="link-formset">
{{ upload_form.file }}
</div>
{% endfor %}
#Note the for loop I add about hidden fields.

Django incremental change in simultaneous ModelForm submission

My model has a field working_hour which is PositiveIntegerField, I am using ModelForm and django-crispy-forms.Assume tow different people has rendered form with working_hour value 20 and both of them want to increase working_hour by 10 and they updates the working_hour form field from 20 to 30.When they both submit the form, updated working_hour becomes 30 in database, but it should be 40.
models.py
class OverallHour(models.Model):
working_hour = models.PositiveIntegerField()
views.py
class OverallHourUpdateView(LoginRequiredMixin, UpdateView):
model = OverallHour
form_class = OverallHourForm
success_url = reverse_lazy('home')
forms.py
class OverallHourForm(ModelForm):
class Meta:
model = OverallHour
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.add_input(Submit('submit', 'UPDATE'))
overall_hour_form.html
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card bg-light">
<div class="card-body">
{% crispy form %}
</div>
</div>
</div>
</div>
{% endblock content %}
I just need change delta from initial form value in the server when they submit the form.Can you please advice me on this issue?
I got the solution!
class OverallHourForm(ModelForm):
class Meta:
model = OverallHour
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.add_input(Submit('submit', 'UPDATE'))
def clean(self):
cleaned_data = super(ProjectForm, self).clean()
print(self.has_changed())
print(self.changed_data)
print(self.initial)
print(cleaned_data)

Django class-based UpdateView

We are using Django 1.9.5. I am writing update company profile view. I want to have two fields on my update profile page:
company name,
apikey(generated with a dedicated function).
I want to change field name by hands and then save by clicking button Update. For filed apikey I want to have separated button which would automatically generate new key, save it to db and update my page.
I am overwriting post method at UpdateView class. But simething isn't working. Please take a look at my code:
models.py:
class Organization(models.Model):
name = models.CharField(_('organisation display name'), max_length=512,
blank=True)
apikey = models.CharField(_("API authentication key"), max_length=APIKEY_LENGTH,
default=get_apikey)
where get_apikey is
from django.utils.crypto import get_random_string
APIKEY_PREFIX = "key-"
APIKEY_LENGTH = 80
def get_apikey():
rnd_len = APIKEY_LENGTH - len(APIKEY_PREFIX)
return "{}{}".format(
APIKEY_PREFIX,
get_random_string(rnd_len, "abcdefghijklmnopqrstuvwxyz0123456789")
)
forms.py:
from django import forms
from crispy_forms.helper import FormHelper, Layout
from orgs.models import Organization
class OrgProfileForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(OrgProfileForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
class Meta:
model = Organization
fields = ['name', 'apikey']
views.py
from django.views.generic.edit import UpdateView, BaseUpdateView
from orgs.forms import OrgProfileForm
from orgs.models import Organization
from local.auth import get_apikey
class OrgProfileView(UpdateView):
model = Organization
template_name = 'orgs/orgprofile.html'
form_class = OrgProfileForm
url_name = 'profile'
success_url = '/'
def get_object(self, queryset=None):
org_obj = Organization.objects.get(orgname=self.kwargs["orgname"])
return org_obj
def post(self, request, *args, **kwargs):
import ipdb
ipdb.set_trace()
if '_key' in request.POST:
org_obj = Organization.objects.get(orgname=self.kwargs["orgname"])
org_obj.apikey = get_apikey()
org_obj.save()
self.object = org_obj
return super(BaseUpdateView, self).post(request, *args, **kwargs)
template.html
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load crispy_forms_tags %}
{% block title %}{% trans "Organization profile" %}{% endblock title %}
{% block header_title %}{% trans "Organization profile" %}{% endblock header_title %}
{% block content %}
<form action="" method="post">{% csrf_token %}
{% crispy form %}
<input type="submit" value="{% trans 'Generate new key' %}" name="_key">
<input type="submit" value="{% trans 'Update form' %}" name="_update">
</form>
{% endblock content %}
Using _key and '_update' I want to differentiate between two buttons and actions update name and regenerate new apikey.
Ohh, I have found the way around. In post request for updating apikey I need to:
update apikey value in model and save it,
redirect back to get request
views.py
class OrgProfileView(UpdateView):
model = Organization
template_name = 'orgs/orgprofile.html'
form_class = OrgProfileForm
url_name = 'profile'
def get_object(self, queryset=None):
org_obj = Organization.objects.get(orgname=self.kwargs["orgname"])
return org_obj
def post(self, request, *args, **kwargs):
if '_key' in request.POST:
org_obj = Organization.objects.get(orgname=self.kwargs["orgname"])
org_obj.apikey = get_apikey()
org_obj.save()
self.object = org_obj
return super(BaseUpdateView, self).get(request, *args, **kwargs)
else:
self.object = self.get_object()
return super(BaseUpdateView, self).post(request, *args, **kwargs)

Django how to validate inlineformset so that a field of each form would add up to 100 percent

I am building a form with subcategory using inlineformset. I use inlineform because I want the subcategory to nest under the category. I want to validate a field (weight) of each subcategory form to add up to 100%. I use the clean() but can't get it to work and the error message doesn't show up on the template. I have done some digging form the past posts but don't see any solutions. I also looked into djangoproject but got nowhere. Please help! What am I missing here?!!
In my forms.py I have:
from django import forms
from django.forms.models import inlineformset_factory
from .models import Category, SubCategory
from django.forms import BaseInlineFormSet
class SubInline(BaseInlineFormSet):
def clean(self):
allocation = 0
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
weight = form.cleaned_data
allocation += int(weight.get('weight'))
if allocation != 100:
raise forms.ValidationError("Weights must sum to 100")
SubCategoryFormSet = inlineformset_factory(Category,
SubCategory,
fields = ['title',
'description',
'weight'],
formset = SubInline,
extra = 0,
can_delete =True)
In my views I have:
class CategorySubCategoryUpdateView(TemplateResponseMixin, View):
template_name = 'manage/subcategory/formset.html'
category = None
def get_formset(self, data = None):
return SubCategoryFormSet(instance = self.category, data=data)
def dispatch(self, request, pk):
self.category = get_object_or_404(Category,
id=pk,
owner=request.user)
return super(CategorySubCategoryUpdateView,
self).dispatch(request)
def get(self, request, *args, **kwargs):
formset = self.get_formset()
return self.render_to_response({'category': self.category,
'formset':formset})
def post(self, request, *args, **kwargs):
formset = self.get_formset(data=request.POST)
if formset.is_valid():
formset.save()
return redirect('categoryportfolios:manage_category_list')
else:
print("100 is needed")
return self.render_to_response({'category':self.category,
'formset': formset})
in my template:
<form class="form-horizontal" action = "" method = "post">
{{formset.management_form}}
{% for form in formset %}
{% for fields in form %}
<div class = "form-group">
{{ fields.errors}}
{{ fields.label_tag }}</br>{{ fields }}
{% csrf_token%}
</div>
{% endfor %}
{% endfor %}
<input type = "submit" class="button" value="save subcategories">
</form>
</div>
</div>
</div>
</div>

Change <input> Types and Attributes for a Form Used in a CreateView and UpdateView

I'm experimenting with a Django 1.4.6 project for tracking sales leads. I want this to be mobile-friendly, so I'm using Twitter Bootstrap (still on version 2.3.2) with django-crispy-forms. According to this article, the best way to do e-mail fields is <input type="email" autocapitalize="off" autocorrect="off"> and the best way to do date fields is <input type="date">. None of these attributes are implemented by default with Django and I'm wondering how best to go about implementing them. Here is the relevant code (simplified):
models.py
from django.db import models
class Lead(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField(blank=True, null=True)
initial_contact_date = models.DateField()
class Meta:
ordering = ('name',)
def __unicode__(self):
return self.name
views.py
from django.core.urlresolvers import reverse
from django.views.generic import CreateView, ListView, UpdateView
from .models import Lead
class LeadAdd(CreateView):
model = Lead
def get_context_data(self, **kwargs):
context = super(LeadAdd, self).get_context_data(**kwargs)
context['title'] = 'Add a Lead'
return context
def get_success_url(self):
return reverse('lead_list')
class LeadEdit(LeadAdd, UpdateView):
def get_context_data(self, **kwargs):
context = super(LeadEdit, self).get_context_data(**kwargs)
context['title'] = 'Edit a Lead'
return context
class LeadList(ListView):
model = Lead
urls.py
from django.conf.urls import patterns, url
from .views import *
urlpatterns = patterns('',
url(r'^$', view=LeadList.as_view(), name='lead_list'),
url(r'^add/$', view=LeadAdd.as_view(), name='lead_add'),
url(r'^edit/(?P<pk>[\d]+)/$', view=LeadEdit.as_view(), name='lead_edit'),
)
lead_form.html
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="page-header">
<h1>{{ title }}</h1>
</div>
<form action="." method="post" class="form-horizontal">
{% csrf_token %}
{{ form|crispy}}
<div class="form-actions">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
{% endblock content %}
Use a field template to do this (see the docs on Field):
Field('email', template="custom-email.html")
OR create a custom widget. There is an abstract class you can use, but one of the existing predefined widgets should work:
# widgets.py
from django.forms.widgets import TextInput
class EmailInput(TextInput):
input_type = 'email'
So it might look like this in your view:
class LeadAdd(CreateView):
model = Lead
form_class = LeadAddForm
...
And then that LeadAddForm class would have your custom widget defined:
from . import widgets
LeadAddForm(forms.Form):
email = forms.CharField(
...
widget = widgets.EmailInput,
...
)
Or you can set the widget in the init:
class LeadAddForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LeadAddForm, self).__init__(*args, **kwargs)
self.fields['email'].widget = widgets.EmailInput()
You should be able to set the extra attributes (autocapitalize="off" autocorrect="off") using the crispy form config:
Field('email', autocapitalize="off", autocorrect="off")
The simpler option:
forms.py:
class LeadForm(forms.ModelForm):
class Meta:
model = Lead
def __init__(self, *args, **kwargs):
super(LeadForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form-horizontal'
self.helper.layout = Layout(
Fieldset('Enter the following information',
'name',
Field('email', type="email",
autocapitalize="off",
autocorrect="off"),
Field('initial_contact_date', type="date")),
FormActions(
Submit('save', 'Create Lead'),
Button('cancel', 'Cancel')
)
)
In your views.py:
class LeadAdd(CreateView):
model = Lead
success_url = reverse('lead_list')
form_class = LeadForm
def get_context_data(self, **kwargs):
context = super(LeadAdd, self).get_context_data(**kwargs)
context['title'] = 'Add a Lead'
return context
class LeadEdit(LeadAdd, UpdateView):
def get_context_data(self, **kwargs):
context = super(LeadEdit, self).get_context_data(**kwargs)
context['title'] = 'Edit a Lead'
return context
In your template:
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="page-header">
<h1>{{ title }}</h1>
</div>
{% crispy form %}
{% endblock content %}