Search code examples
pythondjangoslug

Django: You are trying to add a non-nullable field 'slug' to post without a default; we can't do that


I'm trying to add slug so that the title of a post appears in the url but i get this message on console:

You are trying to add a non-nullable field 'slug' to post without a default; we can't do that Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

models.py

class Post(models.Model):
    
    title = models.CharField(max_length=100)   
    date_posted = models.DateTimeField(default=timezone.now)    
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    slug = models.SlugField(unique=True)

    def save(self, *args, **kwargs):
        self.slug = self.slug or slugify(self.title)
        super().save(*args, **kwargs)

Solution

  • Django can't add a new field by itself without null values being allowed here, especially when you have unique=True set on this field. To solve that issue, you have to perform it in steps:

    1. Add a column with null=True or without unique=True and with some default value
    2. Make sure that all records in database will have a unique value.
    3. Change the field to the final state.

    All of those operations you can do with 3 migrations. Below are the detailed steps to do it. Before proceeding, make sure to remove all migrations created in previous attempts to solve that issue. You may have to undo those migrations from environments on which you were able to successfully apply them.

    1. Add a column with null=True or without unique=True and with some default value

    You can let Django create this migration for you. Simply edit your field to look like:

        slug = models.SlugField(unique=True, null=True)
    

    And run ./manage.py makemigrations after doing that.

    2. Make sure that all records in database will have a unique value.

    This step has to be crafted by hand to some extent. Start with asking Django to create new, empty migration for you by invoking ./manage.py makemigrations YOUR_APP_NAME --empty. Now open this new migration file and add this code (adjusted accordingly to your needs, especially make sure to generate unique values for every single record) before the Migration class:

    def populate_posts_slug_field(apps, schema_editor):
        for post in apps.get_model("YOUR_APP_NAME", "post").objects.all():
            post.slug =  # generate a unique value for every post here
            post.save()
    

    Now, add an operation that will run code defined above when executing this migration. To do that, add this entry into the operations list in Migration class of your migration:

    migrations.RunPython(populate_posts_slug_field, migrations.RunPython.noop),
    

    (2nd argument for RunPython is a special function that just does absolutely nothing, it will be executed when you want to unapply this migration).

    3. Change the field to the final state.

    This also can be handled by Django itself. Change your field to the final state (as in your question) and run ./manage.py makemigrations once again.

    You're all set. Running ./manage.py migrate should succeed now.

    Note: it is possible to run all 3 operations in single migration file, but it should be avoided. Running both data and schema changes in one migration can cause problems on some database backends, so it should be avoided entirely.