How to Restrict Foreign Keys Choices to Related Objects Only in Django

How do I restrict foreign keys choices to related objects only in django

I just came across ForeignKey.limit_choices_to in the Django docs.
Not sure yet how it works, but it might be the right thing here.

Update: ForeignKey.limit_choices_to allows one to specify either a constant, a callable or a Q object to restrict the allowable choices for the key. A constant obviously is of no use here, since it knows nothing about the objects involved.

Using a callable (function or class method or any callable object) seems more promising. However, the problem of how to access the necessary information from the HttpRequest object remains. Using thread local storage may be a solution.

2. Update: Here is what has worked for me:

I created a middleware as described in the link above. It extracts one or more arguments from the request's GET part, such as "product=1", and stores this information in the thread locals.

Next there is a class method in the model that reads the thread local variable and returns a list of ids to limit the choice of a foreign key field.

@classmethod
def _product_list(cls):
"""
return a list containing the one product_id contained in the request URL,
or a query containing all valid product_ids if not id present in URL

used to limit the choice of foreign key object to those related to the current product
"""
id = threadlocals.get_current_product()
if id is not None:
return [id]
else:
return Product.objects.all().values('pk').query

It is important to return a query containing all possible ids if none was selected so that the normal admin pages work ok.

The foreign key field is then declared as:

product = models.ForeignKey(
Product,
limit_choices_to={
id__in=BaseModel._product_list,
},
)

The catch is that you have to provide the information to restrict the choices via the request. I don't see a way to access "self" here.

Django: Limiting foreign key choices based on related object

Give the SoftwareAsset an FK to Company.

class SoftwareAsset(models.Model):
name = models.CharField(max_length=200)
company = models.ForeignKey(Company)

So a company can own many software assets. Now you can easily filter the software assets in your custom modelform.

EmployeeSoftwareForm(forms.ModelForm):
class Meta:
model = EmployeeSoftware

def __init(self, *args, **kwargs):
super(EmployeeSoftwareForm, self).__init__(*args, **kwargs)
if 'instance' in kwargs:
self.fields['assets'].queryset = SoftwareAsset.objects.filter(company = kwargs['instance'].employee.company)

Finally use this custom form for the EmployeeSoftware modeladmin:

class EmployeeSoftwareAdmin(admin.ModelAdmin):
form = EmployeeSoftwareForm

This eliminates the need for the CompanyAsset model.

UPDATE: OK, you want each new instance of EmployeeSoftware to know what assets it can have, based on the employee company. But because you dont know what your employee is until you select it, its impossible to do on form init. You have to use javascript to filter the select based on the employee choice. Something like this (using JQuery):

$("#employee").change(function(){

$.post(<your assets select url>, $("employeeform").serialize(), function (data) {
//populate your returned JSON into the asset select
}, 'json');

})

Obviously, the url your post goes to filters the assets based on the selected employee, which should be simple.

How do I restrict foreign keys choices in admin formset to related objects only in django

You have to pass the parent object from the InlineAdmin to the FormSet to the actual Form. Took me quite a moment to figure that out.
In the example below CalibrationCertificateData is relate to the CalibrationCertificate and I only want to show Quantities that are related with the SensorCategory.

from django.contrib import admin
from django.forms import ModelForm, BaseInlineFormSet
from src.admin import site_wm
from . import models
from quantity_management.models import QuantitySensor

# Register your models here.
site_wm.register(models.CalibrationInstitute, site=site_wm)

class CertDataAdminForm(ModelForm):
class Meta:
model = models.CalibrationCertificateData
fields = "__all__"

def __init__(self, *args, **kwargs):
"""
Make sure that we only show the Quantites that are related to the selected Cert>Sensor>Category
"""
self.parent_obj = None
if 'parent_obj' in kwargs:
self.parent_obj = kwargs['parent_obj']
del kwargs['parent_obj']
super().__init__(*args, **kwargs)
if self.parent_obj:
# Do the actual filtering according to the parent object
category_id = self.parent_obj.Sensor.Category_id
self.fields['Quantity'].queryset = QuantitySensor.objects.filter(Sensor_id=category_id)

class CertDataAdminFormSet(BaseInlineFormSet):
form = CertDataAdminForm

def get_form_kwargs(self, index):
"""
Make sure the form knows about the parent object
"""
kwargs = super().get_form_kwargs(index)
if hasattr(self, 'parent_obj') and self.parent_obj:
kwargs['parent_obj'] = self.parent_obj
return kwargs

class CalibrationDataAdmin(admin.StackedInline):
model = models.CalibrationCertificateData
extra = 0
form = CertDataAdminForm
formset = CertDataAdminFormSet

def get_formset(self, request, obj=None, **kwargs):
"""
give the formset the current object, so it can limit the selection choices according to it
"""
formset = super().get_formset(request, obj, **kwargs)
if formset is not None:
formset.parent_obj = obj
return formset

def formfield_for_foreignkey(self, db_field, request, **kwargs):
print(self.parent_model)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

@admin.register(models.CalibrationCertificate, site=site_wm)
class CalibrationCertificateAdmin(admin.ModelAdmin):
inlines = (
CalibrationDataAdmin,
)

Limit choices and validate django's foreign key to related objects (also in REST)

Here what I've found so far:

To get Foreignkey showed right in admin, you have to specify a form in ModelAdmin

class TodoAdminForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = Category.objects.filter(user__pk=self.instance.user.pk)

@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
form = TodoAdminForm
...

To get ManyToManyField showed right in InlineModelAdmin (e.g. TabularInline) here comes more dirty hack (can it be done better?)

You have to save your quiring field value from object and then manually set queryset in the field. My through model has two members todo and tag

And I'd like to filter tag field (pointing to model Tag):

class MembershipInline(admin.TabularInline):
model = Todo.tags.through

def get_formset(self, request, obj=None, **kwargs):
request.saved_user_pk = obj.user.pk # Not sure if it can be None
return super().get_formset(request, obj, **kwargs)

def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
if db_field.name == 'tag':
kwargs['queryset'] = Tag.objects.filter(user__pk=request.saved_user_pk)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

And finally, to restrict elements only to related in Django REST framework, I have to implement custom Field

class PrimaryKeyRelatedByUser(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return super().get_queryset().filter(user=self.context['request'].user)

And use it in my serializer like

class TodoSerializer(serializers.ModelSerializer):
category = PrimaryKeyRelatedByUser(required=False, allow_null=True, queryset=Category.objects.all())
tags = PrimaryKeyRelatedByUser(required=False, many=True, queryset=Tag.objects.all())

class Meta:
model = Todo
fields = ('id', 'category', 'tags', ...)

Not sure if it actually working in all cases as planned. I'll continue this small investigation.

Question still remains. Could it be done simplier?

How to restrict Foreign Key choices to another Foreign Key in the same model

I'm not familiar with Django, but I if you are trying to solve the "same one the choice is linked to" part of the problem, this is how it can be done at the database level:

Sample Image

Note the usage of identifying relationships, so the DecisionId is migrated down both "branches" and merged at the "bottom". So if a Choice has Status, they both must be linked to the same Decision.

How to restrict foreign key objects for autocomplete search results in django ModelAdmin.autocomplete_fields?

Instead of using autocomplete_fields = ['spm'], overriding change_form.html template and using JavaScript to make HTML select element searchable(with auto-completion) worked for me:

Sample Image

content of change_form.html:

{% extends 'admin/change_form.html' %}

{% block admin_change_form_document_ready %}
{{ block.super }}
<script src="https://code.jquery.com/jquery-2.1.1.min.js" type="text/javascript"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.1/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.1/js/select2.min.js"></script>
<script type="text/javascript">
$("#id_spm").select2({});
</script>

{% endblock %}

The HTML select element on which select2 function operate:

<select name="spm" required="" id="id_spm" >
<option value="" selected="">---------</option>
<option value="67688">apple iphone 7</option>
<option value="69093">apple iphone 7 plus</option>
<option value="71453">apple ipad pro</option>
<option value="71076">apple ipad pro 9.7</option>
<option value="34840">apple ipad pro 10.5</option>
<option value="72303">apple iphone 8 plus</option>
<option value="72301">apple iphone 8</option>
<option value="72307">apple iphone x</option>
<option value="71243">apple ipad pro 12.9</option>
</select>


Related Topics



Leave a reply



Submit