Instantly share code, notes, and snippets.
Created
December 11, 2020 15:50
-
Save mtrebron/dfd600e536a95fffe8a59240a75b983d to your computer and use it in GitHub Desktop.
Plone GlobalSectionsViewlet override for Megamenu
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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