9

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)
Eric
  • 673
  • 1
  • 7
  • 23
  • i'm getting this error now "django.db.utils.IntegrityError: UNIQUE constraint failed: new__blog_post.slug" – Eric Mar 24 '21 at 15:50
  • Does this answer your question? [Updating django models](https://stackoverflow.com/questions/29108200/updating-django-models) – Kemp Mar 24 '21 at 15:56
  • How do you 'Populate existing rows'? this is step 3 of Celcuk's answer because It successfully migrated but it showed the same error after removing 'null=True' – Eric Mar 24 '21 at 16:05
  • You have to go and enter the values you need for existing rows, or potentially write a script to update the values for you. There isn't a built-in idea of what the slug for existing rows should be. – Kemp Mar 24 '21 at 16:12

4 Answers4

13

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.

GwynBleidD
  • 20,081
  • 5
  • 46
  • 77
  • 2
    This is a great summary. For another angle, [the official docs cover the same 3 steps](https://docs.djangoproject.com/en/4.1/howto/writing-migrations/#migrations-that-add-unique-fields). – Bluu Feb 15 '23 at 23:15
  • With Django 4.1 the line `for post in apps.get_model('YOUR_APP_NAME.post'):` causes this error: `TypeError: 'ModelBase' object is not iterable` The link suggested by @bluu gives the right code for Django 4.1: `MyModel = apps.get_model('myapp', 'MyModel') for row in MyModel.objects.all():` – jeromecc Apr 13 '23 at 13:41
  • Thank you for the feedback @jeromecc, indeed the code example wasn't right. I've updated the answer. – GwynBleidD Apr 14 '23 at 14:52
4

It mean you try add a new column in database but you have another old data existing in table Post. So old data don't have value for column slug.

In this case you need:

  • Add null=True, slug = models.SlugField(unique=True, null=True).
  • Migrate your database py manage.py makemigrations and py manage.py migrate .
  • Fill data of slug for existing rows.
  • Remove null=True, slug = models.SlugField(unique=True)
  • Migrate again.
Dharman
  • 30,962
  • 25
  • 85
  • 135
Nguyễn Vũ Thiên
  • 761
  • 1
  • 9
  • 19
2

Try a turnaround, go to models.py and set "default" as an empty string:

author = models.ForeignKey(User, on_delete=models.CASCADE, default="")

Run makemigrations on the shell. Go back to models.py and remove the default. Run makemigrations again. That should have fixed it.

ana_london
  • 74
  • 2
1

If you do not care about your existing data, nor your migration history, the easiest solution is to delete it all and start fresh.

  1. delete the database
  2. delete your migrations
  3. run ./manage.py makemigrations
  4. run ./manage.py migrate
Derek Adair
  • 21,846
  • 31
  • 97
  • 134