Limit Maximum Choices of ManyToManyField - django

I'm trying to limit the maximum amount of choices a model record can have in a ManyToManyField.
In this example there is a BlogSite that can be related to Regions. In this example I want to limit the BlogSite to only be able to have 3 regions.
This seems like something that would have been asked/answered before, but after a couple hours of poking around I haven't been able to find anything close. For this project, I'm using Django 1.3.
#models.py
class BlogSite(models.Model):
blog_owner = models.ForeignKey(User)
site_name = models.CharField(max_length=300)
region = models.ManyToManyField('Region', blank=True, null=True)
....
class Region(models.Model):
value = models.CharField(max_length=50)
display_value = models.CharField(max_length=60)
....
Any ideas?

You can override clean method on your BlogSite model
from django.core.exceptions import ValidationError
class BlogSite(models.Model):
blog_owner = models.ForeignKey(User)
site_name = models.CharField(max_length=300)
regions = models.ManyToManyField('Region', blank=True, null=True)
def clean(self, *args, **kwargs):
if self.regions.count() > 3:
raise ValidationError("You can't assign more than three regions")
super(BlogSite, self).clean(*args, **kwargs)
#This will not work cause m2m fields are saved after the model is saved
And if you use django's ModelForm then this error will appear in form's non_field_errors.
EDIT:
M2m fields are saved after the model is saved, so the code above will not work, the correct way you can use m2m_changed signal:
from django.db.models.signals import m2m_changed
from django.core.exceptions import ValidationError
def regions_changed(sender, **kwargs):
if kwargs['instance'].regions.count() > 3:
raise ValidationError("You can't assign more than three regions")
m2m_changed.connect(regions_changed, sender=BlogSite.regions.through)
Give it a try it worked for me.

Working! I have used this and its working properly.
Validation required before saving the data. So you can use code in form
class BlogSiteForm(forms.ModelForm):
def clean_regions(self):
regions = self.cleaned_data['regions']
if len(regions) > 3:
raise forms.ValidationError('You can add maximum 3 regions')
return regions
class Meta:
model = BlogSite
fields = '__all__'

Related

Automatically update user_id and date fields at django into database

I'm new to Python and Django and there are some things I would like to achieve in models.py:
After a user submits a form I would like to safe both the current date and the current user_id of the user into the database.
I know django offers to use #property decorators for those, but the problem with that is that I would like to make SQL queries using user_id and that doesn't work with decorators.
Another related question is how to establish calc fields like an automatic calculation of two values in the form before submitting.
Try making a model for the submission of the form. And give the model a foreign key which is connected to the user. Then add a DateField and prove the default value as datetime.now() from the datetime module which is probably already installed on your device.
In settings.py I added this line: 'crum.CurrentRequestUserMiddleware',
In models.py:
from django.contrib.auth.models import User
import crum
class PlantSpecies(models.Model):
def save(self, *args, **kwargs):
userid = crum.get_current_user()
self.userid = userid.id
super(PlantSpecies, self).save(*args, **kwargs)
userid = models.CharField(max_length=45, blank=True, default=crum.get_current_user())
date = models.DateTimeField(auto_now_add=True)
species = models.CharField(max_length=45, blank=True)
common_name = models.CharField(max_length=30, null=True, blank=True)
def __str__(self):
return self.species

Creating a unique object using DateField()

I would just like to create a model that essentially prevents the same date being selected by two different users (or the same user).
E.g if User1 has selected 2019-01-10 as a "date" for a booking, then User2 (or any other Users) are not able to create an object with that same date.
I have created a very basic model that can allow different Users to create an object using the DateField(). Using the Django admin page, I can create different instances of objects by two different Users (admin and Test_User).
In order to try to ensure that a new object can't be created if that date has already been used by a different object I have tried the following approach:
a compare function that utilizes __dict__.
models.py
from __future__ import unicode_literals
from django.db import models, IntegrityError
from django.db.models import Q
from django.contrib.auth.models import User
from datetime import datetime
class Booking(models.Model):
date = models.DateField(null=False, blank=False)
booked_at = models.DateTimeField(auto_now_add=True)
booking_last_modified = models.DateTimeField(auto_now=True)
class PersonalBooking(Booking):
user = models.ForeignKey(User, on_delete=models.CASCADE)
def compare(self, obj):
excluded_keys = 'booked_at', '_state', 'booking_last_modified', 'user',
return self._compare(self, obj, excluded_keys)
def _compare(self, obj1, obj2, excluded_keys):
d1, d2 = obj1.__dict__, obj2.__dict__
for k,v in d1.items():
if k in excluded_keys:
continue
try:
if v != d2[k]:
pass
except IntegrityError as error:
print(error)
print('Date already selected by different User. Please select another date.')
admin.py
from django.contrib import admin
from . import models
from .models import Booking, PersonalBooking
class PersonalBookingAdmin(admin.ModelAdmin):
list_display = ('format_date', 'user', )
def format_date(self, obj):
return obj.date.strftime('%d-%b-%Y')
format_date.admin_order_field = 'date'
format_date.short_description = 'Date'
def user(self, obj):
return obj.user()
user.admin_order_field = 'user'
user.short_description = 'User'
admin.site.register(models.PersonalBooking, PersonalBookingAdmin)
It didn't work as I had hoped, objects with the same date could still be created by the same or different users. Perhaps there is a simpler way? Or maybe I need to use the Q() class? I am not very familiar with it.
Any help is greatly appreciated.
Thanks.
You could do this validation at the database level by setting the unique attribute to True in your model's field.
class Booking(models.Model):
date = models.DateField(null=False, blank=False, unique=True)
booked_at = models.DateTimeField(auto_now_add=True)
booking_last_modified = models.DateTimeField(auto_now=True)
But this would present issues if the field was changed later to store time.
If you are going to be storing the time as well, you could override the model's default save function to check that there isn't another Booking with the same date (__date) each time it is saved. exists() returns True if there is a match, so this will throw a ValidationError if there is a match.
from django.core.exceptions import ValidationError
class Booking(models.Model):
date = models.DateTimeField(null=False, blank=False)
booked_at = models.DateTimeField(auto_now_add=True)
booking_last_modified = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
# Make sure there are no bookings on the same day
if Booking.objects.exclude(pk=self.pk).filter(date__date=self.date.date).exists():
raise ValidationError('There cannot be two bookings with the same date.')
super(Booking, self).save(*args, **kwargs)
Try this
https://docs.djangoproject.com/en/2.1/ref/models/fields/#unique-for-date
For user column set unique_for_date=True

Django - one-to-one modelAdmin

I am moderately proficient with django and trying to use model forms for an intranet project.
Essentially, I have a "Resources" model, which is populated by another team.
Second model is "Intake", where users submit request for a resource. It has one-to-one mapping to resource.
Objective is to only allow 1 resource allocation per intake.
Now, Intake model form shows the form, with a drop-down field to resource, with a caveat that it shows all resources, regardless of previous allocation or not.
ex. if resource is taken by an intake, save button detects that disallow saves. This is expected, but then the drop-down should not show that resource in the first place.
How can I do this, i.e. do not show resource already allocated ?
class Resource(models.Model):
label = models.CharField(max_length=50, primary_key=True)
created = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey('auth.User', default=1)
class Meta:
verbose_name_plural = "Resource Pool"
def __str__(self):
return self.label
class Intake(models.Model):
order_id = models.AutoField(primary_key=True)
requestor = models.ForeignKey('auth.User', default=1)
resource = models.OneToOneField(Resource, verbose_name="Allocation")
project = models.CharField(max_length=50)
class Meta:
verbose_name_plural = "Environment Request"
def __str__(self):
print("self called")
return self.project
You can create a custom form in your admin and change the queryset value of the resource field. Something like this:
admin.py
from django import forms
from django.db.models import Q
from .models import Intake
class IntakeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(IntakeForm, self).__init__(*args, **kwargs)
self.fields['resource'].queryset = Resource.objects.filter(
Q(intake__isnull=True) | Q(intake=self.instance)
)
class IntakeAdmin(admin.ModelAdmin):
form = IntakeForm
admin.site.register(Intake, IntakeAdmin)
You could probably use limit_choices_to on the field definition:
resource = models.OneToOneField(Resource, verbose_name="Allocation",
limit_choices_to={'intake__isnull': True})

How can make the admin for a ForeignKey('self') ban referring to itself?

I have a model with a forgein key to itself. For example:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
For my purposes, I never want parent_folder to refer to itself, but the default admin interface for this model does allow the user to choose its own instance. How can I stop that from happening?
Edit: If you're trying to do a hierarchical tree layout, like I was, another thing you need to watch out for is circular parent relationships. (For example, A's parent is B, B's parent is C, and C's parent is A.) Avoiding that is not part of this question, but I thought I would mention it as a tip.
I would personally do it at the model level, so if you reuse the model in another form, you would get an error as well:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
def clean(self):
if self.parent_folder == self:
raise ValidationError("A folder can't be its own parent")
If you use this model in a form, use a queryset so the model itself doesn't appear:
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ('name','parent_folder')
def __init__(self, *args, **kwargs):
super(FolderForm, self).__init__(*args, **kwargs)
if hasattr(self, 'instance') and hasattr(self.instance, 'id'):
self.fields['parent_folder'].queryset = Folder.objects.exclude(id=self.instance.id)
To make sure the user does not select the same instance when filling in the foreign key field, implement a clean_FIELDNAME method in the admin form that rejects that bad value.
In this example, the model is Folder and the foreign key is parent_folder:
from django import forms
from django.contrib import admin
from .models import Folder
class FolderAdminForm(forms.ModelForm):
def clean_parent_folder(self):
if self.cleaned_data["parent_folder"] is None:
return None
if self.cleaned_data["parent_folder"].id == self.instance.id:
raise forms.ValidationError("Invalid parent folder, cannot be itself", code="invalid_parent_folder")
return self.cleaned_data["parent_folder"]
class FolderAdmin(admin.ModelAdmin):
form = FolderAdminForm
admin.site.register(Folder, FolderAdmin)
Edit: Combine my answer with raphv's answer for maximum effectiveness.

Complicated "limit_choices_to" function in Django

I have Django database with 2 models: DeviceModel and Device. Let's say, for example, DeviceModel object is "LCD panel" and Device object is "LCD panel №547". So these two tables have ManyToOne relationship.
class DeviceModel(models.Model):
name = models.CharField(max_length=255)
class Device(models.Model):
device_model = models.ForeignKey(DeviceModel)
serial_number = models.CharField(max_length=255)
Now I need to add some relations between DeviceModel objects. For example "LCD Panel" can be in "Tablet" object or in "Monitor" object. Also another object can be individual, so it doesn't link with other objects.
I decided to do this with ManyToMany relationship, opposed to using serialization with JSON or something like that (btw, which approach is better in what situation??).
I filled all relationships between device models and know I need to add relationship functional to Device table.
For that purpose I added "master_dev" foreignkey field pointing to 'self'. It works exactly as I need, but I want to restrict output in django admin panel. It should display only devices, that are connected through device_links. Current code:
class DeviceModel(models.Model):
name = models.CharField(max_length=255)
device_links = models.ManyToManyField('self')
class Device(models.Model):
device_model = models.ForeignKey(DeviceModel)
serial_number = models.CharField(max_length=255)
master_dev = models.ForeignKey('self', blank=True, null=True)
So, how can I limit output of master_dev field in admin panel?
There is a function "limit_choices_to", but I can't get it to work...
in forms.py:
def master_dev_chioses():
chioses = DeviceModel.objects.filter(do your connection filter here - so not all Devicemodels comes to choicefield)
class DeviceForm(forms.ModelForm):
class Meta:
model = Device
def __init__(self, *args, **kwargs):
super(Device, self).__init__(*args, **kwargs)
self.fields['master_dev'].choices = master_dev_chioses()
While there is no direct answer to my question about "limit_choices_to" function, I post solution that achieves desired output:
from django import forms
from django.contrib import admin
from .models import DeviceModel, Device
class DeviceForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(DeviceForm, self).__init__(*args, **kwargs)
try:
linked_device_models = self.instance.device_model.device_links.all()
linked_devices = Device.objects.filter(device_model__in=linked_device_models)
required_ids = set(linked_devices.values_list("id", flat=True))
self.fields['master_dev'].queryset = Device.objects.filter(id__in=required_ids).order_by("device_model__name", "serial_number")
except:
# can't restrict masters output if we don't know device yet
# admin should edit master_dev field only after creation
self.fields['master_dev'].queryset = Device.objects.none()
class Meta:
model = Device
fields = ["device_model", "serial_number", "master_dev"]
class DeviceAdmin(admin.ModelAdmin):
form = DeviceForm
list_display = ('id', 'device_model', 'serial_number')
list_display_links = ('id', 'device_model')
search_fields = ('device_model__name', 'serial_number')
list_per_page = 50
list_filter = ('device_model',)