Restrict values that appear in Django admin select box - python

In Django 1.11, I have 2 models, Foo and Bar:
class Foo(models.Model):
name = models.CharField()
extra = models.BooleanField(default=False)
class Bar(models.Model):
name = models.CharField()
extra_foo = models.ForeignKey(Foo)
My admin.py looks like this:
class BarInline(admin.StackedInline):
model = Bar
fields = ('name', 'extra_foo')
class FooAdmin(admin.ModelAdmin):
fields('name')
inlines = [BarInline]
My problem is that the in the Bar inline form, the dropdown for extra_foo shows all of my existing Foos. I want it to only show Foos for which extra is true. How can I modify the admin to restrict the available options in a select box to a subset of the whole?

I guess, you can use render_change_form
class FooAdmin(admin.ModelAdmin):
def render_change_form(self, request, context, *args, **kwargs):
context['adminform'].form.fields['extra_foo'].queryset = Foo.objects.filter(extra=True)
return super(FooAdmin, self).render_change_form(request, context, *args, **kwargs)
admin.site.register(Foo, FooAdmin)
If you want this to be global then have a look at limit_choices_to
extra_foo = models.ForeignKey(Foo, limit_choices_to={'extra': True})
Credit : filter foreignkey field in django admin

You can achieve that by using limit_choices_to. Change this line:
extra_foo = models.ForeignKey(Foo)
to:
extra_foo = models.ForeignKey(Foo, limit_choices_to={'extra': True})
This will limit the choices for extra_foo to Foo objects that have the property extra set to True.
This approach will however limit the choices not only in the admin but also beyond. If you need to adjust only the admin, you need a different approach.

Related

Is there any way to customize the order of multiple fields for forms.Form class with a field initiated by __init__?

When I render the form to a HTML template,
a certain field of form which is initiated by init is ordered always at bottom of table, even though I defined the field at the middle of form class.
Is there any way or method to customize the order of fields in the form where a initiated field exists by init.
I wanna put the field in the middle of form table in HTML template.
A screenshot of the rendered template:
In the screenshot,
the field "category_name" is ordered at the bottom of tag
I wanna change the order to the middle of table.
I am using Django 2.2 and python 3.7 on Windows 10.
Thanks
from django import forms
from .models import Category_name
class MakePurchaseLog(forms.Form):
buy_date = forms.DateField(input_formats=['%Y-%m-%d'])
shop = forms.CharField(max_length=50)
def __init__(self, user, *args, **kwargs):
super(MakePurchaseLog, self).__init__(*args, **kwargs)
self.fields['category_name'] = forms.ChoiceField(
choices = [(item.category_name, item.category_name) \
for item in Category_name.objects. \
filter(owner=user)])
goods_name = forms.CharField(max_length=50)
price = forms.IntegerField(min_value=0)
memo = forms.CharField(max_length=50, required=False)
field_order = ['category_name']
The fact that the __init__ is placed in the middle of the class, will not make any difference, since this is a function, and the evaluation is thus "postponed" until you actually create a MakePurchaseLog form object.
I think the most elegant solution here is to just already define the field in your form, and then in the __init__ function alter the choices, like:
class MakePurchaseLog(forms.Form):
buy_date = forms.DateField(input_formats=['%Y-%m-%d'])
shop = forms.CharField(max_length=50)
category_name = forms.ChoiceField()
goods_name = forms.CharField(max_length=50)
price = forms.IntegerField(min_value=0)
memo = forms.CharField(max_length=50, required=False)
def __init__(self, user, *args, **kwargs):
super(MakePurchaseLog, self).__init__(*args, **kwargs)
self.fields['category_name'].choices = [
(item.category_name, item.category_name)
for item in Category_name.objects.filter(owner=user)
]
We thus populate the choices of the category_name field in the __init__ function, but we define the field already at the class level.

Show children inline in Django admin

In Django 1.11, I have 2 models, Foo and Bar:
class Foo(models.Model):
name = models.CharField()
class Bar(models.Model):
name = models.CharField()
foo = models.ForeignKey(Foo)
When I visit the Foo page in the Django admin, I want to be able to see a list of its Bars underneath it. So I do this in admin.py:
class BarInline(admin.StackedInline):
model = Bar
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
list_display = ('name')
fields = ('name')
inlines = [BarInline]
But what I really want is a list of clickable links to a separate page where I can edit each Bar (as well as a Add button to add a new Bar to this Foo). I.e. I don't want the entire inline form. How is this possible in Django?
admin.py
from django.urls import reverse
from django.utils.html import format_html_join
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
list_display = ('name')
fields = ('name', get_related, )
readonly_fields = (get_related, )
def get_related(self, instance):
obj = instance.bar_set.all()
return format_html_join(
',',
'{}',
((
reverse('admin:{{ app_label }}_bar_change', args=(c.id,)),
c.name
) for c in obj),
)
You can create a callable readonly field which will return the reversed admin url of each relation wrapped in the relevant html code.
This will result in something like:
"your readonly field": link1, link2, link3
You can make all the fields readonly in BarInline.
class BarInline(admin.StackedInline):
model = Bar
readonly_fields = ('id', 'name', 'foo')
extra = 0
And for the Add functionality you can modify the inline template or more easy probably, add a custom field in FooAdmin, something like:
class FooAdmin(admin.ModelAdmin):
list_display = ('name')
fields = ('name', 'custom_add_bar')
inlines = [BarInline]
def custom_add_bar(self, obj):
add_url = reverse('admin:appname_bar_add')
return mark_safe(f'Add Bar')
Documentation for reversing admin urls

Django GenericForiegnKey, specify ContentTypes include/exclude for admin

If I have a GenericForeignKey
class Prereq(models.Model):
target_content_type = models.ForeignKey(ContentType, related_name='prereq_parent')
target_object_id = models.PositiveIntegerField()
target_object = GenericForeignKey("target_content_type", "target_object_id")
How can I create an include or exclude list for which models/ContentTypes I want included in the admin form?
Currently, I get a list of about 30 models (all the models in the project), when really I only want about 3 or 4 of those models
You can provide custom ModelForm on your admin and limit queryset inside target_content_type field.
class PrereqAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PrereqAdminForm, self).__init__(*args, **kwargs)
self.fields['target_content_type'].queryset = ContentType.objects.filter(your_conditions='something')
class PrereqAdmin(admin.ModelAdmin):
form = PrereqAdminForm
Also you can add limit_choices_to directly into your target_content_type field in Prereq class:
class Prereq(models.Model):
target_content_type = models.ForeignKey(ContentType, related_name='prereq_parent', limit_choices_to=conditions)
target_object_id = models.PositiveIntegerField()
target_object = GenericForeignKey("target_content_type", "target_object_id")
Where conditions can be an dictionary, Q object (like in filter) or some callable returning dictionary or Q object.

django modelchoicefield with unsaved models

All,
Is there a straightforward way for me to display a form that uses a modelchoicefield for models that have yet to be saved?
class Foo(models.Model):
name = models.CharField(max_length=42)
class Bar(models.Model):
name = models.CharField(max_length=42)
foo = models.ForeignKey("Foo",blank=True,null=True)
class BarForm(ModelForm):
class Meta:
model = Bar
pass
def create_new_bar(request):
foos = [Foo(name='foo1'), Foo(name='foo2'), Foo(name='foo3')]
bar = Bar(name='bar',foo=foos[0])
form = BarForm(instance=bar)
return render_to_response('bar_template.html',{"form" : form},context_instance=RequestContext(request))
But the "select" widget that gets rendered in the form has empty content. Presumably, this is because there is nothing to bind the queryset to for the ModelChoiceField that Bar's ForeignKey to Foo uses. (Since nothing in "foos" has been saved and therefore does not exist in the database).
Is there another way to deal with this besides writing a custom formfield/widget?
Thanks.
You can add additional field to BarForm with predefined choices - fooempty for example. That field will be shown in case if Foo table is empty. Try this (not tested):
class BarForm(ModelForm):
FOO_CHOICES = (
(0, 'Choice1'),
(1, 'Choice2'),
(2, 'Choice3'),
)
fooempty = forms.ChoiceField(required=False, label='lbl', choices=FOO_CHOICES)
class Meta:
model = Bar
def __init__(self, *args, **kwargs):
super(BarForm, self).__init__(*args, **kwargs)
if self.fields['foo'].queryset.count(): # Foo empty?
self.fields['fooempty'].widget = forms.HiddenInput()
else:
self.emptyflag
self.fields['foo'].widget = forms.HiddenInput()
def save(self, commit=True):
bar = super(BarForm, self).save(commit=False)
if self.emptyflag:
bar.foo_id = self.cleaned_data['fooempty']
bar.save()
return bar
EDIT: actually, now that I think about it. Those Foos won't have pks. So there is no way that could ever work. The form needs to know the pk of the Foos. You really just need to save them first. How would django know which one is which when it posts back the form? IGNORE ANSWER BELOW.
I think you can change the choices variable on the field. You will need to that in the form. So pass the list of Foos to the init of the form. Something like this:
class BarForm(ModelForm):
class Meta:
model = Bar
def __init__(self, foo_list, *args, **kwargs):
super(BarForm, self).__init__(*args, **kwargs)
self.fields['foo'].choices = foo_list
Then in the view:
foos = [Foo(name='foo1'), Foo(name='foo2'), Foo(name='foo3')]
bar = Bar(name='bar', foo=foos[0])
form = BarForm(foos, instance=bar)
I know I've done something like this before. I just can't remember the exact way. Something like this approach should work though.

Django: disallow can_delete on GenericStackedInline

I've built this model which contains a generic foreign key:
class MyModel(models.Model):
content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
object_id = models.PositiveIntegerField(_('object id'))
content_object = generic.GenericForeignKey('content_type', 'object_id')
Next I've made a generic stacked inline to put it in any ModelAmin class:
class MyModelStackedInline(generic.GenericStackedInline):
model = MyModel
formset = generic.generic_inlineformset_factory(MyModel, can_delete=False)
extra = 0
class SomeOhterModelAdmin(admin.ModelAdmin):
inlines = [MyModelStackedInline]
However, despite the can_delete=False arg passed by in generic_inlineformset_factory, I always see a Delete checkbox in my admin change_form.
Here is an example: http://img8.imageshack.us/img8/3323/screenshotbe.png
Do you know how to remove this checkbox ?
Thank you :)
Maybe It is a post '09 feature, but you can specify that without overriding the __init__() method :
class StupidCarOptionsInline(admin.StackedInline):
model = models.StupidOption
form = StupidCarOptionAdminForm
extra = 0
can_delete = False
Update 2016: as per Stan's answer below, modern versions of django let you set can_delete = True on the GenericStackedInline subclass, as it inherits from InlineModelAdmin
I've run into this before - for some reason passing can_delete as an argument doesn't work, but setting it in the formset's init method does. Try this:
class MyInlineFormset(generic.generic_inlineformset_factory(MyModel)):
def __init__(self, *args, **kwargs):
super(MyInlineFormset, self).__init__(*args, **kwargs)
self.can_delete = False
then in your admin inline class:
class MyModelStackedInline(generic.GenericStackedInline):
model = MyModel
formset = MyInlineFormset
extra = 0

Categories