Django One-To-Many Model and Admin inlines - django

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!

Related

How to seperate wagtail image's tags from page's tags?

Suppose we have a wagtail page defined like this:
class PostTag(TaggedItemBase):
content_object = ParentalKey(
'PostPage',
related_name='tagged_items',
on_delete=models.CASCADE
)
class PostPage(Page):
...
tags = ClusterTaggableManager(through=PostTag, blank=True)
...
content_panels = Page.content_panels + [
...
FieldPanel('tags')
]
When I want to edit tags field on wagtail admin, it suggests not only pages' tags but also images' tags. I want to some how remove images' tags from suggestions.
In my project, pages' tags are not related to image's tags.
To understand the scenario, look at these two pictures:
The first one shows adding river as a tag into one of the images.
The second one shows the tags field on PostPage.
I don't want to see the river tag as a suggestion on page's tags:
Is that possible? If not, Is it possible to remove tags field from wagtail image model?
You can do this by setting up a custom Tag model that extends from taggit.models.TagBase. This is then handled as a distinct model from the default Tag model used for images and documents.
from django.db import models
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TagBase, ItemBase
class PostTag(TagBase):
class Meta:
verbose_name = "post tag"
verbose_name_plural = "post tags"
class TaggedPost(ItemBase):
tag = models.ForeignKey(
PostTag, related_name="tagged_posts", on_delete=models.CASCADE
)
content_object = ParentalKey(
to='myapp.PostPage',
on_delete=models.CASCADE,
related_name='tagged_items'
)
class PostPage(Page):
...
tags = ClusterTaggableManager(through=TaggedPost, blank=True)
In this example, PostTag is the custom tag model, and TaggedPost is the link table that relates tags to posts (the equivalent of PostTag in your code).

Putting tags in a StructBlock

I want to be able to add tagging to a custom StructBlock that I've created.
The current model looks like this
class MapsIndicatorBlock(blocks.StructBlock):
text_icon = blocks.CharBlock(
label='Maps/Indicators Text or Icon',
required=False
)
pop_up_title = blocks.CharBlock(
label='Pop-Up Title',
required=False
)
pop_up_text = blocks.RichTextBlock(
label ='Pop-Up Text/Image',
required=False
)
pop_up_colour = blocks.CharBlock(
choices=constants.BOOTSTRAP4_BUTTON_COLOUR_CHOICES,
default='btn btn-primary',
max_length=128,
required=False
)
tags = TaggableManager()
objects = models.Manager()
class Meta:
template = 'cityregiontable/map_indicator_block.html'
The TaggableManager() was designed to be used with models.model not blocks.StructBlock.
I have tried to create a way to create the tags using the following to no avail. I get the error RE: not being able to find the model for MapsIndicatorBlock. This is correct as MapsIndicatorBlock is a block, not a model.
class MITag(TaggedItemBase):
content_object = models.ForeignKey(
'MapsIndicatorBlock',
on_delete=models.CASCADE,
related_name='tagged_mi_block'
)
How can I allow a block to be have metadat tags?
Based on the docs for custom block types as a starting point we are able to generate a custom FieldBlock that leverages the existing Wagtail AdminTagWidget.
This widget does almost all of the work for you, it will pull in the available tags for autocomplete plus will save any new tags created on the fly.
It is possible to read out these tags and make them available more conveniently with a model #property or similar. Remember Streamfields store data as JSON so you do not get any of the model / database linking out of the box.
Limitations
The caveat is that the saved tags are stored as the raw strings, this means if you have some more complex use cases of tags you will have to do a bit more work to get this integrated. e.g. a tag page that shows all pages that use that tag or advanced tag editing in Wagtail's ModelAdmin.
In these cases, you can either work out a way to 'sync' the Page's tags with the StreamField tag and maybe abstract this work out to a mixin. Alternatively, you can rework your query on your tags page to also include those with the streamfield data you want.
Example Code
from itertools import chain
from django import forms
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.admin.widgets import AdminTagWidget
from wagtail.core.blocks import CharBlock, FieldBlock, StructBlock, RichTextBlock
from wagtail.core.fields import StreamField
from wagtail.core.models import Page
class TagsBlock(FieldBlock):
"""
Basic Stream Block that will use the Wagtail tags system.
Stores the tags as simple strings only.
"""
def __init__(self, required=False, help_text=None, **kwargs):
# note - required=False is important if you are adding this tag to an existing streamfield
self.field = forms.CharField(widget=AdminTagWidget, required=False)
super().__init__(**kwargs)
class MapBlock(StructBlock):
title = CharBlock(label="Title", required=False)
content = RichTextBlock(label="Content", required=False)
tags = TagsBlock(label="Tags", required=False)
class Meta:
icon = 'site'
class LocationPage(Page):
"""
Detail for a specific location.
"""
# ... other fields
# this is the stream field added
map_info = StreamField([('Map', MapBlock(required=False))], blank=True)
#property
def get_tags(self):
"""
Helpful property to pull out the tags saved inside the struct value
Important: makes some hard assumptions about the names & structure
Does not get the id of the tag, only the strings as a list
"""
tags_all = [block.value.get('tags', '').split(',') for block in self.test_b]
tags = list(chain.from_iterable(tags_all))
return tags
# Fields to show to the editor in the admin view
content_panels = [
FieldPanel('title', classname="full"),
StreamFieldPanel('map_info'),
# ... others
]
# ... rest of page model
Thanks to this similar question about tags in streamfields, answering that helped me answer this one.
Creating a TagsBlock for StreamField

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

Hierarchical data in admin pages in Django

In a Django project, I have a hierarchical model using MPTT defined like this in models.py:
class Structure(MPTTModel):
name = models.CharField(max_length=200, unique=True)
parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
[...]
I'm using FeinCMS to show this hierarchical data in admin pages. I do it like this in admin.py:
class StructureAdmin(tree_editor.TreeEditor):
search_fields = ('name',)
[...]
admin.site.register(Structure, StructureAdmin)
In the admin model page, it works perfectly and the hierarchy can be seen:
It also works when editing or adding:
I have another model in models.py:
class Track(models.Model):
initialStructure = models.ForeignKey(Structure , related_name='track_initialStructure')
finalStructure = models.ForeignKey(Structure, related_name='track_finalStructure')
[...]
However, when adding a new element of this kind, the hierarchy can not be seen:
I've tried to use tree_editor.TreeEditor for the admin view of Track but it gives a lot of errors because Track is not hierarchical, but some of its ForeignKey's are. How could I show the hierarchy when editing an element of model Track?
Thank you very much.
Try changing:
finalStructure = models.ForeignKey(Structure, related_name='track_finalStructure')
to:
finalStructure = TreeForeignKey(Structure, related_name='track_finalStructure')
of course, after importing TreeForeignKey from django-mptt:
from mptt.fields import TreeForeignKey

django admin gives warning "Field 'X' doesn't have a default value"

I have created two models out of an existing legacy DB , one for articles and one for tags that one can associate with articles:
class Article(models.Model):
article_id = models.AutoField(primary_key=True)
text = models.CharField(max_length=400)
class Meta:
db_table = u'articles'
class Tag(models.Model):
tag_id = models.AutoField(primary_key=True)
tag = models.CharField(max_length=20)
article=models.ForeignKey(Article)
class Meta:
db_table = u'article_tags'
I want to enable adding tags for an article from the admin interface, so my admin.py file looks like this:
from models import Article,Tag
from django.contrib import admin
class TagInline(admin.StackedInline):
model = Tag
class ArticleAdmin(admin.ModelAdmin):
inlines = [TagInline]
admin.site.register(Article,ArticleAdmin)
The interface looks fine, but when I try to save, I get:
Warning at /admin/webserver/article/382/
Field 'tag_id' doesn't have a default value
This can also happen if you have a disused field in your database that doesn't allow NULL.
The problem was that in the DB, tag_id wasn't set as an autoincrement field.
What solved this issue in my case was disabling STRICT_TRANS_TABLES SQL mode which was enabled by default.