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')
Related
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>
views.py :
#login_required(login_url='/account/login/')
def TaskCreateView(request,pk,todo_id):
completed={}
if not request.user.is_authenticated:
return redirect('accounts:index')
elif User.objects.filter(pk=request.user.pk,mentor__isnull=True).exists():
instance = get_object_or_404(Level, pk=pk)
messages.warning(request, 'You have not added a trainer yet')
print("TRAINER ILADA")
return HttpResponseRedirect(instance.get_absolute_url())
else:
instance = get_object_or_404(Level, pk=pk)
qs = instance.todo_set.get(id=todo_id)
#user = Task.objects.filter(student=request.user)
todo = Task.objects.filter(todo=qs, student=request.user)
if todo.exists():
messages.warning(request,"ALready completed")
return HttpResponseRedirect(instance.get_absolute_url())
form = StudentTaskForm(request.POST or None, request.FILES or None)
if form.is_valid():
form.instance.user =
User.objects.get(id=request.user.id)
obj = form.save(commit=False)
obj.student = request.user
obj.todo = qs
obj.level = instance
obj.save()
ImageFormSet = modelformset_factory(Images,form=ImageForm,min_num=0,max_num=3,validate_min=True,extra=3)
if request.method == 'POST':
formset = ImageFormSet(request.POST, request.FILES,
queryset=Images.objects.none())
if formset.is_valid():
for form in formset.cleaned_data:
try:
image = form['image']
Images.objects.create(post=todo[0],image=image)
except KeyError:
pass
return redirect('student:dashboard')
else:
formset = ImageFormSet(queryset=Images.objects.none())
notifications = Notification.objects.filter(receiver=request.user)
context={
'form': form,
"qs": qs,
'formset': formset,
'notifications': notifications,
'completed':completed,
'hubnotify': hubnotifications,
'acceptnotify': acceptnotify,
'follownotify': follownotify,
}
return render(request,'task_form.html',context)
inside tempate:
{% for key,value in completed.items }
{% if {{value}} == True %}
<script type="text/javascript">
$('[data-click="swal-taskcompleted"]').click(function (e) {
e.preventDefault(), swal({
title: "Already Completed",
text: "You have already completed this task!",
icon: "error",
buttons: {
cancel: {
text: "Cancel",
value: null,
visible: !0,
className: "btn btn-default",
closeModal: !0
},
}
})
});
</script>
{% endif %}
{% endfor %}
{% for obj in task.todo_set.all %}
<div class="card">
<div class="card-header" id="headingOne">
<h5 class="mb-0">
<button style="width: 100%;" class="btn" data-toggle="collapse" data-target="#{{ obj.id }}" aria-expanded="true">
<span class="mytaskbutton"><i class="fas fa-check"></i></span></i> {{ obj.name }}
</button>
</h5>
</div>
<div id="{{ obj.id }}" class="collapse" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
{{ obj.description }}
<div>
<a data-click="swal-taskcompleted" href="{% url 'student:task-form' task.id obj.id %}" style="width: 100%;" type="button" class="btn btn-primary">Proceed</a>
</div>
</div>
</div>
</div>
{% endfor %}
I want show a popup message if an todo object already exists. What do I pass to the template to load script based on this condition?
if todo.exists(): I want to show the popup instead of messages using django messages ,else go the task_form which enables user to submit the form. todo is a task object. Task objects gets
saved only if the form is valid.
Please refer this link for flash message setup in django.
Note: You have to pass success, warning and info according to the your requiremnent
In my formset I would like to check each reading against a target if the reading is larger than the target do not save to db. For some reason I can't get this to work correctly because it still allows me to save. Any help would be greatly appreciated!
all in views.py
#custom formset validation
def get_dim_target(target):
dim = Dimension.objects.values_list('target', flat=True).filter(id=target)
return dim
#custom formset validation
class BaseInspecitonFormSet(BaseFormSet):
def insp_clean(self):
if any(self.errors):
return
reading = []
for form in self.forms:
dim_id = form.cleanded_data['dimension_id']
reading = form.cleaned_data['reading']
target = get_dim_target(dim_id)
if reading > target:
raise forms.ValidationError("Reading larger than target")
reading.append(reading)
#formset
def update_inspection_vals(request, dim_id=None):
dims_data = Dimension.objects.filter(id=dim_id)
can_delete = False
dims = Dimension.objects.get(pk=dim_id)
sheet_data = Sheet.objects.get(pk=dims.sheet_id)
serial_sample_number = Inspection_vals.objects.filter(dimension_id=24).values_list('serial_number', flat=True)[0]
target = Dimension.objects.filter(id=24).values_list('target', flat=True)[0]
title_head = 'Inspect-%s' % dims.description
if dims.ref_dim_id == 1:
inspection_inline_formset = inlineformset_factory(Dimension, Inspection_vals, can_delete=False, extra=0, fields=('reading',), widgets={
'reading': forms.TextInput(attrs={'cols': 80, 'rows': 20})
})
if request.method == "POST":
formset = inspection_inline_formset(request.POST, request.FILES, instance=dims)
if formset.is_valid():
new_instance = formset.save(commit=False)
for n_i in new_instance:
n_i.created_at = datetime.datetime.now()
n_i.updated_at = datetime.datetime.now()
n_i.save()
else:
form_errors = formset.errors
formset.non_form_errors()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
else:
formset = inspection_inline_formset(instance=dims, queryset=Inspection_vals.objects.filter(dimension_id=dim_id).order_by('serial_number'))
return render(request, 'app/inspection_vals.html',
{
'formset': formset,
'dim_data': dims_data,
'title':title_head,
'dim_description': dims.description,
'dim_target': dims.target,
'work_order': sheet_data.work_order,
'customer_name': sheet_data.customer_name,
'serial_sample_number': serial_sample_number,
})
inspection_val.html
<h1>Inspeciton Values</h1>
<div class="well">
<form method="post">
{% csrf_token %}
<table>
{{ formset.management_form }}
{% for x in formset.forms %}
<tr>
<td>
Sample Number {{ forloop.counter0|add:serial_sample_number }}
</td>
<td>
{{ x }}
{{ x.errors }}
</td>
</tr>
{% endfor %}
</table>
<input type="submit" value="Submit Values" class="btn-primary" />
</form>
</div>
</div>
The Django docs on custom formset validation show that you should create a clean method.
You have named your method insp_clean, so Django will never call it. Rename the method to clean.
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>