Displaying both sides of a ManyToMany relationship in Django admin - python

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

Related

What is the use of class Meta in django?

I have been using django and used class Meta: a lot of times, actually what is the use of it?
for example , In django models
class Accounts(models.Model):
---some code here---
class Meta:
ordering = [-1]
In django forms
class AccountForm(forms.ModelForm):
---some code here---
class Meta:
fields = '__all__'
class Meta is basically the inner class. In Django, the use of the Meta class is simply to provide metadata to the ModelForm or the Model class. It is different from the metaclass of a Class in Python.
class Meta is used to change the behavior of the models such ordering, verbose_name etc. Though it is optional to be included in your models.

Add button for child model in Django admin

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.

How can I add an extra field to a proxied model in Django?

I'm specifically talking about the Tag model, which I have no much experience with. The code goes like this:
#register_snippet
class ArticleTag(index.Indexed,Tag):
class Meta:
proxy=True
search_fields = [
index.SearchField('name', partial_match=True),
index.SearchField('slug', partial_match=True),
]
The Tag model has two fields, 'name' and 'slug'. But now I want to add a third custom field named 'type' that will be simply a CharField.
I tried modifying it like this:
#register_snippet
class ArticleTag(index.Indexed,Tag):
class Meta:
proxy=True
search_fields = [
index.SearchField('name', partial_match=True),
index.SearchField('slug', partial_match=True),
]
merge_to = models.CharField(max_length=500, blank=True, null=True)
panels = [
FieldPanel('name'),
FieldPanel('slug'),
FieldPanel('type'),
]
However the server yields:
ERRORS:
?: (models.E017) Proxy model 'ArticleTag' contains model fields.
How can I achieve what I am trying to do?
Your tag gas to inherit from TagBase and you'll have to create a custom through model. The django-taggit documentation has an example on how to create custom tags.
We can do this below trick :P
class Model(object):
'''
Skip extra field validation "models.E017"
'''
#classmethod
def _check_model(cls):
errors = []
return errors
#register_snippet
class ArticleTag(Model, index.Indexed,Tag):
class Meta:
proxy=True

How to order readonly M2M fields in django admin

I can't seem to work out how to hook into the queryset of a readonly field in Django admin. In particular I want to do this for an inline admin.
# models.py
class Value(models.Model):
name = models.TextField()
class AnotherModel(models.Model):
values = models.ManyToManyField(Value)
class Model(models.Model):
another_model = models.ForeignKey(AnotherModel)
# admin.py
class AnotherModelInline(admin.TabularInline):
# How do I order values by 'name'?
readonly_fields = ('values',)
class ModelAdmin(admin.ModelAdmin):
inlines = (AnotherModelInline,)
Note that this could probably be done by overriding the form and then setting the widget to disabled, but that's a bit of a hack and doesn't look nice (I don't want greyed out multi-select, but a comma-separated list of words.
You can set an ordering metadata in the Values model:
class Value(models.Model):
name = models.TextField()
class Meta:
ordering = ['name']

Django Admin: OneToOne Relation as an Inline?

I'm putting together the admin for a satchmo application. Satchmo uses OneToOne relations to extend the base Product model, and I'd like to edit it all on one page.
Is it possible to have a OneToOne relation as an Inline? If not, what is the best way to add a few fields to a given page of my admin that will eventually be saved into the OneToOne relation?
for example:
class Product(models.Model):
name = models.CharField(max_length=100)
...
class MyProduct(models.Model):
product = models.OneToOne(Product)
...
I tried this for my admin but it does not work, and seems to expect a Foreign Key:
class ProductInline(admin.StackedInline):
model = Product
fields = ('name',)
class MyProductAdmin(admin.ModelAdmin):
inlines = (AlbumProductInline,)
admin.site.register(MyProduct, MyProductAdmin)
Which throws this error: <class 'satchmo.product.models.Product'> has no ForeignKey to <class 'my_app.models.MyProduct'>
Is the only way to do this a Custom Form?
edit: Just tried the following code to add the fields directly... also does not work:
class AlbumAdmin(admin.ModelAdmin):
fields = ('product__name',)
It's perfectly possible to use an inline for a OneToOne relationship. However, the actual field defining the relationship has to be on the inline model, not the parent one - in just the same way as for a ForeignKey. Switch it over and it will work.
Edit after comment: you say the parent model is already registered with the admin: then unregister it and re-register.
from original.satchmo.admin import ProductAdmin
class MyProductInline(admin.StackedInline):
model = MyProduct
class ExtendedProductAdmin(ProductAdmin):
inlines = ProductAdmin.inlines + (MyProductInline,)
admin.site.unregister(Product)
admin.site.register(Product, ExtendedProductAdmin)
Update 2020 (Django 3.1.1)
This method is still working but some types has changed in new Django version since inlines in ExtendedProductAdmin should now be added as list and not tuple, like this:
class ExtendedProductAdmin(ProductAdmin):
inlines = ProductAdmin.inlines + [MyProductInline]
Or you will get this error:
inlines = ProductAdmin.inlines + (MyProductInline,)
TypeError: can only concatenate list (not "tuple") to list
Maybe use inheritance instead OneToOne relationship
class Product(models.Model):
name = models.CharField(max_length=100)
...
class MyProduct(Product):
.....
Or use proxy classes
class ProductProxy(Product)
class Meta:
proxy = True
in admin.py
class MyProductInlines(admin.StackedInline):
model = MyProduct
class MyProductAdmin(admin.ModelAdmin):
inlines = [MyProductInlines]
def queryset(self, request):
qs = super(MyProductAdmin, self).queryset(request)
qs = qs.exclude(relatedNameForYourProduct__isnone=True)
return qs
admin.site.register(ProductProxy, MyProductAdmin)
In this variant your product will be in inline.
Referring to the last question, what would be the best solution for multiple sub-types. E.g class Product with sub-type class Book and sub-type class CD. The way shown here you would have to edit a product the general items plus the sub-type items for book AND the sub-type items for CD. So even if you only want to add a book you also get the fields for CD. If you add a sub-type e.g. DVD, you get three sub-type field groups, while you actually only want one sub-type group, in the mentioned example: books.
You can also try setting 'parent_link=True' on your OneToOneField?
https://docs.djangoproject.com/en/dev/topics/db/models/#specifying-the-parent-link-field
Jun, 2022 Update:
Yes, it's possible to have inline for one-to-one relation.
For example, as shown below, if "MyProduct" class has "models.OneToOneField()" referring to "Product" class which means "MyProduct" class has the ForeignKey referring to "Product" class:
# "models.py"
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
class MyProduct(models.Model):
name = models.CharField(max_length=100)
product = models.OneToOneField( # Here
Product,
on_delete=models.CASCADE,
primary_key=True
)
Then, you can inline "MyProduct" class under "Product" class as shown below:
# "admin.py"
from django.contrib import admin
from .models import Product, MyProduct
class MyProductInline(admin.TabularInline):
model = MyProduct
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
inlines = (MyProductInline, )
Oppositely, as shown below, if "Product" class has "models.OneToOneField()" referring to "MyProduct" class which means "Product" class has the ForeignKey referring to "MyProduct" class:
# "models.py"
from django.db import models
class MyProduct(models.Model):
name = models.CharField(max_length=100)
class Product(models.Model):
name = models.CharField(max_length=100)
my_product = models.OneToOneField( # Here
MyProduct,
on_delete=models.CASCADE,
primary_key=True
)
Then, you can inline "Product" class under "MyProduct" class as shown below:
# "admin.py"
from django.contrib import admin
from .models import Product, MyProduct
class ProductInline(admin.TabularInline):
model = Product
#admin.register(MyProduct)
class MyProductAdmin(admin.ModelAdmin):
inlines = (ProductInline, )

Categories