Search code examples
djangodjango-modelsslug

How to add slug in Django Web-application


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:&nbsp;
              {% for category in post.categories.all %}
              <a href="{% url 'blog_category' category.name %}">
                {{ category.name }}
              </a>&nbsp;
              {% 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 %}

Solution

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

    Edit

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

    Edit 2 (for more clarification)

    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)