How to display changelist of multiple models in django admin? - django

I need to display multiple models in django admin change list view. I want to use single search box to filter all of them at once. Is there an easy way to do it?
My idea was to inherit from admin site, add another view to it and iterate over models in modified change_list.html but i can't import models and ModelAdmins because i get django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. error so i can't get the same context that django uses to render regular change_list.html.
What's the correct way to do it? Is there simpler approach?

As Ohad suggested, the most robust approach is probably to make formal relationships between the models from which you want the objects to display together. You have a couple of options here. Essentially you will want to make a master class and then subclass your models from it. This makes a lot of sense if your models are ontologically related to a parent concept. For example:
Publication
Book
Magazine issue
Books and magazines are both publications. They both share some fields, like title and publication date. But they differ in that a book usually has a single author and a magazine has volumes and issue dates. Django already provides a couple different approaches to subclassing using Model inheritance. However, after trying these myself I found that the django-polymorphic extension is way better. Here is a code example of a Django 3.0 app using django-polymorphic which has a Book model and a Magazine model with a single listing of all publications that shows all of the books and magazines in the system.
models.py
from django.db import models
from polymorphic.models import PolymorphicModel
class Publication(PolymorphicModel):
title = models.CharField(max_length=256)
publication_year = models.IntegerField()
class Book(Publication):
author_first = models.CharField(max_length=256)
author_last = models.CharField(max_length=256)
class Magazine(Publication):
volume_number = models.IntegerField()
issue_name = models.CharField(max_length=256)
admin.py
from django.contrib import admin
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
from .models import Publication, Book, Magazine
class PublicationChildAdmin(PolymorphicChildModelAdmin):
""" Base admin class for all child models """
base_model = Publication # Optional, explicitly set here.
#admin.register(Book)
class BookAdmin(PublicationChildAdmin):
base_model = Book # Explicitly set here!
# show_in_index = True # makes child model admin visible in main admin site
list_display = ('title', 'publication_year', 'author_first', 'author_last')
#admin.register(Magazine)
class MagazineAdmin(PublicationChildAdmin):
base_model = Magazine # Explicitly set here!
# show_in_index = True # makes child model admin visible in main admin site
list_display = ('title', 'publication_year', 'issue_name')
#admin.register(Publication)
class PublicationParentAdmin(PolymorphicParentModelAdmin):
""" The parent model admin """
base_model = Publication # Optional, explicitly set here.
child_models = (Book, Magazine)
list_filter = (PolymorphicChildModelFilter,) # This is optional.
list_display = ('title', 'publication_year')
This will of course only display those fields that are common (in the Publication model). If you want to display fields that are particular to each model there are various tricks for this. Here's one quick way to do it:
admin.py
...
#admin.register(Publication)
class PublicationParentAdmin(PolymorphicParentModelAdmin):
""" The parent model admin """
base_model = Publication # Optional, explicitly set here.
child_models = (Book, Magazine)
list_filter = (PolymorphicChildModelFilter,) # This is optional.
list_display = ('title', 'publication_year', 'issue', 'author')
def author(self, obj):
if obj.polymorphic_ctype.model == 'book':
book = Book.objects.get(pk=obj.pk)
return book.author_first + ' ' + book.author_last
return None
def issue(self, obj):
if obj.polymorphic_ctype.model == 'magazine':
return str(Magazine.objects.get(pk=obj.pk).issue_name)
return None
Tada!

From the docs it seems that there is no easy solution.(if there is no relation between the models)
https://docs.djangoproject.com/en/2.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
So if the search is commonly used build a special model/models that combines the data that might be searched

Related

Django - automate CRUD from models (DRY approach)?

I am new to Django but not to developing.
I need to make an application in which user can do CRUD operations (Create, Read, Update, Delete). This functionality should apply to all models and the fields for Create & Update will be auto-generated from model attributes.
What I describe is pretty much the functionality that comes with the Admin page. However, I want to use it in my own app instead of using the Admin app.
For example, let's suppose we have Author and Book models:
(models.py)
from django.db import models
class Author(models.Model):
first_name = models.CharField(max_length=60)
last_name = models.CharField(max_length=60)
def __str__(self):
return self.last_name
class Book(models.Model):
title = models.CharField(max_length=60)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
def __str__(self):
return self.title
I suppose that the above information (models) could be enough for CRUD operations, without repeating code of the same logic for each model. I am aiming at a functionality like in admin page where all you have to do is register your model.
I am aware of ModelForm and Generic Views but while they help avoiding hard-coding form fields, I have not found a non-repetitive coding approach. I would like to avoid approaches like the following where same code is being duplicated for each model:
(forms.py)
from django import forms
from todo.models import Author, Book
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
fields = '__all__'
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = '__all__'
(views.py)
from django.views.generic import CreateView
from.forms import AuthorForm, BookForm
from.models import Author, Book
class AuthorCreateView(CreateView):
model = Author
form_class = AuthorForm
...
class BookCreateView(CreateView):
model = Book
form_class = BookForm
...
So, what is the best approach for a DRY CRUD solution (like in Admin page)? Am I missing any Django features?
Putting my explanation in the comment together, you would get something like this:
from django.views import generic
from myapp.apps import MyAppConfig
from django.forms import modelform_factory
urlpatterns = []
for model in MyAppConfig.get_models():
create_url = path(
f"{model.__class___.__name__.lower()}/create",
generic.CreateView.as_view(
form_class=modelform_factory(model=model, fields='__all__'),
template_name=f"{model._meta.app_label}/create.html",
model=model
),
),
list_url = path(
f"{model.__class__.__name__.lower()}/",
generic.ListView.as_view(
template_name=f"{model._meta.app_label}/list.html",
model=model
),
),
...
urlpatterns.extend([create_url, list_url, read_url, update_url, delete_url])
So the principle is to use the generic view and model form factory, to generate standard crud views, derive path names from model name and use one template per view, in the application (derived from model's app_label).
Using the documentation I linked before, you should be able to piece things together.

admin.py: "model = Thing" ,what does this code mean?if without it what gonna happen?

every one,,I am reading a Django practice book,,I saw a code "model = Thing" in admin.py,,,however, when I remove "model = Thing",,,the web program still can run,the admin site looks no difference??,what does this code mean?if without it what gonna happen? my models.py class is Thing
admin.py
from django.contrib import admin
from collection.models import Thing
class ThingAdmin(admin.ModelAdmin):
model = Thing #if I remove this code, the program still can run,,why need this code
list_display = ('name', 'description',)
prepopulated_fields = {'slug': ('name',)}
admin.site.register(Thing, ThingAdmin)
modles.py
from django.db import models
class Thing(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
slug = models.SlugField(unique=True)
Setting a model attribute on the ModelAdmin class will have no effect. You can safely remove that line from your code.
In the Django admin, you specify the model when you call admin.site.register(), or by using the register decorator. This allows you to use the same model admin class for more than one model.
admin.site.register(Thing, ThingAdmin)
admin.site.register(OtherThing, ThingAdmin)
As Jon pointed out in the comments, you do need to specify the model for InlineModelAdmin objects.

Django One-To-Many Model and Admin inlines

I 'm trying to define 2 models in django like so:
class Selector(models.Model):
# A Beautiful Soup selector
selector = models.CharField(max_length=ELEMENT_SELECTOR_MAX_LENGTH, null=True, blank=True)
def __str__(self):
return self.selector
class Provider(models.Model):
# Articles' parent container selector
articles_parent_container_selector = models.ForeignKey(Selector, related_name="articles_parent_container",
help_text=_("Beautiful Soup selector for all articles' "
"parent container"))
# Article's parent container selector
article_parent_container_selector = models.ForeignKey(Selector, related_name="article_parent_container_selector",
help_text=_("Beautiful Soup selector for each article"))
etc. etc.
The idea is to have more than one selectors for each field of the Provider model.
What I 'm trying to achieve at the admin application, is have charField inlines for each field of the provider model.
So my admin.py is like
from django.contrib import admin
from .models import Provider, Selector
class SelectorInline(admin.StackedInline):
model = Selector
class ProviderAdmin(admin.ModelAdmin):
inlines = [
SelectorInline,
]
admin.site.register(Provider, ProviderAdmin)
I get the error
<class 'news_providers.admin.SelectorInline'>: (admin.E202) 'news_providers.Selector' has no ForeignKey to 'news_providers.Provider'.
I also tried
class SelectorInline(admin.StackedInline):
model = Selector
fk_name = 'articles_parent_container'
as described here: Django inline forms with multiple foreign keys
but the error now is:
<class 'news_providers.admin.SelectorInline'>: (admin.E202) 'news_providers.Selector' has no field named 'articles_parent_container'.
Also tried changing my relation to ManyToMany(which seems more relevant to my use-case as well) and apply the hack found here: http://www.mc706.com/tip_trick_snippets/18/django-manytomany-inline-admin/ , but no luck :/
This should be pretty straight forward, but I 'm afraid django developers didn't take notice of this use case?
Thanks!
So apparently, there is no built-in functionality to display an inline manyToMany model inside another's model page.
The best you can do is define the model like so
models.py
class Selector(models.Model):
# A Beautiful Soup selector
selector = models.CharField(max_length=70, null=True, blank=True)
class Provider(models.Model):
# Articles' parent container selector
articles_parent_container_selector = models.ManyToManyField(Selector, blank=True,
help_text=_("Beautiful Soup selector for all articles' "
"parent container."),
related_name='articles_parent_container')
admin.py
class ArticlesParentContainerSelectorInline(admin.TabularInline):
model = Provider.articles_parent_container_selector.through
verbose_name = "Articles' parent container selector"
class ProviderAdmin(admin.ModelAdmin):
inlines = [
ArticlesParentContainerSelectorInline,
]
exclude = ('articles_parent_container_selector',)
admin.site.register(Provider, ProviderAdmin)
and what you 'll get looks like this:
which is a bit of a disappointment, as I was expecting to get Text Inputs instead of dropdowns (or even both of them), so I could add Selectors without having to click the plus sign...
I 'm leaning towards creating my own widget for the admin application.
Anyway, thanks to everyone who bothered to read!

Better ArrayField admin widget?

Is there any way to make ArrayField's admin widget allow adding and deleting objects? It seems that by default, it is instead displayed just a text field, and uses comma separation for its values.
Besides being inconvenient, AFAICT in the case the base field of the array is a Char/TextField, this doesn't allow any way of including commas in any of the texts in the array.
I take no credit for this (original source), but if you are using PostgreSQL as the database and are happy to use the Postgres-specific ArrayField implementation there is an even easier option: subclass ArrayField on the model and override the default admin widget. A basic implementation follows (tested in Django 1.9, 1.10, 1.11, 2.0, 2.1 & 2.2):
models.py
from django import forms
from django.db import models
from django.contrib.postgres.fields import ArrayField
class ChoiceArrayField(ArrayField):
"""
A field that allows us to store an array of choices.
Uses Django's Postgres ArrayField
and a MultipleChoiceField for its formfield.
"""
def formfield(self, **kwargs):
defaults = {
'form_class': forms.MultipleChoiceField,
'choices': self.base_field.choices,
}
defaults.update(kwargs)
# Skip our parent's formfield implementation completely as we don't
# care for it.
# pylint:disable=bad-super-call
return super(ArrayField, self).formfield(**defaults)
FUNCTION_CHOICES = (
('0', 'Planning'),
('1', 'Operation'),
('2', 'Reporting'),
)
class FunctionModel(models.Model):
name = models.CharField(max_length=128, unique=True)
function = ChoiceArrayField(
base_field=models.CharField(max_length=256, choices=FUNCTION_CHOICES),
default=list)
For OP, or anyone out there looking, between these helpful bits you should be good to go:
1. Extending SelectMultiple or CheckboxSelectMultiple widget to parse arrayfield and
2. Creating or extending admin form to display the arrayfield using the widget above
This is a better version of an already accepted solution. Using "CheckboxSelectMultiple" makes it more usable in the admin page.
class ChoiceArrayField(ArrayField):
def formfield(self, **kwargs):
defaults = {
'form_class': forms.TypedMultipleChoiceField,
'choices': self.base_field.choices,
'coerce': self.base_field.to_python,
'widget': forms.CheckboxSelectMultiple,
}
defaults.update(kwargs)
return super(ArrayField, self).formfield(**defaults)
The Django better admin ArrayField package provides exactly this functionality. The advantage over the solutions above is that it allows you to add new entries dynamically instead of relying on pre-defined choices.
See the documentation here: django-better-admin-arrayfield
It has a drop-in replacement for the ArrayField and a simple mixin to add to the admin model.
# models.py
from django_better_admin_arrayfield.models.fields import ArrayField
class MyModel(models.Model):
my_array_field = ArrayField(models.IntegerField(), null=True, blank=True)
# admin.py
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin, DynamicArrayMixin):
...
This would show something like:
This is another version using the Django Admin M2M filter_horizontal widget, instead of the standard HTML select multiple.
We use Django forms only in the Admin site, and this works for us, but the admin widget FilteredSelectMultiple probably will break if used outside the Admin. An alternative would be overriding the ModelAdmin.get_form to instantiate the proper form class and widget for the array field. The ModelAdmin.formfields_overrides is not enough because you need to instantiate the widget setting the positional arguments as shown in the code snippet.
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.postgres.fields import ArrayField
from django.forms import MultipleChoiceField
class ChoiceArrayField(ArrayField):
"""
A choices ArrayField that uses the `horizontal_filter` style of an M2M in the Admin
Usage::
class MyModel(models.Model):
tags = ChoiceArrayField(
models.TextField(choices=TAG_CHOICES),
verbose_name="Tags",
help_text="Some tags help",
blank=True,
default=list,
)
"""
def formfield(self, **kwargs):
widget = FilteredSelectMultiple(self.verbose_name, False)
defaults = {
"form_class": MultipleChoiceField,
"widget": widget,
"choices": self.base_field.choices,
}
defaults.update(kwargs)
# Skip our parent's formfield implementation completely as we don't
# care for it.
return super(ArrayField, self).formfield(**defaults)
django-select2 offers a way to render the ArrayField using Select2. In their documentation, the example is for ArrayField:
http://django-select2.readthedocs.io/en/latest/django_select2.html#django_select2.forms.Select2TagWidget
To render the already selected values:
class ArrayFieldWidget(Select2TagWidget):
def render_options(self, *args, **kwargs):
try:
selected_choices, = args
except ValueError: # Signature contained `choices` prior to Django 1.10
choices, selected_choices = args
output = ['<option></option>' if not self.is_required and not self.allow_multiple_selected else '']
selected_choices = {force_text(v) for v in selected_choices.split(',')}
choices = {(v, v) for v in selected_choices}
for option_value, option_label in choices:
output.append(self.render_option(selected_choices, option_value, option_label))
return '\n'.join(output)
def value_from_datadict(self, data, files, name):
values = super().value_from_datadict(data, files, name)
return ",".join(values)
To add the widget to your form:
class MyForm(ModelForm):
class Meta:
fields = ['my_array_field']
widgets = {
'my_array_field': ArrayFieldWidget
}
write a form class for your model and use forms.MultipleChoiceField for ArrayField:
class ModelForm(forms.ModelForm):
my_array_field = forms.MultipleChoiceField(
choices=[1, 2, 3]
)
class Meta:
exclude = ()
model = Model
use ModelForm in your admin class:
class ModelAdmin(admin.ModelAdmin):
form = ModelForm
exclude = ()
fields = (
'my_array_field',
)

How can I enable inline ManyToManyFields on my Django admin site?

Let's say I have Books and Author models.
class Author(models.Model):
name = CharField(max_length=100)
class Book(models.Model):
title = CharField(max_length=250)
authors = ManyToManyField(Author)
I want each Book to have multiple Authors, and on the Django admin site I want to be able to add multiple new authors to a book from its Edit page, in one go. I don't need to add Books to authors.
Is this possible? If so, what's the best and / or easiest way of accomplishing it?
It is quite simple to do what you want, If I am getting you correctly:
You should create an admin.py file inside your apps directory and then write the following code:
from django.contrib import admin
from myapps.models import Author, Book
class BookAdmin(admin.ModelAdmin):
model= Book
filter_horizontal = ('authors',) #If you don't specify this, you will get a multiple select widget.
admin.site.register(Author)
admin.site.register(Book, BookAdmin)
Try this:
class AuthorInline(admin.TabularInline):
model = Book.authors.through
verbose_name = u"Author"
verbose_name_plural = u"Authors"
class BookAdmin(admin.ModelAdmin):
exclude = ("authors", )
inlines = (
AuthorInline,
)
You might need to add raw_id_fields = ("author", ) to AuthorInline if you have many authors.
Well, check out the Django docs on many to many usage with inlines.