Django - загрузка и удаление нескольких изображений
Я хочу улучшить свой код, который позволяет пользователю загружать несколько изображений (файлов), связанных с записью, а также удалять их по мере необходимости.
Я могу справиться с первой частью (несколько изображений) с помощью следующего.
models.py
class APIvisit(ModelIsDeletable, SafeDeleteModel):
_safedelete_policy = SOFT_DELETE
created_date = models.DateTimeField(auto_now_add=True,editable=False, verbose_name=_("Créé le"))
modified_date = models.DateTimeField(auto_now=True,editable=False, verbose_name=u"Modifié le")
visitdate = models.DateField(_('Date de la visite'),default=datetime.date.today)
[...] a lot of other fields
class Meta:
ordering = ('visitdate',)
class APIvisitimage(ModelIsDeletable, SafeDeleteModel):
_safedelete_policy = SOFT_DELETE
created_date = models.DateTimeField(auto_now_add=True,editable=False, verbose_name=_("Créé le"))
modified_date = models.DateTimeField(auto_now=True,editable=False, verbose_name=u"Modifié le")
fk_visit = models.ForeignKey(APIvisit, on_delete=models.PROTECT, related_name=_('ImageVisite'), verbose_name=_('ImageVisite'), blank=False, null=False)
image = models.FileField(upload_to="uploads/%Y/%m/%d/")
forms.py
from django.forms.widgets import ClearableFileInput
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = single_file_clean(data, initial)
return result
[...]
class VisitImageForm(ModelForm):
class Meta:
model = APIvisitimage
fields = ["image"]
image = MultipleFileField(label='Choisir les photos', required=False)
views.py
class VisitEditView(PermissionRequiredMixin, UpdateView):
permission_required = 'gestion.change_apivisit'
model = APIvisit
form_class = VisitForm
template_name = 'visite/edition.html'
success_url = '/visite/'
def form_invalid(self, form):
self.object_list = self.get_queryset()
context = self.get_context_data(task_form=form)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object() # assign the object to the view
context = super().get_context_data(**kwargs)
form = self.get_form()
visit = APIvisit.objects.get(id=self.kwargs.get("pk"))
imageform = VisitImageForm(request.POST, request.FILES)
images= request.FILES.getlist('image')
if form.is_valid():
if imageform.is_valid():
for i in images:
APIvisitimage.objects.create(fk_visit=visit, image=i)
else:
return self.form_invalid(imageform)
form.save()
else:
return self.form_invalid(form)
visit = APIvisit.objects.get(id=self.kwargs.get("pk"))
context["imageform"] = imageform
context["fk_visit"] = visit
return self.render_to_response(context)
def get_context_data(self, **kwargs):
request = self.request
context = super().get_context_data(**kwargs)
imageform = VisitImageForm()
visit = APIvisit.objects.get(id=self.kwargs.get("pk"))
visitimages = APIvisitimage.objects.filter(fk_visit=visit)
context["imageform"] = imageform
context["visitimages"] = visitimages
context["fk_visit"] = visit
return context
class VisitImageDeleteView(PermissionRequiredMixin, DeleteView):
permission_required = 'gestion.delete_apivisitimage'
model = APIvisitimage
success_url = reverse_lazy('VisitListView')
template_name = 'visite/suppression.html'
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
return super(VisitImageDeleteView, self).dispatch(*args, **kwargs)```
Шаблон редакции.html
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<table class="table table-striped">
{{ form.as_table }}
</table>
<input type="submit" value="{% if object %}Mettre à jour{% else %}Créer{% endif %}">
<script type="text/javascript">
function add_image_row(){
var NewRow = document.getElementById("imagetable").insertRow(0);
var Newcell1 = NewRow.insertCell(0);
var Newcell2 = NewRow.insertCell(1);
Newcell1.innerHTML = '<a href="#" onclick="delete_image_row(this);"><i class="fa-regular fa-square-minus"></i></a>';
Newcell2.innerHTML = '{{ imageform.image.label_tab }}{{ imageform.image }}';
}
function delete_image_row(imagetable_row){
var i = imagetable_row.parentNode.parentNode.rowIndex;
document.getElementById("imagetable").deleteRow(i);
}
function removeImage(image_id) {
window.confirm("Voulez-vous vraiment supprimer l'image ?")
fetch("/visite/delete-image/" + encodeURIComponent(image_id) + "/",
{
credentials: 'same-origin',
method: 'DELETE'})
.then(response => {
if (response.status == 200 || response.status == 403) {
removed_image = document.getElementById("image_" + image_id);
removed_image.parentNode.removeChild(removed_image);
} else {
window.alert("Impossible de supprimer l'image (Code " + response.status + ")");
console.log(response);
}
});
}
</script>
<h3>Photos</h3>
<table id="imagetable" class="table table-striped">
<tr><td><a href="#" onclick="add_image_row(this);"><i class="fa-regular fa-square-plus"></i></a></td><td></td></tr>
</table>
<table class="table table-striped">
<tbody>
{% for image in visitimages %}
<div id="image_{{ image.id }}" style="position: relative; display: inline;">
<img src="{{ image.image.url }}" alt="{{ image.id }}" style="height: 250px;">
<i class="fa-regular fa-trash-can" style="color: red; position: absolute; left: 10px; top: 10px;" onclick="removeImage({{ image.id }})"></i>
</div>
{% endfor %}
</tbody>
</table>
</form>
{% endif %}
Это позволяет мне
- иметь форму редактирования для моих «визитов»
- имеет кнопку «Загрузить файл», которая позволяет выбрать картинку для загрузки
- имеет кнопки + и trash, которые позволяют добавить/удалить кнопку «Загрузить файл».
- сохраняю запись «визит» и создаю записи с фотографиями, которые связаны с этим «визитом». Фотографии загружаются в мою папку «uploads».
- отобразить уже загруженные фотографии со значком корзины. При нажатии javascript fetch API вызовет VisitImageDeleteView и удалит запись. Изображение также будет удалено с экрана. С диска она не удаляется (пока) .
Усовершенствование 1 - Хранить токены csrf
Я нашел много статей, которые объясняют, как использовать JavaScript fetch API с токеном csrf (включая запросы POST или DELETE, cookies, X-CSRFToken и даже создание формы с вводом csrf на основе существующего токена csrf, имеющегося на странице).
headers: {
"X-CSRFToken": '{% csrf_token %}',
},
method: 'POST'})
let data = new FormData();
data.append('csrfmiddlewaretoken', $('#csrf-helper input[name="csrfmiddlewaretoken"]').attr('value'));
fetch("/visite/delete-image/" + encodeURIComponent(image_id) + "/", {
method: 'POST',
body: data,
credentials: 'same-origin',
})
Ничего из этого не помогло (читай: я постоянно получаю ошибку 403 csrf). В конце концов я решил отключить защиту csrf только для этого вида. Я все еще получаю ошибку 403, но, по крайней мере, изображение удаляется. Несмотря на то, что этот сайт является внутренним и предназначен только для нескольких пользователей, я бы хотел вернуться к csrf везде.
Усовершенствование 2 - Модели с общим изображением для нескольких родительских моделей
Это работает
для добавления картинки (картинок) в «визит», но если я захочу добавить картинку в другую часть моей модели, мне придется создать еще одну модель xxximage, поскольку ForeignKey связан с моделью APIvisit. Мне также придется создавать дополнительные модели. Я думаю / надеюсь, что должен быть способ использовать поля ManyToMany, и это то, что я буду рассматривать дальше, но прежде чем идти туда, я хотел бы быть уверенным, что это можно сделать. Кто-нибудь делал это?Большое спасибо.
Jm