Django 3 CheckConstraints m2m field - django

I would like to add database constraints to my model, that require at least one of its fields to be not-null. When checking the m2m field, I get a FieldError: Cannot resolve keyword '' into field.
Is it possible to create such constraints?
Sample code:
class A(Model):
id = AutoField()
url = ManyToManyField(Url, blank=True)
description = TextField(null=True, blank=True)
class Meta:
constraints = [CheckConstraints(
check=(Q(description__isnull=False) | Q(url__isnull=False))),
name="someName"
)]

It is not possible to achieve this using the CheckConstraint functionality. Django translates all ORM commands to the low level DB specific commands, and such constraint creation isn't possible even on the DB level. In fact, we can apply CheckConstraint to a single row beeing added/updated only.
See full answer here:
https://stackoverflow.com/a/60799459/15090285

Related

Django admin: Editing records with unique fields fails

Python 3.9, Django 3.2, Database is PostgreSQL hosted on ElephantSQL.
I have a model with a slug field which I have set to unique:
class website_category(models.Model):
fld1 = models.CharField(primary_key=True, max_length=8)
fld2 = models.TextField()
fld3 = models.SlugField(unique=True, db_index=True, max_length=100)
I can create new records for this model without any issue. However, when I try to edit an already existing record via the Django admin interface (e.g., change the text field fld2), Django throws this error:
website_category with this fld3 already exists
I can delete said record and re-enter the modified one without any issues, and I can edit the record if I change the slug field but not otherwise.
My guess is this is happening due to the "unique=True" parameter set in the slug field (fld3). However, I do want the slugs to be unique.
Is this an expected behavior of Django, or can I do something to make it possible for me to edit the records directly without having to delete and recreate them?
====
Edit: Additional Info
the model does not have any custom save method or ModelAdmin class. It is registered simply via admin.site.register(). The model does have a meta class which is being used to define some DB level constraints:
class Meta:
constraints = [
models.CheckConstraint(check=models.Q(fld1__iregex = r'^\w{8}$'),
name="%(app_label)s_%(class)s_id_validator"),
models.CheckConstraint(check=models.Q(fld3__iregex = r'^[\w-]+$'),
name="%(app_label)s_%(class)s_slug_validator"),
# DB level condition to enforce slug format (word characters and '-')
]

Django (DRF) ManyToMany field choices / limit

Working with Django REST Framework I am wondering if it's possible to limit the choices / options of a ManyToMany field on a model to a specific QuerySet?
Using the models below (scroll down to see models), I am curious if it's possible to define this limit in the definition of the model, to achieve the following:
# Having the following Employee instance
emp = Employee(...)
# Should return only the instances with value 'case' in EmployeeSubstitute.type field
emp.substitute_case.all()
# Should return only the instances with value 'phone' in EmployeeSubstitute.type field
emp.substitute_phone.all()
Models:
class Employee(models.Model):
substitute_case = models.ManyToMany(through=EmployeeSubstitute, ...)
substitute_phone = models.ManyToMany(through=EmployeeSubstitute, ...)
class EmployeeSubstitute(models.Model):
from = models.ForeignKey(Employee, ...)
to = models.ForeignKey(Employee, ...)
type = models.CharField(choices=..., ...) # choose between type 'case' and 'phone'
I see that there's the limit_choices_to parameter, but that's not what I am looking for, since that only effects the options shown when using a ModelForm or the admin.
Well, ManyToManyField returns related objects and as docs state
By default, Django uses an instance of the Model._base_manager manager
class when accessing related objects (i.e. choice.question), not the
_default_manager on the related object. This is because Django needs to be able to retrieve the related object, even if it would otherwise
be filtered out (and hence be inaccessible) by the default manager.
If the normal base manager class (django.db.models.Manager) isn’t
appropriate for your circumstances, you can tell Django which class to
use by setting Meta.base_manager_name.
Base managers aren’t used when querying on related models, or when
accessing a one-to-many or many-to-many relationship. For example, if
the Question model from the tutorial had a deleted field and a base
manager that filters out instances with deleted=True, a queryset like
Choice.objects.filter(question__name__startswith='What') would include
choices related to deleted questions.
So if I read it correctly, no, it's not possible.
When you do queries and have through in your ManyToManyField, Django complains you should run these queries on your through model, rather than the "parent". I can't find it in the docs but I remember seeing it a few times.
substitute_case and substitute_phone is something that belongs to substitute and it is it's type. So just do that instead of creating those columns in Employee.
from django.db import models
class SubstituteTypes(models.TextChoices):
case = "case", "case"
phone = "phone", "phone"
class EmployeeSubstituteQueryset(models.QuerySet):
def from_employee(self, e):
return self.filter(_from=e)
def case(self):
return self.filter(type=SubstituteTypes.case)
def phone(self):
return self.filter(type=SubstituteTypes.phone)
class Employee(models.Model):
substitute = models.ManyToManyField(through='EmployeeSubstitute', to='self')
class EmployeeSubstitute(models.Model):
_from = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='a')
to = models.ForeignKey(Employee, on_delete=models.PROTECT, related_name='b')
type = models.CharField(choices=SubstituteTypes.choices, max_length=5, db_index=True)
objects = EmployeeSubstituteQueryset.as_manager()
Then, once you get your emp object (or only its id), you can do
EmployeeSubstitute.objects.from_employee(emp).case().all()
which is designed in Django philosophy.

Why can I save a django model instance without defining all non-null fields

When I define a non nullable field in django it allows me to save a model instance without specifying any value for this non-nullable field. This is not what I would expect. How can I force this to yield an error?
Postgres 9.1
django 2.1
windows
python 3.6
from django.db import models
class Wwtp(models.Model):
name = models.CharField(max_length=100, null=False,
blank=False, unique=True)
short_name = models.CharField(
max_length=10, null=False, blank=False, unique=True)
As expected, I am not allowed to save it with an explicit empty short_name.
mdl.Wwtp.objects.create(name='Wwtp4', short_name=None)
But I am allowed to save an instance of Wwtp without specifying short_name:
mdl.Wwtp.objects.create(name='Wwtp4')
and when I try:
mdl.Wwtp.objects.create()
it gives me
django.db.utils.IntegrityError: duplicate key value violates unique constraint "api_wwtp_short_name_key"
DETAIL: Key (short_name)=() already exists.
Apparently django filled the database with an empty value for short_name while it is not allowed to do so... How can I force the database to not allow this?
You can't with CharField. The empty value is an empty string '', not NULL. You already have blank=False set, so if you clean your model or model forms before saving them, you'll catch that. But it cannot be enforced at the database level.
Note that blank=False, null=False is the default, so you really don't have to specify that.
Also, if you really only want to support PostgreSQL, you could make a custom migration using RunSQL to create your column on the table, manually adding the SQL needed to add the constraint (e.g. using CHECK). See here for how to ensure Django also knows the column was created and doesn't try to add it in the next migration. There's an example here.
[Edit] In Django 2.2, you can add a CheckConstraint in the model's Meta class constraints attribute:
from django.db.models import CheckConstraint, Q
(...)
class Meta:
constraints = [
CheckConstraint(
check=~Q(name=''),
name='name_not_empty'),
CheckConstraint(
check=~Q(short_name=''),
name='short_name_not_empty']

Trying to make a PostgreSQL field with a list of foreign keys in Django

Here is what I'm trying to do:
Make a model in Django that is a PostgreSQL array (database specific type), which contains foreign keys to another model.
class Books(models.Model):
authors = ArrayField(
models.ForeignKey('my_app.Authors', default=None, null=True, blank=True),
blank=True,
default=list()
)
When I try to makemigrations, Django gives me this error:
SystemCheckError: System check identified some issues:
ERRORS:
my_app.Books.authors: (postgres.E002) Base field for array cannot be a related field.
Any Ideas on how to beat that?
You can't create an array of foreign keys. It is not a Django limitation, it is a PostgreSQL "limitation".
The reason is that a foreign key is not a type, it is a constraint on a field. An array of foreign keys does not make any sense.
The general approach to achieve this is to use an intermediate table that will be used as a link between two others :
Authors(id, name)
Books(id, title, pub_date)
BookAuthors(id, book_id, author_id)
In the above exemple, BookAuthors.book_id is a foreign key to Books.id and BookAuthors.author_id is a foreign key to Authors.id. Thus, the table BookAuthors is used to match an author with a book and vice versa.
Django abstracts this intermediate table with ManyToManyField fields:
class Authors(models.Model):
name = models.CharField(...)
class Books(models.Model):
title = models.CharField(...)
pub_date = models.DateField(...)
authors = models.ManyToManyField('my_app.Authors',
related_name='authored_books')
Behind the scenes, Django will create the intermediate table.
Then, you can get all authors of a book using book.authors.all(), or all books authored by an author using author.authored_books.all().
You have to use ManyToManyField, ArrayField can't be related with another model.

django multiple choice model problems

I have two diferents problems with multple choices in models.
The first, i'm trying to do a multiple choice so the user can pick one or more days of the week:
DAYS_CHOICES = (
(1, _('Monday')),
...
(7, _('Sunday')),
)
...
day = models.ManyToManyField('day', choices=DAYS_CHOICES)
The second problem:
I want to make a ManyToMany Relation with a model define in other model:
First (Import to the model):
from events.models import Category
Second (The field related to the model):
type = models.ManyToManyField('Category', null=True, blank=True)
I get this error on syncdb:
Error: One or more models did not
validate: situ.situ: 'day' has an m2m
relation with model day, which has
either not been installed or is
abstract.
situ.situ: 'type' has an m2m relation
with model Category, which has either
not been installed or is abstract.
you could use :
day = forms.ModelMultipleChoiceField(queryset=Day.objects.all())
Unfortunately, the ManyToMany relation only works for relations with other models, not values from a choices set. Django does not provide a built in multiple choice model field type. However, I have used this snippet in the past when using a multiple select choices field: http://www.djangosnippets.org/snippets/1200/
This encodes the multiple selected options into a comma-separated list stored in a CharField, which works great, unless you need to do some sort of join or something on the selections. If you need to do that, you will have to define a new Day model that you can use the ManyToManyField on.
The second problem, I believe, is just the result of the first--if you clear up that issue, you'll be ok.
For the first part of your questions. You should be using a MultipleChoiceField
DAYS_CHOICES = (
(1, _('Monday')),
...
(7, _('Sunday')),
)
...
days = forms.MultipleChoiceField(choices=DAYS_CHOICES)
http://docs.djangoproject.com/en/dev/ref/forms/fields/#multiplechoicefield
This will yield a list of Unicode objects.
For the second problem, You need to either include the app name in the abstract declaration of the model in the m2m field or not declare it abstractly.
type = models.ManyToManyField(Category, null=True, blank=True)
or
type = models.ManyToManyField('events.Category', null=True, blank=True)
If the Category model was defined later in the same app in the models.py you could leave it Category but since it is in another app, you need to specify the app name.