-2

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),
        ]
Gerard
  • 47
  • 8

2 Answers2

1

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.

vagifm
  • 397
  • 2
  • 10
  • Looks easy to follow but I'm not getting the correct order to proceed, not a programmer myself. I'm getting errors. I have to make an empty migration with the generate_slug func and then replace it with the one for old_posts and everything? – Gerard Oct 13 '20 at 16:24
  • Which errors do you get? Can you update you question, with what you have done? – vagifm Oct 14 '20 at 07:23
  • Never mind. If you think that my answered helped you, you can mark it as accepted too. – vagifm Oct 15 '20 at 06:48
1

you may also redirect old urls to the new ones

urls.py

[..]

from django.views.generic.base import RedirectView

urlpatterns = [

    # Redirect old links:
    path('article/<int:pk>', RedirectView.as_view(url='article/<slug:url>', permanent=True)),

    # You won't need this path any more
    # path('article/<int:pk>', ArticleDetailView.as_view(), name="article-detail"),
    
    # The new path with slug
    path('article/<slug:url>', ArticleDetailView.as_view(), name="article-detail"),

    [..]

]

refer to https://docs.djangoproject.com/en/3.1/ref/class-based-views/base/#redirectview

cizario
  • 3,995
  • 3
  • 13
  • 27