How to order readonly M2M fields in django admin - python

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

Related

Specific Queryset for Input on Django ModelAdmin Change Form

I've got a 'Registration' object in place that users can create on the front end without issue.
It looks like this:
class Registration(models.Model):
person = models.ForeignKey(Person, on_delete=models.PROTECT)
course_detail = models.ForeignKey(CourseDetail, on_delete=models.PROTECT)
camp_shirt = models.ForeignKey(CampShirt, on_delete=models.PROTECT)
comments = models.CharField(max_length=200, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return "%s" % (self.course_detail.course.camp)
When I am in the admin and click on a given Registration - it takes a while to load because there are thousands and thousands of Person objects.
For ease of use - there will never be a time when we would need to edit the 'person' associated with a given registration, so I would like to make the 'person' dropdown only show the selected user in the person queryset when editing from the django admin.
So when I go to http://myapp.com/admin/registration/23/change I want the form to only display the currently selected person as the only option in the dropdown.
My admin model looks like this:
class RegistrationAdmin(admin.ModelAdmin):
list_display = ("person", "course_detail")
class Meta:
# I think this is what I do in order to override the default admin form? Not sure.
form = RegistrationAdminForm
My RegistrationAdminForm looks like this:
class RegistrationAdminForm(forms.ModelForm):
# course_detail, person, camp_shirt, comments
person = forms.ModelChoiceField(queryset=Person.objects.filter(
id=registration.person.id)
)
def __init__(self, registration, *args, **kwargs):
super(RegistrationAdminForm, self).__init__(*args, **kwargs)
self.fields['person'].queryset = Person.objects.filter(
id=registration.person.id
)
class Meta:
model = Registration
fields = '__all__'
Main Question : How do I change the admin form so that a specific queryset is returned for one of the fields in the django admin?
If the person field will never be changed you can add the person field to readonly_fields, a select with all Person objects will not be rendered.
class RegistrationAdmin(admin.ModelAdmin):
list_display = ("person", "course_detail")
readonly_fields = ("person", )
Then you do not need your custom form. FYI when you want to add a custom form to a ModelAdmin you do not put it in Meta, you define it on the form itself
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm

Django admin save inline form instances with no fields

I'm attemping to add copies of a book through a books admin page.
Inside models.py I have:
class Book(models.Model):
...
class BookCopy(models.Model):
book = models.ForeignKey(
'Book',
related_name='copies',
on_delete=models.CASCADE
)
# No additional fields here
class BookCopyInline(admin.StackedInline):
model = BookCopy
can_delete = False
verbose_name_plural = 'copies'
Inside admin.py I have the following:
class BookCopyInline(admin.StackedInline):
model = BookCopy
can_delete = False
verbose_name_plural = 'copies'
#admin.register(Book)
class BookAdmin(admin.ModelAdmin):
inlines = (BookCopyInline,)
#admin.register(Book)
class BookAdmin(admin.ModelAdmin):
model = Book
list_display = ('isbn', 'title', 'subtitle')
prepopulated_fields = {'slug': ('title',)}
inlines = (BookCopyInline,)
I'd like to be able to add book copies inside the Books Admin page. But since the BookCopy model defines no additional fields the instances are never saved.
Adding a field to BookCopy and filling that in each time allows BookCopies to be created as normal, but I don't require any additional fields right now.
The image below demonstrates the issue I'm facing, new rows can be added, but when save is clicked, no BookCopies are created
Is there a way to have the admin save the instances regardless?

Reorder FK fields in Django, remain optional

I have a model (Application) tied to a foreign_key (Person) table. I was having trouble getting the Person-dropdown in the admin to sort by name instead of by key, and found this Reorder users in django auth as a solution. However, this made the fields mandatory and I can't figure out how to get them to stay optional.
app/models.py
class Person(models.Model):
Full_Name = models.CharField(max_length=200)
def __unicode__(self):
return self.Full_Name
class Application(models.Model):
Name = models.CharField(max_length=100)
Primary_Contact = models.ForeignKey(Person,blank=True,null=True,related_name='appprimarycontact')
def __unicode__(self):
return self.Name
admin.py
class OwnerAdminForm(forms.ModelForm):
Primary_Contact = forms.ModelChoiceField(queryset=Person.objects.order_by('Full_Name'),)
class Meta:
model = Application
class ApplicationAdmin(admin.ModelAdmin):
form = OwnerAdminForm
list_display = ('Name','Primary Contact')
Just add the required=False option on the form field
forms.ModelChoiceField(required=False, queryset=Person.objects.order_by('Full_Name'))
The thing is, if you override the default form widget that django's ModelForm would provide, you would have to explicitly specify required=False, since the default value is True

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

Django Admin: Ordering of ForeignKey and ManyToManyField relations referencing User

I have an application that makes use of Django's UserProfile to extend the built-in Django User model. Looks a bit like:
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
# Local Stuff
image_url_s = models.CharField(max_length=128, blank=True)
image_url_m = models.CharField(max_length=128, blank=True)
# Admin
class Admin: pass
I have added a new class to my model:
class Team(models.Model):
name = models.CharField(max_length=128)
manager = models.ForeignKey(User, related_name='manager')
members = models.ManyToManyField(User, blank=True)
And it is registered into the Admin:
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'manager')
admin.site.register(Team, TeamAdmin)
Alas, in the admin inteface, when I go to select a manager from the drop-down box, or set team members via the multi-select field, they are ordered by the User numeric ID. For the life of me, I can not figure out how to get these sorted.
I have a similar class with:
class Meta:
ordering = ['name']
That works great! But I don't "own" the User class, and when I try this trick in UserAdmin:
class Meta:
ordering = ['username']
I get:
django.core.management.base.CommandError: One or more models did not validate:
events.userprofile: "ordering" refers to "username", a field that doesn't exist.
user.username doesn't work either. I could specify, like image_url_s if I wanted to . . . how can I tell the admin to sort my lists of users by username? Thanks!
This
class Meta:
ordering = ['username']
should be
ordering = ['user__username']
if it's in your UserProfile admin class. That'll stop the exception, but I don't think it helps you.
Ordering the User model as you describe is quite tricky, but see http://code.djangoproject.com/ticket/6089#comment:8 for a solution.
One way would be to define a custom form to use for your Team model in the admin, and override the manager field to use a queryset with the correct ordering:
from django import forms
class TeamForm(forms.ModelForm):
manager = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
class Meta:
model = Team
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'manager')
form = TeamForm
This might be dangerous for some reason, but this can be done in one line in your project's models.py file:
User._meta.ordering=["username"]
For me, the only working solution was to use Proxy Model. As stated in the documentation, you can create own proxy models for even built-in models and customize anything like in regular models:
class OrderedUser(User):
class Meta:
proxy = True
ordering = ["username"]
def __str__(self):
return '%s %s' % (self.first_name, self.last_name)
After that, in your model just change Foreign Key to:
user = models.OneToOneField(OrderedUser, unique=True)
or even more suitable
user = models.OneToOneField(OrderedUser, unique = True, parent_link = True)

Categories