How to create a tags field for multiple Page classes in Wagtail? - wagtail

I want to add a field for keywords that almost every model in my site will have. It would be ideal if I didn't have to define a "TaggedPage" class for every Page model. So I created a BasePage abstract model but ParentalKey doesn't appear to work with an abstract model. How can I solve this?
I get this error:
home.TaggedPage.content_object: (fields.E300) Field defines a relation with model 'home.BasePage', which is either not installed, or is abstract.
home.TaggedPage.content_object: (fields.E307) The field home.TaggedPage.content_object was declared with a lazy reference to 'home.basepage', but app 'home' doesn't provide model 'basepage'.
home/models.py contains these models:
class PageTag(TagBase):
""" Tag used for the keywords meta html tag"""
class Meta:
verbose_name = "search/meta keyword"
verbose_name_plural = "search/meta keywords"
class TaggedPage(ItemBase):
tag = models.ForeignKey(
PageTag,
related_name="tagged_pages",
on_delete=models.CASCADE,
)
content_object = ParentalKey(
to='home.BasePage',
on_delete=models.CASCADE,
related_name='tagged_items'
)
class BasePage(MetadataPageMixin, Page):
tags = ClusterTaggableManager(
through='home.TaggedPage',
blank=True,
help_text="Used for the keywords meta html tag",
)
promote_panels = Page.promote_panels + [
FieldPanel('tags'),
]
class Meta:
abstract = True
class HomePage(BasePage):
parent_page_types = []

Point the ParentalKey at the Page model instead:
content_object = ParentalKey(
to='wagtailcore.Page',
on_delete=models.CASCADE,
related_name='tagged_items'
)
At the database level this will have the behaviour you're looking for: a foreign key field referencing the ID of the page object. The one minor side effect is that the tagged_items relation will be defined on all Page objects, not just ones inheriting from BasePage, but that shouldn't cause any problems.

Related

model relationship design for Blog app in Wagtail site

Below models.py to build a blog in wagtail site is from this post.
class BlogPage(Page):
description = models.CharField(max_length=255, blank=True,)
content_panels = Page.content_panels + [FieldPanel("description", classname="full")]
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
tags = ClusterTaggableManager(through="blog.PostPageTag", blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
]
class PostPageBlogCategory(models.Model):
page = ParentalKey(
"blog.PostPage", on_delete=models.CASCADE, related_name="categories"
)
blog_category = models.ForeignKey(
"blog.BlogCategory", on_delete=models.CASCADE, related_name="post_pages"
)
panels = [
SnippetChooserPanel("blog_category"),
]
class Meta:
unique_together = ("page", "blog_category")
#register_snippet
class BlogCategory(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True, max_length=80)
panels = [
FieldPanel("name"),
FieldPanel("slug"),
]
def __str__(self):
return self.name
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
class PostPageTag(TaggedItemBase):
content_object = ParentalKey("PostPage", related_name="post_tags")
#register_snippet
class Tag(TaggitTag):
class Meta:
proxy = True
I am wondering, what are the major reasons to introduce extra Intermediary model (class PostPageBlogCategory(models.Model): & class PostPageTag(TaggedItemBase):) to link PostPage to Category & Tag?
Why not just simply use ParentalForeignkey or ParentalManyToManyKey ?
The short answer is that Django's ManyToManyField has some limitations in how the relationship needs to be built when models on either end to not yet exist, adding an 'intermediary model' helps to work around this.
Longer Answer
As with anything in software, there are multiple ways to do something and each has its own pros & cons. Django's built in field is simple and lets you do lots of powerful things to represent relational data but that simplicity comes with some reduced flexibility in how these relationships are managed.
One of the main goals of the Wagtail CMS Admin interface is the ability to work as though the data has been created (including relationships) before actually clicking 'save'. While this may seem simple at first glance, getting to that point requires a bit of nuance under the hood once you start to consider relational data.
Wagtail comes built in with a very powerful library called django-modelcluster which has been purpose built for many of the cases where you want to work with relational data without having all the bits in the DB first.
Each Wagtail Page actually inherits the modelcluster.models.ClusterableModel, which is why some of the features in the blog post seem to work in the editor, even when the DB entries have not yet been saved.
On the blog post you linked, there is a section towards the end with the heading 'ParentalKey' that further explains this nuance and how just using Django's basic approach has some draw backs.
On the Django docs for many-to-many relationships, have a read through and note that each individual model instance must be in the database first and only then can you 'link' the two instances with a second update on each.

Access other object fields in StreamField / RichText field/block in wagtail

I would like to do the following:
Create some django model (or wagtail page) that contains data.
Create another Page type that includes a StreamField or RichtextField.
3.
When the author or editor enters text in this field, he/she can have the information of this appear somewhere in the rendered output of the text. Preferably using common template techniques.
So, lets say, we have a model like this:
class Person(models.Model):
name = models.CharField(max_length=30)
Then I have a normal page that uses a StreamField:
class NormalPage(Page):
body = StreamField(block_types=[
('paragraph', blocks.RichTextBlock()),
])
content_panels = Page.content_panels + [
StreamFieldPanel('body')
]
I would like for the editor to choose which Person he/she would like from the database and the be able to do something like this in the RichTextBlock:
{{ person.name }}
Is this possible?
In order to be able to choose a plain Django model, register it as a snippet as shown here, and then use SnippetChooserPanel to choose the person:
from wagtail.snippets.models import register_snippet
#register_snippet
class Person(models.Model):
name = models.CharField(max_length=30)
And then if you want to associate a particular person with a particular page:
from wagtail.snippets.edit_handlers import SnippetChooserPanel
class NormalPage(Page):
body = StreamField(block_types=[
('paragraph', blocks.RichTextBlock()),
])
person = models.ForeignKeyField(Person)
content_panels = Page.content_panels + [
StreamFieldPanel('body'),
SnippetChooserPanel('person'),
]

Django DRF Foreign Key

This question or many like it has been asked multiple times but for some reason I am unable to find the answer.
I do have this working to an extent in the way that if you go on the api pages, it renders, creates and updates without problem. The issue is displaying a field (title) from the nested object instead of just the primary key on the front end.
Some background before getting into the code:
Races is a finite list (e.g. Race1, Race2, Race3) and the front end does not have the ability to add more.
Cards is not finite, but each card must link to an existing Race (this currently does so by Primary Key).
The front end should display the card_text and race title of the linked race.
It also has the ability to add a new card but this works fine.
I have had this working with separate serializers for read and create/update where the read has a 'depth = 1' to pull through the entire object but the create/update doesn't and you then parse the object and send the primary key back (I couldn't find a way of doing this in the serializer, is it possible?).
So basically my question is, are you meant to pass the entire object through and parse it on a POST method, or do you pass the primary key and pull in the linked objects (Races) and use the primary key as an index (e.g. Races[card_race]). Also, why is 'linked_race' not coming through to the front end?
I realise I've almost answered my own question but as I'm new to Django I'm looking for the correct conventions and who knows, it may save someone else time when searching for the same answer.
urls.py
from .api import CardViewSet, RaceViewSet
from rest_framework.routers import DefaultRouter
from django.conf.urls import url, include
from .views import landing
router = DefaultRouter()
router.register(r'cards', CardViewSet)
router.register(r'races', RaceViewSet)
urlpatterns = [
url(r'^$', landing),
url(r'^api/', include(router.urls)),
]
api.py
from rest_framework.viewsets import ModelViewSet
from .serializers import CardSerializer, RaceSerializer
from .models import Card, Race
class CardViewSet(ModelViewSet):
queryset = Card.objects.filter(active=True)
def get_serializer_class(self):
return CardSerializer
def perform_create(self, serializer):
serializer.save(creator=self.request.user)
class RaceViewSet(ModelViewSet):
queryset = Race.objects.filter(active=True)
serializer_class = RaceSerializer
models.py
from django.db import models
from django.conf import settings
User = settings.AUTH_USER_MODEL
class Race(models.Model):
id = models.IntegerField(primary_key=True)
title = models.CharField(max_length=30, blank=False)
active = models.BooleanField(default=True)
def __str__(self):
return "{}".format(self.title)
def __unicode__(self):
return self.title
class Card(models.Model):
card_text = models.CharField(max_length=100, blank=False)
card_description = models.CharField(max_length=100, blank=True)
card_race = models.ForeignKey(Race, related_name='linked_race', on_delete=models.CASCADE)
creator = models.ForeignKey('auth.User', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
def __str__(self):
return self.card_text
class Meta:
ordering = ('created',)
serializers.py
from rest_framework import serializers
from .models import Card, Race
class RaceSerializer(serializers.ModelSerializer):
class Meta:
model = Race
fields = '__all__'
class CardSerializer(serializers.ModelSerializer):
linked_race = RaceSerializer(read_only=True, many=True)
class Meta:
model = Card
fields = 'id', 'card_text', 'card_description', 'card_race', 'linked_race',
Javascript extract (AngularJS)
$http.get('/api/races/').then(function (response) {
$scope.races = response.data;
$scope.selectedOption = $scope.races[0];
});
$scope.cards = [];
$http.get('/api/cards/').then(function (response) {
$scope.cards = orderBy(response.data, 'created', true);
});
html extract (AngularJS)
<div class="races--row" ng-repeat="c in cards | filter : card_filter |
orderBy : sortVal : sortDir" ng-class-odd="'odd'" ng-click="openModal(c)">
<div class="races--cell race">{{ c.card_race.title }}</div>
<div class="races--cell card-text">{{ c.card_text }}</div>
</div>
Your first "problem" is with the Card model (I say problem because I don't think you intended to do this). You're defining related_name='linked_race' for the card_race field. This related_name is the name you use to refer to a card FROM a race.
I would suggest you leave it out and use the default that Django already gives us (i.e. my_race.card_set.all() in this case). So change change that field in the Card model to:
class Card(models.Model):
...
card_race = models.ForeignKey(Race, on_delete=models.CASCADE)
...
And let's change the card serializer to:
class CardSerializer(serializers.ModelSerializer):
# no more linked_race
class Meta:
model = Card
fields = ('id', 'card_text', 'card_description', 'card_race')
Alright, this is a vary basic model serializer and you won't see details of a race yet. So now let's get to your main problem which was that you wanted to:
see the details of the associated race of a card
perform create/get/update/delete operations using the same serializer
For this, let's further change the CardSerializer to include another field called race_detail:
class CardSerializer(serializers.ModelSerializer):
race_detail = RaceSerializer(source='card_race', read_only=True)
class Meta:
model = Card
fields = ('id', 'card_text', 'card_description', 'card_race', 'race_detail')
We have defined two serializer fields for the same model field. Note the source and read_only attributes. This makes this field available when you GET a card (which is what we want), but not when you're performing POSTs or PUTs (which avoids the problem of sending the whole race object and parsing and stuff). You can just send the race id for the card_race field and it should work.

Wagtail Inlinepanel demo erorr

I am relatively new to Django Wagtail and I was following the demo from the docs.wagtail.io website which can be found here on how to add a list of Links to a Page using an InlinePanel with Related links
I seem to have reached an error that I donot fully understand it's meaning.
The error says
AttributeError: type object 'BookPageRelatedLinks' has no attribute 'rel'
The code for the demo is as follows
from wagtail.wagtailcore.models import Orderable, Page
from modelcluster.fields import ParentalKey
from wagtail.wagtailadmin.edit_handlers import FieldPanel,InlinePanel
from django.db import models
class BookPage(Page):
# The abstract model for related links, complete with panels
class RelatedLink(models.Model):
title = models.CharField(max_length=255)
link_external = models.URLField("External link", blank=True)
panels = [
FieldPanel('title'),
FieldPanel('link_external'),
]
class Meta:
abstract = True
# The real model which combines the abstract model, an
# Orderable helper class, and what amounts to a ForeignKey link
# to the model we want to add related links to (BookPage)
class BookPageRelatedLinks(Orderable, RelatedLink):
page = ParentalKey('demo.BookPage', related_name='related_links')
content_panels = Page.content_panels + [
InlinePanel('BookPageRelatedLinks', label="Related Links"),
]
My primary objective was to learn this so I can add image links to a sidebar on a BlogPage app I am developing.
Your InlinePanel declaration isn't quite correct - it needs to be:
InlinePanel('related_links', label="Related Links")
Here's what's going on:
By defining a ParentalKey with related_name='related_links', you set up a one-to-many relation called related_links on BookPage. This allows you to retrieve all of the BookPageRelatedLinks objects associated with a given BookPage instance (for example, if your BookPage instance was called page, you could write page.related_links.all()).
The InlinePanel declaration then tells Wagtail to make the related_links property editable within the admin.
The reason you're getting a misleading error message is that you've defined the RelatedLink and BookPageRelatedLinks classes inside the BookPage - which is a little bit unusual, but still valid. This results in BookPageRelatedLinks being defined as a property of BookPage (i.e. BookPage.BookPageRelatedLinks). Then, when Wagtail tries to set up the InlinePanel, it retrieves that property and fails because it's not the expected type of object (it's a class definition, not a relation).
If you write your models file in the more conventional way, with the related models defined below (or above) BookPage:
class BookPage(Page):
content_panels = Page.content_panels + [
InlinePanel('BookPageRelatedLinks', label="Related Links"),
]
# The abstract model for related links, complete with panels
class RelatedLink(models.Model):
title = models.CharField(max_length=255)
link_external = models.URLField("External link", blank=True)
panels = [
FieldPanel('title'),
FieldPanel('link_external'),
]
class Meta:
abstract = True
# The real model which combines the abstract model, an
# Orderable helper class, and what amounts to a ForeignKey link
# to the model we want to add related links to (BookPage)
class BookPageRelatedLinks(Orderable, RelatedLink):
page = ParentalKey('home.BookPage', related_name='related_links')
...then you would get a more informative error: AttributeError: type object 'BookPage' has no attribute 'BookPageRelatedLinks'.

Relationships in Django Admin

I get really confused with many-to-many database relationships, so can some one please clarify how I would achieve this?
I need a table of "Tags" (as in tag words) and a table for "Entries", such at many "Entries" could correspond to many Tag words.
Right now I have my models like this:
# models.py
class Tags(models.Model):
tag = models.CharField(max_length=255)
entry = models.ManyToManyField(Entry)
class Entry(models.Model):
entry = models.CharField(max_length=255)
description = models.TextField()
Now I'm confused, how would I setup my admin.py so I could then add tags when I create a new entry?
What you need is using the through feature of models:
class Tag(models.Model):
tag = models.CharField(max_length=255)
entry = models.ManyToManyField(Entry, through='TaggedEntries')
class Entry(models.Model):
entry = models.CharField(max_length=255)
description = models.TextField()
class TaggedEntries(models.Model):
entry = models.ForeignKey(Entry)
tag = models.ForeignKey(Tag)
and now use that model in your admin:
class TagsInline(admin.TabularInline):
model = TaggedEntries
extra = 1
class EntryAdmin(admin.ModelAdmin):
inlines = (TagsInline, )
admin.site.register(Entry, EntryAdmin)
admin.site.register(Tag)
You will need something along the lines of:
# admin.py
from django.contrib import admin
from models import *
class TagsInline(admin.TabularInline):
model = Tag
extra = 1
class EntryAdmin(admin.ModelAdmin):
inlines = (TagsInline, )
admin.site.register(Entry, EntryAdmin)
admin.site.register(Tag)
(Note, this code was written in a browser!)

Resources