Add dynamic field to django admin model form - python

I need to add the field with link to the model view on my site to the django admin view.
When I add field name to the list_display and define method for rendering this url:
class SetAdmin(admin.ModelAdmin):
list_display = ['many other fields', 'show_set_url']
def show_set_url(self, obj):
return 'Set' # render depends on other fields
It shows in Sets list in django admin, but not in model form.
How can I fix this and add link to the Sets Form in django admin?
I've tried also to create custom form for this model:
from core.models import Set
from django import forms
class SetAdminForm(forms.Form):
class Meta:
model = Set
def __init__(self, *args, **kwargs):
super(SetAdminForm, self).__init__(*args, **kwargs)
self.fields['foo'] = forms.IntegerField(label=u"Link")
But there is now visible effect in form.

You can accomplish what you're trying by overriding ModelAdmin but you also need to override ModelAdmin.get_fieldsets. This answer might help you out. The OP in the link has a similar problem as well.
Edit: If you don't want an editable field you can try overriding ModelAdmin.get_readonly_fields. Also check here for more attributes to override.

You can create dynamic fields and fieldset using the form meta class. Sample code is given below. Add the loop logic as per you requirements.
class CustomAdminFormMetaClass(ModelFormMetaclass):
"""
Metaclass for custom admin form with dynamic field
"""
def __new__(cls, name, bases, attrs):
for field in myloop: #add logic to get the fields
attrs[field] = forms.CharField(max_length=30) #add logic to the form field
return super(CustomAdminFormMetaClass, cls).__new__(cls, name, bases, attrs)
class CustomAdminForm(six.with_metaclass(CustomAdminFormMetaClass, forms.ModelForm)):
"""
Custom admin form
"""
class Meta:
model = ModelName
fields = "__all__"
class CustomAdmin(admin.ModelAdmin):
"""
Custom admin
"""
fieldsets = None
form = CustomAdminForm
def get_fieldsets(self, request, obj=None):
"""
Different fieldset for the admin form
"""
self.fieldsets = self.dynamic_fieldset(). #add logic to add the dynamic fieldset with fields
return super(CustomAdmin, self).get_fieldsets(request, obj)
def dynamic_fieldset(self):
"""
get the dynamic field sets
"""
fieldsets = []
for group in get_field_set_groups: #logic to get the field set group
fields = []
for field in get_group_fields: #logic to get the group fields
fields.append(field)
fieldset_values = {"fields": tuple(fields), "classes": ['collapse']}
fieldsets.append((group, fieldset_values))
fieldsets = tuple(fieldsets)
return fieldsets

You have to add it to the readonly_fields list.
class SetAdmin(admin.ModelAdmin):
list_display = ['many other fields', 'show_set_url']
readonly_fields = ['show_set_url']
def show_set_url(self, obj):
return 'Set' # render depends on other fields
Relevant documentation.

I had trouble with all of these answers in Django 1.9 for inline models.
Here is a code snippet that accomplished dynamic fields for me. In this example, I'm assuming you already have a modal called ProductVariant which contains a foreign key relationship to a model called Product:
class ProductVariantForm(forms.ModelForm):
pass
class ProductVariantInline(admin.TabularInline):
model = ProductVariant
extra = 0
def get_formset(self, request, obj=None, **kwargs):
types = ( (0, 'Dogs'), (1, 'Cats'))
#Break this line appart to add your own dict of form fields.
#Also a handy not is you have an instance of the parent object in obj
ProductVariantInline.form = type( 'ProductVariantFormAlt', (ProductVariantForm, ), { 'fancy_select': forms.ChoiceField(label="Animals",choices=types)})
formset = super( ProductVariantInline, self).get_formset( request, obj, **kwargs)
return formset
class ProductAdmin(admin.ModelAdmin):
inlines = (ProductVariantInline, )
My specific use case is ProductVariant has a many to many relationship that should only have limited selections based on business logic grouping of the entries. Thus my need for custom dynamic fields in the inline.

Related

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

I have my models.py like this:
class Category(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=256, db_index=True)
class Todo(models.Model):
user = models.ForeignKey(User)
category = models.ForeignKey(Category)
...
And I want to limit choices of Category for Todo to only those ones where Todo.user = Category.user
Every solutuion that I've found was to set queryset for a ModelForm or implement method inside a form. (As with limit_choices_to it is not possible(?))
The problem is that I have not only one model with such limiting problem (e.g Tag, etc.)
Also, I'm using django REST framework, so I have to check Category when Todo is added or edited.
So, I also need functions validate in serializers to limit models right (as it does not call model's clean, full_clean methods and does not check limit_choices_to)
So, I'm looking for a simple solution, which will work for both django Admin and REST framework.
Or, if it is not possible to implement it the simple way, I'm looking for an advice of how to code it the most painless way.
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?

FilteredSelectMultiple widget with django-taggit field in django admin

I am trying to add Tags to my model instances using django-taggit's package. For this I have added the tags field in my Model as it is defined in django-taggit's definition.
class MyModel(models.Model):
name = models.CharField(max_length=100)
tags = TaggableManager()
I want to add this model to django-admin panel and want to use FilteredSelectMultiple widget for adding tags. for this I have created a model form and changed it's field's widget.
class MyModelForm(forms.ModelForm):
tags = forms.ModelMultipleChoiceField(queryset=Tag.objects.none())
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['tags'].widget = FilteredSelectMultiple('Tags', False)
self.fields['tags'].queryset = Tag.objects.all()
class Meta:
model = MyModel
exclude = []
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
everything is working fine. Tags are being saved after saving the instace. but the problem is that when I am opening the update page. There are no previously selected tags in 'Chosen Tags' part of the field's widget.It is empty and all the choices are in 'Available tags' option.
I tried to provide initial data also for the change_form of model admin but nothing works for me.
def get_changeform_initial_data(self, request):
return {'tags': self.object.tags.all()}
self.object is the object which i got by get_object() method of the ModelAdmin class.
Give me a solution.
Seems the problem is that the prepare_value function in ModelMultipleChoiceField looks at the .pk field on the object which gives a value that's incorrect so it does not render (or renders the wrong selection). You should be looking at it's .tag_id field.
This worked for me but would be interested if there is a more correct or elegant way:
class TagMultipleChoiceField(forms.ModelMultipleChoiceField):
def prepare_value(self, value):
if hasattr(value, 'tag_id'):
return value.tag_id
elif hasattr(value, '__iter__') and not isinstance(value, six.text_type) and not hasattr(value, '_meta'):
return [self.prepare_value(v) for v in value]
else:
return super(TagMultipleChoiceField, self).prepare_value(value)
class AdminCourseForm(forms.ModelForm):
class Meta:
model = Course
exclude = ()
tags = TagMultipleChoiceField(queryset=MyTag.objects.all())

Django admin.ModelAdmin inheritance

Is it possible to create subclasses from a Django admin.ModelAdmin class?
I'm trying to do something like:
class PageAdmin(admin.ModelAdmin):
form = PageAdminForm
# Plus many other stuff here (that I don't want to copy/paste in PlacePageAdmin to keep dry...)
class PlacePageAdmin(PageAdmin):
form = PlacePageAdminForm
def get_form(self, request, obj=None, **kwargs):
return super(PlacePageAdmin, self).get_form(request, obj, **kwargs)
def get_fieldsets(self, request, obj=None):
fieldsets = copy.deepcopy(super(PlacePageAdmin, self).get_fieldsets(request, obj))
fieldsets.insert(0,
('Place', {
'fields': (
('address', 'postcode', 'town'),
),
})
)
return fieldsets
def queryset(self, request):
return PlacePage.objects.filter(page_type=Page.PLACE)
But I can't make it work: I get a u"Key 'address' not found in Form" error when trying to access the PlacePageAdmin form page.
Has someone ever done such a thing?
Thank you very much in advance!
Edit - The forms code:
class PageAdminForm(forms.ModelForm):
extra_field = forms.CharField()
class Meta:
model = Page
class PlacePageAdminForm(PageAdminForm):
class Meta:
model = PlacePage
of course you can, this is basic python, even more you can create mixins you can add to your class inheritance list which may be more useful - as smaller and simpler examples of inheriting from multiple sources
error you are getting is connected with your forms, there are few things that could go wrong
the most popular is definition of which fields in form.ModelForm are defined - those fields are specified in Meta class inside ModelForm class - there are problems with inheritance, so make sure that in inherited forms you will set it properly
there may be a field that is not being defined in the second form or model - then your form inheritance can fail

Add custom form fields that are not part of the model (Django)

I have a model registered on the admin site. One of its fields is a long string expression. I'd like to add custom form fields to the add/update pages of this model in the admin. Based on the values of these fields I will build the long string expression and save it in the relevant model field.
How can I do this?
I'm building a mathematical or string expression from symbols. The user chooses symbols (these are the custom fields that are not part of the model) and when they click save then I create a string expression representation from the list of symbols and store it in the DB. I don't want the symbols to be part of the model and DB, only the final expression.
Either in your admin.py or in a separate forms.py you can add a ModelForm class and then declare your extra fields inside that as you normally would. I've also given an example of how you might use these values in form.save():
from django import forms
from yourapp.models import YourModel
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# ...do something with extra_field here...
return super(YourModelForm, self).save(commit=commit)
class Meta:
model = YourModel
To have the extra fields appearing in the admin just:
Edit your admin.py and set the form property to refer to the form you created above.
Include your new fields in your fields or fieldsets declaration.
Like this:
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
fieldsets = (
(None, {
'fields': ('name', 'description', 'extra_field',),
}),
)
UPDATE:
In Django 1.8 you need to add fields = '__all__' to the metaclass of YourModelForm.
It it possible to do in the admin, but there is not a very straightforward way to it. Also, I would like to advice to keep most business logic in your models, so you won't be dependent on the Django Admin.
Maybe it would be easier (and maybe even better) if you have the two seperate fields on your model. Then add a method on your model that combines them.
For example:
class MyModel(models.model):
field1 = models.CharField(max_length=10)
field2 = models.CharField(max_length=10)
def combined_fields(self):
return '{} {}'.format(self.field1, self.field2)
Then in the admin you can add the combined_fields() as a readonly field:
class MyModelAdmin(models.ModelAdmin):
list_display = ('field1', 'field2', 'combined_fields')
readonly_fields = ('combined_fields',)
def combined_fields(self, obj):
return obj.combined_fields()
If you want to store the combined_fields in the database you could also save it when you save the model:
def save(self, *args, **kwargs):
self.field3 = self.combined_fields()
super(MyModel, self).save(*args, **kwargs)
Django 2.1.1
The primary answer got me halfway to answering my question. It did not help me save the result to a field in my actual model. In my case I wanted a textfield that a user could enter data into, then when a save occurred the data would be processed and the result put into a field in the model and saved. While the original answer showed how to get the value from the extra field, it did not show how to save it back to the model at least in Django 2.1.1
This takes the value from an unbound custom field, processes, and saves it into my real description field:
class WidgetForm(forms.ModelForm):
extra_field = forms.CharField(required=False)
def processData(self, input):
# example of error handling
if False:
raise forms.ValidationError('Processing failed!')
return input + " has been processed"
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# self.description = "my result" note that this does not work
# Get the form instance so I can write to its fields
instance = super(WidgetForm, self).save(commit=commit)
# this writes the processed data to the description field
instance.description = self.processData(extra_field)
if commit:
instance.save()
return instance
class Meta:
model = Widget
fields = "__all__"
You can always create new admin template, and do what you need in your admin_view (override the admin add URL to your admin_view):
url(r'^admin/mymodel/mymodel/add/$','admin_views.add_my_special_model')
If you absolutely only want to store the combined field on the model and not the two seperate fields, you could do something like this:
Create a custom form using the form attribute on your ModelAdmin. ModelAdmin.form
Parse the custom fields in the save_formset method on your ModelAdmin. ModelAdmin.save_model(request, obj, form, change)
I never done something like this so I'm not completely sure how it will work out.
The first (highest score) solution (https://stackoverflow.com/a/23337009/10843740) was accurate, but I have more.
If you declare fields by code, that solution works perfectly, but what if you want to build those dynamically?
In this case, creating fields in the __init__ function for the ModelForm won't work. You will need to pass a custom metaclass and override the declared_fields in the __new__ function!
Here is a sample:
class YourCustomMetaClass(forms.models.ModelFormMetaclass):
"""
For dynamically creating fields in ModelForm to be shown on the admin panel,
you must override the `declared_fields` property of the metaclass.
"""
def __new__(mcs, name, bases, attrs):
new_class = super(NamedTimingMetaClass, mcs).__new__(
mcs, name, bases, attrs)
# Adding fields dynamically.
new_class.declared_fields.update(...)
return new_class
# don't forget to pass the metaclass
class YourModelForm(forms.ModelForm, metaclass=YourCustomMetaClass):
"""
`metaclass=YourCustomMetaClass` is where the magic happens!
"""
# delcare static fields here
class Meta:
model = YourModel
fields = '__all__'
This is what I did to add the custom form field "extra_field" which is not the part of the model "MyModel" as shown below:
# "admin.py"
from django.contrib import admin
from django import forms
from .models import MyModel
class MyModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# Do something with extra_field here
return super().save(commit=commit)
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
You might get help from my answer at :
my response previous on multicheckchoice custom field
You can also extend multiple forms having different custom fields and then assigning them to your inlines class like stackedinline or tabularinline:
form =
This way you can avoid formset complication where you need to add multiple custom fields from multiple models.
so your modeladmin looks like:
inlines = [form1inline, form2inline,...]
In my previous response to the link here, you will find init and save methods.
init will load when you view the page and save will send it to database.
in these two methods you can do your logic to add strings and then save thereafter view it back in Django admin change_form or change_list depending where you want.
list_display will show your fields on change_list.
Let me know if it helps ...
....
class CohortDetailInline3(admin.StackedInline):
model = CohortDetails
form = DisabilityTypesForm
...
class CohortDetailInline2(admin.StackedInline):
model = CohortDetails
form = StudentRPLForm
...
...
#admin.register(Cohort)
class CohortAdmin(admin.ModelAdmin):
form = CityInlineForm
inlines = [uploadInline, cohortDetailInline1,
CohortDetailInline2, CohortDetailInline3]
list_select_related = True
list_display = ['rto_student_code', 'first_name', 'family_name',]
...

django admin filter change_list fields according to the user group

I'm using django admin to generate the form to include some data in the database but i need to hide certain form fields according to the user group.
So, let's say I've a Model such as:
class Product(models.Model):
name = models.CharField(...)
description = models.CharField(...)
approved = models.CharField(max_length=1, choices=(('y', 'yes'), ('n','no'), ('w', 'waiting'))
Where I want the user of the group "basic" see in the form only "name" and "description" and the user of the group "advanced" see also the "approved" status.
Do I need to use a customized template?
Whether you say yes or no please help me referring to some docs or with an example.
Thank you.
You can override get_form method in ProductAdmin class. From original method:
def get_form(self, request, obj=None, **kwargs):
....
....
defaults = {
"form": self.form,
"fields": fields,
"exclude": exclude,
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
}
defaults.update(kwargs)
...
So you can dynamically change either self.form or fields or exclude
You can provide an __init__ method in your form. Here, you will have access to the user object , if present. based on team membership, you can customize your form.
Some snippets:
function signature and getting the user object:
def __init__(self,*args, **kwargs):
self.user=None
if kwargs:
self.user = kwargs.pop('user')
Function to check if user is in group:
def is_user_in_group(user,group_name):
return user.groups.filter(name=group_name).count() == 1
Snippet to make a field hidden -- this will go in your __init__ method:
self.fields['some_field_to_hide'].widget = widgets.HiddenInput
I am going from memory here, so please excuse typos.
If you want to hide specific fields only, while still allowing change, the minimal solution might be to add your admin users to a new group and reduce fields based on group membership. Here is a reusable class to drop-in:
class ExternalAdminModelAdmin(admin.ModelAdmin):
external_admin_group = 'external_admin'
hidden_fields = []
def get_form(self, request, obj=None, **kwargs):
self.fields = [field.name for field in Node._meta.local_concrete_fields if not field.name == 'id']
if Group.objects.get(name=self.external_admin_group).user_set.filter(id=request.user.id).exists():
for field_name in self.hidden_fields:
self.fields.remove(field_name)
return super(ExternalAdminModelAdmin, self).get_form(request, obj, **kwargs)
You only need to set the hidden fields property for your models:
#admin.register(MyModel)
class MyAdmin(ExternalAdminModelAdmin):
hidden_fields = ['sensitive_field']

Categories