Django limit_choices_to at circular relation - django

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.

Related

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

django_filters picking up %(app_label)s_related_name as field name

(Django 1.8.14)
So I have one model, which uses a ContentType to point to a collections of models across separate apps.
I want to be able to "plug in" the functionality linking that model to the other models in the separate app. So we have the follwing:
class MyModelMixin(models.Model):
""" Mixin to get the TheModel data into a MyModel
"""
updated_date = models.DateTimeField(null=True, blank=True)
submission = GenericRelation(
TheModel,
related_query_name='%(app_label)s_the_related_name')
class Meta:
abstract = True
The main model model looks like this:
class TheModel(models.Model):
""" This tracks a specific submission.
"""
updated = models.DateTimeField()
status = models.CharField(max_length=32, blank=True, null=True)
final_score = models.DecimalField(
decimal_places=2, max_digits=30, default=-1,
)
config = models.ForeignKey(Config, blank=True, null=True)
content_type = models.ForeignKey(
ContentType,
blank=True,
null=True)
object_id = models.PositiveIntegerField(blank=True, null=True)
my_model_instance = GenericForeignKey()
And the mixin is included in the MyModel models, which are available in many different apps.
When using a filter for this, using django filters, there is an issue. I have a filter that's supposed to be instantiated to each app when it's used. However when I instantiate, for example, with
class MyFilterSet(Filterset):
def __init__(self, *args, **kwargs):
self.config_pk = kwargs.pop('config_pk', None)
if not self.config_pk:
return
self.config = models.Config.objects.get(pk=self.config_pk)
self.custom_ordering["c_name"] =\
"field_one__{}_the_related_name__name".format(
self.config.app_model.app_label,
)
super(MyFilterSet,self).__init__(*args, **kwargs)
However, when I use this, I get
FieldError: Cannot resolve keyword 'my_app_the_related_name field. Choices are: %(app_label)s_the_related_name, answers, config, config_id, content_type, content_type_id, final_score, form, form_id, id, object_id, section_scores, status, updated
How can %(app_label)s_the_related_name be in the set of fields, and how can I either make it render properly (like it does outside of django filters) or is there some other solution.
You have probably encountered issue #25354. Templating related_query_name on GenericRelation doesn't work properly on Django 1.9 or below.
Added in Django 1.10 was related_query_name now supports app label and class interpolation using the '%(app_label)s' and '%(class)s' strings, after the fix was merged.

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',)

Extending TagBase in Django-Taggit

I have created the following TagBase and each category can have subcategory...
Will this work? How can I override its add function in the TaggableManager?
class Category(TagBase):
parent = models.ForeignKey('self', blank=True, null=True,
related_name='child')
description = models.TextField(blank=True, help_text="Optional")
class Meta:
verbose_name = _('Category')
verbose_name_plural = _('Categories')
django-taggit/docs/custom_tagging.txt describes how. You must define an intermediary model with a foreign key tag to your TagBase subclass.
from django.db import models
from taggit.managers import TaggableManager
from taggit.models import ItemBase
# Required to create database table connecting your tags to your model.
class CategorizedEntity(ItemBase):
content_object = models.ForeignKey('Entity')
# A ForeignKey that django-taggit looks at to determine the type of Tag
# e.g. ItemBase.tag_model()
tag = models.ForeignKey(Category, related_name="%(app_label)s_%(class)s_items")
# Appears one must copy this class method that appears in both TaggedItemBase and GenericTaggedItemBase
#classmethod
def tags_for(cls, model, instance=None):
if instance is not None:
return cls.tag_model().objects.filter(**{
'%s__content_object' % cls.tag_relname(): instance
})
return cls.tag_model().objects.filter(**{
'%s__content_object__isnull' % cls.tag_relname(): False
}).distinct()
class Entity(models.Model):
# ... fields here
tags = TaggableManager(through=CategorizedEntity)