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

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.

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.

How to display changelist of multiple models in django admin?

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

Django admin page outside admin

This might be a simple Django question, but I hope I can get som advice. The following code in admin.py
class ExarbeteMomentInline(admin.TabularInline):
model = ExarbeteMoment
extra=0
class ExarbeteStudent(admin.TabularInline):
model = ExarbeteStudent
extra=0
class ExamensarbeteAdmin(admin.ModelAdmin):
list_display = ('title', 'start_date', 'end_date', 'company')
inlines = [ExarbeteMomentInline, ExarbeteStudent]
admin.site.register(Examensarbete,ExamensarbeteAdmin)
produces what I want, in the admin panel.
But I want a version of it outside the admin panel, so that regular users can enter data. How can I modify the code in a minimal way to get essentially the same page outside the admin region?
Thanks, in advance.
/ADDED/
My models are:
class Examensarbete(models.Model):
title = models.CharField(max_length=10000,default='',blank=True)
start_date = models.DateField('startdatum')
end_date = models.DateField('slutdatum',blank=True,default='',null=True)
company = models.CharField(max_length=200,default='',blank=True)
comment = models.TextField(max_length=1000,default='',blank=True)
kurskod = models.CharField(max_length=100,default='FMAM05')
year = models.CharField(max_length=5,blank=True,default='',null=True)
class ExarbeteMoment(models.Model):
exarbete = models.ForeignKey(Examensarbete,on_delete=models.CASCADE)
typ = models.CharField(max_length=100)
person = models.ForeignKey(Personal,on_delete=models.SET_NULL,null=True)
tim = models.FloatField(null=True)
class ExarbeteStudent(models.Model):
exarbete = models.ForeignKey(Examensarbete,on_delete=models.CASCADE)
first_name=models.CharField(max_length=100)
last_name=models.CharField(max_length=100)
pnr = models.CharField(max_length=13)
program = models.CharField(max_length=5)
kull = models.CharField(max_length=5)
where I have deleted str and Meta. I guess I should be able to solve the problem with the help below, but I can't still figure out how I get what I get in the admin panel, with Examensarbete above and then two subforms with ExarbeteMoment and ExarbeteStudent. And the 'add another item' feature.
Unfortunately I am new to Django, and find it particularly hard to work with forms and formsets. I am not quite sure why, because they should simplify things considerably.
I agree with the comment from Roman Kovalevsky - you will need to write the functions by yourself. Django however supports you with forms and formsets to do this job. As you did not post your models in the question i can only show you an example with some random named variables:
class ExarbeteStudent(forms.Form):
description = forms.CharField(widget=AdminTextareaWidget)
date1 = forms.DateField(widget=AdminDateWidget)
date2 = forms.DateField(widget=AdminDateWidget)
task_list = Task.objects.all()
then you create a formset for the inline models as following
ExarbeteStudentFormSet = forms.formset_factory(ExarbeteStudent,
extra=1,
max_num=60,
can_delete=True,
Here you find some more informations about forms and formsets
https://docs.djangoproject.com/en/2.0/topics/forms/formsets/

django admin edit model select/prefetch_related?

I have a Django website, with model Event, lets say:
class Event(models.Model):
home = models.ForeignKey('Team', related_name='%(class)s_home')
away = models.ForeignKey('Team', related_name='%(class)s_away')
...
class Team(models.Model):
name = models.CharField("team's name", max_length=100)
Using ForeignKeys for this was a bad idea, but anyway, how to make this usable in Django Admin page?
In admin edit event page, a ton of foreign keys is fetched for this:
http://127.0.0.1:8000/admin/event/event/116255/
It produces tons of selects like:
SELECT "event_team"."id", "event_team"."name" FROM "event_team" WHERE "event_team"."id" = 346;
and page dies. I was playing with these:
class EventAdmin(admin.ModelAdmin):
list_display = ('id', 'home', 'away', 'date_game', 'sport', 'result')
search_fields = ['home__name', 'away__name']
list_select_related = (
'home', 'away', 'league', 'sport', ...
)
def get_queryset(self, request):
return super(EventAdmin, self).get_queryset(request).select_related(*self.list_select_related)
admin.site.register(Event, EventAdmin)
But no luck.
The simplest, quickest way
It would be to add raw_id_fields on your ModelAdmin (Django ModelAdmin.raw_id_fields documentation) :
class EventAdmin(admin.ModelAdmin):
raw_id_fields = ("home", "away")
It would result in a inputText with the FK field ids in such as :
.
Loading will be fast as it won't populate a select list with ALL the teams.
You'll have the Django admin change_view of the Team ModelAdmin to select the teams, thanks to the browse icon.
A nicer way ?
A lot more elegant on the UX side of things, it requires you to know part of the name of the team: using an ajax autocomplete widget to represent your field.
You could use for example Django Autocomplete Light (DAL) quick tutorial by having a custom form for your admin and a autocompleteModelSelect2 for your home and away fields (with 2 differents QS in the ajax view).
It will produce a field looking like:
.
The tutorial of this link have all you need!
Or you can chose another third party plugin or build your own field/widget to produce a similar result.
I think #ppython's answer is the simplest and works perfectly but I ended up using autocomplete_fields instead of raw_id_fields. Achieving a more friendly approach, it has been available since Django 2.0.
Following the answer, it'll be something like this:
class EventAdmin(admin.ModelAdmin):
autocomplete_fields = ['home', 'away']
I found the problem, it was my mistake and I did not even mention it in question :(
So this is my model, but I did not mention important part if it:
class Event(models.Model):
home = models.ForeignKey('Team', related_name='%(class)s_home')
away = models.ForeignKey('Team', related_name='%(class)s_away')
merged = models.ForeignKey('Event', null='True', blank='True')
def __unicode__(self):
return str(self.id) + ": " + self.home.name + " - " + self.away.name
Problem was not with home or away, but with merged field, that fetched self.home.name and self.away.name for each event.
Replaced with
def __unicode__(self):
return 'Event id: {}'.format(self.id)
and added merged to list_select_related
fixed my problem. Thanks for help and sorry for incomplete question.

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!