Unique together in models - django

I have a model called Company.
In a second model which is Branch, I use Company as a foreign key.
class Branch(models.Model):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
Now in some other model, I want to set a property(name) unique together with the Company but I use the branch as a foreign key.
class ABC(models.Model):
name = models.CharField()
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
class Meta:
unique_together = (
('branch__company', 'name'),
)
Can I do something like the above? It gives me an error that the field is nonexistent. Or can I use both company and branch in my model as foreign key?
class ABC(models.Model):
name = models.CharField()
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
class Meta:
unique_together = (
('company', 'name'),
)
I want to attach ABC object with a branch but if once added it should be unique to that company (other branches of that company can not have the same name).
Read about the circular error and was thinking of the same here.
Unique together will be depreciated in the future but I'm not thinking about this right now.
Any advice?

I suggest you to perform validation in the clean method (without a database constraint):
from django.core.exceptions import ValidationError
class ABC(models.Model):
name = models.CharField()
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
def clean(self):
super().clean()
if ABC.objects.filter(name=self.name, branch__company=self.branch.company).exists():
raise ValidationError('Error message')
def save(self, *args, **kwargs):
# Forces the clean method to be called
self.full_clean()
super().save(*args, **kwargs)

Related

Django referencing a specific object in a many-to-one relationship

Let's say I have the following models:
from django.db import models
class Reporter(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField()
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name="articles", on_delete=models.CASCADE)
I'd like to add a favorite_article field to my Reporter model that will reference a specific Article from reporter.articles.
One option is put the information into the Article model instead:
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name="articles", on_delete=models.CASCADE)
is_favorite = models.BooleanField()
But this doesn't seem like a very clean solution. Is there a better method to do this?
The approach you've suggested will work, however in its current form it allows for multiple Articles to be the favorite of one Reporter. With a bit of extra processing you can ensure that only one (at most) Article per Reporter is the favorite.
Making a few modifications to a couple of the answers to the question Unique BooleanField value in Django? we can restrict one True value per Reporter rather than one True value for the entire Article model. The approach is to check for other favorite Articles for the same Reporter and set them to not be favorites when saving an instance (rather than using a validation restriction).
I'd also suggest using a single transaction in the save method so that if saving the instance fails the other instances are not modified.
Here's an example:
from django.db import transaction
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name="articles", on_delete=models.CASCADE)
is_favorite = models.BooleanField(default=False)
def save(self, *args, **kwargs):
with transaction.atomic():
if self.is_favorite:
reporter_id = self.reporter.id if self.reporter is not None else self.reporter_id
other_favorites = Article.objects.filter(is_favorite=True, reporter_id=reporter_id)
if self.pk is not None: # is None when creating a new instance
other_favorites.exclude(pk=self.pk)
other_favorites.update(is_favorite=False)
return super().save(*args, **kwargs)
I've also changed the approach to use a filter rather than a get just in case.
Then to get the favorite article for a reporter, you can use:
try:
favorite_article = reporter.articles.get(is_favorite=True)
except Article.DoesNotExist:
favorite_article = None
which you could wrap into a method/property of the Reporter class.

Django: How to check a Form with a m2m relation object already exists or is “unique_together”?

I am testing forms and nesting models in django. In my Project a Person can enter departure, arrival (city names) and choose a weekly day (Mon-Fri). Maybe he drives every “Tuesday” from Amsterdam to Paris. I wanted this constellation to be unique – just for fun. So If another user enters the same route the relation should be linked to the same Car.object.
Models.py
class Person(models.Model):
name = models.CharField(max_length=255, blank=False, unique=True)
route = models.ManyToManyField('Car')
def __str__(self):
return self.name
class Car(models.Model):
name = models.CharField(max_length=255, blank=False, unique=True)
weekdays = models.ForeignKey('Week', null=True, blank=False, on_delete=models.SET_NULL)
departure = models.CharField(max_length=255, blank=False)
arrival = models.CharField(max_length=255, blank=False)
class Meta:
unique_together = ['weekdays', 'departure', 'arrival'] # --- Unique combination
def __str__(self):
return self.name
class Week(models.Model):
day = models.CharField(max_length=255, blank=False, unique=True)
def __str__(self):
return self.day
views.py
class RouteCreateView(CreateView):
model = Person
template_name ="testa/create_route.html"
form_class = RouteForm
success_url = reverse_lazy('testa:testa_home')
def form_valid(self, form):
return super().form_valid(form)
forms.py
class RouteForm(forms.ModelForm):
# --- apply ChoiceField
day = forms.ModelChoiceField(queryset=None)
car_name = forms.CharField()
departure = forms.CharField()
arrival = forms.CharField()
class Meta:
model = Person
fields = [
'name'
]
def __init__(self, *args, **kwargs):
super(RouteForm, self).__init__(*args, **kwargs)
self.fields['day'].queryset = Week.objects.all()
def save(self, commit=True):
personData = super().save(commit)
data = self.cleaned_data
carData = Car(name=data['car_name'], weekdays=data['day'], departure=data['departure'], arrival=data['arrival'])
if commit:
carData.save()
personData.route.add(carData) # --- save m2m relation
return personData
If i enter two times for example „“Tuesday” from Amsterdam to Paris “ then an Error Message appears obviously, this error message (it´s german), telling me I have a double entry / Key.
Question
So my save()Method does not work because I need some kind of logic, so that Django takes the existing car.object or creates a new - if it is not a double entry. But I do not know where to start? The easiest way would be to get some kind of response from my model meta option Car.unique_together so "if it´s an “double-key error” then take the existing object". Is there a way to fetch the response? And what kind of Values it would be, only errors, could not find any hint in the doc? Or should I try some logic with exists()
That was my kind of idea / approach of a new save() 😊
def save(self, commit=True):
personData = super().save(commit)
data = self.cleaned_data
carData = Car(name=data['car_name'], weekdays=data['day'], departure=data['departure'], arrival=data['arrival'])
if commit:
# Check if database sends unique_together response
# if yes
if Car.Meta.unique_together is True:
getAlternative = Car.object.get(Meta.unique_together) # --- get the object which already exist
personData.route.add(getAlternative) # --- save m2m relation
# if not
else:
carData.save() # --- save object
personData.route.add(carData) # --- save m2m relation
return personData
obviously i get a error message: type object 'Car' has no attribute
'Meta'
Theres get_or_create for such use case: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#get-or-create
...
car, created = Car.objects.get_or_create(
weekdays=data['day'],
departure=data['departure'],
arrival=data['arrival'],
defaults = dict(name=data['car_name']),
)
personData.route.add(car)
...
Obviously given name gets ignored if another car with same weekdas, departure, arrival has been found.
I suggest to put the code for creating the car and adding the route in a transaction.atomic() https://docs.djangoproject.com/en/2.2/topics/db/transactions/#django.db.transaction.atomic

Django: How to filter foreign key in admin detail view

core/models.py
from django.db import models
from django.db.models.signals import post_save
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
middle_name = models.CharField(max_length=30)
class Company(models.Model):
name = models.CharField(max_length=100)
class Entity(models.Model):
is_person = models.BooleanField(default=True)
person = models.ForeignKey(Person, on_delete=models.PROTECT, null=True)
company = models.ForeignKey(Company, on_delete=models.PROTECT, null=True)
name = models.CharField(max_length=30)
def __str__(self):
return self.name
#property
def title(self):
return self.name
class Meta:
verbose_name_plural = 'entities'
def post_save_person_receiver(sender, instance, created, *args, **kwargs):
if created:
entity, is_created = Entity.objects.get_or_create(is_person=True, person=instance, company=None, name=instance.last_name) # noqa
post_save.connect(post_save_person_receiver, sender=Person)
def post_save_company_receiver(sender, instance, created, *args, **kwargs):
if created:
entity, is_created = Entity.objects.get_or_create(is_person=False, person=None, company=instance, name=instance.short_name) # noqa
post_save.connect(post_save_company_receiver, sender=Company)
class Group(models.Model):
name = models.CharField(max_length=20)
is_individual = models.BooleanField(default=True)
members = models.ManyToManyField(Entity, through='Membership')
class Membership(models.Model):
group = models.ForeignKey(Group, on_delete=models.PROTECT, null=False)
entity = models.ForeignKey(Entity, on_delete=models.PROTECT, null=False)
class Meta:
unique_together = ("entity", "group")
For every Company and Person created, an Entity is automatically created where Entity.is_person=True if it's a Person. An Entity can then become a member of a Group such as 'Employee', 'Supplier' and 'Customer' through a ManyToMany relationship in the Membership Model.
How do I filter Membership.entity in Admin View (for add and update) that when the Group selected is an 'is_individual=True', such as 'Employee', Entity Field only shows 'is_person=True' Persons in the Entity combobox?
Admin View
I would consider writing your own view for this. In my opinion, Djangos admin is not a good site to base a frontend on. I use it only for making quick changes where validation doesn't need to be done as much because the users of admin should know what they're doing. When you start adding more responsive stuff, that's when I consider using another view and starting from scratch (or a framework) but a different view none-the-less.
There are several different names for what it sounds like you want: dependent dropdowns, chained fields, etc. Django itself doesn't have anything out of the box for this.
Your 2 options are: (1) do it yourself or (2) use a 3rd-party package.
As far as doing it yourself goes, you're going to need to do some work in JS for making it work on the frontend, and you're probably going to need some kind of flexible view that outputs JSON data, and you're going to need some kind of custom select field to handle this.
For using a 3rd-party package, I don't see anything particularly recent that does this. Here's one that's a few years old: https://github.com/runekaagaard/django-admin-flexselect
Hope this helps some!

Django model ForeignKey with subset of data

Is it possible to define a foreign key or OneToOne relation in django model with only subset of data?
For example :
I have 2 models.
#with_author
class Product(models.Model):
GTIN = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
UOM = models.OneToOneField(MaterialUOM)
defaultPrice = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
and
#with_author
class UOM(models.Model):
uomname = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
so I want in my Product model only to allow UOM values that have same material value as in product.
Is it possible on model level or any other place and not to display non relevant values in the dropdown?
You can enforce this constraint by adding some validation to the model's clean() method. Something like:
from django.core.exceptions import ValidationError
class Product(models.Model):
GTIN = models.CharField(max_length=30)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
UOM = models.OneToOneField(MaterialUOM)
defaultPrice = MoneyField(max_digits=10, decimal_places=2, default_currency='USD')
def clean(self):
if not self.material == self.UOM.material:
# This will cause the model not to be saved and report an error
raise ValidationError('Material does not match UOM material')
If you are using a ModelForm to handle edits to your models, then clean() will be called automatically as part of the form validation. If you are modifying models directly in your code, then you need to call it yourself before saving the model. The documentation explains this in detail.
If you want to be doubly sure, you can also override the save() method:
def save(self, *args, **kwargs):
if not self.material == self.UOM.material:
return # Model is not saved
super(Product, self).save(*args, **kwargs)
This will not report any errors - it will just not save the model. Hence you should also use the clean() method above.

Django ManyToMany filter

I have a Region infrastructure modeled as follows: Each region has a ManyToMany of countries, and optionally states (if it's a region within the US)
from django.contrib.auth.models import User
from django.contrib.localflavor.us.models import USStateField
from django.db import models
from django_countries import CountryField
class CountryManager(models.Manager):
def get_by_natural_key(self, country):
return self.get(country=country)
class Country(models.Model):
country = CountryField(unique=True)
objects = CountryManager()
class Meta:
ordering = ('country',)
def __unicode__(self):
return unicode(self.country.name)
def natural_key(self):
return (self.country.code,)
class StateManager(models.Manager):
def get_by_natural_key(self, state):
return self.get(state=state)
class State(models.Model):
state = USStateField(unique=True)
objects = StateManager()
class Meta:
ordering = ('state',)
def __unicode__(self):
return self.get_state_display()
def natural_key(self):
return (self.state,)
class Region(models.Model):
name = models.CharField(max_length=255, unique=True)
coordinator = models.ForeignKey(User, null=True, blank=True)
is_us = models.BooleanField('Is a US region')
countries = models.ManyToManyField(Country)
states = models.ManyToManyField(State, blank=True)
class Meta:
ordering = ('name',)
def __unicode__(self):
return self.name
Each user has a profile defined (partially) as follows:
class UserProfile(models.Model):
user = models.OneToOneField(User, related_name='user_profile')
city = models.CharField(max_length=255)
country = CountryField()
state = USStateField(_(u'US only (determines user's region)'), blank=True, null=True)
I'm trying to filter a bunch of user objects by region. So I have
region = Region.objects.filter(id=self.request.GET['filter_region'])
if len(region) == 0:
raise Exception("Region not found for filter")
if len(region) > 1:
raise Exception("Multiple regions found for filter?")
region = region[0]
queryset = queryset.filter(user_profile__country__in=region.countries.all)
Sadly though, this returns an empty queryset. I suspect it has to do with the fact that the "Country" model has a "country" field within it (terrible ambiguous naming, I know, not my code originally), and I'm only filtering by "Country" models, not the "country" fields within them. (Does that make sense?)
How can I filter by a subfield of the ManyToMany field?
First, why are you using .filter() if you want just a single item do:
region = Region.objects.get(id=self.request.GET['filter_region'])
That will raise an ObjectDoesNotExist exception if the object doesn't exist, but you're raising an exception if the queryset is empty anyways. If you need to catch that exception you can either use a try...except block or get_object_or_404 if you're in a view.
Second, don't use self.request.GET['filter_region'] directly. If the key isn't set you'll raise an IndexError. Use, instead:
self.request.GET.get('filter_region')
Now, as to your actual problem: UserProfile.country is a CountryField which is just a specialized CharField. Whereas Region.countries is a M2M with the model Country. The two are not comparable, which is why your queryset is coming back empty.
Make UserProfile.country a foreign key to Country and you're in business.