Fragment id linking in wagtail's rich text content - wagtail

I have a bunch of content in a Wagtail 2.0 rich text field that looks like
Page heading
(intro blurb)
heading 1
(heading-1-relevant text)
heading 2
(heading-2-relevant text)
...
and I would like to give each heading an id so that any text can be made a link to jump to the relevant content. I can't seem to find an option to give headings an explicit id, and the "link" button in the rich text editor does not seem to let me pick active fragment identifiers in the content.
Is there a way to add fragment identifier based navigation on the same page work with Wagtail's rich text editor?

Revisiting my own question a year later because this is still something we need, the solution we came up with is to simply wrap the RichText html serialization, and putting fragment id injection on top:
import re
from django import template
from django.utils.text import slugify
from wagtail.core.rich_text import RichText
# We'll be wrapping the original RichText.__html__(), so make
# sure we have a reference to it that we can call.
__original__html__ = RichText.__html__
# This matches an h1/.../h6, using a regexp that is only
# guaranteed to work because we know that the source of
# the HTML code we'll be working with generates nice
# and predictable HTML code (and note the non-greedy
# "one or more" for the heading content).
heading_re = r"<h([1-6])([^>]*)>(.+?)</h\1>"
def add_id_attribute(match):
"""
This is a regexp replacement function that takes
in the above regex match results, and then turns:
<h1>some text</h1>
Into:
<h1><a id="some-text"></a>some text</h1>
where the id attribute value is generated by running
the heading text through Django's slugify() function.
"""
n = match.group(1)
attributes= match.group(2)
text_content = match.group(3)
id = slugify(text_content)
return f'<h{n}{attributes}><a id="{id}"></a>{text_content}</h{n}>'
def with_heading_ids(self):
"""
We don't actually change how RichText.__html__ works, we just replace
it with a function that does "whatever it already did", plus a
substitution pass that adds fragment ids and their associated link
elements to any headings that might be in the rich text content.
"""
html = __original__html__(self)
return re.sub(heading_re, add_id_attribute, html)
# Rebind the RichText's html serialization function such that
# the output is still entirely functional as far as wagtail
# can tell, except with headings enriched with fragment ids.
RichText.__html__ = with_heading_ids
This works rather well, does not require any hacking in draftail or wagtail, and is very easy to enable/disable simply by loading this code as part of the server startup process (we have it living in our wagtailcustom_tags.py file, so when Django loads up all template tag sets, the RichText "enrichment" kicks in automatically).
We had initially tried to extend the ... | richtext template filter, but while that's entirely possible, that only works for custom blocks we ourselves wrote, with our own custom templates, and so turned out to not be a solution given the idea that it should "just work".

To have control over the structure of your page body, it's preferable to encourage users to use heading blocks, rather than headings within the rich text block. Then you can have a heading block type which has two fields, a 'text' and an 'id', and you can specify a template that outputs the h element with the id attribute.
class Heading2Block(blocks.StructBlock):
heading = blocks.CharBlock(classname='full title')
link_id = blocks.CharBlock(help_text='For making hyperlinks to this heading')
class Meta:
template = 'blocks/h2.html'
Put the following in blocks/h2.html:
<h1{% if value.link_id %} id="{{ value.link_id|slugify }}"{% endif %}>{{ value.heading }}</h1>
In earlier versions of Wagtail it was possible to remove the h widget from the Hallo.js rich text editor, and this was a good way of encouraging user adoption of the heading block. Similar restriction is not currently present in Draftail, but there is a pull request which reimplements it.

Related

How to parse links (or any HTML tags) from external source (e.g. json) in re-useable pug template?

To my understanding, pug templtes are to be re-used. So I set up a collection for building forms and have partials like input.pug, textarea.pug, checkbox.pug, radio-buttons.pug`, etc.
In order to make sense of their reusability, they of course should show different text whenever they are used. Therefore I use - locals.msg = Some custom Text in the pug template that includes any of the form partials.
But as soon as it comes to provide more complex text that consists of links or contains styling elements (like b or a span tag with classes), the text is not parsed properly. This is what I have: a simple checkbox for GDPR acceptance that a user must click in a form. The explanation text is supposed to provide an external link to a page, where the user can read all about the site's data protection:
// parent pug template
div(class="[...]")
- var form = { id: 'for-gdpr', txt: 'I have read GDPR ...' }
include ./form/checkbox.pug
// checkbox pug template
label(class="flex items-baseline block w-full")
input(class="..." type="checkbox")
div(class="...")= form.txt
Of course this does not generate a proper link, instead that a-tag is "written out".
I also tried splitting text like this:
- var form = { msg1: 'I have read ', msg2: 'a(class="..." href="..." target="_blank" data protextion ', msg: ' and concur.'}
but to no avail.
How would I have to prepare the text in order to then parse it properly in a pug template?
Thank you!

How to put a button on List page for each row in django admin page

I want to add a button for each row as shown in the picture on the button place which shall give a popup of a field content of the same modal or whatever i insert for it to show.
This is actually, quite easy to do :) Although depending upon what you want the button to do, it might get a bit more complicated. You can have anything you want in your list view, just create a method that returns whatever you want to insert, and list it in list_display. For example:
class MyModelAdmin(admin.ModelAdmin):
list_display = (
"post_title",
...
"my_button_field",
)
def my_button_field(self, obj):
# obj is the particular instance of the object representing a particular row
return obj.property
In the case of a button, you'll want to use format_html. This will mark the relevant parts of the string safe, and so that it's displayed as an HTML button, and escape the other parts to protect against various securit issues.
It will look something like this:
from django.utils.html import format_html
class MyModelAdmin(admin.ModelAdmin):
list_display = (
"post_title",
...
"my_button_field",
)
def my_button_field(self, obj):
return format_html(
"<button onclick=`doSomething({})`>{}</button>",
obj.id,
obj.some_property
)
The values of object.id and object.some_property will get inserted into the placeholders.
Of course you'll need to add the javascript for doSomething, there are afew ways to do that, but you can find out more here. If what you want can be achieved by an anchor, it might be easier to use an <a> tag, and that way you won't have to bother with any additional javascrpt.

Append content of field to title field in Wagtail-CMS-admin

In order to differentiate pages in Wagtail CMS admin (in a page listing view, not in the edit page view), the title is - in my case - not enough. I have a long list of pages of one page-type (say class BlogPage), and some of these pages could have the same title. So I would like to add a second identifying field (here: date_from, a DateField) to this title.
I thought of
class BlogPage(Page):
...
def title(self):
if self.date_from:
return self.date_from + " - " + self.title
else:
return self.title
but this does not work, the page.title without my def is used for the corresponding Wagtail-admin-template.
Short version: How to pre/-append an existing field to the title in Wagtail-admin?
If you'd like to replace the title across all of the model admin (Yes, this includes the Edit page), Wagtail has a built in mechanism for that.
def get_admin_display_title(self):
return '{} - {}'.format(self.date_from, super().get_admin_display_title())
title is an actual Django model attribute, don't try to override it: https://docs.djangoproject.com/en/dev/topics/db/models/#field-name-hiding-is-not-permitted
The actual admin template from wagtailadmin/pages/list.html generates the list of pages in a loop using {% for page in pages %} and calls {{ page.title}} via an include (templates/wagtailadmin/pages/listing/_page_title_explore.html) extensively throughout. So based on inspecting the code, there is no support for this in Wagtail itself. Check the other includes templates/wagtailadmin/pages/listing/*.
However, Django supports overriding one app's templates with your own. You can copy this template into your project's template folder, keeping the same path (e.g., templates/wagtailadmin/pages/listing/_page_title_explore.html assuming your project is set up with a templates directory).
You'll have to replace the calls to page.title with your own version. A filter or tag might make this easier. Add the following filter to your templatetags (e.g. myapp/templatetetags/myapp_tags.py):
register = template.Library()
#register.filter
def uniquify_title(page):
specific_page = page.specific
try:
return specific_page.date_from.strftime("%Y-%m-%d") + " - " + specific_page.title
except AttributeError:
return specific_page.title
And then replace usages of {{ page.title}} in the template starting around line 7 and 9 with:
{{ page|uniquify_title }}
The downside of this is you have to update your own version of list.html every time Wagtail is updated. You could try submitting an issue on Github and proposing a fork which supplies an "admin_title" callable or something like that.
wagtailmodeladmin seems to do exactly what I've searched for: extending the wagtailadmin to display a defined set of fields - not only title - on a page model basis; like I would have it in the Django admin site.
This way I do not alter the default wagtailadmin page listing - like I've tried it in my question, but hook in an additional page listing as an extra wagtail-sidebar-navigation-entry.
Thanks to an other answer I stumbled upon this possiblity.

How can I convert a desktop site's navigation into a collapsable menu?

The header has a navigation menu I'd prefer to keep, but it's taking up too much space. What can I do to make it look good on mobile?
One possibility is to use togglers - buttons that make its inner content appear/disappear. The uraniumjs library contains some widgets, one of them being a very simple yet useful toggler implementation. It also does that unobtrusively.
You will need to include the uranium js file, so you can just use it. Then, you can do it as explained below.
You need to transform your menu code into three parts: a wrapper container, a "button" section and a content section. To identify each of those parts, use these data attributes:
data-ur-set="toggler"
(add this attribute to the wrapper)
data-ur-toggler-component="button"
(add this attribute to the "button" section)
data-ur-toggler-component="content"
(add this attribute to the content section)
You need to include these CSS rules somewhere too:
*[data-ur-set='toggler'] *[data-ur-toggler-component='content'] {
display:none;
}
*[data-ur-set='toggler'] *[data-ur-toggler-component='content'][data-ur-state='enabled'] {
display: block;
}
You can see a small example running here: http://play.tritium.io/8af1576e385f5db05f6dc75404f993c16485395b.
Both the Bloomingdales and the Macys mobile sites use that approach. You can see it working there.

Django-taggit: how to search for all tags based on another column or foreign key?

The django-taggit example shows how to get all tags for one specific model, and I know there is a way to get all tags in the system, but how do I get all tags based on a foreign-key?
I have tags for the Event model, and there is a primary-key/foreign-key relationship between the EventOrganizer and Event. Each EventOrganizer will have different set of tags, and when he/she logs in, I only want to show the tags that this organizer is concerned about.
Thanks!
Assuming your Event model looks something like this:
class Event(models.Model):
organizer = models.ForeignKey(EventOrganizer)
tags = TaggableManager(blank=True)
# ...
You can filter tags by event.organizer:
from taggit.models import Tag
tags = Tag.objects.filter(event__organizer=organizer)

Resources