How do I configure a mosaic layout programmatically?

I needed to create a mosaic layout by hand, export it into my package so I can apply it at will.
I want to share with you how I did that to find out if I did something wrong or if I could have done that easier/better.

Goals:

  • Create a frontpage with mosaic programatically with a upgrade-step.
  • The layout must be editable.
  • The layout consist of two rows each with some simple richtext tiles and some contentlisting tiles.

After I created the page and made the custom layout I inspected the object with pdb. I saw two attributes which seem to hold the required information:

customContentLayout and content. According to the behavior ILayoutAware the first is 'Custom content and content layout of this page' and content is 'Transient tile configurations and data for this page'.

So I stored these values:

MOSAIC_FRONTPAGE_CONTENT = """<!DOCTYPE html>
<html lang="en" data-layout="./@@page-site-layout">
<body data-panel="content">
    <div data-tile="@@plone.app.standardtiles.html/8db0d793-eac3-4576-ba8b-d8c106675794">
        <div>
            <h2>Links</h2>
            <p>
                <a href="/Plone/resolveuid/7067534cf4c54a49b570cee6ff56af52" data-linktype="internal" data-val="7067534cf4c54a49b570cee6ff56af52" data-mce-href="../resolveuid/7067534cf4c54a49b570cee6ff56af52">Foo</a>
            </p>
        </div>
    </div>
    <div data-tile="@@plone.app.standardtiles.html/a558f9ab-6d4d-454e-9458-3b85c31bddfa">
        <div>
            <h2>FAQ</h2>
            <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
        </div>
    </div>
    <div data-tile="@@plone.app.standardtiles.contentlisting/210ab0406fda4dab93458c040115c5ac" data-tiledata='{"title": null, "description": "", "query": [{"i": "portal_type", "o": "plone.app.querystring.operation.selection.any", "v": ["News Item"]}], "sort_on": "effective", "sort_reversed": true, "limit": 4, "view_template": "summary_view"}'>
    </div>
    <div data-tile="@@plone.app.standardtiles.contentlisting/3342cfd463aa469aa00f257db8bf34de" data-tiledata='{"title": "Termine", "description": "", "query": [{"i": "portal_type", "o": "plone.app.querystring.operation.selection.any", "v": ["Event"]}, {"i": "review_state", "o": "plone.app.querystring.operation.selection.any", "v": ["internally_published", "published"]}], "sort_on": "start", "sort_reversed": true, "limit": 3, "view_template": "summary_view"}'>
    </div>
</body>
</html>"""

MOSAIC_FRONTPAGE_LAYOUT = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" data-layout="./@@page-site-layout">
<body>
    <div data-panel="content" data-max-columns="4">
        <div class="mosaic-grid-row">
            <div class="mosaic-grid-cell mosaic-width-full mosaic-position-leftmost">
                <div class="movable removable mosaic-tile mosaic-IDublinCore-title-tile">
                    <div class="mosaic-tile-content">
                        <div data-tile="./@@plone.app.standardtiles.field?field=IDublinCore-title">
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="mosaic-grid-row">
            <div class="mosaic-grid-cell mosaic-width-full mosaic-position-leftmost">
                <div class="movable removable mosaic-tile mosaic-IDublinCore-description-tile">
                    <div class="mosaic-tile-content">
                        <div data-tile="./@@plone.app.standardtiles.field?field=IDublinCore-description">
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="mosaic-grid-row top-row">
            <div class="mosaic-grid-cell mosaic-position-leftmost mosaic-width-quarter">
                <div class="movable removable mosaic-tile mosaic-plone.app.standardtiles.html-tile">
                    <div class="mosaic-tile-content">
                        <div data-tile="./@@plone.app.standardtiles.html/8db0d793-eac3-4576-ba8b-d8c106675794">
                        </div>
                    </div>
                </div>
            </div>
            <div class="mosaic-grid-cell mosaic-position-quarter mosaic-width-half">
                <div class="movable removable mosaic-tile mosaic-plone.app.standardtiles.contentlisting-tile">
                    <div class="mosaic-tile-content">
                        <div data-tile="./@@plone.app.standardtiles.contentlisting/210ab0406fda4dab93458c040115c5ac">
                        </div>
                    </div>
                </div>
            </div>
            <div class="mosaic-grid-cell mosaic-position-three-quarters mosaic-width-quarter">
                <div class="movable removable mosaic-tile mosaic-plone.app.standardtiles.contentlisting-tile">
                    <div class="mosaic-tile-content">
                        <div data-tile="./@@plone.app.standardtiles.contentlisting/3342cfd463aa469aa00f257db8bf34de">
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="mosaic-grid-row bottom-row">
            <div class="mosaic-grid-cell mosaic-width-full mosaic-position-leftmost">
                <div class="movable removable mosaic-tile mosaic-plone.app.standardtiles.html-tile">
                    <div class="mosaic-tile-content">
                        <div data-tile="./@@plone.app.standardtiles.html/a558f9ab-6d4d-454e-9458-3b85c31bddfa">
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>"""

This configures four tiles in a simple two-row-layout:

  • plone.app.standardtiles.html/8db0d793-eac3-4576-ba8b-d8c106675794
  • plone.app.standardtiles.contentlisting/210ab0406fda4dab93458c040115c5ac
  • plone.app.standardtiles.contentlisting/3342cfd463aa469aa00f257db8bf34de
  • plone.app.standardtiles.html/a558f9ab-6d4d-454e-9458-3b85c31bddfa

Note that the id of the tile in the references the id of a tile in the content.

I then wrote the upgrade-step. The following code then sets the content and then the layout:

frontpage.setLayout('layout_view')
ILayoutAware(frontpage).content = MOSAIC_FRONTPAGE_CONTENT
ILayoutAware(frontpage).customContentLayout = MOSAIC_FRONTPAGE_LAYOUT

Together they replicate the layout I configured by hand in the beginning. It seems that at least theoretically content and layout might be able to be stored together in the attribute customContentLayout.

I wa able to store the html as url-encoded wit the query-string content like this: <div data-tile="./@@plone.app.standardtiles.html/8db0d793-eac3-4576-ba8b-d8c106675794?content=%3Cp%3Efoo%3C%2Fp%3E"></div> (which is <p>foo</p>).

That does not seem to work with contentlisting though. I did not find any documentation about that so it might be either a nonexistent feature or a bug. The same is true when I store a manually created layout and reapply it wo new content: Richtext is kept but contentlisting configuration is lost.

The layout can be made reuseable by other content on the site when it is stored as a html-file in a folder layouts and registerd in zcml:

<plone:static
    directory="layouts"
    name="mylayouts"
    type="contentlayout"
    /> 

I can then set this layout with

ILayoutAware(frontpage).contentLayout = '++contentlayout++mylayouts/frontpage.html' 

At this point I had everything I needed for my project but some questions remained in my mind:

  1. How do I inspect the tile-configuration? I saw I can use LayoutAwareTileDataStorage:

     (Pdb++) from plone.app.blocks.layoutbehavior import LayoutAwareTileDataStorage
     (Pdb++) storage = LayoutAwareTileDataStorage(self.context, self.request)
     (Pdb++) pp storage['plone.app.standardtiles.contentlisting/210ab0406fda4dab93458c040115c5ac']
     {'description': '',
      'limit': 2,
      'query': [{'i': 'portal_type', 'o': 'plone.app.querystring.operation.selection.any', 'v': ['News Item']}],
      'sort_on': 'effective',
      'sort_reversed': True,
      'title': None,
      'view_template': 'summary_view'}
    

    But I can also traverse to the tiles:

     (Pdb++) tile = self.context.restrictedTraverse('@@plone.app.standardtiles.contentlisting/210ab0406fda4dab93458c040115c5ac')
     (Pdb++) tile
     <Products.Five.browser.metaconfigure.ContentListingTile object at 0x11373b750>
     (Pdb++) tile.data
     {'title': None, 'description': '', 'query': [{'i': 'portal_type', 'o': 'plone.app.querystring.operation.selection.any', 'v': ['News Item']}], 'sort_on': 'effective', 'sort_reversed': True, 'limit': 2, 'view_template': 'summary_view'}
    
  2. Which is the right way to modify existing tiles programatically so that the changes are actually stored?

  3. How do I create and add new tiles in python to a content object?

  4. Can I safely change the tile-id to something more readable?

1 Like

I did this the other day for a customer project (2 column homepage layout with banner on top, custom themefragments and persistent tile from plone.app.standardtiles):

First, in the template you use you have UUIDs. They will change. Use a custom ID for the tiles like this (they will later be used when setting the content for persistent tiles):

<div data-tile="./@@collective.themefragments.fragment/autogenerated_slideshow_001?fragment=slideshow&amp;banner=UID_BANNER"></div>
<div data-tile="./@@collective.themefragments.fragment/autogenerated_content_001?fragment=content_link&amp;content_item=UID_LINK_1_1&amp;show_summary=1"></div>
<div data-tile="./@@plone.app.standardtiles.html/autogenerated_html_001"></div>
<div data-tile="./@@plone.app.standardtiles.contentlisting/autogenerated_content_listing_001"></div>

In this example they are all called autogenerated_type_xx. The name you choose doesn't matter, as long as it does not start with an _, which will raise a NotFound on traversal.
Next, add your custom layout to the content:

def set_homepage_layout(self, obj, config):
    """Set homepage with mosaic layout."""
    if obj.getLayout() == 'layout_view':
        return

    obj.setLayout('layout_view')
    layout = ILayoutAware(obj)
    layout_path = os.path.join(
        os.path.dirname(my.package.__file__),
        'layouts',
        'content',
        'homepage.html',
    )
    custom_layout = open(layout_path, 'r').read()
    custom_layout = custom_layout \
        .replace('UID_BANNER', config.get('uid_banner', '')) \
        .replace('UID_LINK_1_1', config.get('uid_link_1_1', '')) \
        .replace('UID_LINK_1_2', config.get('uid_link_1_2', '')) \
        .replace('UID_LINK_1_3', config.get('uid_link_1_3', '')) \
        .replace('UID_LINK_1_4', config.get('uid_link_1_4', '')) \
        .replace('UID_LINK_2_1', config.get('uid_link_2_1', '')) \
        .replace('UID_LINK_2_2', config.get('uid_link_2_2', '')) \
        .replace('UID_LINK_2_3', config.get('uid_link_2_3', '')) \
        .replace('UID_LINK_2_4', config.get('uid_link_2_4', ''))
    layout.customContentLayout = custom_layout

    # Store persistent tile configuration
    layout.content = copy.deepcopy(TILE_DATA_HOMEPAGE).replace(
        'LINK_NEWS_HEADING',
        config.get('link_news_heading', 'News'),
    ).replace(
        'UID_LINK_NEWS',
        config.get('uid_link_news', ''),
    ).replace(
        'LINK_NEWS_TEXT',
        config.get('link_news_text', 'News'),
    )

This method is called from a config view, but could be from an import step etc.. First we get the dynamic data, the we pass this to the set_homepage_layout method:

link_1_1 = plone.api.content.get('/path/to/first/content/item')
link_1_1 = link_1_1 and plone.api.content.get_uuid(link_1_1) or ''
...
link_news = plone.api.content.get('/de/aktuelles/news')
link_news = link_news and plone.api.content.get_uuid(link_news) or ''

config_de = {
    'uid_banner': plone.api.content.get_uuid(
        plone.api.content.get('/de/banner'),
    ),
    'uid_link_1_1': link_1_1,
    'uid_link_1_2': link_1_2,
    'uid_link_1_3': link_1_3,
    'uid_link_1_4': link_1_4,
    'uid_link_2_1': link_2_1,
    'uid_link_2_2': link_2_2,
    'uid_link_2_3': link_2_3,
    'uid_link_2_4': link_2_4,
    'uid_link_news': link_news,
    'link_news_heading': 'News',
    'link_news_text': '...weitere News',
}
self.set_homepage_layout(homepage_de, config_de)

The TILE_DATA_HOMEPAGE contains the template for the persistent tile configuration. By using the autogenerated_type_xx IDs from earlier we are now able to address specific tiles much easier. And the UIDs are replaced dynamically to point to the correct content items.

TILE_DATA_HOMEPAGE = '''<!DOCTYPE html>
<html lang="en" data-layout="./@@page-site-layout">
  <body data-panel="content">
    <div data-tile="@@plone.app.standardtiles.html/autogenerated_html_001">
      <div>
        <h2>
          <a href="../resolveuid/UID_LINK_NEWS" data-linktype="internal"
              data-val="UID_LINK_NEWS"
              data-mce-href="../../resolveuid/UID_LINK_NEWS"
              data-mce-selected="inline-boundary">LINK_NEWS_HEADING</a>
        </h2>
      </div>
    </div>
    <div data-tile="@@plone.app.standardtiles.contentlisting/autogenerated_content_listing_001"
        data-tiledata=\'{
          "description": "",
          "title": "",
          "sort_on": "created",
          "limit": 4,
          "query": [
            {
              "i": "path",
              "o": "plone.app.querystring.operation.string.absolutePath",
              "v": "UID_LINK_NEWS::-1"
            },
            {
              "i": "portal_type",
              "o": "plone.app.querystring.operation.selection.any",
              "v": ["NewsItem"]
            }
          ],
          "sort_reversed": true,
          "view_template": "summary_view"
        }\'>
    </div>
    <div data-tile="@@plone.app.standardtiles.html/autogenerated_html_002">
      <div>
        <p>
          <a href="../resolveuid/UID_LINK_NEWS" data-linktype="internal"
              data-val="UID_LINK_NEWS"
              data-mce-href="../../resolveuid/UID_LINK_NEWS"
              data-mce-selected="inline-boundary">LINK_NEWS_TEXT</a>
        </p>
      </div>
    </div>
  </body>
</html>'''

It's important that tiles always have a UUID, otherwise they can be rendered, but you might get problems editing them.

2 Likes

Yes, absolutely! When a tile ID doesn't exist when you first access it, it uses the defaults from the tile configuration or the params you provide. When you customize such a tile using Mosaic and save it, the data will be written to the content item with your ID.

I would use the LayoutAwareTileDataStorage method you showed above.

Is that enough to 'add them to the mosaic layouts editor ?

Dont they need an entry in the registry ? Or a manifest.cfg file ?

Layouts don't need registry entry, and layouts can be referenced even without manifest.cfg.

As you guessed, manifest.cfg is required for displaying layouts in editors.

In addition, with special registry entry it is possible to disable layouts from being displayed for specific content types.

Yes. It was part of the original deco/blocks design that having a set of layouts with matching tile-ids could allow to switch between them like switch between views without losing any locally customized tile configuration.

For later reference (correct me if something is wrong):

  <plone:static
  directory="layouts"
  name="mosaic_layouts"
  type="contentlayout"
/>
Add folder 'layouts', with content 'somelayout.html'
  1. If you want to show the layout (for manually choosing it):

in layouts folder, add file 'manifest.cfg'.

[contentlayout]
title = My Mosaic thing
description =  Mosaic layout for my site
file = somelayout.html
1 Like

Looks correct to me. Single manifest can include as many layouts as required and there are more options available: preview for the preview for layout selector, for for restricting layout to a specific content type and seemingly also permission for defining the permission required to show the layout on layout selector.

1 Like

Plone Foundation Code of Conduct