我有一个非常具体的问题,与 django-bootstrap-datepicker-plus 包相关。
在我的待办事项列表应用程序中,我希望能够在多个特定日期弹出任务。我设置了模型,设置了包含表单集的表单,甚至还有一个处理动态添加/删除过程的 JavaScript。
我遇到的问题是我的
DateField
的克隆过程在某种程度上与 DatePicker
div 混在一起 - 请参阅下面最后一个代码块中的结果。
# model.py
from django.db import models
from datetime import time
# Create your models here.
class Task(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=50, default="")
description = models.CharField(max_length=500, default="")
entry_date = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)
specific_dates = models.ManyToManyField('SpecificDate', blank=True)
due_until = models.TimeField(auto_now_add=False, default=time(12, 0))
class SpecificDate(models.Model):
todo = models.ForeignKey(Task, on_delete=models.CASCADE)
date = models.DateField(auto_now_add=False, blank=True, null=True)
class Meta:
# ensure each specific date is unique per task
unique_together = ('todo', 'date')
# forms.py
from bootstrap_datepicker_plus.widgets import DatePickerInput, TimePickerInput
from django import forms
from .models import Task, SpecificDate
class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = [
'title',
'description',
'due_until',
]
widgets = {
'due_until': TimePickerInput(options={'stepping': 5, 'format': 'HH:mm'}),
'description': forms.Textarea({'rows': 3}),
}
class SpecificDateForm(forms.ModelForm):
class Meta:
model = SpecificDate
fields = ['date']
widgets = {
'date': DatePickerInput(),
}
SpecificDateFormset = forms.inlineformset_factory(
Task, SpecificDate,
form=SpecificDateForm,
fields=('date',),
extra=1,
can_delete=True
)
<!-- task_form.html -->
{% extends "task/base.html" %}
{% load crispy_forms_tags %}
{% load bootstrap4 %}
{% bootstrap_css %}
{% bootstrap_javascript jquery='full' %}
{% block content %}
<div class="content-section">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">New entry</legend>
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% crispy form %}
<div id="formset-container">
{{ formset.management_form }}
{% for form in formset %}
{% if form.errors %}
<div class="alert alert-danger">
{{ form.errors }}
</div>
{% endif %}
<div id="formset-date" class="formset-date d-flex align-items-center">
<div class="flex-grow-1 mr-2">
{{ form.date|as_crispy_field }}
</div>
<div>
<button type="button" class="btn btn-sm btn-danger remove-date" name="remove-date">-</button>
<button type="button" class="btn btn-sm btn-success add-date" name="add-date">+</button>
</div>
</div>
{% endfor %}
</div>
</fieldset>
<div class="form-group">
<button class="btn btn-primary" type="submit" name="add_new">Save</button>
<a class="btn btn-warning" href="{% url 'task-new' %}">Restart</a>
</div>
</form>
{{ form.media }}
</div>
{% endblock content %}
// JavaScript for add/remove buttons
document.addEventListener('DOMContentLoaded', function() {
const formsetContainer = document.getElementById('formset-container');
const formsetPrefix = 'specificdate_set';
const totalFormsElement = document.getElementById('id_'+formsetPrefix+'-TOTAL_FORMS');
// Adjusts name and id for cloned form elements
function adjustNamesAndIds(newForm, newIndex) {
newForm.querySelectorAll('[id], label').forEach(element => {
// debugger;
// First up, let's do a few if statements for elements we want
// to skip.
// ... buttons
if (element.tagName === 'BUTTON') {
console.log('Skipping button ' + element.name);
return;
}
// ... formset management fields as they already have valid names
if (element.type === 'hidden' && (element.value === '')) {
console.log('Skipping formset management field or empty hidden field: ' + element.name);
return;
}
if (element.tagName === 'LABEL' && element.htmlFor) {
const htmlForMatch = element.htmlFor.match(/^(.+)-\d+-(.+)$/);
if (htmlForMatch) {
element.htmlFor = `${htmlForMatch[1]}-${newIndex}-${htmlForMatch[2]}`;
}
}
if (element.dataset.name) {
const nameMatch = element.dataset.name.match(/^(.+)-\d+-(.+)$/);
if (nameMatch) {
element.dataset.name = `${nameMatch[1]}-${newIndex}-${nameMatch[2]}`;
}
}
if (element.name) {
const nameMatch = element.name.match(/^(.+)-\d+-(.+)$/);
if (nameMatch) {
element.name = `${nameMatch[1]}-${newIndex}-${nameMatch[2]}`;
}
}
// Update IDs for inputs and corresponding labels, if applicable
if (element.id) {
const idMatch = element.id.match(/^(.+)-\d+-(.+)$/);
if (idMatch) {
const newId = `${idMatch[1]}-${newIndex}-${idMatch[2]}`;
element.id = newId;
const label = newForm.querySelector(`label[for="${element.id}"]`);
}
}
});
// // Additionally, update div IDs related to the form fields for
// // consistency
// newForm.querySelectorAll('div[id]').forEach(div => {
// const idMatch = div.id.match(/^(.+)-\d+-(.+)$/);
// if (idMatch) {
// div.id = `${idMatch[1]}-${newIndex}-${idMatch[2]}`;
// }
// });
}
function cloneForm() {
let totalForms = parseInt(totalFormsElement.value, 10);
let newFormIndex = totalForms;
totalFormsElement.value = totalForms + 1;
let newForm = formsetContainer.querySelector('.formset-date:last-of-type').cloneNode(true);
if (newForm && typeof newFormIndex !== 'undefined') {
adjustNamesAndIds(newForm, newFormIndex);
formsetContainer.appendChild(newForm);
} else {
console.error('Error cloning form: newForm or newFormIndex is invalid.');
}
}
formsetContainer.addEventListener('click', function(event) {
if (event.target.classList.contains('add-date')) {
console.log('Add');
event.preventDefault();
cloneForm();
} else if (event.target.classList.contains('remove-date')) {
console.log('Remove');
event.preventDefault();
if (formsetContainer.querySelectorAll('.formset-date').length > 1) { // Ensure at least one form remains
let formToRemove = event.target.closest('.formset-date');
formToRemove.remove();
}
}
});
});
单击“添加”按钮后带有表单集的 HTML 片段如下所示:
<div id="formset-container">
<input type="hidden" name="specificdate_set-TOTAL_FORMS" value="2" id="id_specificdate_set-TOTAL_FORMS"><input type="hidden" name="specificdate_set-INITIAL_FORMS" value="0" id="id_specificdate_set-INITIAL_FORMS"><input type="hidden" name="specificdate_set-MIN_NUM_FORMS" value="0" id="id_specificdate_set-MIN_NUM_FORMS"><input type="hidden" name="specificdate_set-MAX_NUM_FORMS" value="1000" id="id_specificdate_set-MAX_NUM_FORMS">
<div id="formset-date" class="formset-date d-flex align-items-center">
<div class="flex-grow-1 mr-2">
<div id="div_id_specificdate_set-0-date" class="form-group">
<label for="id_specificdate_set-0-date" class="">
Date
</label>
<div>
<div class="input-group dbdp">
<input type="text" class="datepickerinput form-control" id="id_specificdate_set-0-date" data-dbdp-config="{"variant": "date", "backend_date_format": "YYYY-MM-DD", "options": {"locale": "de-DE", "format": "DD.MM.YYYY"}}" data-dbdp-debug="" data-name="specificdate_set-0-date">
<div class="input-group-addon input-group-append input-group-text">
<i class="bi-calendar"></i>
</div>
</div><input type="hidden" name="specificdate_set-0-date" value="">
</div>
</div>
</div>
<div>
<button type="button" class="btn btn-sm btn-danger remove-date" name="remove-date">-</button>
<button type="button" class="btn btn-sm btn-success add-date" name="add-date">+</button>
</div>
</div>
<div id="formset-date" class="formset-date d-flex align-items-center">
<div class="flex-grow-1 mr-2">
<div id="div_id_specificdate_set-1-date" class="form-group">
<label for="id_specificdate_set-1-date" class="">
Date
</label>
<div>
<div class="input-group dbdp">
<input type="text" class="datepickerinput form-control" id="id_specificdate_set-1-date" data-dbdp-config="{"variant": "date", "backend_date_format": "YYYY-MM-DD", "options": {"locale": "de-DE", "format": "DD.MM.YYYY"}}" data-dbdp-debug="" data-name="null">
<div class="input-group-addon input-group-append input-group-text">
<i class="bi-calendar"></i>
</div>
</div><input type="hidden" name="null" value=""><input type="hidden" name="specificdate_set-0-date" value="">
</div>
</div>
</div>
<div>
<button type="button" class="btn btn-sm btn-danger remove-date" name="remove-date">-</button>
<button type="button" class="btn btn-sm btn-success add-date" name="add-date">+</button>
</div>
</div>
</div>
如您所见(如果您仔细观察),DatePicker 的 dataset.name 属性不会更新。或者,好吧,它得到了更新 -
element.dataset.name
在处理过程中具有正确的名称 - 但在渲染的 HTML 中,它又回到了原始名称,并且我还得到了那些 <input type="hidden" name="null" value="">
元素。
问题很简单:如何在各个
<input>
字段中获取正确的 dataset.names,以便提交时的 POST
请求具有正确的元素?
虽然经过了几次迭代,但我最终得到了解决我的问题的有效 JavaScript 代码。它还负责禁用除最后一个添加/删除按钮之外的所有按钮。虽然不漂亮,但是很管用。
document.addEventListener('DOMContentLoaded', function() {
const formsetContainer = document.getElementById('formset-container');
const formsetPrefix = 'specificdate_set';
const totalFormsElement = document.getElementById('id_' + formsetPrefix + '-TOTAL_FORMS');
function updateLabels() {
$('.formset-date').each(function(index) {
$(this).find('label').text(`Date ${index + 1}`);
});
}
function manageAddRemoveButtons() {
let formsetDivs = formsetContainer.querySelectorAll('.formset-date');
formsetDivs.forEach((div, index) => {
// disable add button for all but the last row
div.querySelector('.add-date').disabled = (
index !== formsetDivs.length - 1);
// disable remove button for all but the last
// row, but not if only one row is present
div.querySelector('.remove-date').disabled = (
formsetDivs.length === 1) || (
index !== formsetDivs.length - 1);
});
}
function adjustNamesAndIds(newForm, newIndex) {
const regex = /^(.+)-\d+-(.+)$/;
newForm.querySelectorAll('[id], label, .datepickerinput').forEach(element => {
if (element.tagName === 'BUTTON') return; // Skip buttons entirely
['id', 'htmlFor', 'name'].forEach(attribute => {
if (element[attribute]) {
const match = element[attribute].match(regex);
if (match) {
element[attribute] = `${match[1]}-${newIndex}-${match[2]}`;
}
}
});
if (element.classList.contains('datepickerinput')) {
const $datepickerInput = $(element);
if ($datepickerInput.length && $datepickerInput.data('name')) {
const nameMatch = $datepickerInput.data('name').match(regex);
if (nameMatch) {
$datepickerInput.attr('name', `${nameMatch[1]}-${newIndex}-${nameMatch[2]}`);
}
}
}
});
}
function cloneForm() {
let totalForms = parseInt(totalFormsElement.value, 10);
let newFormIndex = totalForms;
totalFormsElement.value = totalForms + 1;
let newForm = formsetContainer.querySelector('.formset-date:last-of-type').cloneNode(true);
if (newForm && typeof newFormIndex !== 'undefined') {
adjustNamesAndIds(newForm, newFormIndex);
formsetContainer.appendChild(newForm);
// Remove Extra Hidden Fields
let inputName = `${formsetPrefix}-${totalForms-1}-date`;
newForm.querySelectorAll(`input[type="hidden"][name="${inputName}"]`).forEach(
hiddenInput => hiddenInput.remove()
);
} else {
console.error('Error cloning form: newForm or newFormIndex is invalid.');
}
}
formsetContainer.addEventListener('click', function(event) {
if (event.target.classList.contains('add-date')) {
event.preventDefault();
cloneForm();
} else if (event.target.classList.contains('remove-date')) {
event.preventDefault();
let formToRemove = event.target.closest('.formset-date');
formToRemove.remove();
}
updateLabels();
manageAddRemoveButtons();
});
updateLabels();
manageAddRemoveButtons();
});