I wish to only display a subset of the choices for a model form field. E.g Depending on the url the user is at I might want only 'weight gain' and 'parkinsonism' displayed as options for the 'se_name' field.
I can work out how to get the url as a parameters in the view (p = self.request.GET.get("p", None)) But I cant work out how to use this parameter to limit the choices available.
This is the formset
SideeffectFormSet = inlineformset_factory(
Case,
SideEffect,
fields=("se_name",),
widgets={'concern': RangeInput()},
extra=0,
min_num=1,
validate_min=True,
)
Which is based on the model:
class SideEffect(TimeStampedModel):
SE_CHOICES = [
("weight_gain", "Weight Gain"),
("parkinsonism", "Parkinsonism"),
("dystonia", "Dystonia"),
("none", "None"),
]
se_name = models.CharField("",max_length=200, choices=SE_CHOICES, default="none")
case = models.ForeignKey(Case, on_delete=models.CASCADE)
And the form is rendered by this view:
class CaseView(LoginRequiredMixin, TemplateView):
model = Case
template_name = "se_balance/se_balance.html"
def get(self, *args, **kwargs):
p = self.request.GET.get("p", None)
sideeffect_formset = SideeffectFormSet(queryset=SideEffect.objects.none(),)
return self.render_to_response(
{ "sideeffect_formset": sideeffect_formset,
"sideeffect_formsethelper": SideEffectFormSetSetHelper,
}
)
To change the choices of a field in a formset dynamically you need to define a custom form class that does the choice alteration on __init__
from django import forms
from .models import SideEffect
class SideEffectForm(ModelForm):
class Meta:
model = SideEffect
fields = ['se_name']
def __init__(self, *args, p, **kwargs):
super().__init__(*args, **kwargs)
if p == 'foo':
self.fields['se_name'].choices = [
("weight_gain", "Weight Gain"),
("parkinsonism", "Parkinsonism"),
]
else:
...
Use the form class in your formset
SideeffectFormSet = inlineformset_factory(
Case,
SideEffect,
form=SideEffectForm,
extra=0,
min_num=1,
validate_min=True,
)
Then in your view you can pass custom parameters to the formset form
sideeffect_formset = SideeffectFormSet(queryset=SideEffect.objects.none(), form_kwargs={'p': p})
Related
I have a simple CategoryForm which has a hidden field that automatically gets added during save on the front-end. In the Admin panel I would like is_staff users to be able to add a Category while the field is hidden there as well. To superusers I would like the field to be shown. How do I get the excluded fields back in my Admin form?
Forms.py:
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = ('category', 'company',)
exclude = ['company']
widgets = {'company': forms.HiddenInput()}
Admin.py:
class CustomCategoryAdmin(admin.ModelAdmin):
form = CategoryForm
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if not request.user.is_superuser:
self.fields = ('category',)
else:
self.fields = ('category', 'company',) # this throws a key error because company is excluded
return form
def save_related(self, request, form, formsets, change):
super(CustomCategoryAdmin, self).save_related(request, form, formsets, change)
company = request.user.company
form.instance.company.add(company) # add object to company field while saving
What I decided to do was create another form named AdminCategoryForm and set the form = AdminCategoryForm as an attribute of CustomCategoryAdmin.
So now my Forms.py:
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = ('category', 'company',)
exclude = ['company']
widgets = {
'company': forms.HiddenInput()
}
class AdminCategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = ('category', 'company',)
And my Admin.py:
class CustomCategoryAdmin(admin.ModelAdmin):
form = AdminCategoryForm
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if not request.user.is_superuser:
self.fields = ('category',)
else:
self.fields = ('category', 'company',)
self.filter_horizontal = ('company',)
return form
def save_related(self, request, form, formsets, change):
super(CustomCategoryAdmin, self).save_related(request, form, formsets, change)
company = request.user.company
form.instance.company.add(company)
Even though I am repeating my code I feel that this may be the more elegant solution than overriding a ModelAdmin method as it becomes much more clear to outside programmers what is going on here.
If anybody else has other solutions I would love to hear..
I am trying to dynamically filter which choices are displayed in a form. the form fields are generated from model. Currently all choices are being displayed without any filters applying.
in the view I get the currents site_type then pass this to the form, to filter the subnets that also have the same site_type, or this is whats supposed to happen anyway.
can anyone see why the filter would not be applying?
forms.py
class AutoSubnetForm(forms.Form):
subnet_type_data = SubnetTypes.objects.all()
def __init__(self, *args, **kwargs):
self.site_type = kwargs.pop("site_type")
# get site type if set and filter against it
if self.site_type:
self.subnet_type_data = SubnetTypes.objects.filter(related_sites=self.site_type)
super(AutoSubnetForm, self).__init__(*args, **kwargs)
# create list for types
subnet_types = []
for stype in subnet_type_data:
# add tuple for each type
subnet_types.append((stype.id,stype.subnet_type))
subnets = forms.MultipleChoiceField(
choices=subnet_types,
widget = forms.CheckboxSelectMultiple(
attrs = {'class': 'form-control'}
)
)
views.py
#login_required
#user_passes_test(lambda u: u.has_perm('config.add_subnet'))
def auto_gen_subnets(request, site_id):
#generate_subnets(site_id)
from config.models import SubnetTypes
site_data = get_object_or_404(SiteData.objects.select_related('site_type'),pk=site_id)
subnets = None
if request.method == 'GET':
form = AutoSubnetForm(site_type=site_data.site_type)
else:
# A POST request: Handle Form Upload
form = AutoSubnetForm(request.POST)
# If data is valid, proceeds to create a new post and redirect the user
if form.is_valid():
subnets = form.cleaned_data['subnets']
return render(request, 'sites/generate_subnets.html', {
'data': subnets,
'form': form,
'SiteName' : site_data.location,
'SiteID' : site_id,
}
)
If you are generating form from the model you need ModelMultipleChoiceField.
Try something like this:
class AutoSubnetForm(forms.Form):
def __init__(self, *args, **kwargs):
site_type = kwargs.pop("site_type")
queryset = SubnetTypes.objects.all()
if site_type:
queryset = queryset.filter(related_sites=site_type)
super(AutoSubnetForm, self).__init__(*args, **kwargs)
self.fields['the_name_of_your_form_field'] = forms.ModelMultipleChoiceField(
queryset=queryset,
widget = forms.CheckboxSelectMultiple(
attrs = {'class': 'form-control'}
)
)
Im trying to implement auocomplete in my django project.
I want to pass a parameter through the url in the form. This is my Form
class SongForm(forms.ModelForm):
song_title = forms.ModelChoiceField(
queryset=Snr.objects.all(),
widget=autocomplete.ModelSelect2(url='login:song-autocomplete',attrs={'data-placeholder': 'Autocomplete ...',
'data-minimum-input-length': 3,},)
)
class Meta:
model = Song
fields = ['song_title']
my url pattern is
url(r'^(?P<playlist_id>[0-9]+)/create_song/song-autocomplete/$', views.SongAutocomplete.as_view(), name='song-autocomplete', ),
so while calling the url song-autocomplete it needs a parameter playist_id. How do I send it using the url='login:song-autocomplete'?
Thank you.
full example using my own code:
forms.py
class AddRelationshipForm(autocomplete.FutureModelForm):
first = autocomplete.QuerySetSequenceModelField(
queryset=autocomplete.QuerySetSequence(Person.objects.all()),
required=False,
widget=autocomplete.QuerySetSequenceSelect2(
url='PersonAutoUrl', attrs={
'style':'width:15em;', 'data-placeholder': 'find'
}
),
)
def __init__(self, *args, **kwargs):
type = kwargs.pop('type')
super(AddRelationshipForm, self).__init__(*args, **kwargs)
self.fields['first'] = autocomplete.QuerySetSequenceModelField(
queryset=autocomplete.QuerySetSequence(Person.objects.all()),
required=False,
widget=autocomplete.QuerySetSequenceSelect2(
url='PersonAutoUrl/'+type, attrs={
'style':'width:15em;', 'data-placeholder': 'search'
}
),
)
class Meta:
model = Relation
fields = ('first',)
urls.py
url(
'^relationships/RelationshipAutoUrl/(?P<type>\w+)/$',
RelationshipAutoView.as_view(),
name='PersonAutoUrl',
),
views.py
class RelationshipAutoView(Select2QuerySetSequenceView):
def get_queryset(self):
excluded = Person.objects.filter(first__owner=self.request.user, first__type=self.kwargs['type'])
queries = Person.objects.exclude(id__in=excluded)
if self.q:
queries = queries.filter(first_name__icontains=self.q)
return queries
initiating form and passing kwarg type in view:
def relationships(request):
relationships = ['Friend', 'Family','Business']
forms = {}
for key in relationships:
forms[key] = AddRelationshipForm(request.POST or None, prefix='new'+key, type=key)
Using Django 1.8 on Python 3.4.1 with models:
class Product(models.Model):
name = models.CharField(max_length=255)
# some more fields here
def __str__(self):
return self.name
class PricedProduct(models.Model):
product = models.ForeignKey(Product, related_name='prices')
# some more fields here
def __str__(self):
return str(self.product)
class Coming(models.Model):
# some unimportant fields here
class ComingProducts(models.Model):
coming = models.ForeignKey(Coming)
priced_product = models.ForeignKey(PricedProduct)
# more unimportant fields
and the following admin.py:
class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
model = ComingProducts
class ComingAdmin(admin.ModelAdmin):
inlines = [ComingProductsInline]
Of course, i have a problem with multiply queries to database: i have a query for each item in list and a query for each line. So, having 100 items i get 100 ^ 2 queries.
I've solved the problem with queries for each line with Caching queryset choices for ModelChoiceField or ModelMultipleChoiceField in a Django form
But i still having problem with str method. I've tried the following:
1) adding prefetch_related to ComingAdmin:
def get_queryset(self, request):
return super(ComingAdmin, self).get_queryset(request). \
prefetch_related('products__product')
2) adding select_related to ComingProductInline:
def get_queryset(self, request):
return super(ComingProductsInline, self).get_queryset(request). \
select_related('priced_product__product')
3) Defining custom form for inline and adding select_related to field queryset:
class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
model = ComingProducts
form = ComingProductsAdminForm
class ComingProductsAdminForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ComingProductsAdminForm, self).__init__(args, kwargs)
self.fields['priced_product'].queryset = PricedProduct.objects.all(). \
select_related('product')
class Meta:
model = ComingProducts
fields = '__all__'
4) Defining a custom formset:
class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
model = ComingProducts
formset = MyInlineFormset
class MyInlineFormset(BaseInlineFormSet):
def __init__(self, data=None, files=None, instance=None,
save_as_new=False, prefix=None, queryset=None, **kwargs):
super(MyInlineFormset, self).__init__(data, files, instance,
save_as_new, prefix, queryset, **kwargs)
self.queryset = ComingProducts.objects.all(). \
prefetch_related('priced_product__product')
5) Different combinations for previous 4 methods
And nothing helps: each call of str for PricedProduct makes Django to perform a query for Product table. All of these methods were mentioned on stackoverflow, but they treated ModelAdmin, and do not help with Inline. What do i miss?
The formset solution does work for me, but with a slightly different approach:
class MyInlineFormset(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInlineFormset, self).__init__(*args, **kwargs)
self.queryset = self.queryset.prefetch_related('priced_product__product')
The BaseInlineFormSet class filters the queryset for you, and you need to take that filtered queryset and add the prefetch. With your formset implementation (the all() queryset) you get unrelated ComingProduct objects and it probably takes much too long to render. When it's the filtered queryset it renders very quickly.
You will find this approach very useful:
project/admin.py
from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.db.models.constants import LOOKUP_SEP
class AdminBaseWithSelectRelated(BaseModelAdmin):
"""
Admin Base using list_select_related for get_queryset related fields
"""
list_select_related = []
def get_queryset(self, request):
return super(AdminBaseWithSelectRelated, self).get_queryset(request).select_related(*self.list_select_related)
def form_apply_select_related(self, form):
for related_field in self.list_select_related:
splitted = related_field.split(LOOKUP_SEP)
if len(splitted) > 1:
field = splitted[0]
related = LOOKUP_SEP.join(splitted[1:])
form.base_fields[field].queryset = form.base_fields[field].queryset.select_related(related)
class AdminInlineWithSelectRelated(admin.TabularInline, AdminBaseWithSelectRelated):
"""
Admin Inline using list_select_related for get_queryset and get_formset related fields
"""
def get_formset(self, request, obj=None, **kwargs):
formset = super(AdminInlineWithSelectRelated, self).get_formset(request, obj, **kwargs)
self.form_apply_select_related(formset.form)
return formset
class AdminWithSelectRelated(admin.ModelAdmin, AdminBaseWithSelectRelated):
"""
Admin using list_select_related for get_queryset and get_form related fields
"""
def get_form(self, request, obj=None, **kwargs):
form = super(AdminWithSelectRelated, self).get_form(request, obj, **kwargs)
self.form_apply_select_related(form)
return form
class FilterWithSelectRelated(admin.RelatedFieldListFilter):
list_select_related = []
def field_choices(self, field, request, model_admin):
return [
(getattr(x, field.remote_field.get_related_field().attname), str(x))
for x in self.get_queryset(field)
]
def get_queryset(self, field):
return field.remote_field.model._default_manager.select_related(*self.list_select_related)
app/admin.py
from django.contrib import admin
from project.admin import AdminWithSelectRelated, AdminInlineWithSelectRelated, FilterWithSelectRelated
from .models import FormaPago, Comprobante, ItemServicio, ItemBazar
class ItemServicioInlineAdmin(AdminInlineWithSelectRelated):
model = ItemServicio
list_select_related = (
'alumno_servicio__alumno__estudiante__profile',
'alumno_servicio__servicio__grado',
'comprobante__forma_pago',
)
class ItemBazarInlineAdmin(AdminInlineWithSelectRelated):
model = ItemBazar
list_select_related = (
'alumno_item__alumno__estudiante__profile',
'alumno_item__item__anio_lectivo',
'comprobante__forma_pago',
)
class ComprobanteAdmin(AdminWithSelectRelated):
list_display = ('__str__', 'total', 'estado', 'fecha_generado', 'forma_pago', 'tipo', )
list_filter = ('estado', 'forma_pago', )
list_select_related = ('forma_pago', )
inlines = (ItemServicioInlineAdmin, ItemBazarInlineAdmin, )
class AlumnoFilter(FilterWithSelectRelated):
list_select_related = ('estudiante__profile', )
class ItemServicioAdmin(AdminWithSelectRelated):
list_display = ('nombre', 'alumno', 'monto_pagado', 'comprobante', )
list_filter = (
'alumno_servicio__alumno__seccion__grado',
('alumno_servicio__alumno', AlumnoFilter),
)
list_select_related = (
'comprobante__forma_pago',
'alumno_servicio__alumno__estudiante__profile',
'alumno_servicio__alumno__seccion__grado',
'alumno_servicio__servicio__grado',
)
class ItemBazarAdmin(AdminWithSelectRelated):
list_display = ('nombre', 'alumno', 'monto_pagado', 'comprobante', )
list_filter = (
'alumno_item__alumno__seccion__grado',
('alumno_item__alumno', AlumnoFilter),
)
list_select_related = (
'comprobante__forma_pago',
'alumno_item__alumno__estudiante__profile',
'alumno_item__alumno__seccion__grado',
'alumno_item__item__anio_lectivo',
)
admin.site.register(FormaPago)
admin.site.register(Comprobante, ComprobanteAdmin)
admin.site.register(ItemServicio, ItemServicioAdmin)
admin.site.register(ItemBazar, ItemBazarAdmin)
All I have to do is define the select_related fields, and the Custom AdminWithSelectRelated, AdminInlineWithSelectRelated, and FilterWithSelectRelated make use of them for Changelists, Changeforms, and even inline Formsets.
Works like a charm.
Inspired from #helpse answer you can also do the following if you just want to override the queryset for a single admin inline:
class ComingProductsInline(admin.TabularInline):
model = ComingProducts
def get_formset(self, request, obj=None, **kwargs):
formset = super(ComingProductsInline, self).get_formset(request, obj, **kwargs)
queryset = formset.form.base_fields["priced_product"].queryset
queryset = queryset.select_related("product")
formset.form.base_fields["priced_product"].queryset = queryset
return formset
It might be sufficient for most of the cases.
I'm working currently on a similar problem. What I have found is documented at this thread: Translatable Manytomany fields in admin generate many queries
One important observation I did is that my solution works only for Django 1.7x and not for 1.8. Exactly same code, with d1.7 I have order of 10^1 queries, and with new installation of d1.8 I have 10^4.
note : This is closely related to the answer in this question :
django admin - add custom form fields that are not part of the model
In Django it is possible to create custom ModelForms that have "rouge" fields that don't pertain to a specific database field in any model.
In the following code example there is a custom field that called 'extra_field'. It appears in the admin page for it's model instance and it can be accessed in the save method but there does not appear to be a 'load' method.
How do I load the 'extra_field' with data before the admin page loads?
# admin.py
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def load(..., obj):
# This method doesn't exist.
# extra_field = obj.id * random()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
return super(YourModelForm, self).save(commit=commit)
class Meta:
model = YourModel
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
fieldsets = (
(None, {
'fields': ('name', 'description', 'extra_field',),
}),
)
source code by #vishnu
Override the form's __init__ method and set the initial property of the field:
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def __init__(self, *args, **kwargs):
super(YourModelForm, self).__init__(*args, **kwargs)
initial = '%s*rnd' % self.instance.pk if self.instance.pk else 'new'
self.fields['extra_field'].initial = initial