Django admin.ModelAdmin inheritance - python

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

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?

Add dynamic field to django admin model form

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.

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',]
...

How to customize a Queryset of a Field from a admin Inline that uses a pk of the primary form in Django?

Customizing a queryset of a form field in Django isn't a hard job. Like this
But, assuming I have the following models:
#models.py
class Work(Model):
name = models.CharfField(...)
#some fields
class Gallery(Model):
work = models.ForeignKey(Work)
class Photo(Model):
gallery = models.ForeignKey(Gallery)
class StageOfWork(Model):
work = models.ForeignKey(Work)
gallery = models.ForeignKey(Gallery)
#some fields
And an admin.py like this
#admin.py
class StageOfWorkAdmin(admin.TabularInline):
model = StageOfWork
form = StageOfWorkForm
extra = 1
class WorkAdmin(admin.ModelAdmin):
inlines = [EtapaObraAdmin]
I have this problem: when I edit a Work, exists many Form Inlines of StageOfWorks, these StageOfWorks inline forms have a Gallery selector.
I need to customize de queryset of this Galleries like this:
class StageOfWorkForm(ModelForm):
def __init__(self, *args, **kwargs):
super(StageOfWorkForm, self).__init__(*args, **kwargs)
if 'instance' in kwargs:
self.fields['gallery'].queryset = Gallery.objects.filter(work__id=self.instance.work.id)
But this only works in the Forms who is editing forms. I need to get a work id in context of init method to do the right queryset anyway.
How could I do that?
The only way I have been able to do it is pass in the data you need into the instantiaton of the form class.
i.e., in your view:
def view(request):
...
work = <whatever>
form = StageOfWorkForm(work, request.POST)
...
Then, your form needs to the work object:
class StageOfWorkForm(ModelForm):
def __init__(self, work, *args, **kwargs):
super(StageOfWorkForm, self).__init__(*args, **kwargs)
self.fields['gallery'].queryset = work.gallery_set.all()
I haven't done this exact thing, but I did something similar. I used the Smart Selects Django plugin. This can be found here: https://github.com/digi604/django-smart-selects
I've use this to a filtered select in the admin, but it was in a regular model, not an inline, but it is quite possible that the plugin works in inlines too. I'd at least check it out.
Hailey

Django ModelForm with User data in Generic View

I have a model with a foreign key to group (the other fields don't matter):
class Project(models.Model) :
group = models.ForeignKey(Group)
...
I have a model form for this model:
class AddProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ["group","another"]
In my urls, I am using this in a generic view:
(r'^$', create_object, {'form_class':AddProjectForm, 'template_name':"form.html", 'login_required':True, 'extra_context':{'title':'Add a Project'}}),
That all works, but I want to have the group field display only the groups that the current user belongs to, not all of the groups available. I'd normally do this by passing in the user to the model form and overriding init if I wasn't in a generic view. Is there any way to do this with the generic view or do I need to go with a regular view to pass in that value?
This is gonna look dirty, since the generic view instantiates the form_class with no parameters. If you really want to use the generic_view you're gonna have to generate the class dynamically :S
def FormForUser(user):
class TmpClass(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TmpClass, self).__init__(*args, **kwargs)
self.fields['group'].queryset = user.group_set.all()
class Meta:
model = Project
fields = ['group', 'another']
Then wrap the create object view
#login_required # Only logged users right?
def create_project(request):
user = request.user
form_class = FormForUser(user)
return create_object(request, form_class=form_class, ..... )
My recommendation is to write your own view, it will give you more control on the long term and it's a trivial view.
No, you'll need to make a regular view. As can be seen by looking at the source code for create_object(), there's no functionality to pass in extra parameters to the modelform (in django 1.2):
http://code.djangoproject.com/svn/django/branches/releases/1.2.X/django/views/generic/create_update.py

Categories