Skip to content

Instantly share code, notes, and snippets.

@mtrebron
Created December 11, 2020 15:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mtrebron/dfd600e536a95fffe8a59240a75b983d to your computer and use it in GitHub Desktop.
Save mtrebron/dfd600e536a95fffe8a59240a75b983d to your computer and use it in GitHub Desktop.
Plone GlobalSectionsViewlet override for Megamenu
# -*- coding: utf-8 -*-
from plone import api
from Acquisition import aq_base
from Acquisition import aq_inner
from plone.app.layout.viewlets import ViewletBase
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.CMFCore.permissions import ModifyPortalContent
from Products.CMFCore.utils import getToolByName
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from zope.component import getMultiAdapter
from zope.component import getUtility
from zope.component import queryMultiAdapter
from plone.app.layout.navigation.root import getNavigationRoot
from plone.app.layout.navigation.root import getNavigationRootObject
from collections import defaultdict
from Products.CMFPlone.utils import safe_unicode
from plone.registry.interfaces import IRegistry
from Products.CMFPlone.interfaces import ISearchSchema
from Products.CMFPlone.interfaces import ISiteSchema
from Products.CMFPlone.interfaces.controlpanel import ILanguageSchema
from Products.CMFPlone.interfaces.controlpanel import INavigationSchema
from zope.i18n import translate
import logging
log = logging.getLogger(__name__)
class GlobalSectionsViewlet(ViewletBase):
""" overrides default GlobalSectionsViewlet in plone.mainnavigation
see: https://github.com/plone/plone.app.layout/blob/master/plone/app/layout/viewlets/common.py
adds two aside elements, one containing teaser images of the subitems and the other containing related items
"""
index = ViewPageTemplateFile('./templates/viewlet_global_sections.pt')
_item_opener_template = (
u'<input id="navitem-{uid}" type="checkbox" class="opener" />'
u'<label for="navitem-{uid}" role="button" aria-label="{title}"></label>' # noqa: E 501
)
_item_content_template = (
u'<li class="{id}{has_sub_class}">'
u'<a href="{url}" class="state-{review_state}"{aria_haspopup}>'
u'<span class="nav-marker"></span>{title}</a>' # noqa: E 501
u'{opener}'
u'{item_subtree}'
u'</li>'
)
_subtree_wrapper_template = (
u'<ul class="has_subtree dropdown">'
u'{subtree_html}'
u'</ul>'
u'<aside class="carousel-items" style="display:none;">'
u'<ul>{carousel_html}</ul>'
u'</aside>'
u'<aside class="related-items" style="display:none;">'
u'<div>{relation_caption}</div>'
u'<ul>{related_html}</ul>'
u'</aside>'
)
_subitem_content_template = (
u'<li class="{id}{has_sub_class}">'
u'<a href="{url}" class="state-{review_state}"{aria_haspopup}>{title}</a>' # noqa: E 501
#u'{opener}'
u'</li>'
)
_subitem_carousel_template = (
u'<li class="carousel-item">'
u'<a href="{url}"><img src="{related_image_path}/@@images/image/{image_size}" class="img-responsive" alt=""></a>'
u'<p>{related_image_caption}</p>'
u'</li>'
)
_subitem_related_template = (
u'<li class="related-item">'
u'<a href="{url}">{title}</a>'
u'<p>{description}</p>'
u'</li>'
)
@property
@memoize_contextless
def settings(self):
registry = getUtility(IRegistry)
settings = registry.forInterface(INavigationSchema, prefix='plone')
return settings
@property
def language_settings(self):
registry = getUtility(IRegistry)
settings = registry.forInterface(ILanguageSchema, prefix='plone')
return settings
@property
def navtree_path(self):
return getNavigationRoot(self.context)
@property
def navtree_depth(self):
return self.settings.navigation_depth
@property
def current_language(self):
return (
self.request.get('LANGUAGE', None)
or (self.context and aq_inner(self.context).Language())
or self.language_settings.default_language
)
@property
@memoize
def navtree(self):
navtree_entries = defaultdict(list)
navtree_path = self.navtree_path
for tab in self.portal_tabs:
entry = tab.copy()
entry.update({
'path': '/'.join((navtree_path, tab['id'])),
'uid': tab['id'],
})
if 'review_state' not in entry:
entry['review_state'] = None
if 'title' not in entry:
entry['title'] = (
tab.get('name')
or tab.get('description')
or tab['id']
)
else:
# translate Home tab
entry['title'] = translate(
entry['title'],
domain='plone',
context=self.request)
entry['title'] = safe_unicode(entry['title'])
navtree_entries[navtree_path].append(entry)
if not self.settings.generate_tabs:
return navtree_entries
query = {
'path': {
'query': self.navtree_path,
'depth': self.navtree_depth,
},
'portal_type': {'query': self.settings.displayed_types},
'Language': self.current_language,
'sort_on': self.settings.sort_tabs_on,
'is_default_page': False,
}
if self.settings.sort_tabs_reversed:
query['sort_order'] = 'reverse'
if not self.settings.nonfolderish_tabs:
query['is_folderish'] = True
if self.settings.filter_on_workflow:
query['review_state'] = list(
self.settings.workflow_states_to_show or ()
)
if not self.settings.show_excluded_items:
query['exclude_from_nav'] = False
context_path = '/'.join(self.context.getPhysicalPath())
portal_catalog = getToolByName(self.context, 'portal_catalog')
brains = portal_catalog.searchResults(**query)
for brain in brains:
brain_path = brain.getPath()
brain_parent_path = brain_path.rpartition('/')[0]
if brain_parent_path == navtree_path:
# This should be already provided by the portal_tabs_view
continue
if brain.exclude_from_nav and not context_path.startswith(brain_path): # noqa: E501
# skip excluded items if they're not in our context path
continue
entry = self.make_entry(brain)
self.customize_entry(entry, brain)
navtree_entries[brain_parent_path].append(entry)
return navtree_entries
def make_entry(self, brain):
registry = getUtility(IRegistry)
types_using_view = registry.get(
'plone.types_use_view_action_in_listings', [])
url = brain.getURL()
if brain.portal_type in types_using_view:
url += '/view'
entry = {
'id': brain.getId,
'path': brain.getPath(),
'uid': brain.UID,
'url': url,
'title': safe_unicode(brain.Title),
'review_state': brain.review_state,
}
return entry
def customize_entry(self, entry, brain):
"""a little helper to add custom entry keys/values."""
related_image_path = ''
related_image_uuid = getattr(brain, 'related_image_uuid', None)
if related_image_uuid:
related_image_path = get_path_from_uuid(self.context, related_image_uuid)
related_image_caption = getattr(brain, 'image_caption', None)
if not related_image_caption:
related_image_caption = brain.Description
entry['description'] = safe_unicode(brain.Description)
entry['related_image_caption'] = safe_unicode(related_image_caption)
entry['related_image_path'] = related_image_path
entry['image_size'] = 'slider_mega_menu'
# log.info('%s related image %s %s' % (get_linenumber(), related_image_uuid, related_image_path))
def render_item(self, item, path, top_level, nav_type):
item_markup_template = u''
item_carousel = u''
item_related = u''
item_subtree = u''
opener = u''
aria_has_popup = u''
has_sub_class = u''
if nav_type == 'subtree':
item_subtree = self.build_tree(item['path'], top_level=False, nav_type=nav_type)
item_markup_template = top_level and self._item_content_template or self._subitem_content_template
if top_level and '<li ' in item_subtree:
opener = self._item_opener_template.format(**item)
aria_has_popup = u' aria-haspopup="true"'
has_sub_class = u' has_subtree'
#log.info('rendering: %s %s %s %s' % (nav_type, top_level, path, item['url']))
elif not top_level:
if nav_type == 'carousel' and item.get('related_image_path', None):
item_carousel = self.build_tree(item['path'], top_level=False, nav_type=nav_type)
item_markup_template = self._subitem_carousel_template
# log.info('rendering: %s %s %s %s' % (nav_type, top_level, path, item['url']))
elif nav_type == 'related':
item_related = self.build_tree(item['path'], top_level=False, nav_type=nav_type)
item_markup_template = self._subitem_related_template
# log.info('rendering: %s %s %s %s' % (nav_type, top_level, path, item['url']))
item.update({
'item_subtree' : item_subtree,
'item_carousel' : item_carousel,
'item_related' : item_related,
'opener' : opener,
'aria_haspopup' : aria_has_popup,
'has_sub_class' : has_sub_class
})
return item_markup_template.format(**item)
def build_tree(self, path, top_level=True, nav_type=''):
""" Non-template based recursive tree building.
3-4 times faster than template based.
"""
portal_catalog = getToolByName(self.context, 'portal_catalog')
subtree_html = u''
carousel_html = u''
related_html = u''
relation_caption = u''
navtree_paths = self.navtree.get(path, [])
for item in navtree_paths:
subtree_html += self.render_item(item, path, top_level, 'subtree')
if not top_level:
carousel_html += self.render_item(item, path, top_level, 'carousel')
if navtree_paths[-1] == item:
query = {'path' : {'query': item['path'].rpartition('/')[0], 'depth': 0}}
parent_brain = portal_catalog(**query)[0]
relation_caption = getattr(parent_brain, 'relation_caption', None)
uuids = getattr(parent_brain, 'related_item_uuids', [] )
related_item_paths = (uuids and not uuids is Missing) and [get_path_from_uuid(self.context, uuid) for uuid in uuids] or []
# log.info('%s %s %s'% (get_linenumber(), uuids, related_item_paths))
if related_item_paths:
if not relation_caption:
# translate the default related items field label
# 2011111 must give a default value here in case the requested language is en,
# plone.po does not contain the actual messagestring
# - we can not get it from the field as we would have to wake up the parent object
# - we can not store the default value in the FieldIndex
# because in the schema relatedItems.title from the field gives us 'label_related_items'
# see https://community.plone.org/t/untranslated-translated-field-title-returns-msgid-instead-of-default-value/10752/2
relation_caption = translate(
u'label_related_items',
domain='plone',
context=self.request,
default='Related Items')
for related_item_path in related_item_paths:
query = {'path' : {'query': related_item_path, 'depth': 0}}
related_brain = portal_catalog(**query)[0]
related_item = self.make_entry(related_brain)
self.customize_entry(related_item, related_brain)
related_html += self.render_item(related_item, related_item_path, top_level, 'related')
content_html = top_level and subtree_html or self._subtree_wrapper_template.format(
subtree_html = subtree_html,
carousel_html = carousel_html,
relation_caption = relation_caption,
related_html = related_html
)
return content_html
def render_globalnav(self):
return self.build_tree(self.navtree_path)
@property
@memoize
def portal_tabs(self):
portal_tabs_view = getMultiAdapter((self.context, self.request),
name='portal_tabs_view')
return portal_tabs_view.topLevelTabs()
def update(self):
context = aq_inner(self.context)
self.selected_tabs = self.selectedTabs(portal_tabs=self.portal_tabs)
self.selected_portal_tab = self.selected_tabs['portal']
def selectedTabs(self, default_tab='index_html', portal_tabs=()):
portal = getToolByName(self.context, 'portal_url').getPortalObject()
plone_url = getNavigationRootObject(
self.context, portal).absolute_url()
plone_url_len = len(plone_url)
request = self.request
valid_actions = []
url = request['URL']
path = url[plone_url_len:]
path_list = path.split('/')
if len(path_list) <= 1:
return {'portal': default_tab}
for action in portal_tabs:
if not action['url'].startswith(plone_url):
# In this case the action url is an external link. Then, we
# avoid issues (bad portal_tab selection) continuing with next
# action.
continue
action_path = action['url'][plone_url_len:]
if not action_path.startswith('/'):
action_path = '/' + action_path
action_path_list = action_path.split('/')
if action_path_list[1] == path_list[1]:
# Make a list of the action ids, along with the path length
# for choosing the longest (most relevant) path.
valid_actions.append((len(action_path_list), action['id']))
# Sort by path length, the longest matching path wins
valid_actions.sort()
if valid_actions:
return {'portal': valid_actions[-1][1]}
return {'portal': default_tab}
<tal:sections
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
tal:define="portal_tabs view/portal_tabs"
tal:condition="portal_tabs"
i18n:domain="plone">
<nav class="plone-navbar pat-navigationmarker" id="portal-globalnav-wrapper">
<div class="container">
<div class="plone-navbar-header">
<button type="button" class="plone-navbar-toggle" data-toggle="collapse" data-target="#portal-globalnav-collapse">
<span class="sr-only" i18n:translate="toggle_navigation">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="plone-collapse plone-navbar-collapse" id="portal-globalnav-collapse">
<ul class="plone-nav plone-navbar-nav"
id="portal-globalnav"
tal:define="selected_tab python:view.selected_portal_tab">
<navtree tal:replace="structure view/render_globalnav" />
</ul>
</div>
</div>
</nav>
</tal:sections>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment