Django - Multiple images upload and delete

I'm looking at improving my code that allows the user to upload multiple images (files) linked to a record, and also delete them as needed.

I am able to handle the first part (multiple images) with the following.

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)```

Template edition.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 %}

This allows me to

  • have a edit form for my "visits"
  • have an "Upload file" button that allows to select a picture to be uploaded
  • have + and trash buttons that allows to add / remove an "Upload File" button.
  • save my "visit" record and have the pictures records created and linked to this "visit". Pictures are uploaded in my "uploads" folder.
  • display the already uploaded pictures with a trash icon. When pressed, the javascript fetch API will call the VisitImageDeleteView and delete the record. The picture will also be removed from the screen. It is not removed from the disk (yet)

Improvement 1 - Keep csrf tokens

I have found a lot of articles that explain how to use the JavaScript fetch API with a csrf token (including POST or DELETE requests, cookies, X-CSRFToken, and even creating a form with csrf input, based on the existing csrf token available in the page).

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',
})

None of this worked (read: I'm always getting a 403 csrf error). I eventually decided to disable csrf protection on this view only. I'm still getting a 403 error, but at least the image is deleted. Even though this website is internal only and for a few user, I'd like to revert to csrf everywhere.

Improvement 2 - Shared Image models for multiple parent models

This works to add picture(s) to a "visit", but if I would like to add picture to another part of my model, I would have to create another xxximage model, as the ForeignKey is linked to the APIvisit model. I would also have to create addi I think / hope they should be a way to use ManyToMany fields, and that what I'll be looking at next, but before going there, I'd like to be sure it can be done. Has anybody done this ?

Thanks a lot.

Jm

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