Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More
publisher : render database content as styled html¶
The lino.modlib.publisher plugin adds the notion of content pages used to produce the pages of a website.
It also adds choicelists, model mixins, actions and a printing build method
(lino.modlib.printing.BuildMethods).
This page contains code snippets (lines starting with >>>), which are
being tested during our development workflow. The following
snippet initializes the demo project used throughout this page.
>>> from lino_book.projects.noi2.startup import *
>>> from django.db.models import Q
Content pages¶
- content page¶
The basic building unit of a website or book, consisting of a title and a body.
Locations¶
The locations setting specifies shortcuts for publishing a selection of
actors.
As a hosting provider or an application developer you
- lino.modlib.publisher.locations¶
A tuple of 2-tuples (loc, cls) where loc is a location string and cls a data table.
>>> pprint(dd.plugins.publisher.locations) (('b', lino_xl.lib.blogs.models.LatestEntries), ('p', lino.modlib.publisher.ui.Pages), ('r', lino.modlib.publisher.ui.RootPages), ('f', lino.modlib.uploads.ui.Uploads), ('books', lino_xl.lib.books.ui.Books), ('src', lino_xl.lib.sources.models.Sources), ('sng', lino_xl.lib.songs.ui.LatestSongs), ('e', lino_xl.lib.cal.ui.UpcomingEvents), ('t', lino_xl.lib.tickets.ui.Tickets), ('tp', lino_xl.lib.topics.models.Topics))
When setting this setting (usually in a
get_plugin_configs()method), the application developer should specify the data tables using their names. The above locations have been set inlino_cms.lib.cms.settingsas follows:yield ('publisher', 'locations', ( ('b', 'blogs.LatestEntries'), ('p', 'publisher.Pages'), ... ('t', 'tickets.Tickets'), ('tp', 'topics.Topics')))
Named pages¶
Named pages are pages with a user-given name in their Page name field. If this field is not empty, the page will have this name instead of its primary key in the url.
lino.core.actors.Actor.get_row_by_pk() uses a new class attribute
unique_keys, which defaults to ['id'], and on Pages it is
overridden to ['id', 'ref'].
Page overrides the Publishable.get_publisher_pk() method to
return self.page_name or str(self.pk).
Root pages¶
>>> ar = rt.login('robin')
>>> options = dict(column_names="id title", display_mode="grid")
>>> ar.show('publisher.RootPages', **options)
===== ===============
ID Title
----- ---------------
1 -Home
22 (footer)
40 Laundry
100 Flying Circus
===== ===============
Hiding the title of a page¶
>>> publisher.Page.hide_title_marker
'-'
>>> home = publisher.SpecialPages.home.get_object()
>>> home.title
'-Home'
>>> str(home)
'-Home'
>>> repr(home)
"Page #1 ('-Home')"
>>> home.as_str(ar)
'Home'
Multiple domains served by a same Lino sites¶
The noi2 site has a non-empty host2config and is a showcase of a Lino
site that serves multiple domains (Serve multiple domains with a single Lino site).
>>> ar.show(system.SiteConfigs)
==== =========
ID Name
---- ---------
1 Default
2 Circus
3 Laundry
==== =========
>>> ar.show(users.AllUsers)
========== ========= ===================== ============ ===========
Username Domain User type First name Last name
---------- --------- --------------------- ------------ -----------
andy Circus 100 (Customer) Andreas Anderson
bert Laundry 100 (Customer) Albert Bernstein
chloe Default 100 (Customer) Chloe Cleoment
jean Default 400 (Developer) Jean
luc Circus 400 (Developer) Luc
marc Laundry 100 (Customer) Marc
mathieu Default 200 (Contributor) Mathieu
robin 900 (Administrator) Robin Rood
roby 900 (Administrator) Roby Raza
rolf 900 (Administrator) Rolf Rompen
========== ========= ===================== ============ ===========
>>> rt.models_by_base(system.DomainSpecific)
[<class 'lino_noi.lib.groups.models.Group'>, <class
'lino.modlib.publisher.models.Page'>, <class
'lino.modlib.publisher.models.PageType'>, <class
'lino.modlib.users.models.User'>]
This means for example that in a noi, contacts are always shared among all tenants. That’s a decision of the application developer.
>>> ar = rt.login('andy')
>>> ar.get_user().my_domain
SiteConfig #2 ('Circus')
>>> ar.site_config_id
2
>>> ar.show('publisher.RootPages', **options)
===== ===============
ID Title
----- ---------------
1 -Home
22 (footer)
100 Flying Circus
===== ===============
>>> rt.login('bert').show('publisher.RootPages', **options)
==== ==========
ID Title
---- ----------
1 -Home
22 (footer)
40 Laundry
==== ==========
>>> rt.login('chloe').show('publisher.RootPages', **options)
==== ==========
ID Title
---- ----------
1 -Home
22 (footer)
==== ==========
Publishing workflow¶
>>> rt.show(publisher.PublishingStates, language="en")
======= =========== ========= ============= ========
value name text Button text public
------- ----------- --------- ------------- --------
10 draft Draft No
20 published Public Yes
30 revoked Revoked No
======= =========== ========= ============= ========
>>> rt.login('robin').show('publisher.PublicPages', display_mode="grid", language="en",
... column_names="id title language workflow_buttons")
...
===== ========================================= ========== ================================
ID Title Language Workflow
----- ----------------------------------------- ---------- --------------------------------
1 -Home en **Public** → [Draft] [Revoked]
4 About us en **Public** → [Draft] [Revoked]
7 Terms and conditions en **Public** → [Draft] [Revoked]
10 Privacy policy en **Public** → [Draft] [Revoked]
13 Cookie settings en **Public** → [Draft] [Revoked]
16 Copyright en **Public** → [Draft] [Revoked]
19 Root pages en **Public** → [Draft] [Revoked]
22 (footer) en **Public** → [Draft] [Revoked]
25 Contact en **Public** → [Draft] [Revoked]
28 Register en **Public** → [Draft] [Revoked]
31 Subscribe en **Public** → [Draft] [Revoked]
34 Blog en **Public** → [Draft] [Revoked]
37 Calendar en **Public** → [Draft] [Revoked]
40 Laundry en **Public** → [Draft] [Revoked]
41 Services en **Public** → [Draft] [Revoked]
42 Washing en **Public** → [Draft] [Revoked]
43 Drying en **Public** → [Draft] [Revoked]
44 Air drying en **Public** → [Draft] [Revoked]
45 Machine drying en **Public** → [Draft] [Revoked]
46 Drying technologies en **Public** → [Draft] [Revoked]
47 Types of drying cycles en **Public** → [Draft] [Revoked]
48 Benefits of drying clothes in a machine en **Public** → [Draft] [Revoked]
49 Energy saving en **Public** → [Draft] [Revoked]
50 Time saving en **Public** → [Draft] [Revoked]
51 Space saving en **Public** → [Draft] [Revoked]
52 Ironing en **Public** → [Draft] [Revoked]
53 Prices en **Draft** → [Public] [Revoked]
54 Photos en **Public** → [Draft] [Revoked]
55 About us en **Public** → [Draft] [Revoked]
56 Team en **Public** → [Draft] [Revoked]
57 History en **Draft** → [Public] [Revoked]
58 Contact en **Public** → [Draft] [Revoked]
59 Terms & conditions en **Draft** → [Public] [Revoked]
100 Flying Circus en **Public** → [Draft] [Revoked]
101 Places en **Public** → [Draft] [Revoked]
102 Programme en **Public** → [Draft] [Revoked]
103 Mission en **Public** → [Draft] [Revoked]
104 Blog en **Public** → [Draft] [Revoked]
105 Comments en **Public** → [Draft] [Revoked]
106 About us en **Public** → [Draft] [Revoked]
107 Team en **Public** → [Draft] [Revoked]
108 History en **Public** → [Draft] [Revoked]
109 Contact en **Public** → [Draft] [Revoked]
110 Terms & conditions en **Public** → [Draft] [Revoked]
133 FlyingCon en **Public** → [Draft] [Revoked]
134 Cascaded Continuous Voting en **Public** → [Draft] [Revoked]
135 Liquid democracy en **Public** → [Draft] [Revoked]
136 Digital vs analog en **Public** → [Draft] [Revoked]
137 Software should be free en **Public** → [Draft] [Revoked]
138 Synodality en **Public** → [Draft] [Revoked]
===== ========================================= ========== ================================
>>> rt.login('robin').show('publisher.DraftPages', display_mode="grid", language="en",
... column_names="id title language workflow_buttons")
...
==== ====================== ========== ================================
ID Title Language Workflow
---- ---------------------- ---------- --------------------------------
53 Prices en **Draft** → [Public] [Revoked]
57 History en **Draft** → [Public] [Revoked]
59 Terms & conditions en **Draft** → [Public] [Revoked]
73 Prices bn **Draft** → [Public] [Revoked]
77 History bn **Draft** → [Public] [Revoked]
79 Terms & conditions bn **Draft** → [Public] [Revoked]
93 Prices de **Draft** → [Public] [Revoked]
97 History de **Draft** → [Public] [Revoked]
99 Nutzungsbestimmungen de **Draft** → [Public] [Revoked]
==== ====================== ========== ================================
Translations¶
The English version of the home page has two translations, i.e. two other
Page: rows whose Page.translated_from points to the English
original.
>>> home = publisher.Page.objects.get(pk=1)
>>> home.language
'en'
>>> ar = rt.login('robin')
>>> ar.show('publisher.TranslationsByPage', master_instance=home)
`ⓘ <…>`__ `⏏ <…>`__ | (bn) `হোম <…>`__, (de) `Startseite <…>`__
>>> ar.show('publisher.TranslationsByPage', master_instance=home, nosummary=True)
============= ========== ====
Title Language ID
------------- ---------- ----
-হোম bn 2
-Startseite de 3
============= ========== ====
>>> publisher.Page.objects.get(pk=1).language
'en'
>>> publisher.Page.objects.get(pk=2).language
'bn'
>>> publisher.Page.objects.get(pk=3).language
'de'
When a request asks for a home page using its primary key or its page name, Lino does not redirect:
>>> home1 = beautiful_soup(test_client.get("/").content.decode())
>>> home2 = beautiful_soup(test_client.get("/p/1").content.decode())
>>> home1.title == home2.title
True
Memo references and translations¶
When the body of a named page in language 1 contains a memo command [ref page:2], then the end user wants to link to the specified page without redirecting back to the current language.
>>> sar = rt.login(renderer=dd.plugins.publisher.renderer, show_urls=True)
>>> home.memo2html(sar, None)
'<a href="/" style="text-decoration:none; white-space:nowrap;">Home</a>'
>>> publisher.Page.objects.get(pk=2).memo2html(sar, None)
'<a href="/?ul=bn" style="text-decoration:none; white-space:nowrap;">হোম</a>'
>>> with translation.override('de'):
... home.memo2html(sar, None)
'<a href="/?ul=de" style="text-decoration:none; white-space:nowrap;">Home</a>'
Redirections and translations¶
When a request asks for a page that is in another language than the requested language, then Lino redirects me to the corresponding page in the requested language.
>>> test_client.force_login(rt.login('chloe').user)
>>> test_client.get("/p/1?ul=de")
<HttpResponseRedirect status_code=302, "text/html; charset=utf-8", url="/?ul=de">
>>> test_client.get("/p/2?ul=de")
<HttpResponseRedirect status_code=302, "text/html; charset=utf-8", url="/?ul=de">
There is no redirect when asking with ul=de for a page that is already German:
>>> test_client.get("/p/3?ul=de")
<HttpResponse status_code=200, "text/html;charset="utf-8"">
Let’s define a function and use it do more testing. We fetch the page and then
use BeautifulSoup to extract the the <title> HTML tag, which most browsers
use as title of the window or tab that shows a page.
>>> def test(url):
... res = test_client.get(url)
... if res.status_code == 302:
... print(f"redirect to {res.url}")
... res = test_client.get(res.url)
... soup = beautiful_soup(res.content)
... if not soup.title:
... raise Exception(f"No title tag in {soup}")
... print(soup.title.text)
... for a in soup.find_all('a', {"class": "l-lngsel"}):
... print(f"{a.text} -> {a['href']}")
>>> test("/p/1?ul=de")
redirect to /?ul=de
noi2
en -> /?ul=en
bn -> /?ul=bn
Yes, page #1 is the Home page, whose title is hidden. And a hidden title is
also hidden in the <title> HTML tag. So let’s use another named page for the
following tests.
>>> test("/p/4?ul=de")
redirect to /p/about?ul=de
Über uns – noi2
en -> /p/about?ul=en
bn -> /p/about?ul=bn
>>> test("/p/5?ul=de")
redirect to /p/about?ul=de
Über uns – noi2
en -> /p/about?ul=en
bn -> /p/about?ul=bn
When using the Page.page_name of a page instead of its primary
key, there is no redirect because Lino will look up the page directly in the
requested language:
>>> test("/p/terms")
Terms and conditions – noi2
bn -> /p/terms?ul=bn
de -> /p/terms?ul=de
‘None’ has no attribute ‘get_html_page_title’¶
The following requests caused an internal server error until 20260525:
>>> test("/b")
Latest blog entries – noi2
en -> /b?ul=en
bn -> /b?ul=bn
de -> /b?ul=de
>>> test("/e")
Upcoming events – noi2
en -> /e?ul=en
bn -> /e?ul=bn
de -> /e?ul=de
Previous and next page¶
The previous_page fields have been updated:
TODO: run the following test in another demo project where we don’t have side effects caused by Bengali language.
>>> rt.login('robin').show('publisher.Pages', display_mode="grid", language="en",
... column_names="id title language root_page previous_page")
...
===== ================================= ========== =============== =================================
ID Title Language Root page Previous page
----- --------------------------------- ---------- --------------- ---------------------------------
1 Home en
2 হোম bn
3 Startseite de
4 About us en Home Home
5 আমাদের সম্পর্কে bn হোম হোম
6 Über uns de Startseite Startseite
7 Terms and conditions en Home About us
8 শর্তাবলী bn হোম আমাদের সম্পর্কে
9 Allgemeine Geschäftsbedingungen de Startseite Über uns
10 Privacy policy en Home Terms and conditions
11 গোপনীয়তা নীতি bn হোম শর্তাবলী
12 Datenschutz de Startseite Allgemeine Geschäftsbedingungen
13 Cookie settings en Home Privacy policy
14 কুকি সেটিংস bn হোম গোপনীয়তা নীতি
15 Cookie settings de Startseite Datenschutz
16 Copyright en Home Cookie settings
17 কপিরাইট bn হোম কুকি সেটিংস
18 Autorenrecht de Startseite Cookie settings
19 Root pages en Home Copyright
20 মূল পৃষ্ঠাসমূহ bn হোম কপিরাইট
21 Root pages de Startseite Autorenrecht
22 (footer) en
23 (footer) bn
24 (footer) de
25 Contact en Home Root pages
26 যোগাযোগ bn হোম মূল পৃষ্ঠাসমূহ
27 Kontakt de Startseite Root pages
28 Register en Home Contact
29 নিবন্ধন করুন bn হোম যোগাযোগ
30 Registrieren de Startseite Kontakt
31 Subscribe en Home Register
32 Subscribe bn হোম নিবন্ধন করুন
33 Subscribe de Startseite Registrieren
34 Blog en Home Subscribe
35 Blog bn হোম Subscribe
36 Blog de Startseite Subscribe
37 Calendar en Home Blog
38 ক্যালেন্ডার bn হোম Blog
39 Kalender de Startseite Blog
40 Laundry en
41 Services en Laundry Laundry
42 Washing en Laundry Services
43 Drying en Laundry Washing
44 Air drying en Laundry Drying
45 Machine drying en Laundry Air drying
46 Drying foos en Laundry Machine drying
47 Drying bars en Laundry Drying foos
48 Drying bazes en Laundry Drying bars
49 Ironing en Laundry Drying bazes
50 Prices en Laundry Ironing
51 Photos en Laundry Prices
52 About us en Laundry Photos
53 Team en Laundry About us
54 History en Laundry Team
55 Contact en Laundry History
56 Terms & conditions en Laundry Contact
57 Laundry bn
58 পরিষেবা bn Laundry Laundry
59 Washing bn Laundry পরিষেবা
60 Drying bn Laundry Washing
61 Air drying bn Laundry Drying
62 Machine drying bn Laundry Air drying
63 Drying foos bn Laundry Machine drying
64 Drying bars bn Laundry Drying foos
65 Drying bazes bn Laundry Drying bars
66 Ironing bn Laundry Drying bazes
67 Prices bn Laundry Ironing
68 Photos bn Laundry Prices
69 আমাদের সম্পর্কে bn Laundry Photos
70 Team bn Laundry আমাদের সম্পর্কে
71 History bn Laundry Team
72 যোগাযোগ bn Laundry History
73 Terms & conditions bn Laundry যোগাযোগ
74 Laundry de
75 Dienstleistungen de Laundry Laundry
76 Washing de Laundry Dienstleistungen
77 Drying de Laundry Washing
78 Air drying de Laundry Drying
79 Machine drying de Laundry Air drying
80 Drying foos de Laundry Machine drying
81 Drying bars de Laundry Drying foos
82 Drying bazes de Laundry Drying bars
83 Ironing de Laundry Drying bazes
84 Prices de Laundry Ironing
85 Photos de Laundry Prices
86 Über uns de Laundry Photos
87 Team de Laundry Über uns
88 History de Laundry Team
89 Kontakt de Laundry History
90 Nutzungsbestimmungen de Laundry Kontakt
91 Flying Circus en
92 Places en Flying Circus Flying Circus
93 Programme en Flying Circus Places
94 Mission en Flying Circus Programme
95 Blog en Flying Circus Mission
96 Comments en Flying Circus Blog
97 About us en Flying Circus Comments
98 Team en Flying Circus About us
99 History en Flying Circus Team
100 Contact en Flying Circus History
101 Terms & conditions en Flying Circus Contact
102 Flying Circus bn
103 স্থান bn Flying Circus Flying Circus
104 Programme bn Flying Circus স্থান
105 Mission bn Flying Circus Programme
106 Blog bn Flying Circus Mission
107 মন্তব্য bn Flying Circus Blog
108 আমাদের সম্পর্কে bn Flying Circus মন্তব্য
109 Team bn Flying Circus আমাদের সম্পর্কে
110 History bn Flying Circus Team
111 যোগাযোগ bn Flying Circus History
112 Terms & conditions bn Flying Circus যোগাযোগ
113 Flying Circus de
114 Orte de Flying Circus Flying Circus
115 Programme de Flying Circus Orte
116 Mission de Flying Circus Programme
117 Blog de Flying Circus Mission
118 Kommentare de Flying Circus Blog
119 Über uns de Flying Circus Kommentare
120 Team de Flying Circus Über uns
121 History de Flying Circus Team
122 Kontakt de Flying Circus History
123 Nutzungsbestimmungen de Flying Circus Kontakt
124 FlyingCon en Home Calendar
125 Cascaded Continuous Voting en Home FlyingCon
126 Liquid democracy en Home Cascaded Continuous Voting
127 Digital vs analog en Home Liquid democracy
128 Software should be free en Home Digital vs analog
129 Synodality en Home Software should be free
===== ================================= ========== =============== =================================
Classes reference¶
- class lino.modlib.publisher.Page¶
The Django model that represents a content page.
Inherits from
PublishableContent,TranslatableContent.- title¶
- body¶
- language¶
- parent¶
- seqno¶
- root_page¶
The page that defines the top of the menu tree containing this page.
- special_page¶
If this page implements a “special page”, then this field points to a choice in
SpecialPages.
- pub_date¶
- pub_time¶
- publishing_state¶
See
PublishingStates
- class lino.modlib.publisher.Publishable¶
Model mixin to add to models that are potentially publishable.
- publisher_template¶
The name of the template to use when rendering a database row via the publisher interface.
“publisher/page.pub.html”
- preview_publication¶
Show this database row via the publisher interface.
Icon: 🌐
- class lino.modlib.publisher.PublishableContent¶
Model mixin to add to models that are potentially publishable.
Inherits from
Publishable.- language¶
The language of this content.
- publishing_state¶
Default value is ‘draft’
Pointer to
PublishingStates
- filler¶
Pointer to
PageFillers
- class lino.modlib.publisher.PublisherBuildMethod¶
This deserves better documentation. Maybe useless.
- class lino.modlib.publisher.PublishingStates¶
A choicelist with the possible states of a publisher page.
- class lino.modlib.publisher.SpecialPages¶
A choicelist with the special pages available on this site.
>>> rt.show(publisher.SpecialPages, language="en") =========== ====================== ====================================== name text Pages ----------- ---------------------- -------------------------------------- home Home `en <…>`__ | `bn <…>`__ | `de <…>`__ about About us `en <…>`__ | `bn <…>`__ | `de <…>`__ terms Terms and conditions `en <…>`__ | `bn <…>`__ | `de <…>`__ privacy Privacy policy `en <…>`__ | `bn <…>`__ | `de <…>`__ cookies Cookie settings `en <…>`__ | `bn <…>`__ | `de <…>`__ copyright Copyright `en <…>`__ | `bn <…>`__ | `de <…>`__ roots roots `en <…>`__ | `bn <…>`__ | `de <…>`__ footer footer `en <…>`__ | `bn <…>`__ | `de <…>`__ contact Contact `en <…>`__ | `bn <…>`__ | `de <…>`__ register Register `en <…>`__ | `bn <…>`__ | `de <…>`__ subscribe Subscribe `en <…>`__ | `bn <…>`__ | `de <…>`__ blog blog `en <…>`__ | `bn <…>`__ | `de <…>`__ events events `en <…>`__ | `bn <…>`__ | `de <…>`__ =========== ====================== ======================================
Note that the name of the SpecialPage is not necessarily the page_name
Skins¶
- lino.modlib.publisher.skin¶
Which skin to use. Default value is ‘boots’.
Currently the only alternative to ‘boots’ is ‘silly’, which is there just to show an alternative. You can see it by activating a line in the
noi2demo projectsettings.pyfile.
- lino.modlib.publisher.use_markup¶
(Deprecated)
Whether to use markup (instead of wysiwyg) for editing content.
When this is False, the body of pages gets edited using a wysiwyg editor and stored as (bleached) html.