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)
In the Foo detail page in the Django admin, I list all child Bars underneath the Foo details:
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
def bars(self):
html = ''
bs = self.bar_set.all()
for b in bs:
html += '%s<br>' % (reverse('admin:app_bar_change', args=(b.id,)), b.name)
html += '<a href="%s">Add a bar</button>' % (reverse('admin:app_bar_add'))
return html
bars.allow_tags = True
fields = ('name', bars)
readonly_fields = (bars,)
As you can see, I also add a button to add a new Bar. This works, but what I want is to automatically prepopulate the Foo dropdown when adding a new Bar. I.e. I want to add a Bar to the currently open Foo. How can I do this in Django?
For this django allows you to include inlines in your admin class. For example the following would display the related objects in a table without displaying the form to change values for the name field;
class BarInline(admin.TabularInline):
model = Bar
fields = (
'name'
)
readonly_fields = (
'name'
)
show_change_link = True
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
fields = ('name', )
inlines = [
BarInline,
]
By setting show_change_link you can display a change link for the inline object which I believe is what you're really looking for here.
Django provides two subclasses of InlineModelAdmin and they are:
TabularInline
StackedInline
The difference between these two is merely the template used to render them.
If these inlines don't meet your needs, you could just create your own to use a custom template.
from django.contrib.admin.options import InlineModelAdmin
class ListInlineAdmin(InlineModelAdmin):
template = 'admin/edit_inline/list.html'
Then in the new list.html template you can get it to display the object name & change URL as you want.
I found the solution in this SO answer. Pre-populated fields can be specified in the URL's query string. So my code now looks like this:
if self.id:
html += 'Add a bar' % (reverse('admin:app_bar_add'), self.id)
Edit: I need to check first that the instance has an id, i.e. that it has already been saved.
Related
Say I have the following models that have a many-to-many relationship:
models.py:
class Foo(models.Model):
name = models.TextField()
class Bar(models.Model):
name = models.TextField()
foos = models.ManyToManyField(Foo, related_name='bars')
And then having defined them in admin in the following way:
admin.py
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
pass
#admin.register(Bar)
class BarAdmin(admin.ModelAdmin):
pass
In Django admin, when browsing Bar instances, I can see the Foo instances Bar is associated with and can modify them from there.
However, no such luck with Foo, I can't see the Bar instances that every Foo object is associated with.
Can Django define automatic handling for this or would I need to roll my own methond?
I'm using Python 3.6.1 and Django 1.11.
You could define a custom InlineModelAdmin like so:
class BarInline(admin.TabularInline):
model = Bar.foos.through
and use it in your FooAdmin:
class FooAdmin(admin.ModelAdmin):
"""Foo admin."""
model = Foo
inlines = [
BarInline,
]
Have a look at this part of the django documentation.
You can define custom fields for list_display like this:
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
"""Foo admin."""
list_display = ('name', 'get_bars')
search_fields = ('name',)
def get_bars(self, obj):
return obj.bars.all()
This is a very simple example, but I hope it can help you as a starting point.
EDIT:
You can display the associated objects in the edit form as readonly:
readonly_fields = ('get_bars',)
fields = ('name', 'get_bars')
There is a module called django-admin-extend which provides a generic mechanism to define "Bidirectional many to many fields". I'm not sure if it still works, because the last contribution is two years old, bit it should be worth giving it a try.
This code below can display both Foo and Bar models in many-to-many relationship on the same page in Django Admin:
class BarInline(admin.TabularInline):
model = Bar.foos.through
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
inlines = (BarInline,)
Be careful, if using model = Bar instead of model = Bar.foos.through in many-to-many relationship as shown below:
class BarInline(admin.TabularInline):
# model = Bar.foos.through
model = Bar # This will get error
#admin.register(Foo)
class FooAdmin(admin.ModelAdmin):
inlines = (BarInline,)
You will get the error below:
ERRORS: <class 'xxx.admin.BarInline'>: (admin.E202)
'xxx.Bar' has no ForeignKey to 'xxx.Foo'.
Suppose I have a model like this:
class Foo(models.Model):
name = models.CharField(max_length=50)
year = models.IntegerField(max_length=4)
some_value = models.IntegerField(default=0)
and
class Bar(models.Model)
name = models.CharField(max_length=50)
foo = models.ForeignKey(Foo)
Then I register my models in django AdminSite using FooAdmin class:
class FooAdmin(admin.ModelAdmin):
list_display = ['name', 'year', 'some_value']
It works fine, but for some reason I need to add two buttons for each Foo row in my admin site. Let's say I'd call them 'Related_button' and 'Action_button'
I need those buttons to be in each row and to behave as follows:
-when user clicks on 'Related_button', he is redirected to django admin site where all Bars related with particullar Foo object are listed
-when user clicks on 'Action_button', a field some_value in object of class Foo is set to a custom value, let's say 15. But before that, confirm popup (Are you sure? y/n) should appear.
How to do that? I figured out that I could do something like:
def button(self, obj):
return '''<input type="button" value="button" />'''
button.short_description = 'Action'
button.allow_tags = True
list_display = ['name', 'year', 'some_value', 'action']
in my FooAdmin which causes a button to appear in each row. But how can I set an action for that button?
The list page is a form.
<form id="changelist-form" action="" method="post">
Adding buttons to it will submit the form. To do something with the formdata you have to create a custom admin form. It will be dirty because it's a fromset containing all displayed rows, and not just your single row. I guess it can be done, but there are better ways:
Most of the time Admin Actions will do just fine. Admin actions look like this:
def rename_action(modeladmin, request, queryset):
queryset.update(name='Ni')
make_published.short_description = "Rename selected objects to 'Ni'"
class FooAdmin(admin.ModelAdmin):
actions = [rename_action, ...]
But if you want more flexibility than an Admin Action, feel free to write custom views for the admin. The admin is powered by Django itself, and you can write custom views that hook into the authentication system, check permissions and do whatever else they need to do. It goes like this:
class Foo(models.Model):
...
def my_action_link(self, obj):
return 'Action name' %obj.id
my_action_link.short_description = 'My action'
my_action_link.allow_tags = True
Now my action is a link that points to some custom url and custom admin view. Create a custom admin view:
class FooAdmin(admin.ModelAdmin):
...
list_display = ['name', 'my_action_link', ...]
def get_urls(self):
urls = super(FooAdmin, self).get_urls()
my_urls = patterns('',
(r'^my_custom_action/(?P<pk>\d+)/$', self.my_view)
)
return my_urls + urls
#permission_required('foo.can_change')
def my_view(self, request):
obj = get_object_or_404(Foo, pk=pk)
obj.do_something()
# Redirect back to the change list. Or something else?
# You could add some modelform to this view. :)
I didn't test this code. But I hope you get the idea. Happy hacking!
[EDITED]: Linked reference Django 1.6 URLs not working anymore. Added Django 1.11 URLs
Add custom js to admin class:
class FooAdmin(admin.ModelAdmin):
list_display = ['name', 'year', 'some_value']
class Media:
js = ("/media/javascript/yourjs.js",)
I would like to implement a very simple feature with the django admin panel but I couldn't find the right way to achieve this so far:
model.py
class Author(models.Model):
name = models.CharField()
class Books(models.Model):
title = models.CharField()
author = models.ForeignKey(Author)
admin.py
class AuthorAdmin(admin.ModelAdmin):
pass
admin.site.register(Author, AuthorAdmin)
How can I add a hyperlink to every item (Author) in the authors' list overview that links to a view showing all books of the specific author? For Example:
J.K. Rowling (books)
J.R.R. Tolkien (books)
where books is a hyperlink to a site showing all books of the author.
You are looking for a ModelAdmin.list_filter.
Set list_filter to activate filters in the right sidebar of the change list page of the admin. A listfilter can be a field name, where the specified field should be either a BooleanField, CharField, DateField, DateTimeField, IntegerField, ForeignKey or ManyToManyField, for example:
# Add a list filter author to BookAdmin.
# Now you can filter books by author.
class BookAdmin(ModelAdmin):
list_filter = ('author', )
Now you can use #Wolph suggestion to add a link in the Author list_display. This link points to the book list filtered by author:
# Add hyperlinks to AuthorAdmin.
# Now you can jump to the book list filtered by autor.
class AuthorAdmin(admin.ModelAdmin):
def authors(self):
return '%s' % (self.author_id, self.author)
authors.allow_tags = True
ALTERNATIVE. To save a click you can also point to the change view of a book directly:
class Books(models.Model):
title = models.CharField()
author = models.ForeignKey(Author)
def get_admin_url(self):
return "/admin/appname/books/%d/" %self.id
class BookAdmin(admin.ModelAdmin):
def authors(self):
html = ""
for obj in Books.objects.filter(author__id_exact=self.id):
html += '<p>%s</p>' %(obj.get_admin_url(), obj.title)
return html
authors.allow_tags = True
list_display = ['title', authors]
Disclaimer: Not tested, but if you fix the typo's it'll work! :)
Note that these links can also be injected at other points in the admin. When you add it to a widget, you can go from change view to change view.
Look into implementing an InlineModelAdmin object.
# admin.py
class BooksInline(admin.TabularInline):
model = Books
class AuthorAdmin(admin.ModelAdmin):
inlines = [BooksInline]
Edited answer in response to OP's comment:
class AuthorAdmin(admin.ModelAdmin):
list_display = ['name', 'books_published']
def books_published(self, obj):
redirect_url = reverse('admin:books_changelist')
extra = "?author__id__exact=%d" % (obj.id)
return "<a href='%s'>Books by author</a>" % (redirect_url + extra)
books_published.allow_tags = True
It's not as simple as you might think actually, possible but not trivial since there is not that much documentation of this feature :)
What you need is a custom column which is generated by a function which gives you a link to the Django admin with a given filter. Something like this (from the top of my head so not tested) should do the trick:
class AuthorAdmin(admin.ModelAdmin):
def authors(self):
return '%s' % (self.author_id, self.author)
authors.allow_html = True
list_display = ['title', authors]
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',]
...
I have a ManyToMany relationship:
class Book:
title = models.CharField(...)
isbn = models.CharField(...)
def unicode(self):
return self.title
def ISBN(self):
return self.isbn
class Author:
name = models.CharField(...)
books = models.ManyToManyField(Book...)
In the admin interface for Author I get a multiple select list that uses the unicode display for books. I want to change the list in two ways:
1) Only for the admin interface I want to display the ISBN number, everywhere else I just print out a "Book" object I want the title displayed.
2) How could I use a better widget than MultipleSelectList for the ManyToMany. How could I specify to use a CheckBoxSelectList instead?
To display the ISBN you could make a custom field like this:
class BooksField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return obj.isbn
There's a CheckboxSelectMultiple for the ManyToManyField but it doesn't display correctly on the admin, so you could also write some css to fix that.
You need to create a form for the model, and use that in your admin class:
class AuthorForm(forms.ModelForm):
books = BooksField(Book.objects.all(), widget=forms.CheckboxSelectMultiple)
class Meta:
model = Author
class Media:
css = {
'all': ('booksfield.css',)
}
class AuthorAdmin(admin.ModelAdmin):
form = AuthorForm
For 2), use this in your AuthorAdmin class:
raw_id_fields = ['books']
Check here: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#ref-contrib-admin for instructions on creating a custom ModelAdmin class. I've thought about this a lot myself for my own Django project, and I think 1) would require modifying the admin template for viewing Author objects.