Here is the problem: I'm trying to 'fresh' my django web blog, so instead of having /post/2/
I want to have slugged link that's exactly like my title (smth like this: /post/today-is-friday
Here is some code, I've tried couple of things, but there is nothing working:
models.py
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.urls import reverse
from django.template.defaultfilters import slugify
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
categories = models.ManyToManyField('Category', related_name='posts')
image = models.ImageField(upload_to='images/', default="images/None/no-img.jpg")
slug= models.SlugField(max_length=500, unique=True, null=True, blank=True)
def save(self, *args, **kwargs):
self.url= slugify(self.title)
super(Post, self).save(*args, **kwargs)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})
class Category(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
urls.py
from django.urls import path
from django.conf.urls import include, url
from . import views
from .views import PostListView, PostDetailView, PostCreateView, PostUpdateView, PostDeleteView, UserPostListView
urlpatterns = [
#Blog section
path("", PostListView.as_view(), name='blog-home'),
path("user/<str:username>", UserPostListView.as_view(), name='user-posts'),
path("<slug:slug>/", PostDetailView.as_view(), name='post-detail'),
path("post/new/", PostCreateView.as_view(), name='post-create'),
path("<slug:slug>/update/", PostUpdateView.as_view(), name='post-update'),
path("<slug:slug>/delete/", PostDeleteView.as_view(), name='post-delete'),
path("about/", views.about, name="blog-about"),
path("<category>/", views.blog_category, name="blog_category"),
]
user_posts.html(this is for accessing blog post itself)
{% extends 'blog/base.html' %}
{% block content %}
<h1 class='mb-3'>Post by {{ view.kwargs.username }} ({{ page_obj.paginator.count }})</h1>
{% for post in posts %}
<article class="media content-section">
<img class="rounded-circle article-img" src="{{ post.author.profile.image.url }}" alt="">
<div class="media-body">
<div class="article-metadata">
<a class="mr-2 author_title" href="{% url 'user-posts' post.author.username %}">@{{ post.author }}</a>
<small class="text-muted">{{ post.date_posted|date:"N d, Y" }}</small>
<div>
<!-- category section -->
<small class="text-muted">
Categories:
{% for category in post.categories.all %}
<a href="{% url 'blog_category' category.name %}">
{{ category.name }}
</a>
{% endfor %}
</small>
</div>
</div>
<h2><a class="article-title" href="{% url 'post-detail' post.id %}">{{ post.title }}</a></h2>
<p class="article-content">{{ post.content|slice:200 }}</p>
</div>
</article>
{% endfor %}
{% endblock content %}
post_form.html(It's for creating a new post, have trouble with redirecting after post created)
{% extends 'blog/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="content-section">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Blog Post</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group">
<button class="btn btn-outline-info" type="submit">Post</button>
</div>
</form>
</div>
{% endblock content %}
If you are going to change the value of the slug
field before saving, you can use signals.
Also the slugify
method of django is located in django.utils.text
not django.template.defaultfilters
.
urls.py
# ...
path('post/<slug:slug>/', PostDetailView.as_view(), name='post-detail'),
# ...
models.py
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils.text import slugify
import string
class Post(models.Model):
# ...
slug= models.SlugField(max_length=500, unique=True, null=True, blank=True)
# do not override save method here
def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
def unique_slug_generator(instance, new_slug=None):
if new_slug is not None:
slug = new_slug
else:
slug = slugify(instance.title)
class_ = instance.__class__
qs_exists = class_.objects.filter(slug=slug).exists()
if qs_exists:
new_slug = f"{slug}-{random_string_generator(size=5)}"
return unique_slug_generator(instance, new_slug=new_slug)
return slug
@receiver(pre_save, sender=Post)
def post_pre_save_receiver(sender, instance, *args, **kwargs):
if not instance.slug:
instance.slug = unique_slug_generator(instance)
Those two functions, unique_slug_generator
and random_string_generator
, together will guaranty that you won't have same slug on two posts, even if the title of those posts are same! (it will add some randomly generated string at the end)
In your html template for user_posts.html
, replace
<h2><a class="article-title" href="{% url 'post-detail' post.id %}">{{ post.title }}</a></h2>
with
<h2><a class="article-title" href="{% url 'post-detail' post.slug %}">{{ post.title }}</a></h2>
Also, in your view (not template) of post_form
, you should override get_success_url
like this:
def get_success_url(self):
return reverse('post-detail', kwargs={'slug': self.object.slug})
First, we need a url for each post, we implement it as:
path('post/<slug:slug>/', PostDetailView.as_view(), name='post-detail'),
Next, you should change your previous links to the post-detail
. These include your 1)template links and 2)links in the views/models:
In your templates, wherever you have {% url 'post-detail' post.pk %}
, you should change that to {% url 'post-detail' post.slug %}
.
and in your views/models, you should change reverse('post-detail', kwargs={'pk': self.pk})
tp reverse('post-detail', kwargs={'slug': self.slug})
(not self.object.slug
)