Search code examples
pythondjangodropzone.jsdropzone

How can I properly save dropzone files from existing django form and get it from DB when need?


I am trying to handle drag&drop uploads with my django app and facing lack of actual information about dropzone + django. Previously I used just a multiple upload, as you could see below, and it worked great, but it is a time to handle drag&drop thing, cause it a way more convenient. I handled it to be inside my main form, but I totally don't understand how I suppose to save this files and get them from DB in template, when I need it. Maybe, somebody can help me with that? Don't be too angry on me, I am not an experienced JS user..

models.py

class Post(models.Model):
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE
    )
    title = models.CharField(
        max_length=200,
    )
    ...


class PostImages(models.Model):
    post = models.ForeignKey(
        Post,
        on_delete=models.CASCADE,
        related_name='images',
    )
    file = models.FileField(
        upload_to="posts/images",
        validators=[],
    )

views.py

class PostCreateView(AuthRequiredMixin, SuccessMessageMixin, View):
    template_name = 'create_post.html'
    model = Post
    form = PostCreationForm
    success_url = reverse_lazy('index')
    success_message = _('Post created successfully')

    def get(self, request, *args, **kwargs):
        form = self.form()
        return render(request, self.template_name, {'form': form})

    def post(self, request, *args, **kwargs):
        form = self.form(request.POST, request.FILES)
        if form.is_valid():
            new_post = form.save(commit=False)
            new_post.author = request.user
            new_post.save()
            form.save()
            for img in request.FILES.getlist('images'):
                data = img.read()
                new_file = PostImages(post=new_post)
                new_file.file.save(img.name, ContentFile(data))
                new_file.save()
            messages.success(request, self.success_message)
        else:
            messages.error(request, 'Something went wrong!')
        return redirect(self.success_url)

forms.py

class PostCreationForm(ModelForm):
    # COMMENT IT CAUSE I ASSUME TO USE DROPZONE FIELD WITH THIS NAME
    # images = forms.FileField(
    #     label='Images',
    #     required=False,
    #     help_text='Upload your images here!',
    #     widget=forms.ClearableFileInput(attrs={'multiple': True}),
    # )

    class Meta:
        model = Post
        fields = [
            'title',
            ...

        ]
        labels = {
            'title': 'Title',
            ...
        }

create_post.html

<div class="card border-0 rounded shadow mb-5 p-5">
    <h1>Create Post</h1>
    <form id="create-post" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {% bootstrap_form form %}
        <div id="previewsContainer" class="dropzone">
            <div class="dz-default dz-message">
                <button class="dz-button" type="button">
                    Drop files here to upload
                </button>
            </div>
        </div>
        {% bootstrap_button content="Create"%}
    </form>
</div>

<script>
    Dropzone.autoDiscover = false;
    new Dropzone("#create-post",{
      clickable: ".dropzone",
      url: "{% url 'create_post' request.user.slug %}",
      previewsContainer: "#previewsContainer",
      paramName: "images",
      maxFiles: 10,
      maxFilesize: 10,
      uploadMultiple: true,
      autoProcessQueue: false,
      init() {
        var myDropzone = this;
        this.element.querySelector("[type=submit]").addEventListener("click", function(e){
          e.preventDefault();
          e.stopPropagation();
          myDropzone.processQueue();
        });
      }
    });
</script>

Solution

  • So, for the people who'll face this kinda problem too, I share my working solution of the problem. Models were not affected, so let's jump straight into create_post.html.

    <div class="card border-0 rounded shadow mb-5 p-5">
        <h1>Create Post</h1>
        <form id="create-post" method="post" enctype="multipart/form-data">
            {% csrf_token %}
            {% bootstrap_form form %}
            <div id="previewsContainer" class="dropzone">
                <div class="dz-default dz-message">
                    <button class="dz-button" type="button">
                        Drop files here to upload
                    </button>
                </div>
            </div>
            <button id="submit-all" class="btn btn-primary">Create</button>
        </form>
    </div>
    
    <script>
        Dropzone.autoDiscover = false;
        new Dropzone("#create-post",{
          clickable: ".dropzone",
          url: "{% url 'create_post' request.user.slug %}",
          previewsContainer: "#previewsContainer",
          paramName: "images",
          maxFiles: 10,
          maxFilesize: 30,
          acceptedFiles: '.png, .jpg, .jpeg',
          uploadMultiple: true,
          parallelUploads: 20,
          autoProcessQueue: false,
          init() {
            var myDropzone = this;
            this.element.querySelector("#submit-all").addEventListener("click", function(e){
              e.preventDefault();
              e.stopPropagation();
              myDropzone.processQueue();
            });
            this.on("success", function(file, response) {
                window.location.href=JSON.parse(file.xhr.response).url
            });
          }
        });
    </script>
    

    I've replaced the bootstrap button and gave it an id of "submit-all" for the below script to find it and properly process the click action. For some reasone a CSS selector didn't work for me. It raised an error in console, where I found it. Script itself didn't change much, but these lines were added for the redirect to work properly, cause Dropzone uses an AJAX to upload the images:

    this.on("success", function(file, response) {
        window.location.href=JSON.parse(file.xhr.response).url
    

    view.py

        def post(self, request, *args, **kwargs):
            form = self.form(request.POST, request.FILES)
            if form.is_valid():
                new_post = form.save(commit=False)
                new_post.author = request.user
                new_post.save()
                form.save()
                files = [request.FILES.get(f'images[{i}]') for i in range(0, len(request.FILES))]
                for img in files:
                    data = img.read()
                    new_file = PostImages(post=new_post)
                    new_file.file.save(img.name, ContentFile(data))
                    new_file.save()
                messages.success(request, self.success_message)
                link = reverse('profile_detail', kwargs={'slug': request.user.slug})
                response = {'url': link}
                return JsonResponse(response)
            else:
                messages.error(request, 'Something went wrong!')
            return redirect(self.success_url)
    

    In views I added this line to get the images and process them below:

    files = [request.FILES.get(f'images[{i}]') for i in range(0, len(request.FILES))]
    

    The way I process the images didn't changed at all, but now I return JsonResponse:

    link = reverse('profile_detail', kwargs={'slug': request.user.slug})
    response = {'url': link}
    return JsonResponse(response)
    

    forms.py

    class PostCreationForm(ModelForm):
        images = forms.FileField(label='', required=False, widget=forms.HiddenInput(attrs={'multiple': True}))
    
        class Meta:
            model = Post
            ...
    

    In forms I created an "images" file field with hidden widget. For the sake of dropzone to find it in html.

    Now it works! Not much changes were required in compare to the standart approach. Good luck!