0

I am creating a blog in Django and I'm stuck at adding writing post_update_view, where form.is_valid() returns False and I'm not able to update the post.

I've also checked this but I could not navigate to the right spot.

Also, it's weird as for post creation form.is_valid() works fine (I'm assigning "form = PostModelForm(request.POST or None)" there).

I would appreciate any suggestions.

Below is:

Views.py

def post_update_view(request, slug):
    obj = get_object_or_404(Post, slug=slug)
    form = PostModelForm(request.POST or None, instance=obj)
    print(form.is_valid())
    if form.is_valid():
        form.save()
    template_name = 'blog/post_update.html'
    context = {'form': form, 'title': f'Update {obj.title}'}
    return render(request, template_name, context)

models.py:

from django.conf import settings
from django.db import models

User = settings.AUTH_USER_MODEL

class Post(models.Model):
    CATEGORY_CHOICES = (
        (None, '--------'),
        ('CB', 'City Break'),
        ('1W', 'Week holidays'),
        ('2W', 'Two weeks holidays'),
        ('MO', 'Month holidays'),
        ('SB', 'Sabbatical'),
    )

    CONTINENT_CHOICES = (
        (None, '--------'),
        ('AS', 'Asia'),
        ('AU', 'Australia and Oceania'),
        ('AF', 'Africa'),
        ('EU', 'Europe'),
        ('NA', 'North America'),
        ('SA', 'South America'),
    )

    user = models.ForeignKey(
        User,
        default=1,
        null=True,
        on_delete=models.SET_NULL
    )
    title = models.CharField(max_length=150)
    date = models.DateField()
    category = models.CharField(
        max_length=100,
        choices=CATEGORY_CHOICES,
        blank=True
    )
    continent = models.CharField(
        max_length=100,
        choices=CONTINENT_CHOICES,
        blank=True
    )
    body = models.TextField()
    slug = models.SlugField()

    def __str__(self):
        return self.slug

forms.py:

from django import forms
from blog.models import Post


class PostModelForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'date', 'category', 'continent', 'slug', 'body']

    def clean_slug(self):
        instance = self.instance
        slug = self.cleaned_data.get('slug')
        qs = Post.objects.filter(slug__iexact=slug)
        qs_title_id = 0
        if instance is not None:
            qs = qs.exclude(pk=instance.pk)
        if qs.exists():
            raise forms.ValidationError(f"Slug has already been used in "
                                        f"'{qs[qs_title_id]}'.\n Please "
                                        f"change.")
        return slug

templates

{% extends "site_general/header.html" %}

{% block content %}
    {% if title %}
        <h1>{{ title }}</h1>
    {% endif %}

    <form method="POST" action="."> {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">Update</button>
    </form>

{% endblock %}

urls

from django.urls import path
from .views import (
    post_delete_view,
    post_detail_view,
    post_list_view,
    post_update_view,
)

urlpatterns = [
    path('', post_list_view),
    path('<str:slug>/', post_detail_view),
    path('<str:slug>/edit', post_update_view),
    path('<str:slug>/delete', post_delete_view),
]

Gregory
  • 3
  • 2
  • 1
    print `form.errors` and add the form errors. – Nalin Dobhal Jan 23 '20 at 12:52
  • Please post your template and urls too. – bruno desthuilliers Jan 23 '20 at 13:13
  • 1
    Where is the unbound form for the initial GET coming from? Unbound forms are never valid. – nigel222 Jan 23 '20 at 13:18
  • This probably won't solve your problem, but [here is what your view should look like](https://pastebin.com/PrjrAdSS). – bruno desthuilliers Jan 23 '20 at 13:22
  • Bruno - added urls and templates as requested – Gregory Jan 23 '20 at 14:42
  • Nalin - I've added print(form.errors) right before return in post_update_view but it only prints blank line – Gregory Jan 23 '20 at 14:49
  • @Gregory note that an unbound form will never validate AND that it's `.errors` dict will be empty, so the fact that your current snippet always prints `False` and that `print(form.errors)` prints an empty dict is perfectly normal and expected. I __strongly__ suggest you start by fixing your view code (cf my pastebin snippet). Then, if you still cannot validate your form, this time it should be because it's REALLY invalid, and then you'll see the error messages. – bruno desthuilliers Jan 23 '20 at 15:01
  • @Bruno - i will read more about unbound forms. I am just surprised as i'm executing similar line when creating a post and validation is positive (code: def post_create_view(request): form = PostModelForm(request.POST or None) if form.is_valid(): obj = form.save(commit=False) obj.user = request.user obj.save() form = PostModelForm() template_name = 'blog/post_create.html' context = {'form': form} return render(request, template_name, context). It looks like add 'instance=obj' messes around ;/ – Gregory Jan 23 '20 at 16:24
  • @Gregory I'm not saying there aren't other issues, just that your current code will always report the form as invalid when doing a GET request. So first get rid of this issue, so you can _properly_ check what happens ;-) – bruno desthuilliers Jan 23 '20 at 16:27
  • It looks like it was enough to save context inside "if form.is_valid()" i.e.: ``` def post_update_view(request, slug): obj = get_object_or_404(Post, slug=slug) form = PostModelForm(request.POST or None, instance=obj) print(form.is_valid()) if form.is_valid(): form.save() context = {'form': form, 'title': f'Post has been updated'} else: context = {'form': form, 'title': f'Update "{obj.title}"'} template_name = 'blog/post_update.html' return render(request, template_name, context) ``` But, I'm not crystal clear why - do you know? – Gregory Jan 24 '20 at 13:14

1 Answers1

0

Too long for a comment, and I'm not sure whether this will solve the problem, but bruno desthuillers has linked to "what a view should look like". The important thing to note is that there is one code path for an unbound form (URL accessed via HTTP GET) and a different one for data submitted from the browser by HTTP POST.

I have always detested the way that canonical form sandwiches the processing of validated form data with boilerplate on both sides, and would suggest negating the validity test, and using a little Python magic to combine the form instantiation for both GET and POST cases:

def post_update_view(request, slug):
    obj = get_object_or_404(Post, slug=slug)
    args = [request.POST] if request.method == "POST" else []
    form = PostModelForm(*args, instance=obj)
    if request.method != "POST" or not form.is_valid(): 
        # GET or not valid 
        context = {'form': form, 'title': f'Update {obj.title}'}
        return render(request, 'blog/post_update.html', context)

    # the form validated, do the work and redirect
    form.save()
    return redirect("wherever you want, but redirect")

If this isn't the answer, replace your template with the simplest possible {{ form.as_p }} and a submit button, and see whether that fixes it.

nigel222
  • 7,582
  • 1
  • 14
  • 22