Search code examples
djangodjango-urlsdjango-migrations

Triyng to slugify url from title posts in a Django blog


Made the slug variable for futures urls and did ./makemigrations and migrate and the parameter appears in the admin panel, but when I try to migrate after I made an empty "theblog" migration I get this error:

class Migration(migrations.Migration):                                                                                                                                                                                                                                              
  File "xxx/theblog/models.py", line 104, in Migration                                                                                                                                                                                            
    migrations.RunPython(generate_slugs_for_old_posts, reverse=reverse_func),                                                                                                                                                                                                           
TypeError: __init__() got an unexpected keyword argument 'reverse'

changed the slug parameter from null and blank to unique but that doesn't seem to be the problem now. I know that the problem is coming from get_success_url but really don't know how to fix it.

models.py:

from django.utils.text import slugify
        
class Post(models.Model):
        title= models.CharField(max_length=100)
        header_image = models.ImageField(null=True , blank=True, upload_to="images/")
        title_tag= models.CharField(max_length=100)
        author= models.ForeignKey(User, on_delete=models.CASCADE)
        body = RichTextUploadingField(extra_plugins=
        ['youtube', 'codesnippet'], external_plugin_resources= [('youtube','/static/ckeditor/youtube/','plugin.js'), ('codesnippet','/static/ckeditor/codesnippet/','plugin.js')])
        post_date = models.DateTimeField(auto_now_add=True)
        category = models.CharField(max_length=50, default='uncategorized')
        slug = models.SlugField(unique=True)
        snippet = models.CharField(max_length=200)
        status = models.IntegerField(choices=STATUS, default=0)
        likes = models.ManyToManyField(User, blank=True, related_name='blog_posts')
    
        def save(self, *args, **kwargs):
            self.slug = self.generate_slug()
            return super().save(*args, **kwargs)
    
        def generate_slug(self, save_to_obj=False, add_random_suffix=True):
    
            generated_slug = slugify(self.title)
    
            random_suffix = ""
            if add_random_suffix:
                random_suffix = ''.join([
                    random.choice(string.ascii_letters + string.digits)
                    for i in range(5)
                ])
                generated_slug += '-%s' % random_suffix
    
            if save_to_obj:
                self.slug = generated_slug
                self.save(update_fields=['slug'])
    
            return generated_slug

def generate_slugs_for_old_posts(apps, schema_editor):
        Post = apps.get_model("theblog", "Post")
    
        for post in Post.objects.all():
            post.slug = slugify(post.title)
            post.save(update_fields=['slug'])
    
    
def reverse_func(apps, schema_editor):
        pass  # just pass
    
class Migration(migrations.Migration):
        dependencies = []
        operations = [
            migrations.RunPython(generate_slugs_for_old_posts, reverse=reverse_func),
        ]

Solution

  • First of all you must add nullable slug field to your Post model. Here is Django docs on slug field. You must implement way of generating slug values too. You can implement generate_slug(...) method on your model for example:

    import string  # for string constants
    import random  # for generating random strings
    
    # other imports ...
    from django.utils.text import slugify
    # other imports ... 
    
    class Post(models.Model):
        # ...
        slug = models.SlugField(null=True, blank=True, unique=True)
        # ...
    
        def save(self, *args, **kwargs):
            self.slug = self.generate_slug()
            return super().save(*args, **kwargs)
    
        def generate_slug(self, save_to_obj=False, add_random_suffix=True):
            """
            Generates and returns slug for this obj.
            If `save_to_obj` is True, then saves to current obj.
            Warning: setting `save_to_obj` to True
                  when called from `.save()` method
                  can lead to recursion error!
    
            `add_random_suffix ` is to make sure that slug field has unique value.
            """
    
            # We rely on django's slugify function here. But if
            # it is not sufficient for you needs, you can implement
            # you own way of generating slugs.
            generated_slug = slugify(self.title)
    
            # Generate random suffix here.
            random_suffix = ""
            if add_random_suffix:
                random_suffix = ''.join([
                    random.choice(string.ascii_letters + string.digits)
                    for i in range(5)
                ])
                generated_slug += '-%s' % random_suffix
    
            if save_to_obj:
                self.slug = generated_slug
                self.save(update_fields=['slug'])
            
            return generated_slug
    

    Now on every object save you will automatically generate slug for your object. To deal with old posts that do not have slug field set. You must create custom migration using RunPython (Django docs):

    First run this command

    python manage.py makemigrations <APP_NAME> --empty
    

    Replace <APP_NAME> with your actual app name where Post model is located. It will generate an empty migration file:

    from django.utils.text import slugify
    from django.db import migrations
    
    def generate_slugs_for_old_posts(apps, schema_editor):
        Post = apps.get_model("<APP_NAME>", "Post")  # replace <APP_NAME> with actual app name
    
        # dummy way
        for post in Post.objects.all():
            # Do not try to use `generate_slug` method
            # here, you probably will get error saying
            # that Post does not have method called `generate_slug`
            # as it is not the actual class you have defined in your
            # models.py!
            post.slug = slugify(post.title)
            post.save(update_fields=['slug'])
    
        
    
    def reverse_func(apps, schema_editor):
        pass  # just pass
    
    class Migration(migrations.Migration):
    
        dependencies = []
    
        operations = [
            migrations.RunPython(generate_slugs_for_old_posts, reverse=reverse_func),
        ]
    

    After that point you may alter you slug field and make it non nullable:

    class Post(models.Model):
        # ...
        slug = models.SlugField(unique=True)
        # ... 
    

    Now python manage.py migrate, this will make slug fields non nullable for future posts, but it may give you a warning saying that you are trying to make existing column non nullable. Here you have an option where it says "I have created custom migration" or something like that. Select it.

    Now when your posts have slugs, you must fix your views so they accept slug parameters from url. The trick here is to make sure that your posts are also acceptable by ID. As someone already may have a link to some post with ID. If you remove URLs that takes an ID argument, then that someone might not be able to use that old link anymore.