Django Admin: OneToOne Relation as an Inline? - python

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, )

Related

Django - display OneToOneField field in other model django admin view

I have a OneToOneField between a Vehicle model and a Person model. I would like to view the assigned_person field (using its dropdown functionality) in Person's admin view. The idea is that if I view a Person, I can change the assigned_person value and have it affect Vehicle assigned_person.
Admin view: Vehicle
Admin view: Person
models.py:
class Vehicle(models.Model):
...
assigned_person = models.OneToOneField('Person', related_name='person', on_delete=models.CASCADE, blank=True, null=True)
class Person(models.Model):
...
Try implementing Tabular inline in admin.py file instead of registering Vehicle directly.
class VehicleInline(admin.TabularInline):
model = Vehicle
extra = 1
class PersonAdmin(admin.ModelAdmin):
inlines = (VehicleInline,)
admin.site.register(Person, PersonAdmin)
Refer here for more: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#working-with-many-to-many-intermediary-models
I would also use inlines to do this, either way, you're asking for some overriding or customization - but I don't like the prospects of having to customize the admin, you will waste a lot of time on it and it's not guaranteed to work.
Person admin.py
from vehicle.forms import VehicleAssignedPersonForm
class PersonAdmin(admin.ModelAdmin):
change_form = PersonChangeForm
...
inlines = [
VehicleInline
]
...
class VehicleInline(admin.TabularInline):
change_form = VehicleAssignedPersonForm
model = Vehicle
Person forms.py
class PersonChangeForm(forms.form):
...
//override the save method
def save():
//here you will have the ability to call a save for the vehicle's assigned person
Vehicle forms.py
class VehicleAssignedPersonForm(forms.ModelForm):
assigned_person = forms.ChoiceField()
//override the __init___ to give the choices that you want to see
def __init__(self, *args, **kwargs):
...
class Meta:
model = Vehicle
Obviously I have no way of figuring out the details, just pointing you in the direction I would go for this problem.

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 TabularInline - Getting Foreign Key through a OneToOne Relationship

As shown below, I want to display one model (Attendance) in the admin change view of another (Event) as a TabularInline. However, Attendance doesn't have a direct ForeignKey pointing to Event. Rather, it has a OneToOne relationship with Registration, which in turn points to Event. Any simple ways of achieving this?
As you can see from the code, I tried to access the Event using a method, and pass it into the TabularInline class as fk_name, but it threw an error saying "'myapp.Attendance' has no field named 'get_fk'".
myapp/models.py
from django.db import models
class Event(models.Model):
...
class Registration(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
...
class Attendance(models.Model):
registration = models.OneToOneField(Registration, primary_key=True, one_delete=models.CASCADE)
...
def get_fk(self):
return self.registration.event
myapp/admin.py
from django.contrib import admin
class AttendanceInline(admin.TabularInline):
model = Attendance
fk_name = 'get_fk'
class EventAdmin(admin.ModelAdmin):
inlines = [AttendanceInline]
My current workaround is to add another 'event' ForeignKey to Attendance, but it seems redundant as the same data is already stored inside 'registration'.
Appreciate it if anyone could help!
fk_name should be field name that have Foreignkey relation according to documentation not any property of model.
https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#django.contrib.admin.InlineModelAdmin.fk_name

Displaying both sides of a ManyToMany relationship in Django admin

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

How to use filter_horizontal on a ForeignKey between two apps using ContentTypes in Django admin?

Say I have this app named Pantry that is to connect to any other app I may come along. To keep the app decoupled, generic relations are used through the model LinkedItem which connects the Ingredients model to apps outside Pantry.
I would like the content on the other end of the generic relation, say an app named Bakery, to be able to do a filter_horizontal with Ingredients.
Pantry
models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import fields
class Ingredient(models.Model):
'''
Model containing all the ingredients, their slugs, and their descriptions
'''
name = models.CharField(unique=True, max_length=100)
slug = models.SlugField(unique=True, max_length=100)
description = models.CharField(max_length=300)
# method to return the name of the db entry
def __str__(self):
return self.name
class LinkedItem(models.Model):
'''
Model that links ingredients to various other content models
'''
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = fields.GenericForeignKey('content_type', 'object_id')
ingredient = models.ForeignKey(Ingredient)
# method to return the name of the db entry
def __str__(self):
return self.ingredient.name
# defines options for the model itself
class Meta:
unique_together = (('content_type','object_id')) # prevents duplicates
Bakery
admin.py
from django.contrib import admin
from bakery.models import Cake
class CakeAdmin(admin.ModelAdmin):
filter_horizontal = ('') # what to put here so ingredients show up?
Any ideas?
I think this is what the GenericRelation is for, so you need to add one to your Cake model and use it's name in your CakeAdmin.
But you need to use Inlines if you don't want to do a lot of workaround as M2M fields are not supported for relations with intermediary models.

Categories