I'm new to django and I'm trying to write an application that given an activity allows me to save the information of the partecipants (i.e. Person) and how long the partecipation was.
I was able to setup a CreateView, with bootstrap and JavaScript to allow my formset to be dynamic and I'm able to register the activity and the partecipants though the create view.
However, when I try to modify an already existing activity, using an UpdateView, I get several errors.
I tried to reproduce a minimal example of my problem and I got the error "This field is required" that is one of the error I obtain in my main application. I'm trying also to reproduce the second error: the instruction formset.is_valid() in views.py return False since it has an Activity already linked to the Person (violating the unique_together rule). This happens everytime: even if I'm just trying to add a second person to the form without changing anything about the first one.
Please, let me know if I missed some files: here's the small example.
models.py
from django.db import models
# Create your models here.
class Person(models.Model):
surname = models.CharField(max_length=30)
class Activity(models.Model):
name = models.CharField(max_length=30)
person = models.ManyToManyField(Person, through='ActivityThrough')
def get_absolute_url(self):
return reverse('activity-update', kwargs={'pk': self.pk})
class ActivityThrough(models.Model):
activity = models.ForeignKey(to='Activity', on_delete=models.PROTECT)
person = models.ForeignKey(to='Person', on_delete=models.PROTECT)
hours = models.DecimalField(max_digits=5, decimal_places=2)
class Meta:
unique_together = [['person', 'activity']]
views.py
from django.contrib import messages
from django.shortcuts import render
from .models import Activity, ActivityThrough
from .forms import activity_through_formset, CreateActivityForm
from django.views.generic.edit import UpdateView
from django.http import HttpResponseRedirect
# Create your views here.
class UpdateActivity(UpdateView):
model = Activity
form_class = CreateActivityForm
template_name = 'minimal/activities.html'
def get_context_data(self, **kwargs):
context = super(UpdateActivity, self).get_context_data(**kwargs)
if self.request.POST:
context['formset'] = activity_through_formset(self.request.POST, instance=context['activity'])
else:
context['formset'] = activity_through_formset(instance=context['activity'])
context['empty_formset'] = activity_through_formset().empty_form
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form(self.form_class)
qs = ActivityThrough.objects.filter(activity=self.object)
formset = activity_through_formset(self.request.POST, queryset=qs)
if (form.is_valid() and formset.is_valid()):
return self.form_valid(form, formset)
else:
for f in formset:
print(f.errors)
return self.form_invalid(form, formset)
def form_valid(self, form, formset):
self.object = form.save()
formset.instance = self.object
formset.save()
messages.add_message(self.request, messages.SUCCESS, 'Update was successful')
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, formset):
messages.add_message(self.request, messages.ERROR, 'Updat cannot be performed')
return self.render_to_response(self.get_context_data(form=form, formset=formset))
In my urls.py
path('activity/<int:pk>/', views.UpdateActivity.as_view(), name='activity-update'),
forms.py
from django import forms
from .models import Person, Activity, ActivityThrough
class CreateActivityForm(forms.ModelForm):
class Meta:
model = Activity
fields = ['name']
name = forms.CharField(required=True, label='Name')
class ActivityThroughForm(forms.ModelForm):
class Meta:
model = ActivityThrough
fields = '__all__'
person = forms.ModelChoiceField(
queryset=Person.objects.all(),
required=True,
label='Person'
)
hours = forms.DecimalField(label='Hours', required=True)
activity_through_formset = forms.inlineformset_factory(
Activity,
ActivityThrough,
fields=['person', 'hours'],
form=ActivityThroughForm,
extra=1,
can_delete=False
)
And the template activities.html
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<div style="padding: 0 0 1rem">
<!-- field label -->
<b>People:</b>
*
<!-- end field label -->
<br>
<!-- field rendering -->
<div id="formset-template-{{ formset.prefix }}" style="display:none;" generic_id="NNNNN">
<div class="form-container">
<div class="row form-row spacer">
<div class="col">
<div class="input-group">
<div class="table-responsive">
<table class="table table-bordered formset-table">
{% for hidden in empty_formset.hidden_fields %}
{{ hidden }}
{% endfor %}
<tr>
{% for field in empty_formset.visible_fields %}
<th>{{ field.label_tag }}</th>
{% endfor %}
</tr>
<tr>
{% for field in empty_formset.visible_fields %}
<td>{{ field }}</td>
{% endfor %}
</tr>
</table>
</table>
</div>
</div>
<button class="btn btn-danger btn-xs remove-form-row mb-4" id="remove-{{ formset.prefix }}-NNNNN">
Remove row
</button>
</div>
</div>
</div>
</div>
{{ formset.management_form }}
{% for fs in formset %}
<div class="form-container">
<div class="row form-row spacer">
<div class="col">
<div class="input-group">
<div class="table-responsive">
<table class="table table-bordered formset-table">
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
<tr>
{% for field in fs.visible_fields %}
<th>{{ field.label_tag }}</th>
{% endfor %}
</tr>
<tr>
{% for field in fs.visible_fields %}
<td>{{ field }}</td>
{% endfor %}
</tr>
</table>
</div>
</div>
{% if fs.empty %}
<button class="btn btn-danger btn-xs remove-form-row mb-4" id="remove-{{ fs.prefix }}">
Remove row
</button>
{% endif %}
</div>
</div>
</div>
{% endfor %}
<div class="input-group-append">
<button id="add-form-{{ formset.prefix }}" class="btn btn-xs float-left btn-success add-form-row">
Add row
</button>
</div>
</div>
<div class="submit-row">
<input type="submit" class='btn btn-primary' value="Update">
</div>
<script>
/**
* Create a new form using a template
* #param {HTML} formset_template: the default HTML schema to fill
* #param {String} prefix: the Django formset prefix
* #param {div} position:
* #param {String} generic_id:
* #return false
*/
function clone(formset_template, prefix, position, generic_id) {
// get total forms number in formset
total = $('#id_' + prefix + '-TOTAL_FORMS').val();
// build the Regular Expression to replace generic elements
var prefix_regex = new RegExp('('+prefix+'-'+generic_id+')', 'g')
// generate new HTML block with "prefix+total indexed" form
newElement = $(formset_template).html().replace(prefix_regex,
prefix+'-'+total);
// place the new form before the "add new" button
$(position).before(newElement);
// increase total forms number in formset
total++;
$('#id_' + prefix + '-TOTAL_FORMS').val(total);
// return false
return false;
}
/**
* Update the ID indexes of a form input fields
* #param {element} parent_div: the element that contains the form
* #param {String} prefix: the Django formset prefix
* #return false
*/
function reindex_forms(parent_div, prefix){
// build a Regular Expression to find all "form-INDEX" elements
var id_regex = new RegExp('(' + prefix + ')-(\\d+)','g');
// get the inner HTML of parent_div
var parent_html = parent_div.html()
// find the form in the HTML code
var target = parent_html.match(id_regex);
// get the INDEX value (es: form-1 -> 1)
var splitted = target[0].split('-');
var index = splitted[splitted.length-1];
// decrease the index (es: form-1 -> form-0)
var new_html = parent_html.replace(id_regex,
prefix+'-'+(parseInt(index)-1));
// update the parent_div HTML code
parent_div.html(new_html);
// return false
return false;
}
/**
* Delete the form and update the ID indexes of successive forms
* #param {element} to_remove: the element that contains the "remove" button and the form
* #param {String} prefix: the Django formset prefix
* #param {String} container_class_div: the class of div that contains the form
*/
function deleteForm(to_remove, prefix, container_class_div) {
// calculate the number of total forms in formset
total = $('#id_' + prefix + '-TOTAL_FORMS').val();
// retrieve all the forms following the one I'm deleting
var successive_forms = to_remove.nextAll(container_class_div);
// reindex each of them
successive_forms.each(function(){
reindex_forms($(this), prefix);
});
// remove the form
to_remove.remove();
// decrease total number
total--;
$('#id_' + prefix + '-TOTAL_FORMS').val(total);
}
// "add new form" event using the generic form template
$(document).on('click', '.add-form-row', function(e){
// If this method is called, the default action of the event will not be triggered.
e.preventDefault();
// get formset prefix
var formset_prefix = $(this).attr('id').split('-')[2];
// get formset template
var formset_template = $('#formset-template-'+formset_prefix);
// get generic_id attribute
var generic_id = formset_template.attr('generic_id')
// get the position of "add new" button
var position = $(this).parent();
// clone the template and create a new form in formset
clone(formset_template, formset_prefix, position, generic_id);
// return false
return false;
});
// "remove form" event
$(document).on('click', '.remove-form-row', function(e){
// If this method is called, the default action of the event will not be triggered.
e.preventDefault();
// get the CSS class of element that contains form and button
var container_class = '.form-container';
// get formset prefix
var prefix = $(this).attr('id').split('-')[1];
// get the element to remove (contains the form and the "remove" button)
var to_remove = $(this).closest(container_class);
// call deleteForm method
deleteForm(to_remove, prefix, container_class);
// return false
return false;
});
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
Related
I am trying to have one form and one modelin one view which is working. But when I am trying to save the form I am getting the error message: Generic detail view Index must be called with either an object pk or a slug in the URLconf.
Here are my views nad urls:
class Index(generic.CreateView):
template_name='home.html'
form_class=DistributionForm
models=Lecturer
queryset = Lecturer.objects.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context ['lecturer_list'] = Lecturer.objects.order_by('lecturer')
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
urls.py
from django.urls import path
from . import views
from django.conf.urls.static import static
from django.conf import settings
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
app_name='distribution'
urlpatterns=[
path('',views.Index.as_view(),name='home'),
path('hocalar/<slug:slug>/',views.LecturerDistribution.as_view(),name='lecturer_distribution'),
path('dersler/<slug:slug>/',views.LectureDistribution.as_view(),name='lecture_distribution'),
]
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
EDIT
Here are my other two views
class LecturerDistribution(generic.DetailView):
model=Lecturer
template_name='lecturer.html'
def get_success_url(self):
return reverse('lecturer_distribution', kwargs={'slug': self.object.slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context ['distribution'] = Distribution.objects.filter(lecturer=self.object).order_by('-created_on')
return context
class LectureDistribution(generic.DetailView):
model=Lecture
template_name='lecture.html'
def get_success_url(self):
return reverse('lecture_distribution', kwargs={'slug': self.object.slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context ['distribution_'] = Distribution.objects.filter(lecture=self.object).order_by('-created_on')
return context
And here my Template
{% extends "base.html" %}
{%block content%}
{% load crispy_forms_tags %}
<div class="container">
<div class="form-group pull-right">
<input type="text" class="search form-control" placeholder="Ara">
</div>
<span class="counter pull-right"></span>
<table class="table table-hover results">
<thead>
<tr>
<th >Hoca</th>
<th >Ders</th>
</tr>
<tr class="warning no-result">
<td><i class="fa fa-warning"></i> Sonuç Yok</td>
</tr>
</thead>
<tbody>
{%for lec in lecturer_list%}
<tr>
<td>
<p ><a style="text-decoration:none" href="{% url 'distribution:lecturer_distribution' slug=lec.slug%}">{{lec.lecturer}}</a></p>
</td>
<td>
{%for ders in lec.lecture.all%}
<a style="text-decoration:none" href="{% url 'distribution:lecture_distribution' slug=ders.slug%}">{{ders.lecture}}</a>,
{% endfor%}
</td>
</tr>
{%endfor%}
</tbody>
</table>
</div>
<div class="col-md-2 float-right ">
<button style= "position: fixed; top:175px; " type="button" class="btn btn-primary" data-toggle="modal" data-target="#exampleModal" data-whatever="#mdo">Add New Distribution</button>
</div>
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">New Distribution</h5>
</div>
<div class="modal-body">
<form method="post" style="margin-top: 1.3em;">
{% csrf_token %}
{{ form|crispy }}
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="submit" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
</div>
<style >
body{
padding:20px 20px;
}
.results tr[visible='false'],
.no-result{
display:none;
}
.results tr[visible='true']{
display:table-row;
}
.counter{
padding:8px;
color:#ccc;
}
</style>
<script>
$(document).ready(function() {
$(".search").keyup(function () {
var searchTerm = $(".search").val();
var listItem = $('.results tbody').children('tr');
var searchSplit = searchTerm.replace(/ /g, "'):containsi('")
$.extend($.expr[':'], {'containsi': function(elem, i, match, array){
return (elem.textContent || elem.innerText || '').toLowerCase().indexOf((match[3] || "").toLowerCase()) >= 0;
}
});
$(".results tbody tr").not(":containsi('" + searchSplit + "')").each(function(e){
$(this).attr('visible','false');
});
$(".results tbody tr:containsi('" + searchSplit + "')").each(function(e){
$(this).attr('visible','true');
});
var jobCount = $('.results tbody tr[visible="true"]').length;
if(jobCount == '0') {$('.no-result').show();}
else {$('.no-result').hide();}
});
});
</script>
{% endblock content%}
Thank You in Advance
Try adding a success_url to your CreateView to redirect you to the homepage
from django.core.urlresolvers import reverse_lazy
class Index(generic.CreateView):
template_name='home.html'
form_class=DistributionForm
models=Lecturer
queryset = Lecturer.objects.all()
success_url = reverse_lazy('home')
Otherwise it will redirect you to the model's get_absolute_url path
I think i found your problem, because you are using the get_object() in your post function in your index view.
see code:
def get_object(self, queryset=None):
"""
Return the object the view is displaying.
Require `self.queryset` and a `pk` or `slug` argument in the URLconf.
Subclasses can override this to return any object.
"""
# Use a custom queryset if provided; this is required for subclasses
# like DateDetailView
if queryset is None:
queryset = self.get_queryset()
# Next, try looking up by primary key.
pk = self.kwargs.get(self.pk_url_kwarg)
slug = self.kwargs.get(self.slug_url_kwarg)
if pk is not None:
queryset = queryset.filter(pk=pk)
# Next, try looking up by slug.
if slug is not None and (pk is None or self.query_pk_and_slug):
slug_field = self.get_slug_field()
queryset = queryset.filter(**{slug_field: slug})
# If none of those are defined, it's an error.
if pk is None and slug is None:
raise AttributeError(
"Generic detail view %s must be called with either an object "
"pk or a slug in the URLconf." % self.__class__.__name__
)
try:
# Get the single item from the filtered queryset
obj = queryset.get()
except queryset.model.DoesNotExist:
raise Http404(_("No %(verbose_name)s found matching the query") %
This is the code for get_object(), because you don't have a pk and no slug available. It returns the generic detail view error.
raise AttributeError(
"Generic detail view %s must be called with either an object "
"pk or a slug in the URLconf." % self.__class__.__name__
)
You can delete the def post in your index view because the form_valid() that your are using is already in the create view. You don't need it.
I'm trying my django application through different browsers (Chrome, Firefox, IE11 and Edge) and I got an issue with the csrf_token and Edge only.
This issue is in reference with my django form.
My view file :
class ManageDocView(AdminRequiredMixin, View):
""" Render the Admin Manage documents to update year in the filename"""
template_name = 'omcl/manage_doc_form.html'
form_class = ManageDocForm
success_url = 'omcl/manage_doc_form.html'
#staticmethod
def get_title():
return 'Change Document Title'
def get(self, request):
form = self.form_class()
context = {
"form": form,
"title": self.get_title()
}
return render(request, self.template_name, context)
def post(self, request):
form = self.form_class()
query_document_updated = None
query_omcl = None
query_document = None
if "submitButton" in request.POST:
omcl_list = request.POST.get('omcl_list', False)
query_omcl = Omcl.objects.get(id=omcl_list)
query_document = Document.objects.filter(omcl=omcl_list)
form.fields['omcl_list'].initial = query_omcl
elif "UpdateDocument" in request.POST:
checkbox_id = request.POST['DocumentChoice']
checkbox_id_minus_1 = int(checkbox_id) - 1
query_document_updated = Document.objects.get(id=checkbox_id)
omclcode = query_document_updated.omcl.code
src_filename = query_document_updated.src_filename
filename, file_extension = os.path.splitext(src_filename)
category = query_document_updated.category
if category == "ANNUAL":
category = "ANNUAL_REPORT"
year = self.request.POST['pts_years']
# Create the new document title updated by the new year
new_document_title = f"{year}_{category}_{omclcode}_{checkbox_id_minus_1} - {src_filename}"
# Create the new document file updated by the new year
new_document_file = f"omcl_docs/{omclcode}/{year}_{category}_{omclcode}_{checkbox_id_minus_1}{file_extension}"
# Get file.name in order to rename document file in /media/
document_path = query_document_updated.file.name
try:
actual_document_path = os.path.join(settings.MEDIA_ROOT, document_path)
new_document_path_temp = f"{settings.MEDIA_ROOT}/{new_document_file}"
new_document_path = os.rename(actual_document_path, new_document_path_temp)
except FileNotFoundError:
messages.error(self.request, _(f"Document {src_filename} doesn't exist on the server"))
return redirect('manage_doc')
else:
# Assign modifications to selected document and save it into the database
query_document_updated.title = new_document_title
query_document_updated.file = new_document_file
query_document_updated.save()
messages.success(self.request, _(f"The modification has been taken into account"))
context = {
'form': form,
'query_omcl': query_omcl,
'query_document': query_document,
'query_document_updated': query_document_updated,
'title': self.get_title(),
}
return render(request, self.template_name, context)
My forms file :
class ManageDocForm(forms.Form):
def __init__(self, *args, **kwargs):
super(ManageDocForm, self).__init__(*args, **kwargs)
omcl_list = forms.ModelChoiceField(
queryset=Omcl.objects.filter(is_obsolete=False),
label=_('OMCL Choice'),
widget=ModelSelect2Widget(
model=Omcl,
search_fields=['code__icontains', 'name__icontains'],
attrs={'data-placeholder': "Please select an OMCL"}
)
)
now = datetime.today().year
year_choices = ((i, str(i)) for i in range(now, now - 30, -1))
pts_years = forms.ChoiceField(
label='PTS years',
choices=year_choices,
required=True,
widget=Select2Widget(
attrs={'data-placeholder': "Please select a new year"}),
)
My template file with a little part of javascript :
{% block extra_script %}
<!-- Submit OMCL list with change and not submit button + Previous/Next pagination button -->
<script>
$('#select-omcl-form').on('change', function () {
$(this).submit();
});
</script>
{% endblock %}
{% block main %}
<h2>{{ title }}</h2>
<div class="row manage-doc">
<div class="col-md-12">
<form id="select-omcl-form" name="select-omcl-form" action="" method="POST">
{% csrf_token %}
<fieldset>
<legend><span class="name">{% trans 'Select an OMCL' %}</span></legend>
{{ form.omcl_list }}
<input type="hidden" name="submitButton">
</fieldset>
</form>
</div>
</div>
<br/>
<div class="row manage-doc">
<div class="col-md-12">
<fieldset>
<legend><span class="name">{% trans 'Select a document' %}</span></legend>
<form action="" method="POST">
{% csrf_token %}
<div id="table-document">
<table id="document-table" class="table table-bordered table-striped table-condensed table_model">
<thead>
<tr>
<th id="radio-column"></th>
<th id="document-title-column">{% trans 'Document title' %}</th>
</tr>
</thead>
<tbody>
{% for document in query_document|dictsortreversed:'title' %}
<tr>
<td><input type="radio" class="radio-document" id="document-radiobox" name="DocumentChoice"
value="{{ document.id }}"></td>
<td>{{ document.title }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<br><br>
<legend><span class="name">{% trans 'Select a new year' %}</span></legend>
{{ form.pts_years }}
<button class="btn btn-default" id="document-button" type="submit"
name="UpdateDocument">{% trans "Change year" %}</button>
</form>
</fieldset>
<br>
</div>
</div>
{% endblock main %}
A gif presentation :
This is a little gif which explains the process and the issue according to csrf_token only with Edge browser :
Link to my gif
What I tried :
I tried to add CSRF_COOKIE_DOMAIN in my settings.py file but it doesn't work.
Do you have any idea ? It's pretty weird because I don't have any issue with others browsers. Ony with Microsoft Edge.
Cookies are allowed in my browser.
I found the issue thanks to my collegue. I used redirect but I had to use render because if I redirect, the CSRF_TOKEN is not actualized and it sends a second POST request with the previous token.
So it should be :
except FileNotFoundError:
messages.error(self.request, _(f"Document {src_filename} doesn't exist on the server"))
context = {
'form': form,
'query_omcl': query_omcl,
'query_document': query_document,
'query_document_updated': query_document_updated,
'title': self.get_title(),
}
return render(request, self.template_name, context)
Instead of :
except FileNotFoundError:
messages.error(self.request, _(f"Document {src_filename} doesn't exist on the server"))
return redirect('manage_doc')
What is the crispiest way to add a new, empty form to a modelformset with existing forms?
forms.py:
class MyForm(ModelForm):
class Meta:
model = MyModel
fields = '__all__'
MyFormSet = modelformset_factory(MyModel, extra=1, exclude=(), form=MyForm)
class MyFormsetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(MyFormsetHelper, self).__init__(*args, **kwargs)
self.form_method = 'post'
self.template = 'bootstrap/table_inline_formset.html'
self.add_input(Submit("submit", "Save"))
self.add_input(Button("add", "Add New"))
template:
{% extends "base.html" %}
{% load staticfiles %}
{% load crispy_forms_tags %}
{% block stylesheets %}
{{ block.super }}
{% endblock stylesheets %}
{% block content %}
{% crispy formset helper %}
{% endblock content %}
I have an add button, but I don't see how to tie that to an action. I also don't know how crispy forms would create an empty form. Prior to discovering crispy forms, I was writing out all the template code and used something like the below to render the empty form.
<div id="empty_form" style="display:none">
<table class='no_error'>
<tr>
<td>{{ modelformset.empty_form.Field1 }}</td>
<td>{{ modelformset.empty_form.Field2 }}</td>
<td>{{ modelformset.empty_form.Field3 }}</td>
</tr>
</table>
</div>
<input type="button" value="Add" id="add_more">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$('#add_more').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
$('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
</script>
If there's no crispy way to do this, is there a way to combine the code above with the crispy formset? I tried, but nothing happens.
Thank you for any insight you all might have.
I use this code and it works fine...
button
<input type="button" value="Add" id="add_more" onclick="add_form('empty_form')">
javascript
<script>
function add_form(formset) {
let total = $("#id_" + formset + "-TOTAL_FORMS").val();
let row = $("#" + formset + "_table tr:last")[0]; // find row to copy
let table = $("#" + formset + "_table tbody")[0]; // find table to append to
let clone = row.cloneNode(true); // copy children too
clone.innerHTML = clone.innerHTML.replaceAll("-" + (total - 1) + "-", "-" + total + "-");
table.appendChild(clone); // add new row to end of table
$("#id_" + formset + "-TOTAL_FORMS").val(++total);
}
</script>
I've made some attempted to use formwizard but there wasn't much documentation about it so I decided to stay with the basic. I've successfully obtained and display the data from the first form to the second form and added some checkbox next to the data to allow user to choose whether to overwrite or ignore the duplicate data found in the backend process. The problem I have is the second form doesn't know how retrieve the data of the first form after hitting "Confirm" button. The form2.html template invalidated the data completely since it called itself again by the form action after submitting the data. Is there a way to solve this or a better approach to this?
forms.py
class NameForm (forms.Form):
first_name = forms.CharField (required = False)
last_name = forms.CharField (required = False)
class CheckBox (forms.Form):
overwrite = forms.BooleanField (required = False)
views.py
def form1 (request):
NameFormSet = formset_factory (NameForm, formset = BaseNodeFormSet, extra = 2, max_num = 5)
if request.method == 'POST':
name_formset = NameFormSet (request.POST, prefix = 'nameform')
if name_formset.is_valid ():
data = name_formset.cleaned_data
request.session ['data'] = data
context = {'data': data}
return render (request, 'nameform/form2.html', context)
else:
name_formset = NameFormSet (prefix = 'nameform')
context = {......}
return render (request, 'nameform/form1.html', context)
def form2 (request):
request.session ['data'] = data
CheckBoxFormSet = formset_factory (CheckBox, extra = 2, max_num = 5)
if request.method == 'POST':
checkbox_formset = CheckBoxFormSet (request.POST, prefix = 'checkbox')
if checkbox_formset.is_valid ():
data = checkbox_formset.cleaned_data
context = {'data': data}
return render (request, 'nameform/success.html', context)
else:
checkbox_formset = CheckBoxFormSet (prefix = 'checkbox')
return HttpResponse ('No overwrite data.')
form2.html
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
{% load staticfiles %}
<link rel="stylesheet" type="text/css" href="{% static 'nodeform/style.css' %}" >
<title>User Information</title>
</head>
<body>
<h1>User Information:</h1>
<form action="form2" method="POST">
{{ checkbox_formset.management_form }}
<div id="tablefont">
<table id="table01">
<tr>
<th>First Name</th>
<th>Last Name</th>
<th class="center">Overwrite</th>
</tr>
{% for info in data %}
<tr>
<td>{{ info.first_name }}</td>
<td>{{ info.last_name }}</td>
<td class="center"><input id="id_checkbox-{{ forloop.counter0 }}-overwrite" name="checkbox-{{ forloop.counter0 }}-overwrite" type="checkbox" /></td>
</tr>
{% endfor %}
</table>
</div>
<br>
<p><input type="submit" value="Confirm">
<a href="{% url 'form1' %}">
<button type="button">Cancel</button></a></p>
</form>
</body>
</html>
Personally I find that class based views always make life simpler, and in this case specifically there is the Form Wizard views which use the session or a cookie. Check out the docs; https://django-formtools.readthedocs.io/en/latest/wizard.html
I've just finished writing something very similar to this;
from django.contrib.formtools.wizard.views import SessionWizardView
class SignupWizard(SessionWizardView):
template_name = 'wizard_form.html'
form_list = [Form1, Form2, Form3]
model = MyModel
def get_form_initial(self, step, **kwargs):
initial = self.initial_dict.get(step, {})
current_step = self.storage.current_step
if step or current_step in ['0', 0]:
initial.update({'field': 'data'})
elif step or current_step in ['1', 1]:
initial.update({'field': 'data'})
return initial
def get_context_data(self, form, **kwargs):
"""
Supply extra content data for each stage.
e.g. stage page title
"""
previous_data = None
if self.steps.current == self.steps.last:
previous_data = self.get_all_cleaned_data()
context = super(SignupWizard, self).get_context_data(form, **kwargs)
context.update(
{
'review_data': previous_data,
}
)
return context
def done(self, form_list, **kwargs):
"""
Final page, this is rendered after the post method to the final form.
"""
form_data_dict = self.get_all_cleaned_data()
mymodelform = form_list[0].save()
return HttpResponseRedirect()
Essentially there my first form is a ModelForm for MyModel and I use other standard forms to collect other information. So at the end, grab the first form from the list and save that to create the MyModel instance.
Equally as you can see you can grab self.get_all_cleaned_data() to get all of the data entered in to the forms through to whole process to handle the data yourself.
The template for something this would be along the lines of;
{% extends 'base_wizard.html' %}
{% load i18n %}
{% block form %}
{% for error in wizard.form.non_field_errors %}
{{ error }}
{% endfor %}
{{ wizard.form.media }}
{{ wizard.management_form }}
{% wizard.form.as_p %}
<div class="controls">
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">
{% trans "Previous step" %}
</button>
{% endif %}
<button type="submit" name="submit">
{% trans "Next step" %}
</button>
</div>
{% endblock form %}
I have 2 models, Father and Son.
I have a page to register Father. On the same page I have a formset to register Son.
On page has a button "more" to add another Father and their respective Son on the same page.
Does anyone have any examples using CreateView?
Class based views are still new, so I'll write this out. The process is simple:
First, create the forms for your objects. One of the forms will be repeated. Nothing special to be done here.
class SonInline(ModelForm):
model = Son
class FatherForm(ModelForm):
model = Father
Then, create your formset:
FatherInlineFormSet = inlineformset_factory(Father,
Son,
form=SonInline,
extra=1,
can_delete=False,
can_order=False
)
Now, to integrate it with your CreateView:
class CreateFatherView(CreateView):
template_name = 'father_create.html'
model = Father
form_class = FatherForm # the parent object's form
# On successful form submission
def get_success_url(self):
return reverse('father-created')
# Validate forms
def form_valid(self, form):
ctx = self.get_context_data()
inlines = ctx['inlines']
if inlines.is_valid() and form.is_valid():
self.object = form.save() # saves Father and Children
return redirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
# We populate the context with the forms. Here I'm sending
# the inline forms in `inlines`
def get_context_data(self, **kwargs):
ctx = super(CreateFatherView, self).get_context_data(**kwargs)
if self.request.POST:
ctx['form'] = FatherForm(self.request.POST)
ctx['inlines'] = FatherInlineFormSet(self.request.POST)
else:
ctx['form'] = Father()
ctx['inlines'] = FatherInlineFormSet()
return ctx
Finally, here is the template:
The key part is the jquery django-dynamic-formset plugin that keeps adding new inline forms:
<form id="father-form" method="POST" enctype="multipart/form-data" action=".">
{% csrf_token %}
<div class="row">
{% for f in form %}
<div class="span3">{{ f.label }}<br />{{ f }}
{% if f.errors %}
{% for v in f.errors %}
<br /><span style="color:red;">{{ v }}</span>
{% endfor %}
{% endif %}
</div>
{% endfor %}
</div>
<hr />
<h2>Sons:</h2>
<table class="table-striped">
<table>
{% for f2 in inlines %}
<tr id="{{ f2.prefix }}-row">
{% for i in f2 %}
<td>
{{ i }}{% if i.errors %}<span style="color:red;">{{ i.errors }}</span>{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{{ inlines.management_form }}
<input type="submit" class="btn btn-primary" value="Go Go Gadget →">
</form>
<script type="text/javascript">
$(function() {
$('#father-form tr').formset({
prefix: '{{ inlines.prefix }}'
});
})
</script>