We have a content model with a couple of many-to-many (type) relationships:
Issue has many Articles
Article has many Authors
In this specific case, our Article and Author models are fairly small. The use case is to construct a table of contents for a magazine Issue by creating a list of Article titles and one or more Authors for each Article. Ideally, the end-user could edit the entire Issue from a single form, since the articles are only used as a table of contents.
I have tried to model this using the Page and Orderable classes, as follows. However, I am getting a KeyError when wagtail attempts to render the Issue edit form.
class Author(Orderable):
"""
Represents authorship for an article
allowing multiple authors per article
and multiple articles per author
"""
article = models.ForeignKey(
"magazine.Article",
null=True,
on_delete=models.PROTECT,
related_name="authors",
)
author = models.ForeignKey(
"wagtailcore.Page",
null=True,
on_delete=models.PROTECT,
related_name="articles_authored",
)
panels = [
PageChooserPanel(
"author", ["contact.Person", "contact.Organization"]
)
]
class Article(Orderable):
"""
An article, which can have multiple authors
"""
title = models.CharField(max_length=255)
issue = ParentalKey(
"magazine.ArchiveIssue",
null=True,
on_delete=models.PROTECT,
related_name="articles",
)
panels = [
FieldPanel("title", classname="full"),
# Multiple authors can contribute to an article
InlinePanel(
"authors",
heading="Authors",
help_text="Select one or more authors, who contributed to this article",
),
]
class Issue(Page):
"""
Represents a magazine issue
with table of contents
"""
# Add articles inline, since it is a table of contents
content_panels = Page.content_panels + [
InlinePanel(
"articles",
heading="Table of contents",
help_text="Select one or more authors, who contributed to this article",
),
]
Django throws a KeyError exception when trying to render the Wagtail edit page:
Exception Type: KeyError
Exception Value: 'authors'
I realize I may be asking too much of Wagtail here, but this seems like it might be possible. At the very least, I hope to get a better understanding of why this isn't working and might not be possible.
I have not tested the nested orderables aspect, but if planning to order Authors on Article then Author.article should be a ParentalKey, and Article would also need to extend ClusterableModel.
Related
I would like to limit the choices available of a field in the Wagtail-Admin edit view. My Wagtail version is 2.16.3.
In my case, if have a page model, that describes product categories. To each category might exist some tags.
There is another page model, that describes concrete products and always is a subpage of a category page.
A product can now be described with some tags, but only tags, that belong to the product's category make sense, so I would like to restrict the edit handler of the products to these tags.
My models.py looks similar to this:
class ProductTag(ClusterableModel):
name = models.CharField(_("Name"), max_length=50)
category = ParentalKey(
"ProductCategoryIndexPage", related_name="tags", on_delete=models.CASCADE
)
class ProductPage(Page):
parent_page_types = ["products.ProductCategoryIndexPage"]
tags = ParentalManyToManyField("ProductTag", related_name="products")
content_panels = Page.content_panels + [
FieldPanel("tags", widget=widgets.CheckboxSelectMultiple()),
]
class ProductCategoryIndexPage(Page):
subpage_types = ["products.ProductPage"]
content_panels = Page.content_panels + [
MultiFieldPanel(
[InlinePanel("tags", label="Tags")],
heading=_("Produkt Tags"),
),
]
My approach was to create a custom edit handler and inject the wigdet overriding the on_instance_bound method using the correct choices argument for the widget.
class ProductTagEditPanel(FieldPanel):
def on_instance_bound(self):
self.widget = widgets.CheckboxSelectMultiple(
# of_product is a custom manager method, returning only the tags, that belong to the product's category
choices=ProductTag.objects.of_product(self.instance)
)
return super().on_instance_bound()
But somewhere in the form creation process, the choices are overridden again and I cannot find a good location, where to inject the custom query.
Also looking through the wagtail issues I could only find remotely related stuff and I'm wondering, if my plans are to exotic or I'm sitting to long in front of this problem..
I recently added a "through" model to allow sorting connected objects.
In the example below, a Stage has an ordered list of Blocks linked through StageBlock (with the StageBlock.order field)
#register_snippet
class Block(index.Indexed, models.Model):
title = models.CharField(max_length=100, verbose_name=_("Block Name"))
#register_snippet
class Stage(index.Indexed, models.Model):
title = models.CharField(max_length=100, verbose_name=_("Stage Name"))
blocks = models.ManyToManyField(
to="app.Block",
blank=True,
help_text=_("Blocks associated to this stage"),
related_name="stages",
verbose_name=_("Blocks"),
through="StageBlock",
)
panels = [
FieldPanel("title", classname="title full"),
FieldPanel(
"blocks",
widget=autocomplete.ModelSelect2Multiple(
url="block-autocomplete",
attrs={"data-maximum-selection-length": 3},
),
),
class StageBlock(models.Model):
block = models.ForeignKey("app.Block", on_delete=models.CASCADE)
stage = models.ForeignKey("app.Stage", on_delete=models.CASCADE)
order = models.PositiveSmallIntegerField()
The problem is that the related Wagtail admin form breaks, since it tries to associate Block objects to Stage, without providing the "through" model "order" field value.
I'm wondering what is the cleanest/least effort solution to allow an ordered selection of elements in the admin panel, then to properly save the Stage instance with its blocks and related stageblocks.
For the moment, I will add a custom form to the snippet, and auto-assign the order from the position of blocks in the form data (hoping that it always matches the order of blocks as selected in the fieldpanel).
It feels like this use-case could be auto-handled, either by the wagtail-autocomplete plugin, or by wagtail fieldpanel.
But as far as I understand, fieldpanel will simply re-use the Django ModelMultipleChoiceField field, which returns a html element.
A many-to-many relation with a 'through' model is structurally the same as a one-to-many child relationship on that 'through' model, so one possibility is to implement this with an InlinePanel (as described here):
from django_modelcluster.fields import ParentalKey
from django_modelcluster.models import ClusterableModel
from wagtail.core.models import Orderable
#register_snippet
class Stage(index.Indexed, ClusterableModel):
title = models.CharField(max_length=100, verbose_name=_("Stage Name"))
panels = [
FieldPanel("title", classname="title full"),
InlinePanel("blocks", label="Blocks"),
]
class StageBlock(Orderable):
stage = ParentalKey("app.Stage", on_delete=models.CASCADE, related_name='blocks')
block = models.ForeignKey("app.Block", on_delete=models.CASCADE)
panels = [
FieldPanel('block'),
]
I am having trouble deciding how to structure my models for a particular data structure.
The models I have would be Posts, Groups, Users.
I want the Post model that can be posted from a groups page or user page and potentially more, like an events page.
Posts would contain fields for text, images(fk), user, view count, rating score (from -- a reference to where ever it was posted from like user or group page, though I am unsure how to make this connection yet)
I thought about using a Generic Foreign Key to assign a field to different models but read articles suggesting to avoid it. I tried the suggested models, but I wasn't unsure if they were the right approach for what I required.
At the moment I went with Alternative 4 - multi-table inheritance
class Group(models.Model):
name = models.CharField(max_length=64)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='_groups')
members = models.ManyToManyField(
settings.AUTH_USER_MODEL)
def __str__(self):
return f'{self.name} -- {self.created_by}'
def save(self, *args, **kwargs):
# https://stackoverflow.com/a/35647389/1294405
created = self._state.adding
super(Group, self).save(*args, **kwargs)
if created:
if not self.members.filter(pk=self.created_by.pk).exists():
self.members.add(self.created_by)
class Post(models.Model):
content = models.TextField(blank=True, default='')
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s_posts",
related_query_name="%(app_label)s_%(class)ss")
# class Meta:
# abstract = True
def __str__(self):
return f'{self.content} -- {self.created_by}'
class PostImage(models.Model):
image = models.ImageField(upload_to=unique_upload)
post = models.ForeignKey(
Post, related_name='images', on_delete=models.CASCADE)
def __str__(self):
return '{}'.format(self.image.name)
class UserPost(models.Model):
post = models.OneToOneField(
Post, null=True, blank=True, related_name='_uPost', on_delete=models.CASCADE)
class GroupPost(models.Model):
post = models.OneToOneField(
Post, null=True, blank=True, related_name='_gPost', on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
To do some specific filters ex:
Filter specific group post
Post.objects.filter(_gPost__group=group)
Filter specific user post
Post.objects.filter(created_by=user) # exclude groups with _gPost__isnull=False
Create post to user/group
p = Post.objects.create(...)
up = UserPost.objects.create(post=p)
gp = GroupPost.objects.create(post=p)
Really I am wondering if this is a sensible approach. The current way of a filter and creating feel odd. So only thing making me hesitant on this approach is just how it looks.
So, is Generic ForeignKey the place to use here or the current multi-table approach. I tried going with inheritance with abstract = True and that was unable to work as I need a foreign key to base post model. Even with no abstract, I got the foreign key reference, but filter became frustrating.
Edit:
So far only weird issues(but not really) are when filtering I have to be explicit to exclude some field to get what I want, using only .filter(created_by=...) only would get all other intermediate tables.
Filter post excluding all other tablets would requirePost.objects.filter(_uPost__isnull=True, _gPost__isnull=True, _**__isnull=True) which could end up being tedious.
I think your approach is sensible and that's probably how I would structure it.
Another approach would be to move the Group and Event foreignkeys into the Post model and let them be NULL/None if the Post wasn't posted to a group or event. That improves performance a bit and makes the filters a bit more sensible, but I would avoid that approach if you think Posts can be added to many other models in the future (as you'd have to keep adding more and more foreignkeys).
At the moment I will stick with my current pattern.
Some extra reading for anyone interested.
https://www.slideshare.net/billkarwin/sql-antipatterns-strike-back/32-Polymorphic_Associations_Of_course_some
I've got some problems to understand the right way serializing data with django-rest-framework e.g. using data from related models.
Let me explain a little bit my situation:
I have an Organization model with a manytomany relation to a Subtopic model classifying each organiaztion. Further each subtopic belongs to a general topic.
In addition there is an OrgaDataSet model to save crawled data for each organization in a PostgreSQL JSONField. The field "type" in the OrgaDataSet Model should give me a kind of flexibility to classify crawled data in further stages.
# models.py
class Topic(models.Model):
name = models.CharField(max_length=200, unique=True)
class Subtopic(models.Model):
name = models.CharField(max_length=100, unique=True)
topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
class Organization(models.Model):
name = models.CharField(max_length=300, unique=True)
description = models.TextField(blank=True, null=True)
subtopics = models.ManyToManyField(Subtopic)
class OrgaDataSet(models.Model):
data_set_types = (
('ADDRESS', 'address'),
('PERSON', 'person'),
('DIVISION', 'division'),
)
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
type = models.CharField(max_length=20, choices=data_set_types)
crawled_data = JSONField(verbose_name='Data set')
But lets come up with my questions / problems:
1. How can I serialize the related data with minimized database requests and get an customized serialized field like:
"topics_list": [
{
"topic_name": "Medical science",
"subtopics": [
"Dental products",
"Hygiene"
]
},
{
"topic_name": "Biotechnology",
"subtopics": [
"Microbiology"
]
}
],
I tried different approaches: amongst others a custom model manager to add a method "get_topics_list", however I'm sticking with the right way to query with "prefetch_related" and "select_related" ... But is this even the richt way?
Also I tried to set up a serializedMethodField in the serializer itself. However I was asking myself what is the best place to do the related query, in the view.py or in the serializers.py?
My second question concerns the OrgaDataSet model. I am absolutly uncertain how and where to place the queries, one for each "type". Should I do a specific method in the custom model manager for each type, like "get_type_address"?
I would appreciate your ideas and any hint to understand the utilization of the django-framework a little bit more.
Thanks a lot,
Mike
Question 1)
This is typical Category/Subcategory problem. I would suggest to look at the following django package : https://github.com/django-mptt/django-mptt
It would store Topics/Subtopics in one table ( tree structure ), making the queries efficient and you can easily produce the desired output efficiently
Question 2)
This depends how you want to display the data. Do you have one endpoint for each type ?
If you have one endpoint for each type, you can do a view like this :
class OrgaDataSetListView(generics.ListAPIView):
type = None
serializer_class =<your serializer>
def get_queryset(self):
return OrgaDataSet.objects.filter(type=self.type)
And use it like this in urls:
urlpatterns = [
path('orgadata/address', OrgaDataSetListView.as_view(type='ADDRESS')),
path('orgadata/person', OrgaDataSetListView.as_view(type='PERSON')),
]
Say I have two models, Article and Category:
class Article(models.Model):
category = models.ForeignKey(Category, related_name='articles')
class Category(models.Model):
...
When I run Category.objects.select_related(), the ORM does not select the articles. I realize it's because of the way the foreignkey is shuffled around, but I'm not sure how to go about doing that. Any ideas?
Here's what I ended up doing, at the advice of the kind people on #django:
articles = Article.objects.select_related()
categories = {}
for article in articles:
if not categories.get(article.category, None):
categories[article.section] = []
categories[article.category].append(article)