Goal
The following should raise a ValidationError
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1', 'name2'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...
In plain English, if an element in a model's ArrayField matches an element in the database table a ValidationError should be raised
Failed Solutions
ArrayField docs don't mention the unique keyword so I tried doing this a couple ways (these are minimal code examples).
Adding unique=True to the base_field didn't seem to the anything at all after running makemigrations and migrate
# models.py
...
class MyModel(models.Model)
title = ArrayField(
models.CharField(max_length=255, unique=True),
)
...
# shell_plus from Django-extensions
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1'])
>>> m1.save()
>>> m2.save()
# no errors raised
I tried adding unique=True to only the ArrayField. Django raises an error if an array is exactly the same.
# models.py
...
class MyModel(models.Model)
title = ArrayField(
models.CharField(max_length=255),
unique=True,
)
# shell_plus from Django-extensions
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...
>>> m3 = MyModel(names=['name1', 'name2'])
>>> m3.save()
# no error raised
The above makes sense when I think it about. I then tried to add unique=True to both the base_field and to the ArrayField but the behavior didn't change. I ran makemigrations and migrate.
class MyModel(models.Model)
title = ArrayField(
models.CharField(max_length=255, unique=True),
unique=True,
)
# shell_plus from Django-extensions
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...
>>> m3 = MyModel(names=['name1', 'name2'])
>>> m3.save()
# no error raised
TL;DR
Can I make the following raise an error with unique=True or do I need to write my own validators?
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1', 'name2'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...
This is a minimal example of what I ended up doing
# models.py
...
UNIQUE_ARRAY_FIELDS = ('name',)
class MyManager(models.Manager):
def prevent_duplicates_in_array_fields(self, model, array_field):
def duplicate_check(_lookup_params):
fields = self.model._meta.get_fields()
for unique_field in UNIQUE_ARRAY_FIELDS:
unique_field_index = [getattr(field, 'name', '') for field in fields]
try:
# if model doesn't have the unique field, then proceed to the next loop iteration
unique_field_index = unique_field_index.index(unique_field)
except ValueError:
continue
all_items_in_db = [item for sublist in self.values_list(fields[unique_field_index].name).exclude(**_lookup_params) for item in sublist]
all_items_in_db = [item for sublist in all_items_in_db for item in sublist]
if not set(array_field).isdisjoint(all_items_in_db):
raise ValidationError('{} contains items already in the database'.format(array_field))
if model.id:
lookup_params = {'id': model.id}
else:
lookup_params = {}
duplicate_check(lookup_params)
...
class MyModel(models.Model):
name = ArrayField(
models.CharField(max_length=255),
default=list
)
objects = MyManager()
...
def save(self, *args, **kwargs):
self.full_clean()
MyModel.objects.prevent_duplicates_in_array_fields(self, self.name)
super().save(*args, **kwargs)
...
Related
[UPDATE] The relevant models (thinned):
class Contact(TimeStampedModel, ComputedFieldsModel):
.... # Irrelevant, see reverse lookups on RegisteredStaffContact
class RegisteredStaffContact(TimeStampedModel):
contact = models.ForeignKey(Contact, models.CASCADE, related_name='registered_staffing')
staff_user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, related_name='contact_registered_staffing')
is_primary = models.BooleanField(default=False)
....
class User(TimeStampedModel, AbstractBaseUser, PermissionsMixin):
....
assistants = models.ManyToManyField('self', symmetrical=False, related_name='principals')
.... used in the method in question under variable user_principals_ids
class ShareRequest(TimeStampedModel):
contact = models.ForeignKey(to=Contact, on_delete=models.CASCADE, related_name='share_requests')
....
#classmethod
def get_inbox_for_user(cls, user: User) -> 'models.QuerySet[ShareRequest]':
user_principals_ids = list(user.principals.values_list('id', flat=True))
annotated_share_requests = cls._default_manager\
.annotate(
registered_staff_ids=ArrayAgg(
'contact__registered_staffing__staff_user',
distinct=True,
)
)\
.annotate(
primary_registered_staff_ids=ArrayAgg(
'contact__registered_staffing__staff_user',
filter=models.Q(contact__registered_staffing__is_primary=True),
distinct=True,
)
)
filtered_shared_requests = annotated_share_requests\
.filter(
(models.Q(primary_registered_staff_ids__isnull=True) & models.Q(registered_staff_ids__contains=[user.id]))
| (models.Q(primary_registered_staff_ids__isnull=True) & models.Q(registered_staff_ids__overlap=user_principals_ids))
| models.Q(primary_registered_staff_ids__contains=[user.id])
| models.Q(primary_registered_staff_ids__contained_by=user_principals_ids)
)
return filtered_shared_requests
Skipping the details, the important part is my annotate statements and further filtering by them.
From the example below, you can see that registered_staff_ids results in [None], which looks like a part of the problem:
In [39]: annotated_share_requests.first().registered_staff_ids
Out[39]: [None]
I was able to remedy it by providing an additional filter to my annotate statement, turning that expression into:
annotated_share_requests = cls._default_manager\
.annotate(
registered_staff_ids=ArrayAgg(
'contact__registered_staffing__staff_user',
filter=models.Q(contact__registered_staffing__staff_user__isnull=False),
distinct=True,
)
)\
...
Which now shows the result as:
In [40]: annotated_share_requests.first().registered_staff_ids
Out[40]: []
However, the further filtering (specifically __overlap and __contained_by lookups fail, for example:
In [56]: annotated_share_requests.values_list('primary_registered_staff_ids', flat=True)
Out[56]: <QuerySet [[], []]> # looks great
In [57]: annotated_share_requests.values_list('registered_staff_ids', flat=True)
Out[57]: <QuerySet [[], []]> # looks great
In [58]: user_principals_ids
Out[58]: [] # looks great
# BUT!
In [59]: annotated_share_requests.filter(registered_staff_ids__overlap=user_principals_ids)
Out [59]: FieldError: Cannot resolve expression type, unknown output_field
# AND SIMILARLY!
In [61]: annotated_share_requests.filter(primary_registered_staff_ids__contained_by=user_principals_ids)
Out [62]: FieldError: Cannot resolve expression type, unknown output_field
What's interesting is that it only seems to be an issue with an empty array on the right side of comparison, so this works:
In [76]: annotated_share_requests.filter(primary_registered_staff_ids__contained_by=[1])
Out[76]: <QuerySet []>
In [77]: annotated_share_requests.filter(registered_staff_ids__overlap=[1])
Out[77]: <QuerySet []>
Looking for any pointers.
My scenario is this:
class Partnership(models.Model):
partner_a = models.ForeignKey(
"Person",
on_delete=models.CASCADE,
related_name='%(class)s_partner_a',
)
partner_b = models.ForeignKey(
"Person",
on_delete=models.CASCADE,
related_name='%(class)s_partner_b',
)
class Meta:
unique_together = (partner_a, partner_b)
This works just fine at refusing a duplicate partnership if a=a and b=b:
Ex:
>>> a = Person('a')
>>> b = Person('b')
>>> Partnership.objects.create(person_a=a, person_b=b)
>>> Partnership.objects.create(person_a=a, person_b=b) # IntegretyError as expected
However, it does not correctly refuse the following
>>> a = Person('a')
>>> b = Person('b')
>>> Partnership.objects.create(person_a=a, person_b=b)
>>> Partnership.objects.create(person_a=b, person_b=a) # Allowed
Is there some class Meta I am missing? Or is there another way to ensure this? (I know I can override the save class, but I'm asking if there is a way to do this without doing that).
I'm new to django thus the question. I've the following Feed object and an User object which have a many-to-many relationship
class Feed(Base):
headline = models.CharField(max_length=255)
link = models.CharField(max_length=255)
summary = models.TextField()
reader = models.ManyToManyField(User, through='Bookmark')
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True, max_length=255)
mobile = PhoneNumberField(null=True)
username = models.CharField(null=True, unique=True, max_length=255)
full_name = models.CharField(max_length=255, blank=True, null=True)
The two are related using the Bookmark object.
class Bookmark(Base):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
feed = models.ForeignKey(Feed, on_delete=models.CASCADE, null=True)
My question is,
How do I add a bookmark(or rather feed) to the user?
How do I fetch all the feeds the User has bookmarked?
Any help appreciated.
Well, let's start from the beginning.
As you probably know, when you generate M2M rels with Django, you use the ManyToManyField. If you do not care about M2M table details, Django will manage it for you. If you want to specify the intermediary table you can use ManyToManyField.through. Exactly as you did. I'm going to semplify your model for explanation purposes. Something like this:
class User(models.Model):
username = models.CharField(null=True, unique=True, max_length=255)
class Feed(models.Model):
headline = models.CharField(max_length=255)
reader = models.ManyToManyField(User, through='Bookmark')
class Bookmark(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
feed = models.ForeignKey(Feed, on_delete=models.CASCADE, null=True)
Let's start Django interactive shell. I assume you have an empty database.
$ django manage.py shell
First of all import your models
>>> from yourAppName.models import *
Now, create some data:
>>> from bat.models import *
>>> u1 = User(username = 'foo')
>>> u1.save()
>>> u2 = User(username = 'bar')
>>> u2.save()
>>> User.objects.all() # get Users u1 and u2
<QuerySet [<User: User object>, <User: User object>]>
>>> f1 = Feed(headline = 'How to use M2M in Django')
>>> f1.save()
>>> Feed.objects.all() # get Feed f1
<QuerySet [<Feed: Feed object>]>
How do I add a bookmark (or rather feed) to the user?
In this case, you cannot use Feed.reader.add(u1), you have to use the Bookmark's Manager since you specified that's your intermediary model.
>>> b1 = Bookmark(user=u1, feed = f1) # add Feed f1 to User u1
>>> b1.save() # save into database
We can also add another bookmark:
>>> f2 = Feed(headline = 'Fetching data in Django like a pro!')
>>> f2.save()
>>> b2 = Bookmark(user=u1, feed = f2) # add Feed f2 to User u1
>>> b2.save() # save into database
You are done! Now, we can check if everything is fine.
>>> brandNewBookMark = Bookmark.objects.all()[0] # get the first bookmark
>>> print(brandNewBookMark.user.username) # it's Foo!
foo
>>> print(brandNewBookMark.feed.headline) # Foo subscribed to f1!
u'How to use M2M in Django'
How do I fetch all the feeds the User has bookmarked?
You can simply leverage the Feed.reader field. E.g.,
>>> for f in Feed.objects.filter(reader = u1):
... print(f.headline)
...
How to use M2M in Django
Fetching data in Django like a pro!
That's it! Further info here.
This is a good example for your problem
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self): # __unicode__ on Python 2
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self): # __unicode__ on Python 2
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
For this models, if you want to add memberships, you do this:
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
... date_joined=date(1962, 8, 16),
... invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(person=paul, group=beatles,
... date_joined=date(1960, 8, 1),
... invite_reason="Wanted to form a band.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
Unlike normal many-to-many fields, you can’t use add(), create(), or set() to create relationships:
>>> # The following statements will not work
>>> beatles.members.add(john)
>>> beatles.members.create(name="George Harrison")
>>> beatles.members.set([john, paul, ringo, george])
You can see this better in Django Docs
I can not pick a department has a MySQL database using a primary key CharField. I get the following error:
DoesNotExist: Department matching query does not exist.
I made tested by retrieving the primary key with all() and then re-injecting into the get() and it works. But indicating directly the value that does not work:
>>> dept = Department.objects.all()[43].pk
>>> print dept
Haute-Vienne
>>> print type(dept)
<type 'unicode'>
>>> test = unicode('Haute-Vienne')
>>> print test
Haute-Vienne
>>> print type(test)
<type 'unicode'>
>>> print '---'
>>> dept1 = Department.objects.get(name=dept)
>>> print dept1
Haute-Vienne
>>> dept2 = Department.objects.get(name=test)
...
Department matching query does not exist.
Models.py:
class Department(models.Model):
num_reg = models.ForeignKey(Region)
num_depts = models.CharField(_(u'Department number'), max_length=3, unique=True)
name = models.CharField(_(u'Name'), max_length=255, primary_key=True, blank=False)
def __unicode__(self):
return self.pk
Instead of:
dept2 = Department.objects.get(name=test)
Try:
dept2 = Department.objects.get(name=test.name)
I'm trying to create a recipe/ingredient model in Django
In my models.py I got
class Ingredient(models.Model):
name = models.CharField(max_length=20)
class Recipe(models.Model):
name = models.CharField(max_length=50)
ingredients = models.ManyToManyField(Ingredient, blank=True)
But when I create a Recipe or Ingredient in my admin, I get :
IntegrityError at /admin/menuview/ingredient/add/
menuview_ingredient.recipe_id may not be NULL
What am I doing wrong here?
I think you have to give relationship a null=True parameter too.
ingredients = models.ManyToManyField(Ingredients, blank=True, null=True,)
Your problem is similar to this one: Foreign keys and IntegrityError: id may not be NULL
To fix it, you will do something similar to this when saving:
>>> s = Recipe()
>>> s.name = 'Blah'
>>> obj = Ingredient(...)
>>> obj.save()
>>> s.ingredients = obj
>>> s.save()
The Django Doc has more examples for ManyToManyField. For example, for your case:
>>> i = Ingredient.objects.get(id=1)
>>> e = i.recipe_set.create(
... name ='strawberry pancake',
... )
# No need to call e.save() at this point -- it's already been saved.
This is equivalent to (but much simpler than):
>>> i = Ingredient.objects.get(id=1)
>>> e = Recipe(
... ingredients=i,
... name='strawberry pancacke',
... )
>>> e.save(force_insert=True)