While submitting data using dynamically generated forms, sometimes getting error CSRF verification failed. Request aborted. And also suppose we generated 3 forms and after that if we delete second form, then it removes second form and also blanks third form.
index.html:
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="shadow-sm border rounded mt-2 mx-3" style=" min-height: 75vh;">
<div class="d-flex justify-content-between align-items-center m-3">
<h5 class="mx-2 text-slateblue" id="title"></h5>
<div class="col"></div>
<button type="button" class="btn btn-slateblue mx-2" data-bs-toggle="modal" data-bs-target="#TimesheetEntry"
data-bs-whatever="@mdo"><i class="bi bi-plus-circle"></i> Add New Record</button>
<form method="post" id="timesheetForm">
<div class="modal fade" id="TimesheetEntry" tabindex="-1" aria-labelledby="TimesheetEntryLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-fullscreen d-flex justify-content-center">
<div class="modal-content">
<div class="modal-header row">
<h5 class="modal-title text-slateblue col" id="TimesheetEntryLabel">New Timesheet Entry</h5>
<div class="col"></div>
<button class="btn btn-slateblue col-2" type="button" id="addForm"><i class="bi bi-plus-circle"></i>
Add entry</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="max-width: 100%; overflow-x: auto;">
{% csrf_token %}
{{formset.management_form}}
<div class="container-fluid" id="user-forms" style="width: 1800px;">
{% for form in formset %}
<div class="row user-form" id="form-{{ forloop.counter0 }}">
<div class="" style="flex: 0 0 150px;
max-width: 150px;">
{{form.emp_name|as_crispy_field }}
</div>
<div class="" style="flex: 0 0 200px;
max-width: 200px;">
{{form.client_name|as_crispy_field }}
</div>
<div class="" style="flex: 0 0 300px;
max-width: 300px;">
{{form.project_name|as_crispy_field }}
</div>
<div class="" style="flex: 0 0 300px;
max-width: 300px;">
{{form.category|as_crispy_field }}
</div>
<div class="" style="flex: 0 0 300px;
max-width: 300px;">
{{form.description|as_crispy_field }}
</div>
<div class="" style="flex: 0 0 150px;
max-width: 150px;">
{{form.billable|as_crispy_field }}
</div>
<div class="" style="flex: 0 0 170px;
max-width: 170px;">
{{form.date|as_crispy_field }}
</div>
<div class="" style="flex: 0 0 100px;
max-width: 100px;">
{{form.hours|as_crispy_field }}
</div>
<!-- <div class="col"> -->
{{form.approval.as_hidden}}
<!-- </div> -->
<div class="remove-col " style="flex: 0 0 100px;
max-width: 100px;">
{% if forloop.counter0 > 0 %}
<button type="button" class="remove-form btn btn-outline-danger" onclick="removeForm('{{forloop.counter0}}')">Remove</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<p>Total Hours: <span class="fw-bold" id="total_hours"></span> </p>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-outline-slateblue" id="submit">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="table-responsive " style=" height: 75vh;
overflow-y: auto;">
<table id="myTable" class="table table-bordered table-striped border text-center rounded shadow-sm m-0">
<thead class="sticky-top top-0 shadow">
<tr>
<!-- <th scope="col" style="background-color: #4E45AC; color: white;">#</th> -->
<th scope="col" style="background-color: #4E45AC; color: white; min-width: 150px;">Emp. Name</th>
<th scope="col" style="background-color: #4E45AC; color: white; min-width: 200px;">Client Name</th>
<th scope="col" style="background-color: #4E45AC; color: white; white-space: normal; min-width: 200px;">
Project Name</th>
<th scope="col" style="background-color: #4E45AC; color: white; min-width: 300px;">Description</th>
<th scope="col" style="background-color: #4E45AC; color: white; white-space: normal; min-width: 180px;">
Category</th>
<th scope="col" style="background-color: #4E45AC; color: white; white-space: normal; min-width: 100px;">
Billable</th>
<th scope="col" style="background-color: #4E45AC; color: white; white-space: normal; min-width: 150px;">Date
</th>
<th scope="col" style="background-color: #4E45AC; color: white;">Hours</th>
<th scope="col" style="background-color: #4E45AC; color: white; white-space: normal; min-width: 130px;">Month
</th>
<th scope="col" style="background-color: #4E45AC; color: white; white-space: normal; min-width: 150px;">Week
Start Date</th>
<th style="background-color: #4E45AC; color: white;"></th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr>
<!-- <th scope="row">{{forloop.counter}}</th> -->
<td style="text-align: left;">{{entry.emp_name.first_name}}</td>
<td style="text-align: left;">{{entry.client_name}}</td>
<td style="text-align: left;">{{entry.project_name}}</td>
<td id="description">{{entry.description}}</td>
<td id="description">{{entry.category}}</td>
<td style="text-align: left;">{{entry.billable}}</td>
<td class="date" style="text-align: right;">{{entry.date}}</td>
<td>{{entry.hours}}</td>
<td class="month" style="text-align: right;"></td>
<td class="weekStartDate" style="text-align: right;"></td>
{% if entry.approval != 'Approved' %}
<td>
<a href="{% url 'timesheet_update_view' id=entry.id %}?current_path=index"><i class=" bi bi-pencil-square"></i></a>
<a href="#" data-bs-toggle="modal" data-bs-target="#DeleteTimesheetEntry-{{entry.id}}" onclick=""><i
class="mx-2 text-danger bi bi-trash3"></i></a>
<div class="modal fade" id="DeleteTimesheetEntry-{{entry.id}}" tabindex="-1" aria-labelledby="TimesheetEntryLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-md">
<div class="modal-content">
<div class="modal-body">
<div class="container border">
<div class="row my-4">
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="red"
class="bi bi-exclamation-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path
d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z" />
</svg>
</div>
<div class="row my-2">
<h5>Are you sure you want to delete this entry ?</h5>
</div>
<div class="row my-2">
<h7>Deleting records from this directory cannot be undone {{entry.id}}</h7>
</div>
<div class="row my-4">
<div class="col">
<a href="{% url 'timesheet_delete_view' id=entry.id %}" class="btn btn-danger">Delete</a>
</div>
<div class="col">
<div class="btn btn-secondary" data-bs-dismiss="modal">Cancel</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</td>
{% else %}
<td></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
<tfoot class="sticky-bottom bottom-0 shadow">
<tr id="foot" style="border-top: solid 2px #4E45AC; ">
<!-- <th scope="col"></th> -->
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<script>
var title = document.getElementById('title');
let currentDate = new Date();
let currentMonth = currentDate.toLocaleString('default', { month: 'long' });
let year = currentDate.getFullYear();
title.innerHTML = `Timesheet For ${currentMonth} ${year}`
$(document).ready(function(){
{% if formset_errors %}
$('#TimesheetEntry').modal('show');
{% endif %}
});
function handleInput(event) {
var target = event.target;
if (target.classList.contains("numberinput")) {
var max = parseFloat(target.getAttribute("max"));
if (parseFloat(target.value) > max) {
target.value = max;
}
}
var numberInputs = document.getElementsByClassName("numberinput");
var sum = 0;
for (var i = 0; i < numberInputs.length; i++) {
var inputValue = parseFloat(numberInputs[i].value);
if (!isNaN(inputValue)) {
sum += inputValue;
}
}
document.getElementById('total_hours').innerText = sum;
}
document.body.addEventListener("input", handleInput);
let addButton = document.querySelector("#addForm");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
addButton.addEventListener('click', addForm);
function addForm(e) {
e.preventDefault();
let formsContainer = document.querySelector("#user-forms");
let lastForm = formsContainer.lastElementChild;
let newForm = lastForm.cloneNode(true);
let inputs = newForm.querySelectorAll('.form-control');
inputs.forEach(input => {
input.value = null;
});
let labels = newForm.querySelectorAll('label');
labels.forEach(label => {
label.remove();
});
let formNum = formsContainer.querySelectorAll(".user-form").length;
// console.log(formNum)
newForm.id = `form-${formNum}`;
let fieldPrefix = `id_form-${formNum}`;
newForm.querySelectorAll('[id^=id_form-]').forEach(function (field) {
let oldId = field.getAttribute('id');
let oldName = field.getAttribute('name');
field.setAttribute('id', oldId.replace(/\d+/, formNum));
field.setAttribute('name', oldName.replace(/\d+/, formNum));
field.classList.add("dynamic");
});
formsContainer.appendChild(newForm);
if (formNum > 0) {
addRemoveButton(newForm);
}
totalForms.setAttribute('value', `${formNum + 1}`);
}
function addRemoveButton(form) {
let removeButton = form.querySelector('.remove-col .remove-form');
if (removeButton) {
removeButton.addEventListener('click', function () {
form.remove();
updateFormCount();
});
} else {
removeButton = document.createElement('button');
removeButton.textContent = 'Remove';
removeButton.type = 'button';
removeButton.classList.add('remove-form');
removeButton.classList.add('btn');
removeButton.classList.add('btn-outline-danger');
form.querySelector('.remove-col').appendChild(removeButton);
removeButton.addEventListener('click', function () {
form.remove();
updateFormCount();
});
}
}
function updateFormCount() {
let formsContainer = document.querySelector("#user-forms");
let formNum = formsContainer.querySelectorAll(".user-form").length;
totalForms.setAttribute('value', `${formNum}`);
}
function removeForm(formNum){
let form = document.getElementById(`form-${formNum}`);
form.remove()
var form_idx = $('#id_form-TOTAL_FORMS').val();
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) - 1);
// console.log(`form-${formNum} removed`);
});
}
$(document).on('change', '.row select[id^="id_"][id$="-client_name"]', function () {
var rowId = $(this).closest('.row').attr('id'); // Get the ID of the closest parent div with class 'row'
var selectedClientId = $('#id_' + rowId + '-client_name').val();
// console.log(`Row Id: ${rowId}`);
$.ajax({
url: '{% url "ajax_load_projects" %}',
data: {
client: Number(selectedClientId),
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val()
},
dataType: 'json',
success: function (data) {
$('#id_' + rowId + '-project_name').empty();
$('#id_' + rowId + '-category').empty();
$('#id_' + rowId + '-project_name').append('<option value>---------</option>');
$('#id_' + rowId + '-category').append('<option value>---------</option>');
$.each(data, function (key, value) {
$('#id_' + rowId + '-project_name').append('<option value="' + key + '">' + value + '</option>');
});
},
error: function (xhr, status, error) {
console.error('AJAX Error:', error);
alert('An error occurred while loading projects. Please try again.');
}
});
});
$(document).on('change', '.row select[id^="id_"][id$="-project_name"]', function () {
var rowId = $(this).closest('.row').attr('id');
var selectedProjectId = $('#id_' + rowId + '-project_name').val();
var selectedClientId = $('#id_' + rowId + '-client_name').val();
// Check if both IDs are valid
if (!selectedProjectId || !selectedClientId) {
return; // Exit if either value is invalid
}
$.ajax({
url: '{% url "ajax_load_categories" %}',
data: {
client: Number(selectedClientId),
project: Number(selectedProjectId),
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val()
},
dataType: 'json',
success: function (data) {
$('#id_' + rowId + '-category').empty();
$('#id_' + rowId + '-category').append('<option value>---------</option>');
$.each(data, function (key, value) {
$('#id_' + rowId + '-category').append('<option value="' + key + '">' + value + '</option>');
});
},
error: function (xhr, status, error) {
console.error('AJAX Error:', error);
alert('An error occurred while loading categories. Please try again.');
}
});
});
function extractSubstring(str) {
const match = str.match(/\d+/);
return match ? match[0] : null;
}
$(document).on('change', '.row select[id^="id_"][id$="-category"]', function () {
var rowId = extractSubstring($(this).closest('.row').attr('id'));
var noBillableCategories = ["Holiday", "Full Day Leave", "First Half Leave", "Second Half Leave"];
var selectedCategoryText = $('#id_form-'+rowId+'-category option:selected').text();
var selectedBillableText = $('#id_form-'+rowId+'-billable option:selected').text();
var billables = ['Yes India', 'Yes', 'No'];
if (noBillableCategories.includes(selectedCategoryText)){
$('#id_form-' + rowId + '-billable').empty();
$('#id_form-' + rowId + '-billable').append('<option value="No" selected>No</option>');
$('#id_form-' + rowId + '-hours').val(0.00);
$('#id_form-' + rowId + '-hours').prop('disabled', true);
}else{
$('#id_form-' + rowId + '-billable').empty();
$.each(billables, function (key, value) {
var optionTag = '<option value="' + value + '">' + value + '</option>';
if (value == selectedBillableText) {
optionTag = '<option value="' + value + '" selected>' + value + '</option>';
}
$('#id_form-' + rowId + '-billable').append(optionTag);
});
$('#id_form-' + rowId + '-hours').prop('disabled', false);
}
});
function getFormattedDate(date) {
var date = new Date(date);
const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const monthsOfYear = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const dayOfWeek = daysOfWeek[date.getDay()];
const month = monthsOfYear[date.getMonth()];
const day = date.getDate();
const year = date.getFullYear();
return `${dayOfWeek}, ${month} ${day}, ${year}`;
}
function getWeekStartDate(date) {
var dayOfWeek = date.getDay();
var diff = date.getDate() - dayOfWeek + (dayOfWeek == 0 ? -6 : 1);
return new Date(date.setDate(diff));
}
function formatDate(inputDate) {
let dateObj = inputDate;
let month = dateObj.toLocaleString('default', { month: 'long' });
// console.log(`${inputDate}------${month}`)
let year = dateObj.getFullYear();
let formattedDate = month + ' ' + year;
return formattedDate;
}
var dateElements = document.querySelectorAll('.date');
dateElements.forEach(function (dateElement) {
var dateString = dateElement.textContent;
dateElement.textContent = getFormattedDate(dateElement.textContent)
var date = new Date(dateString);
var hours = dateElement.nextElementSibling;
var monthElement = hours.nextElementSibling;
monthElement.textContent = formatDate(date)
var weekStartDate = getWeekStartDate(date);
var weekStartDateElement = monthElement.nextElementSibling;
weekStartDateElement.textContent = getFormattedDate(weekStartDate.toISOString().split('T')[0].toString());
});
document.addEventListener("DOMContentLoaded", function () {
$('#liveToast').toast();
});
$(document).ready(function () {
function cbDropdown(column) {
return $('<ul>', {
'class': 'cb-dropdown form-select'
}).appendTo($('<div>', {
'class': 'cb-dropdown-wrap'
}).appendTo(column));
}
$('#myTable').DataTable({
autoWidth: false,
paging: true,
"aLengthMenu": [[100, 200], ["100", "200"]],
"bSort": true,
"aoColumnDefs": [{
'bSortable': false,
'aTargets': [3, 5, 8, 9]
},
{ orderSequence: ['asc', 'desc'], targets: [0,1,2,3,4,5,6,7,8,9] },
{ type: 'date' , targets: [6, 9]}
],
"footerCallback": function (row, data, start, end, display) {
var api = this.api();
var totalHours = api
.column(7, { search: 'applied', page: 'current' })
.data()
.reduce(function (a, b) {
return parseFloat(a) + parseFloat(b);
}, 0);
$(api.column(7).footer()).html('Total: ' + totalHours.toFixed(2)).css('font-size', '13px');
},
initComplete: function () {
var table = this.api();
$(this.api().table().header()).css({ 'font-size': '13px' });
table.columns([0, 1, 2, 4, 5, 6, 8, 9]).every(function () {
var column = this;
var ddmenu = cbDropdown($(column.header()))
.on('change', ':checkbox', function (event) {
var active;
var vals;
if (event.target.value === 'All') {
if ($(event.target).is(':checked')) {
// Move "All" to the top if checked
var $allOption = $(event.target).closest('li');
ddmenu.prepend($allOption);
// Uncheck other options
$(':checkbox', ddmenu).not(event.target).prop('checked', false);
vals = [""];
} else {
// Don't move "All" if unchecked
vals = [];
}
} else {
vals = $(':checked', ddmenu).map(function (index, element) {
if ($(element).val() !== 'All') {
active = true;
return $.fn.dataTable.util.escapeRegex($(element).val());
}
}).toArray();
// Move selected option to the top
var $selectedItem = $(this).closest('li');
ddmenu.prepend($selectedItem);
}
vals = vals.join("|");
// console.log(`vals: ${vals}`);
// Untick "All" option when other options are selected
if (vals !== "") {
$(':checkbox[value="All"]', ddmenu).prop('checked', false);
}
column
.search(vals.length > 0 ? '^(' + vals + ')$' : '', true, false)
.draw();
// Highlight the current item if selected.
if (this.checked) {
$(this).closest('li').addClass('active');
} else {
$(this).closest('li').removeClass('active');
}
// Highlight the current filter if selected.
var active2 = ddmenu.parent().is('.active');
if (active && !active2) {
ddmenu.parent().addClass('active');
} else if (!active && active2) {
ddmenu.parent().removeClass('active');
}
// Ensure no matching rows if no option is selected
if ($(':checked', ddmenu).length === 0) {
$(':checkbox[value="All"]', ddmenu).prop('checked', true);
table.search('').draw();
}
});
// Add "All" option at the top and tick it by default
ddmenu.prepend($('<li>').append(
$('<label>').append(
$('<span>').text("All"),
$('<input>', { type: 'checkbox', value: 'All', checked: true })
)
));
column.data().unique().sort().each(function (d, j) {
var $label = $('<label>');
var $text = $('<span>', { text: d });
var $cb = $('<input>', { type: 'checkbox', value: d });
$text.appendTo($label);
$cb.appendTo($label);
ddmenu.append($('<li>').append($label));
});
});
$(document).ready(function () {
$('#myTable').DataTable().columns([3]).every(function () { // Specify column number 3 (zero-based index)
var column = this;
// Create container for header elements
var headerContainer = $('<div class="header-container">').appendTo($(column.header()));
// Create search input element
var searchInput = $('<input type="text" class="form-control" placeholder="Search..." style="height: 24px; font-size: 14px; text-align: centre;">')
.appendTo(headerContainer) // Append search input to header container
.on('keyup change', function () { // On keyup or change in the search input
if (column.search() !== this.value) {
column.search(this.value).draw(); // Set column search value and redraw table
}
});
});
});
document.getElementById('loading').style.display = "none";
document.getElementById('myTable').hidden = false;
}
});
});
$(document).ready(function () {
var element = $('#liveToast');
setTimeout(function () {
element.removeClass('show');
element.addClass('hide');
}, 5000);
});
</script>
{% endblock %}
views.py:
def timesheet_view(request):
if request.user.is_authenticated:
login(request, request.user)
context = {}
context['entries'] = filtered_queryset(request)
context['formset_errors'] = False
userFormSet = modelformset_factory(Timesheet, form=UserTimesheetCreateForm, formset=BaseTimesheetForm, fields='__all__', can_delete=True)
if request.method == "POST":
modified_entries = request.POST.copy()
categories = [item for item in modified_entries if item.endswith('category')]
for category in categories:
parts = category.split('-')
number = parts[1]
selected_cat = Category.objects.get(id=int(modified_entries[category]))
if selected_cat.category in ["Holiday", "Full Day Leave", "First Half Leave", "Second Half Leave"]:
modified_entries["form-"+number+"-hours"] = Decimal(0)
formset = userFormSet(modified_entries, user=request.user)
context['formset'] = formset
if formset.is_valid():
for form in formset:
if (form.cleaned_data['category'] in ["Holiday", "Full Day Leave", "First Half Leave", "Second Half Leave"]):
form.cleaned_data['hours'] = 0
formset.save()
messages.success(request, 'Entry Submitted Successfully')
return redirect('index')
else:
context['formset_errors'] = True
messages.error(request, 'Entry is Invalid')
else:
formset = userFormSet(user=request.user)
context['formset'] = formset
return render(request, "timesheet/index.html", context=context)
else:
return redirect(reverse_lazy('login_view'))
forms.py:
class UserTimesheetCreateForm(forms.ModelForm):
date = forms.DateField(initial=now().date(),
widget=forms.widgets.DateInput(attrs={'type': 'date'}), required=True)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(UserTimesheetCreateForm, self).__init__(*args, **kwargs)
self.empty_permitted = False
instance = kwargs.pop('instance', None)
if instance is not None:
self.fields['project_name'].initial = instance.project_name
self.fields['category'].initial = instance.category
self.fields['emp_name'].initial = instance.emp_name
# if not self.user.is_superuser and not self.user.is_manager:
# self.fields['date'].widget.attrs['min'] = last_monday
# self.fields['date'].widget.attrs['max'] = next_to_next_friday
else:
if self.user.is_superuser:
self.fields['emp_name'].initial = self.user
self.fields['emp_name'].queryset = User.objects.all()
elif self.user.is_manager:
self.fields['emp_name'].initial = self.user
self.fields['emp_name'].queryset = User.objects.filter(Q(username=self.user)|Q(manager=self.user))
else:
self.fields['emp_name'].initial = self.user
self.fields['emp_name'].queryset = User.objects.filter(username=self.user)
# self.fields['date'].widget.attrs['min'] = last_monday
# self.fields['date'].widget.attrs['max'] = next_to_next_friday
# print("I'm not superuser or manager")
class Meta:
model = Timesheet
fields = "__all__"
widgets = {
'hours': forms.NumberInput(attrs={'step': 0.25, 'min': 0, 'max':8.5},),
'description': forms.Textarea(attrs={'style': 'height: 40px;'})
}
def clean(self):
cleaned_data = super().clean()
emp_name = cleaned_data.get('emp_name')
hours = cleaned_data.get('hours')
date = cleaned_data.get('date')
if(self.instance == None):
print("create")
total_user_entries = Timesheet.objects.filter(emp_name__first_name=emp_name, date=date).aggregate(total=models.Sum('hours'))['total'] or 0.00
else:
print("update")
total_user_entries = Timesheet.objects.exclude(id=self.instance.id).filter(emp_name__first_name=emp_name, date=date).aggregate(total=models.Sum('hours'))['total'] or 0.00
number_str = str(hours)
decimal_point = number_str.find('.')
if len(number_str[decimal_point + 1:]) > 2:
self.add_error('hours', "Hours: There should be maximum two digits after decimal point")
raise forms.ValidationError(f'Hours: There should be maximum two digits after decimal point')
if hours and (float(total_user_entries) + float(hours)) > 12.00:
self.add_error('hours', f'The total working hours cannot exceed 12.00 on date {date}')
raise forms.ValidationError(f'The total working hours cannot exceed 12.00 on date {date}')
return cleaned_data