AndrewIngram / django-extra-views

Django's class-based generic views are awesome, let's have more of them.
MIT License
1.39k stars 172 forks source link

Uploading files with CreateWithInlinesView does NOT work with django 1.11.3 #146

Closed ghost closed 7 years ago

ghost commented 7 years ago

Hello!

I have troubles with uploading multiple files with inline formset. I want to write a post and attach to this post multiple text (.txt) files. I have two models: Post and related PostFile model:

models.py

from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User

class Post(models.Model):
    DRAFT = 'd'
    PUBLISHED = 'p'
    POST_STATUS_CHOICES = (
        (DRAFT, 'Draft'),
        (PUBLISHED, 'Published'),
    )
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    body = models.TextField()
    image = models.FileField(null=True, blank=True, upload_to='img/')
    status = models.CharField(max_length=1, choices=POST_STATUS_CHOICES, default='d')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('post_detail', kwargs={'pk': self.id})

class PostFile(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    code = models.FileField(null=True, blank=True, upload_to='txt/')

forms.py

from django.forms import ModelForm
from .models import Post

class PostForm(ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'body', 'image', 'status']

views.py

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from extra_views import InlineFormSet, CreateWithInlinesView
from .models import Post, PostFile
from .forms import PostForm

class PostFileFormSet(InlineFormSet):
    model = PostFile
    fields = ['code']
    max_num = 10
    extra = 1

class PostCreateView(CreateWithInlinesView):
    model = Post
    form_class = PostForm
    inlines = [PostFileFormSet]
    template_name = 'form_create.html'

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(PostCreateView, self).dispatch(*args, **kwargs)

    def form_valid(self, form):
        messages.success(self.request, 'Post was successfully created!')
        return super(PostCreateView, self).form_valid(form)

    def form_invalid(self, form):
        messages.error(self.request, 'Oooops! Please correct the error below and try again!')
        return super(PostCreateView, self).form_invalid(form)

    def get_success_url(self):
        return self.object.get_absolute_url()

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            instance = form.save(commit=False)
            instance.author = self.request.user
            instance.save()
            form.save_m2m()
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

templates/form_create.html

{% extends "base.html" %}

{% block content %}

<form method="post" action="{% url 'post_create' %}" enctype="multipart/form-data">

{% csrf_token %}

{% for field in form %}
    {{ field }}
{% endfor %}

{% for formset in inlines %}

    {{ formset.management_form }}

    {% for form in formset %}
        {% for field in form %}
            {{ field }}
        {% endfor %}
    {% endfor %}

{% endfor %}

    <button type="submit" class="btn btn-primary">Submit</button>

</form>

{% endblock %}

Image in Post model is uploaded but code in PostFile model is NOT uploaded :( Could you help me ? Thanks in advance!

sdolemelipone commented 7 years ago

Hi! In the post method you have written you do not create the inline forms or call the forms_valid method. Creating the inline forms with the appropriate methods will process the file data from the inline forms submitted in the request. It looks like you've used the wrong version of post as the basis for your method. Take a look at ProcessFormWithInlinesView.post. Your code would probably work fine if you didn't override post but instead set instance.author in forms_valid().

ghost commented 7 years ago

You are right, I have used wrong post method. I created ProcessFormWithInlinesViewMixin which inherits from ProcessFormWithInlinesView and override correct post method:

models.py

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from extra_views import InlineFormSet, CreateWithInlinesView
from extra_views.advanced import ProcessFormWithInlinesView
from .models import Post, PostFile
from .forms import PostForm

class PostFileFormSet(InlineFormSet):
    model = PostFile
    fields = ['code']
    max_num = 10
    extra = 1

class ProcessFormWithInlinesViewMixin(ProcessFormWithInlinesView):
    def post(self, request, *args, **kwargs):
        form_class = self.get_form_class()
        form = self.get_form(form_class)

        if form.is_valid():
            self.object = form.save(commit=False)
            self.object.author = self.request.user
            self.object.save()
            form.save_m2m()
            messages.success(self.request, 'Post was successfully created!')
            form_validated = True
        else:
            messages.error(self.request, 'Oooops! Please correct the error below and try again!')
            form_validated = False

        inlines = self.construct_inlines()

        if all_valid(inlines) and form_validated:
            return self.forms_valid(form, inlines)
        return self.forms_invalid(form, inlines)

class PostCreateView(ProcessFormWithInlinesViewMixin, CreateWithInlinesView):
    model = Post
    form_class = PostForm
    inlines = [PostFileFormSet]
    template_name = 'form_create.html'

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(PostCreateView, self).dispatch(*args, **kwargs)

    def get_success_url(self):
        return self.object.get_absolute_url()

Now inline formsets work :) But another problem occurs. When I type wrong data into my form and I press submit button, I get this error: 'PostCreateView' object has no attribute 'object' env/lib/python3.5/site-packages/extra_views/advanced.py in construct_inlines, line 68

def construct_inlines(self):
    """
    Returns the inline formset instances
    """
    inline_formsets = []
    for inline_class in self.get_inlines():
        inline_instance = inline_class(self.model, self.request, self.object, self.kwargs, self)
        inline_formset = inline_instance.construct_formset()
        inline_formsets.append(inline_formset)
    return inline_formsets

When I type wrong data into form, then construct_inlines method tries to recreate inline formsets with typed wrong data, but object is not yet available to use (because it is CreateWithInlinesView) :( I need to override construct_inlines method too.

Thank you very much for your help.

sdolemelipone commented 7 years ago

Hi, that's happening because you're actually overriding the post method of BaseUpdateWithInlinesView which sets self.object. The code as you have written it will also create the Post object even if the inline data contains an error...are you sure you want to do that? I think the code below might be more straightforward, assuming self.object.author does not need to be set in order to validate the inlines:

class PostCreateView(CreateWithInlinesView):
    model = Post
    form_class = PostForm
    inlines = [PostFileFormSet]
    template_name = 'form_create.html'

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(PostCreateView, self).dispatch(*args, **kwargs)

    def get_success_url(self):
        return self.object.get_absolute_url()

    def forms_valid(self, form, inlines):
        """
        If the form and formsets are valid, save the associated models.
        """
        self.object = form.save(commit=False)
        self.object.author = self.request.user
        self.object.save()
        form.save_m2m()
        messages.success(self.request, 'Post was successfully created!')
        for formset in inlines:
            formset.save()
        return HttpResponseRedirect(self.get_success_url())

    def forms_invalid(self, form, inlines):
        """
        If the form or formsets are invalid, re-render the context data with the
        data-filled form and formsets and errors.
        """
        messages.error(self.request, 'Oooops! Please correct the error below and try again!')
        return self.render_to_response(self.get_context_data(form=form, inlines=inlines))
ghost commented 7 years ago

Hi, thank you for your response! Your solution works perfectly! :) If you want you can see my simple blog here: http://91.189.34.115

sdolemelipone commented 7 years ago

Very flashy! :+1: Glad that fixed it.

ghost commented 7 years ago

Thanks! But what do you mean by "flashy"? Do you think it has too much animations? I have one more question to you, but I would like not to ask you here in this issue. Could you give me your email or mail me?