Implementing one to many between an article and page models in Wagtail - python

I'm trying to setup a Wagtail site with an article to pages structure but I'm struggling. A review article for example may have an introduction page, a benchmark page and a conclusion page. I want to work out how to allow this relationship in wagtail and have it so that editors can add multiple pages to the same article on the same page. I can imagine the pages interface looking a bit like how you have content, promote and settings on pages but with the ability to add, rename and reorder pages. I've tried using a foreign key on a page model that links to an article but I can't get it to be shown in the admin the way I want.
Here is the django version of model layout I was looking to use. You have a parent article that is then made up of one or multiple pages. The pages should be editable, orderable and be created from within one panel in the admin with streamfields:
Class Article(models.Model)
STATE_DRAFT = 0
STATE_REVIEW= 1
STATE_PUBLICATION = 2
STATE_HIDDEN = 3
​
STATE = (
(STATE_DRAFT, 'draft'),
(STATE_REVIEW, 'pending review'),
(STATE_PUBLICATION, 'ready for publication'),
(STATE_HIDDEN, 'hide and ignore'),
)
title = models.CharField(_('title'), max_length=256)
slug = models.SlugField(
_('slug'), unique=True, blank=True, default='', max_length=256
)
description = models.TextField(
_('description'), max_length=256, blank=True, default=''
)
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='article'
)
publication = models.DateTimeField(
null=True, blank=True, default=None, db_index=True, help_text='''
What date and time should the article get published
'''
)
state = models.PositiveIntegerField(
default=0, choices=STATE, help_text='What stage is the article at?'
)
featured = models.BooleanField(
default=False,
help_text='Whether or not the article should get featured'
)
​
class Page(Page):
article = models.ForeignKey(
'Article', on_delete=models.CASCADE, related_name='pages'
)
title = models.CharField(max_length=256)
number = models.PositiveIntegerField(default=1) # So pages are ordered
body = models.TextField(blank=True)

As per my comment I don't think you'll be able to achieve everything you're looking for short of implementing an entirely bespoke CMS - but if you're able to bend the UI and data modelling requirements, then Wagtail's RoutablePageMixin is one possible way of achieving the general pattern of editing an article as a single unit, while presenting it as multiple pages on the front-end.
In this approach, you'd make Article a Wagtail Page model, with all of the sub-page content defined as fields (or InlinePanel child models) on that model. (If you want to split the content entry into tabs within the editing interface, see Customising the tabbed interface, although this won't support dynamically adding / reordering them.) You'd then define a URL route and template for each subpage of the article:
from wagtail.core.models import Page
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
class ArticlePage(RoutablePageMixin, Page):
intro = StreamField(...)
main_page = StreamField(...)
conclusion = StreamField(...)
#route(r'^$')
def intro_view(self, request):
render(request, 'article/intro.html', {
'page': self,
})
#route(r'^main/$')
def main_page_view(self, request):
render(request, 'article/main_page.html', {
'page': self,
})
#route(r'^conclusion/$')
def conclusion_view(self, request):
render(request, 'article/conclusion.html', {
'page': self,
})
In this example the three sub-pages are hard-coded, but with some more work (perhaps an InlinePanel child model with a slug field and a StreamField) you could make the subpages dynamic.

I saw gasman already provided an answer to you question, but I'm still going to write up an answer for two reasons:
I think you need some more pointers as to why gasmans' proposal is a better solution than yours, but it's way to much to write in a comment.
I have implemented a similar solution before, where there is a top level 'Article'-like object with multiple reorderable child objects, where the actual content resides.
Why you should make Article a Page subclass
You chose not to make Article a subclass of Page, and you said it was because the Article itself does not contain any content, only metadata about an article. That is not a very strange thought process, but I think you're looking at the wrong requirements for your Article model.
Let's look at Wagtail's own Page model. What kind of functionality does it provide out of the box?
It provides a tree structure with parent and child pages, so that your page can be placed somewhere in the hierarchy of your website
It provides a slug_field, so that Wagtail can automatically handle linking to your page.
It provides functionality for drafting, publishing and unpublishing.
Wagtail doesn't dictate anything about content, leaving you to decide what kind of content you want to put on your Page subclass, if any. Examples of Pages that do not have a body would be:
Contact forms.
Blog index pages.
Good questions you could ask when deciding whether you want a Model to be a subclass of a Page are:
Do I want this object to have it's own url?
Do I want to be able to place this object somewhere inside my website hierarchy?
Do I want to have SEO advantages for the object?
Do I want to be able to publish/unpublish this object or not?
In your case of the Article, you could say yes to almost al these question, so it'd be wise to make it a Page subclass. That way, you don't have to reinvent the wheel.
How you define the actual 'body' of your page is up to you.
You can place the actual content in either snippets, or subpages to that Article. Or you can even choose to create a list of StreamFields inside your model.
How to implement ordered subcontent.
I have implemented a structure like this before.
The way I did this was very similar to what gasman proposes.
In my case, I needed to create a website where you could find an object (like you article) and display different types of explanation modules for it. For each document, I created a ArticlePage, and for each explanation module, I created a snippet called ExplanationModule.
I then created a through model with an ordering, and added a RoutablePageMixin to the class like gasman explains.
The structure looks something like this:
#register_snippet
class ArticlePageModule(models.Model):
...
title = models.CharField(max_length=100)
body = StreamField(LAYOUT_STREAMBLOCKS, null=True, blank=True)
panels = [
FieldPanel('title'),
StreamFieldPanel('body'),
]
class ArticlePageModulePlacement(Orderable, models.Model):
page = ParentalKey('articles.ArticlePage', on_delete=models.CASCADE, related_name='article_module_placements')
article_module = models.ForeignKey(ArticlePageModule, on_delete=models.CASCADE, related_name='+')
slug = models.SlugField()
panels = [
FieldPanel('slug'),
SnippetChooserPanel('article_module'),
]
class ArticlePage(Page, RoutablePageMixin):
# Metadata and other member values
....
content_panels = [
...
InlinePanel('article_module_placements', label="Modules"),
]
#route(r'^module/(?P<slug>[\w\-]+)/$')
def page_with_module(self, request, slug=None):
self.article_module_slug = slug
return self.serve(request)
def get_context(self, request):
context = super().get_context(request)
if hasattr(self, 'article_module_slug'):
context['ArticlePageModule'] = self.article_module_placements.filter(slug = self.article_module).first().article_module
return context
What this does is the following:
Create a ArticlePageModule snippet, which is just some kind of content, like a title and a body.
Create a ArticlePageModulePlacement which links a ArticlePage to a module, and adds the following:
A slug
An Ordering (Because it subclasses the Orderable mixing)
Create a ArticlePage which does two things:
Define a ArticlePageModuleplacement panel, which allows you to add ArticlePageModulePlacements
Subclass RoutablePagemixin, as described in gasman's answer.
This provides you with a Wagtail-proof, reusable and robust way of creating Articles with SubContent.
The modules don't show up in tabs, but will be shown on the page's layout page under a panel called 'Modules'.

Related

User manually create model fields for post [django]

So I want to create a post by letting the user add a section/subtitle as they need it, but I have no idea how to go about creating this, I currently just have a title, preview and content block in a form but want the user to be able to create as many subtitle and content blocks as they want. Any help is very much appreciated :)
Well that's really basic database design question... What you describe is a "one to many" relationship (one "post" has many "subtitle-plus-content"), which is implemented using a table for the "subtitle-plus-content" records, with a foreign key on the "post" table (so we know which "subtitle-plus-content" records belong to which "post" record).
In Django, this is simply done by creatin a matching model:
class Post(models.Model):
title = models.CharField(max_lenght=xxx)
content = models.TextField()
# etc
class ContentBlock(models.Model):
""" A block with additionnal subtitle and content for posts """
post = models.ForeignKey(Post, on_delete=models.CASCADE)
subtitle = models.CharField(max_lenght=xxx)
content = models.TextField()
# etc
You'll find more documentation here

How To Separate Objects In Django Admin?

I have an app called 'Product' with the following models.py:
class Product(models.Model):
product_id = models.CharField(max_length=50)
pub_date = models.DateTimeField(default=datetime.now)
title = models.CharField(max_length=255)
price = models.DecimalField(max_digits=8, decimal_places=2)
user = models.ForeignKey(User, on_delete=models.CASCADE)
featured = models.BooleanField(default=False)
I want to have two separate sections in Django Admin: Products and Featured Products, depending if featured = True or False.
So by default all products are listed under the Products section. But if featured = True they will be moved to Featured Products section. Can you please help me how to do that? Thanks in advance.
Three steps:
Write a proxy model for model Product.
Change the default manager to only returns featured products.
Register your proxy model in the admin like any other model.
You can read more about it here: Using Proxy Models to Customize the Django Admin
There are a couple of ways to do this. The simplest perhaps is to create a database view, and then encapsulate it using a django model. You can create a view like so in your database console:
CREATE VIEW view_name AS
SELECT columns
FROM tables
[WHERE conditions];
Once you have done that, you can reference the view in django like so:
class FeaturedProduct(modes.Model):
attr1 = models.CharField()
class Meta:
managed = False
db_table = '<name of your view here>'
Make sure that managed is set to False. Here is the relevant documentation for that. You want to do that because django is not creating this model for you, but rather you are creating it yourself.
Another way to do this would be to create a custom Manager. These managers allow you to modify the objects attribute of your model, allowing you to set a queryset that you want. I think you'd want to take a look at the Manager documentation and you can take a look at defining custom querysets for your objects.

Django CMS - How to add multiple (zero or more) links on a model in admin

I'm fairly new to Django CMS, and I've inherited a website with these specs:
Django 1.10.8
Django CMS 3.4.5
Python 2.7
Unfortunately, there is no option to upgrade at the moment. Also, please pardon if I the terminology wrong - I'd appreciate corrections.
I have a Staff app with models for Staff and StaffTranslation (etc), and I have a task to update the admin to allow adding one or more links to each StaffTranslation. However, I'm having trouble identifying the best way to do this, and what components to use.
I have identified the djangocms-link plugin, which could be a possibility, but I can't find instructions on how to add multiple instances to a model or admin page. Also, the latest version requires Django 1.11, at minimum.
I have also found the PageSmartLinkField - which seems smart - but I haven't been able to discover how to integrate it with data for the StaffTranslation object or how to display it on the admin page for that object.
I would appreciate any suggestions as to how to structure this - it seems like a simple enough task, I think I am just missing a piece or two, of the puzzle.
The end purpose is to have zero-or-many links on a StaffTranslation (which has its own admin page), with the ability to add at least url and link text to each of them. It would be smart if there is some UI helper, allowing for easy selection of internal links, but a text field would be ok.
EDIT: the StaffTranslation model
class StaffTranslation(TimeStampedModel):
staff = models.ForeignKey(Staff, related_name='translations')
language = models.ForeignKey(Language)
specialist_work_title = HTMLField(max_length=128, null=True, blank=True) # SpecialistWorkTitle
description = HTMLField(null=True, blank=True) # PersonText
experience = HTMLField(null=True, blank=True) # Experience
phone_office = models.CharField(max_length=128, null=True, blank=True) # OfficePhone
phone_cell = models.CharField(max_length=128, null=True, blank=True) # CellPhone
mentions = HTMLField(null=True, blank=True) # Mentions
education = HTMLField(null=True, blank=True) # Education
other_qualifications = HTMLField(null=True, blank=True) # OtherQualifications
meta_title = models.TextField(null=True, blank=True) # MetaTitle
meta_keywords = models.TextField(null=True, blank=True) # MetaKeywords
meta_description = models.TextField(null=True, blank=True) # MetaDescription
legacy_data = models.TextField(null=True, blank=True)
objects = querysets.StaffTranslationQuerySet.as_manager()
# I would like to add something here like links = models.ForeignKey(Link, related_name='links') - so should I make a Link model and add as a many to many relation, and how do I integrate that into the admin?
I left out the functions defined on the model.
Again, it would be nice to upgrade, but that won't happen until the summer at the earliest, and this task has to be resolved now. :)
I think a solution for you here will be a placeholder field so you can add CMS plugins like any other plugin, for each instance of this model. For example;
from cms.models.fields import PlaceholderField
class StaffTranslation(TimeStampedModel):
...
content = PlaceholderField(
'staff_translation'
)
Then in your detail view template you do {% render_placeholder object.content %} and it'll show a placeholder for that object like it would for placeholders in any standard page template.
You can obviously then add any CMS plugins, links or otherwise, and the'll render alongside the other attributes on your StaffTranslation instance.
The docs on this are here; http://docs.django-cms.org/en/latest/how_to/placeholders.html

Loading tree of relations at once

I've got model called Post:
class Post(models.Model):
poster = models.ForeignKey(User)
content = models.TextField(verbose_name='Text', max_length=1000)
reply_to = models.ForeignKey('self', null=True, blank=True, default=None)
This allows to add 'first post' (with blank reply_to), and reply to post and even 'reply to reply'
For example I've got in my database something like that:
First Post
Reply one
Reply to reply one
Reply two
Reply to reply two
How to load that tree of replies?
When I use:
r = Post.objects.filter(reply_to=FirstPost)
It returns of course:
Reply one
Reply two
Is it possible to load all related posts at once?
I need it mainly to count all replies to first post.
You can use MPTT (http://django-mptt.github.com/django-mptt/tutorial.html#the-problem). I have not used this library before so let me know how it goes.
models.py
class Post(MPTTModel):
poster = models.ForeignKey(User)
content = models.TextField(verbose_name='Text', max_length=1000)
parent = models.TreeForeignKey('self', null=True, blank=True, related_name='children')
class MPTTMeta:
order_insertion_by = ['poster']
views.py
....
r = FirstPost.get_children()
No, I don't think there is a way to load all replies at once.
But, you can add extra metadata to the post type to be able to run a in-order-style query, where counting the number of replies becomes a simple calculation with data already loaded for the parents node.
See this article on how you could do that (it uses the MySQL SQL dialect, and PHP, but the principles still apply).
Basically, you add left and right fields to the nodes in your tree that define an ordering, letting you easily count the number of items below a given root element in the tree. It's like a Binary Tree in a database table. The principle is taken from this excellent database design book: "Joe Celko's Trees and Hierarchies in SQL for Smarties".

dynamic slug with permanent redirect or store slug on database

Which is the best option for urls on a news page:
Dynamically generate slugs. Load the page from the object id. If the slug doesn't match, permanent redirect to the correct slug.
myweb.com/542/my-news-item
Cons i see: If the news title is changed the slug changes, but the old slug will redirect to the new one, so i dont know if it is a problem for lookups.
OR:
Static slug that will never change, even if the news title is changed.
myweb.com/my-news-item
Cons i see: One more field on the db. If i change the news title radically, the slug will be very different
If you want slugs to update, without breaking the old urls, you could separate slugs and link the latest in your list views, something along these lines should do it:
class Article(models.Model):
title = models.CharField() # etc
class ArticleSlug(models.Model):
article = models.ForeignKey(Article)
slug = models.SlugField(unique=True)
date_created = models.DateTime(auto_now_add=True, editable=False)
class Meta:
get_latest_by = "date_created"
In your enlisting template you could simply call the latest slug, but you'll probably want to have some M/Y/D hierarchy in the urls aswell
{{ article.title }}
If your site gets a lot of traffic, you might want to add a celery task that retrieves the latest slug and copies it to a field on your article model now and then. That will save you some SQL JOINs.
How about a combination of using the redirects app and a post save signal like what this guy wrote.

Categories