Django FileField not respecting null=False parameter - python

I have a Django FileField which is set to not nullable. However it is behaving like it is nullable.
class Thing(models.Model):
document = models.FileField(null=False, blank=False)
thing_id = models.CharField()
# This does not raise but I would like it to
Thing.objects.create(thing_id='123')
edit: the migrations
class Migration(migrations.Migration):
dependencies = [
('data_source', '0002_auto_20190212_1913'),
]
operations = [
migrations.CreateModel(
name='Thing',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('document', models.FileField(upload_to='')),
('thing_id', models.CharField(default=None, max_length=200)),
],
options={
'abstract': False,
},
),
]

The problem is not with your field definition. It's got to do with validation when manually creating a model instance. You need to perform that full validation yourself, since you're not using a ModelForm.
Use this example:
from django.core.exceptions import ValidationError
thing = Thing(thing_id='123')
try:
thing.full_clean()
thing.save()
except ValidationError:
# Handle validation issues.
Read this section in the documentation: https://docs.djangoproject.com/en/2.1/ref/models/instances/#validating-objects
Quoting from there:
Note that full_clean() will not be called automatically when you call
your model’s save() method. You’ll need to call it manually when you
want to run one-step model validation for your own manually created
models

You can add the validation directly in your model overriding save() method:
class Thing(models.Model):
document = models.FileField(null=False, blank=False, default=None)
thing_id = models.CharField(max_length=200)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)

By default null and blank are False. Link for every model field. Don't need to add those.
from django.db import models
class Thing(models.Model):
document = models.FileField()
thing_id = models.CharField()

Related

How to set Django constraints to allow one enabled (BooleanField) object per item? [duplicate]

Here I am not deleting the model objects from database. I am just changing the is_deleted status to True while delete. But while doing this unique=True gives error for the deleted objects so how can I handle this ?
I want to exclude is_deleted=True objects from unique True.
class MyModel(models.Model):
name = models.CharField(max_length=20, unique=True)
is_deleted = models.BooleanField(default=False)
#views
class MyViewSet(ModelViewSet):
serializer_class = MySerializer
queryset = MyModel.objects.all()
def destroy(self, request, *args, **kwargs):
object = self.get_object()
object.is_deleted = True
object.save()
return Response(status=status.HTTP_204_NO_CONTENT)
You can use a UniqueConstraint [Django docs] with a condition instead of the unique kwarg on the field. Although there is a caveat that validation (by forms etc.) will not be done automatically for a unique constraint with a condition and you will need to do that yourself.
from django.db.models import Q
class MyModel(models.Model):
name = models.CharField(max_length=20)
is_deleted = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(fields=['name'], condition=Q(is_deleted=False), name='unique_undeleted_name')
]
Note: Since Django 4.1 validation is automatically performed for all constraints using the Model.validate_constraints method and hence the above mentioned caveat doesn't apply.
Since django-2.2, you can work with Django's constraint API, you can then specify a UniqueConstraint [Django-doc] that has a condition:
class MyModel(models.Model):
# no unique=True ↓
name = models.CharField(max_length=20)
is_deleted = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['name'],
name='unique_name_not_deleted',
condition=Q(is_deleted=False)
)
]
It is of course the database that enforces this, and thus some databases might not have implemented that feature.
You may use following also:
class Meta:
unique_together = ("name", "is_deleted")

Migrate from multi-table inheritance model to abstract base classes in Django

My current project uses multi-table inheritance models:
from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
class Cinema(Place):
sells_tickets = models.BooleanField(default=False)
sells_popcorn = models.BooleanField(default=False)
I want to switch to abstract base classes instead. Since my model is already deployed I need to write some custom migrations to convert the above schema to this one:
from django.db import models
class AbstractPlace(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Meta:
abstract = True
class Restaurant(AbstractPlace):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
class Cinema(AbstractPlace):
sells_tickets = models.BooleanField(default=False)
sells_popcorn = models.BooleanField(default=False)
Does anyone have any advice on the steps to take to achieve this?
I recently tackled this exact problem, which I solved by writing and running the migration in the code block below - loosely translated to fit the models in your case.
I'm pretty sure that it's not possible to alter the tables of the old Restaurant and Cinema models directly, as if you try to add fields to them, they will collide with the existing fields of the base model, and if you try to "decouple" the derived models from the base model by e.g. by manually setting abstract=True in the base model's options, Django reports that it's unable to find the base models of Restaurant and Cinema. (These issues might be caused by a bug, for all I know.) To circumvent this problem, I created new tables for the derived models, copied the data from the old tables to the new ones, deleted the old tables, and renamed the new tables to match the names of the old ones.
I got large parts of the code below from code generated by Django, which can be reproduced by creating a temporary migration (before creating one with the code below) which only deletes Restaurant, Cinema and Place, running makemigrations, and copying the CreateModel()s and AlterField()s (for related fields pointing to Restaurant or Cinema) from the generated migration.
For the record, I'm using Django 3.1.4.
from django.db import migrations, models
def copy_objects_from_restaurant_and_cinema_to_restaurant_tmp_and_cinema_tmp(apps, schema_editor):
Restaurant_Tmp = apps.get_model('<app name>', 'Restaurant_Tmp')
Cinema_Tmp = apps.get_model('<app name>', 'Cinema_Tmp')
Restaurant = apps.get_model('<app name>', 'Restaurant')
Cinema = apps.get_model('<app name>', 'Cinema')
# The `_meta.fields` list includes the PK
copy_objects_from_old_model_to_new_model(Restaurant, Restaurant_Tmp, Restaurant_Tmp._meta.fields)
copy_objects_from_old_model_to_new_model(Cinema, Cinema_Tmp, Cinema_Tmp._meta.fields)
def copy_objects_from_old_model_to_new_model(old_model, new_model, fields_to_copy):
field_names = [field.name for field in fields_to_copy]
for old_obj in old_model.objects.all():
old_obj_field_dict = {
field_name: getattr(old_obj, field_name)
for field_name in field_names
}
new_model.objects.create(**old_obj_field_dict)
def copy_objects_from_restaurant_tmp_and_cinema_tmp_to_restaurant_and_cinema(apps, schema_editor):
Restaurant_Tmp = apps.get_model('<app name>', 'Restaurant_Tmp')
Cinema_Tmp = apps.get_model('<app name>', 'Cinema_Tmp')
Restaurant = apps.get_model('<app name>', 'Restaurant')
Cinema = apps.get_model('<app name>', 'Cinema')
copy_objects_from_old_model_to_new_model(Restaurant_Tmp, Restaurant, Restaurant_Tmp._meta.fields)
copy_objects_from_old_model_to_new_model(Cinema_Tmp, Cinema, Cinema_Tmp._meta.fields)
class Migration(migrations.Migration):
dependencies = [
('<app name>', '<last migration>'),
]
operations = [
migrations.CreateModel(
name='Restaurant_Tmp',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('address', models.CharField(max_length=80)),
('serves_hot_dogs', models.BooleanField(default=False)),
('serves_pizza', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Cinema_Tmp',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('address', models.CharField(max_length=80)),
('sells_tickets', models.BooleanField(default=False)),
('sells_popcorn', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
migrations.RunPython(copy_objects_from_restaurant_and_cinema_to_restaurant_tmp_and_cinema_tmp, migrations.RunPython.noop),
# Update foreign keys to reference the non-abstract models directly,
# instead of through the (automatically generated) `place_ptr` field of the old models
<
Run `migrations.AlterField()` here for each related field (like ForeignKey) of other models that point to Restaurant or Cinema,
but change their `to` argument from e.g. `<app name>.restaurant` to `<app name>.restaurant_tmp`
>
migrations.RunPython(migrations.RunPython.noop, copy_objects_from_restaurant_tmp_and_cinema_tmp_to_restaurant_and_cinema),
migrations.DeleteModel(
name='Restaurant',
),
migrations.DeleteModel(
name='Cinema',
),
migrations.DeleteModel(
name='Place',
),
migrations.RenameModel(
old_name='Restaurant_Tmp',
new_name='Restaurant',
),
migrations.RenameModel(
old_name='Cinema_Tmp',
new_name='Cinema',
),
]
Note that the migration I originally wrote was only tested to work using SQLite; other database management systems might not accept such a large variety of migration operations, and you might have to split it into multiple migrations. (I'm somewhat unsure what exactly could cause this problem, but I can recall that I've experienced it with PostgreSQL.)
Please let me know if this solves your problem! 😊

How to use a user as a ForeignKey in a Django model

I've got this model in my Django application:
class ClubSession(models.Model):
location = models.CharField(max_length=200)
coach = models.ForeignKey('auth.User', on_delete=models.CASCADE)
date = models.DateTimeField(default=now)
details = models.TextField()
def __str__(self):
return self.title
I can run python manage.py makemigrations club_sessions without issue but when I thn run python manage.py migrate club_sessions I get ValueError: Field 'id' expected a number but got 'username'. username is a superuser and already exists.
How do I resolve this?
This is the latest migration:
# Generated by Django 3.0.6 on 2020-05-28 15:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('club_sessions', '0004_auto_20200528_1450'),
]
operations = [
migrations.AlterField(
model_name='clubsession',
name='coach',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='clubsession',
name='location',
field=models.CharField(max_length=200),
),
]
By default Django lets a ForeignKey refer to the primary key of the target model. This also has some advantages to make relations more uniform.
If you really want to save the username in the ForeignKey, you can specify a to_field=… parameter [Django-doc] and let it refer to a column that is unique (the username of the default User model is unique), so we can refer to it with:
from django.conf import settings
class ClubSession(models.Model):
location = models.CharField(max_length=200)
coach = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
to_field='username'
)
date = models.DateTimeField(default=now)
details = models.TextField()
def __str__(self):
return self.title
You will need to remove the already existing migration and make a new one in order to migrate the database properly.
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

django rest framework - foreign key fields are read only

The model:
class Item(models.Model):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
item_num = models.IntegerField()
# other fields...
class Meta:
unique_together = [('company', 'item_num') ]
Serializer:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('company_id', 'item_num', )
The problem is that django rest framework generates a ReadOnlyField() for the company_id field, so this field is not editable when I create a new instance in view code like this:
s = ItemSerializer(data=request.POST)
s.save()
I also lose the default UniqueTogetherValidator that is defined in the model.
Though, if I change the serializer field name from 'company_id' to 'company', I do get the validator, as drf will generate PrimaryKeyRelatedField so it will be editable.
How can I still name my foreign key objects like 'company_id', because I do prefer naming them like this, and still get the default validation and saving behavior? Preferably without adding to much code to the serializer.
company_id is a read-only field because it lacks a proper definition in the Model for the serializer to understand it.
Steps to get it working:
Add an explicit field definition
Add the constraint
Serializer would be:
class ItemSerializer(serializers.ModelSerializer):
company_id = serializers.PrimaryKeyRelatedField(source=company, queryset=Company.objects.all())
class Meta:
model = Item
fields = ('company_id', 'item_num', )
validators = [
UniqueTogetherValidator(
queryset=Item.objects.all(),
fields=('company_id', 'item_num')
)
]

Django migration not calling overridden save() method?

I have a model SessionCategory which is similar to the following:
from django.db import models
from django.utils.text import slugify
class SessionCategory(models.Model):
name = models.CharField(max_length=255, unique=True)
name_slug = models.CharField(max_length=255, null=True)
def save(self, *args, **kwargs):
if not self.name_slug:
self.name_slug = slugify(self.name)
super().save(*args, **kwargs)
So the name_slug field, which I'd like to add, is a slugified version of the name field.
I've run the following data migration:
from __future__ import unicode_literals
from django.db import migrations, models
def generate_name_slugs(apps, schema_editor):
SessionType = apps.get_model('lucy_web', 'SessionType')
for session_type in SessionType.objects.all():
session_type.save()
class Migration(migrations.Migration):
dependencies = [
('lucy_web', '0163_auto_20180627_1309'),
]
operations = [
migrations.AddField(
model_name='sessioncategory',
name='name_slug',
field=models.CharField(max_length=255, null=True),
),
migrations.RunPython(
generate_name_slugs,
reverse_code=migrations.RunPython.noop),
]
However, if I check the database afterward, the name_slug fields are all null:
I've also reversed the migration and re-run it setting a trace (import ipdb; ipdb.set_trace()) in the overridden save() method, but it didn't cause Python to drop into the debugger, confirming that that method is not called.
Why is the overridden save() method not getting called? Do I have to replicate the code in the generate_name_slugs function?
This should help for SessionType... SessionCategory can be modified the same way...
def generate_name_slugs(apps, schema_editor):
import lucy_web.models as m
for session_type in m.SessionType.objects.all():
session_type.save()

Categories