Django - one-to-one modelAdmin - django

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})

Related

django manytomany model relationship crashing admin on object create

I have an Event object in my postgres db, and created a new Collection object to group events by theme via a ManyToMany field relationship:
class Collection(models.Model):
event = models.ManyToManyField('Event', related_name='collections')
name = models.CharField(blank=True, max_length=280)
slug = AutoSlugField(populate_from='name')
image = models.ImageField(upload_to='collection_images/', blank=True)
description = models.TextField(blank=True, max_length=1000)
theme = models.ManyToManyField('common.Tag', related_name='themes')
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=False)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('collection', args=[self.slug])
def clean(self):
# because of the way db saves M2M relations, collection doesn't have a
# type at this time yet, so image inheritance is
# called from the signal which is triggered when M2M is created
# (that means if an image is later deleted, it won't inherit a new
# one when collection is saved)
if self.image:
validate_hero_image(self.image, 'image')
def save(self, *args, **kwargs):
try:
self.full_clean()
except ValidationError as e:
log.error('Collection validation error (name = %s): %s' % (self.name, e))
return super(Collection, self).save(*args, **kwargs)
in my admin, I'm defining and registering CollectionAdmin like this:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ( 'name', )
however, if I go into admin and attempt to create a Collection "GET /admin/app/collection/add/" 200, the request frequently times out and the query load on my database from the Event M2M relationship seems quite heavy from logging. For reference currently the db has ~100,000 events. are there better ways to (re)structure my admin fields so I can select specific events (by name or id) to add to a Collection without effectively requesting a QuerySet of all events when that view is loaded (or creating them in db via shell)? thanks
There are multiple ways to accomplish this. For example, you could override the form fields the admin uses and specify another widget to use, like NumberInput.
You could also add your event model field to the raw_id_fields attribute of ModelAdmin. By doing so, Django won't try to create a fully populated select input but will offer you a way to search for events manually if needed:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ('name', )
raw_id_fields = ('event', )

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!

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.

M2M using through and form with multiple checkboxes

I'd like to create a form allowing me to assign services to supplier from these models. There is no M2M relationship defined since I use a DB used by others program, so it seems not possible to change it. I might be wrong with that too.
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
contact = models.ForeignKey(Contact, null=False, blank=False)
class SupplierPrice(models.Model):
service_user = models.ForeignKey('ServiceUser')
price_type = models.IntegerField(choices=PRICE_TYPES)
price = models.DecimalField(max_digits=10, decimal_places=4)
I've created this form:
class SupplierServiceForm(ModelForm):
class Meta:
services = ModelMultipleChoiceField(queryset=Service.objects.all())
model = ServiceUser
widgets = {
'service': CheckboxSelectMultiple(),
'contact': HiddenInput(),
}
Here is the view I started to work on without any success:
class SupplierServiceUpdateView(FormActionMixin, TemplateView):
def get_context_data(self, **kwargs):
supplier = Contact.objects.get(pk=self.kwargs.get('pk'))
service_user = ServiceUser.objects.filter(contact=supplier)
form = SupplierServiceForm(instance=service_user)
return {'form': form}
I have the feeling that something is wrong in the way I'm trying to do it. I have a correct form displayed but it is not instantiated with the contact and checkboxes aren't checked even if a supplier has already some entries in service_user.
You are defining services inside your Meta class. Put it outside, right after the beginning of SupplierServiceForm. At the very least it should show up then.
Edit:
I misunderstood your objective. It seems you want to show a multiple select for a field that can only have 1 value. Your service field will not be able to store the multiple services.
So, by definition, your ServiceUser can have only one Service.
If you don't want to modify the database because of other apps using it, you can create another field with a many to many relationship to Service. That could cause conflicts with other parts of your apps using the old field, but without modifying the relationship i don't see another way.
The solution to my problem was indeed to redefine my models in oder to integrate the m2m relationship that was missing, using the through argument. Then I had to adapt a form with a special init method to have all selected services displayed in checkboxes, and a special save() method to save the form using m2m relationship.
class Supplier(Contact):
services = models.ManyToManyField('Service', through='SupplierPrice')
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
supplier = models.ForeignKey(Supplier, null=False, blank=False)
price = models.Decimal(max_digits=10, decimal_places=2, default=0)
And the form, adapted from the very famous post about toppings and pizza stuff.
class SupplierServiceForm(ModelForm):
class Meta:
model = Supplier
fields = ('services',)
widgets = {
'services': CheckboxSelectMultiple(),
'contact_ptr_id': HiddenInput(),
}
services = ModelMultipleChoiceField(queryset=Service.objects.all(), required=False)
def __init__(self, *args, **kwargs):
# Here kwargs should contain an instance of Supplier
if 'instance' in kwargs:
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault('initial', {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data (checked boxes).
initial['services'] = [s.pk for s in kwargs['instance'].services.all()]
ModelForm.__init__(self, *args, **kwargs)
def save(self, commit=True):
supplier = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
def save_m2m():
new_services = self.cleaned_data['services']
old_services = supplier.services.all()
for service in old_services:
if service not in new_services:
service.delete()
for service in new_services:
if service not in old_services:
SupplierPrice.objects.create(supplier=supplier, service=service)
self.save_m2m = save_m2m
# Do we need to save all changes now?
if commit:
self.save_m2m()
return supplier
This changed my first models and will make a mess in my old DB but at least it works.

Django limit_choices_to at circular relation

I've implemented a circular OneToMany relationship at a Django model and tried to use the limit_choices_to option at this very same class.
I can syncdb without any error or warning but the limit is not being respected.
Using shell I'm able to save and at admin I receive the error message:
"Join on field 'type' not permitted.
Did you misspell 'neq' for the lookup
type?"
class AdministrativeArea(models.Model):
type = models.CharField(max_length=1, choices=choices.ADMIN_AREA_TYPES)
name = models.CharField(max_length=60, unique=True)
parent = models.ForeignKey('AdministrativeArea',
null=True,
blank=True,
limit_choices_to = Q(type__neq='p') & Q(type__neq=type)
)
The basic idea for the limit_choices_to option is to guarantee that any type "p" cannot be parent ofr any other AdministrativeArea AND the parent cannot be of the same type as the current AdministrativeArea type.
I'm pretty new to Django ... what am I missing?
Thanks
You can create a model form that adjusts specific field's queryset dynamically when working with existing model instance.
### in my_app/models.py ###
class AdministrativeArea(models.Model):
type = models.CharField(max_length=1, choices=choices.ADMIN_AREA_TYPES)
name = models.CharField(max_length=60, unique=True)
parent = models.ForeignKey('AdministrativeArea',
null=True,
blank=True,
limit_choices_to = Q(type__neq='p')
)
### in my_app/admin.py ###
from django.contrib import admin
import django.forms as forms
from my_app.models import AdministrativeArea
class class AdministrativeAreaAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(AdministrativeAreaAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
if instance is not None:
parentField = self.fields['parent']
parentField.queryset = parentField.queryset.filter(type__neq=instance.type)
class Meta:
model = AdministrativeArea
class AdministrativeAreaAdmin(admin.ModelAdmin):
form = AdministrativeAreaAdminForm
Such form could be used also outside admin site if you need the filtering there.