Single piece of content, multiple URLs? - wagtail

I have a use case that I could use some advice on.
We publish multiple products, each of which has it's own subtree on the site. Generally, a piece of content gets published to just a single product, e.g. a news article gets published to product A and can be accessed at one URL.
However, sometimes we have content that we want to publish to multiple products, e.g. a single news article gets published to products A, B, and C and will be available at 3 different URLs.
With our current CMS we end up doing this by copying and pasting the content, which is a hassle for editors, especially if the content needs to be updated.
An ideal scenario would be where and editor edits the content in one place, specifies the products to publish to, and the content is served by more than one URL and with a template that is product-specific.
It seems that RoutablePageMixin could be useful here, but I'm not sure how to handle letting the editor specify the destination products and making the routing aware of that choice.
Has anyone solved a similar problem using Wagtail?

I have solved a similar problem in Wagtail, the RoutablePageMixin is the key to solving this problem.
If you have /blog/A/slug-product/, /blog/B/slug-product/, /blog/C/slug-product/ , then you can get the slug value slug-product here, then use this value to search the distinct content in your db.
class BlogPage(RoutablePageMixin, Page):
def get_posts(self):
return PostPage.objects.descendant_of(self).live()
#route(r'^(\d{4})/(\d{2})/(\d{2})/(.+)/$')
def post_by_date_slug(self, request, year, month, day, slug, *args, **kwargs):
post_page = self.get_posts().filter(slug=slug).first()
return Page.serve(post_page, request, *args, **kwargs)
As you can see, I did not use the date info of the url but the slug value to get the blog post object, you can follow the pattern here to use regex to match the url you want.
If the slug values in urls are also different, this solution might not work very well, but in most cases, this solution can work fine.
I have written a blog post talking about how to use RoutablePageMixin to make the page routable, you can check this link if you wang to get more about RoutablePageMixin.
Routable Page

Rather than thinking of your news articles as being child objects of one or more products, it might help to think of them as one big pool of news articles which are categorised by product. Your product page will then effectively be a filtered index page of news articles.
Here's how I'd model it:
If you want your news articles to exist at a canonical URL that's independent of any particular category, or you want to make use of page moderation and/or previewing, then define NewsArticle as a page model; otherwise, define it as a snippet or a ModelAdmin-managed model.
On the NewsArticle model, have an InlinePanel where editors can associate as many related products as required:
class NewsArticle(Page):
body = RichTextField()
date = models.DateField()
content_panels = Page.content_panels + [
FieldPanel('body'),
FieldPanel('date'),
InlinePanel('related_products', label="Related products"),
]
class NewsArticleRelatedProduct(Orderable):
news_article = ParentalKey(NewsArticle, related_name='related_products')
product = models.ForeignKey(ProductPage, on_delete=models.CASCADE, related_name='news_articles')
panels = [
PageChooserPanel('product'),
]
On your ProductPage model, add a method that returns a queryset of news items, filtered and sorted appropriately:
class ProductPage(Page):
# ...
def get_news_articles(self):
return self.news_articles.live().order_by('-date')
You can then loop over the news articles in your product page template, using a tag like {% for news_article in page.get_news_articles %}.

Related

Is there a way to show images in a Wagtail Model Admin Record Listing page?

I have reviewed the question on Is there any way to show a field on a listing page in Wagtail admin? but my situation seems to similar but also different enough that that particular solution won't work for me. Instead of on the Page listing I wish to achieve a similar thing on the Model Admin listing and I would think this should be such a common requirement that I am picking that someone must have done this before I have attempted it.
I haven't really figured out how to even try anything to get started but what I have looked at is the modeladmin template tags under wagtail.contrib.modeladmin on GitHub but I am completely guessing.
Can anyone point me to which templates I need to modify and whether I need to modify any of the template tags and how to override anything I need to override?
There's no need to override templates for this - this is standard functionality in ModelAdmin. Adding extra fields to the listing is done by setting list_display on the ModelAdmin class:
class BookAdmin(ModelAdmin):
model = Book
list_display = ('title', 'author')
For displaying images, ModelAdmin provides ThumbnailMixin:
from wagtail.contrib.modeladmin.mixins import ThumbnailMixin
from wagtail.contrib.modeladmin.options import ModelAdmin
class BookAdmin(ThumbnailMixin, ModelAdmin):
model = Book
thumb_image_field_name = 'cover_image'
list_display = ('title', 'author', 'admin_thumb')
('admin_thumb' is a special-purpose field name provided by ThumbnailMixin, and should be used rather than the actual image field on your model - cover_image in this example.)

Template and model reuse in Wagtail

I am building a fairly basic Wagtail site and have run into an issue regarding the reuse of models and templates.
Say my site has two kinds of entries:
blog posts and
events.
Both pages look the same and share many model fields (e.g., author, category, intro, etc.). However, there are some model fields that only make sense for the event entry type (e.g., event_date, event_venue).
What would be the ideal way of creating templates and models for this use-case without repeating myself in the code?
Right now, both blog and event entries use the same HTML template and the same model. However, when the user creates a blog post in the Wagtail admin, he or she has to "ignore" the event-specific fields (which may become even more in the future).
Do I have to create two separate template files and two separate models despite both blogs and events being 95% the same code? What would be the correct way to solve this in Wagtail?
If you want to maintain it the way it is, contained within one model and template, you could create separate model admins for each pseudo-type (Blogs and Events), and override the queryset function to make each separate modeladmin only show the ones you're looking for, and then edit the panels that are shown on create/edit/delete.
class EventAdmin(ModelAdmin):
...
panels = [
FieldPanel('your_field'),
...
]
def get_queryset(self, request):
qs = super().get_queryset(request)
events = qs.filter(your_field__isnull=False)
return events
More information at https://docs.wagtail.io/en/stable/reference/contrib/modeladmin/index.html

Dynamic queryset for foreignKey in Single Page Application with Django

I have a single page application with AngularJs and Django. On my main page, I get all the forms needed when loading the page. BUT, some fields are dynamically updated.
Let's say I have
class Model1(models.Model):
pass
class Model2(models.Model):
model_1 = models.ForeignKey(Model1)
forms:
class Model2Form(forms.ModelForm):
class Meta:
model = Model2
fields = ('model_1', )
My SPA allows me to create instances of Model1 (without reloading the page). I know how to filter the options shown and dynamically add the new instances in the select field BUT, doing so, when the html is first rendered, before angular magic takes place and filter the available options, I get the queryset made by django which is by default model.objects.all(). All right, I'd like to display none of that. I tried to add in the init of my function:
self.fields['model_1'].queryset = Model1.objects.none()
and indeed no option is displayed in the select field when the form is first rendered but then, I can't validate my form, I get the error: Select a valid choice. That choice is not one of the available choices. (obviously, it had no option available due to the queryset.none() )
I'd really like not to load forms when called but doing it when my page first load. Is there any option to help me do so?
Cheers guyz,
Keep rocking
You need to specify that the model_1 field of Model2 can be null, as specified here:
Allow null in foreign key to user. Django
model_1 = models.ForeignKey(Model1, null=True, blank=True, default = None)
I find out how to handle that problem. It is quite stupid, I did not give you all the parameters of the problem.
The forms are rendered on load but when I validate it, it goes through a CRUD operation and an OTHER form is initialized at this point which will handle the data I'm sending. So I can override the queryset in the init of that (second) form based on some extra kwargs to differentiate between the form I'm using for the first rendering and the form to handle my data.
No need to make any field nullable or add extra validation.
Hope I'm clear enough. Cheers

How to iterate through an attribute of a single element Grails-Gsp with the <g:each> tag

I´m working in a blog with grails, the thing is I´ve created a domain class named Post, where I defined as attributes String content, Date date, String title and since a post can have multiple comments, I also created a domain class "Comment" with: String author, File avatar, String content, Date commentDate; so I declared a one to many relationship as follows: static hasmany = [statements: Comment] in the Post domain class. Then in the blog.gsp I want to display a single post with all of it´s comments so I´m trying to use the < g:each > tag with a post as a variable, the idea is this tag to iterate through the comments list of this single post, not through all of the posts. How to achieve this?.
I'm going to use "standard" Grails variable names to avoid confusion.
If your controller sends back a Post object you can iterate through like this:
//PostController.groovy
def blog() {
def postInstance = Post.read(params.id)
[postInstance: postInstance]
}
//blog.gsp
${postInstance.title} //just to make sure your postInstance is correctly populated
<g:each in="${postInstance?.statements}" var="commentInstance">
${commentInstance.content}
<g:each>
This should work whether there are 1 or 1000 statements.
Also make sure it is
//Post.groovy
static hasMany = [statements: Comment]
You might want to have the Comment belong to the Post
//Comment.groovy
static belongsTo = [post : Post]
This makes it a bidirectional relationship.
If you are using auto binding features in Grails, make sure that the naming and the hierarchy in your classes it matching to the HTML.
debug is your best friend in this case, on the action at your server, print out the received request data.
Another note, when dealing with auto binder also, sometimes, even if the datatype in your class is defined as a list, if one element in that list is retrieved from the client side, you will notice that grails will not consider it as list.
Example,
referring to your design,
Post
{
hasmany = [statements: Comment]
}
If one comment found in this post, statements will be of type Comment, not Comment[]
I faced this many times, maybe it is something related to my grails version i used, but it worth checking, again debug is your friend in such cases

Django model defining list of URLFields

I'm pretty new to relational databases and this may be why I'm having this problem but I have a model - Post.
I want it to have variable number of URLs, however Django only seems to have the OneToManyField which requires a model (not a field - which URLField is).
In relational database design, the fields in a table are always scalar values. in your case, such a field would 'a url'. The way you get a collection to apply to a row, you join that row with the rows of another table. In django parlance, that would mean that you need two models, one for the Post objects, and another that links multiple urls with that post.
class Post(models.Model):
pass
class Url(models.Model):
url = models.URLField()
post = models.ForeignKey(Post)
myPost = Post.objects.all().get()
for url in myPost.url_set.all():
doSomething(url.url)
Now you can access urls through a urls member
But if you want to get the admin page for Post to also let you add urls, you need to do some tricks with InlineModelAdmin.
from django.db import models
from django.contrib import admin
class Post(models.Model):
pass
class Url(models.Model):
url = models.URLField()
post = models.ForeignKey(Post)
class UrlAdmin(admin.TabularInline):
model = Url
class PostAdmin(admin.ModelAdmin):
inlines = [UrlAdmin]
admin.site.register(Post, PostAdmin)

Resources