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

Вернуться на верх