--- a/src/pyams_content/root/zmi/search.py Mon Dec 04 15:36:40 2017 +0100
+++ b/src/pyams_content/root/zmi/search.py Fri Dec 08 10:41:32 2017 +0100
@@ -16,5 +16,263 @@
# import standard library
# import interfaces
+from hypatia.interfaces import ICatalog
+from pyams_content.profile.interfaces import IAdminProfile
+from pyams_content.root import ISiteRoot
+from pyams_content.shared.common.interfaces.zmi import ISiteRootDashboardTable
+from pyams_content.zmi.interfaces import IAllContentsMenu
+from pyams_form.search import ISearchFields, SearchForm, SearchView, SearchResultsView
+from pyams_i18n.interfaces import INegotiator
+from pyams_sequence.interfaces import ISequentialIntIds
+from pyams_skin.interfaces import IInnerPage, IContentSearch, IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_workflow.interfaces import IWorkflowVersions
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues
+from zope.intid import IIntIds
# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Eq, Any, Contains, Ge, Le
+from pyams_catalog.query import CatalogResultSet
+from pyams_content.shared.common import CONTENT_TYPES
+from pyams_pagelet.interfaces import PageletCreatedEvent
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.schema import Principal
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.skin import apply_skin
+from pyams_skin.table import BaseTable
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.list import unique
+from pyams_utils.registry import get_utility
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.view import AdminView
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer
+from zope.schema import Datetime, Choice, List
+
+from pyams_content import _
+
+
+#
+# Quick search adapters
+#
+
+@view_config(name='quick-search.html', context=ISiteRoot, request_type=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def site_root_quick_search_view(request):
+ """Site root quick search view"""
+ results = SiteRootQuickSearchResults(request.context, request)
+ if len(results.values) == 1:
+ result = results.values[0]
+ return {'status': 'redirect',
+ 'location': absolute_url(result, request, 'admin')}
+ else:
+ results.update()
+ return {'status': 'info',
+ 'content': {'html': results.render()}}
+
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootQuickSearchResults(BaseTable):
+ """Site root quick search results table"""
+
+ title = _("Quick search results")
+
+ sortOn = None
+
+ @property
+ def data_attributes(self):
+ attributes = super(SiteRootQuickSearchResults, self).data_attributes
+ attributes['table'] = {'data-ams-datatable-sorting': '[]',
+ 'data-ams-datatable-display-length':
+ IAdminProfile(self.request.principal).table_page_length}
+ return attributes
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootQuickSearchResults), provides=IValues)
+class SiteRootQuickSearchValues(ContextRequestViewAdapter):
+ """Site root quick search results view values adapter"""
+
+ @property
+ def values(self):
+ intids = get_utility(IIntIds)
+ catalog = get_utility(ICatalog)
+ params = Eq(catalog['parents'], intids.register(self.context)) & \
+ Any(catalog['content_type'], CONTENT_TYPES.keys())
+ query = self.request.params.get('query')
+ if query:
+ sequence = get_utility(ISequentialIntIds)
+ if query.startswith('+'):
+ params &= Eq(catalog['oid'], sequence.get_full_oid(query))
+ else:
+ query_params = Eq(catalog['oid'], sequence.get_full_oid(query))
+ negotiator = get_utility(INegotiator)
+ for lang in {self.request.registry.settings.get('pyramid.default_locale_name', 'en'),
+ self.request.locale_name,
+ negotiator.server_language} | negotiator.offered_languages:
+ index_name = 'title:{0}'.format(lang)
+ if index_name in catalog:
+ index = catalog[index_name]
+ if index.check_query(query):
+ query_params |= Contains(index, ' and '.join((w+'*' for w in query.split())))
+ params &= query_params
+ return unique(map(lambda x: IWorkflowVersions(x).get_last_versions()[0],
+ CatalogResultSet(CatalogQuery(catalog).query(params,
+ sort_index='modified_date',
+ reverse=True))))
+
+
+#
+# Advanced search adapters
+#
+
+@viewlet_config(name='advanced-search.menu', context=ISiteRoot, layer=IAdminLayer,
+ manager=IAllContentsMenu, permission=VIEW_SYSTEM_PERMISSION, weight=90)
+class SiteRootAdvancedSearchMenu(MenuItem):
+ """Site root advanced search menu"""
+
+ label = _("Advanced search")
+ icon_class = None
+ url = '#advanced-search.html'
+
+
+class ISiteRootAdvancedSearchFields(ISearchFields):
+ """Site root advanced search fields"""
+
+ content_type = List(title=_("Content types"),
+ value_type=Choice(vocabulary='PyAMS content types'),
+ required=False)
+
+ owner = Principal(title=_("Owner"),
+ required=False)
+
+ created_after = Datetime(title=_("Created after..."),
+ required=False)
+
+ created_before = Datetime(title=_("Created before..."),
+ required=False)
+
+ modified_after = Datetime(title=_("Modified after..."),
+ required=False)
+
+ modified_before = Datetime(title=_("Modified before..."),
+ required=False)
+
+
+@template_config(template='templates/advanced-search.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class SiteRootAdvancedSearchForm(SearchForm):
+ """Site root advanced search form"""
+
+ legend = _("Advanced search")
+
+ def __init__(self, context, request):
+ super(SiteRootAdvancedSearchForm, self).__init__(context, request)
+ request.registry.notify(PageletCreatedEvent(self))
+ apply_skin(self.request, 'PyAMS admin skin')
+
+ fields = field.Fields(ISiteRootAdvancedSearchFields)
+ ajax_handler = 'advanced-search-results.html'
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootAdvancedSearchForm), provides=IContentSearch)
+class SiteRootAdvancedSearchFormSearchAdapter(ContextRequestViewAdapter):
+ """Site root advanced search form search adapter"""
+
+ def get_search_results(self, data):
+ intids = get_utility(IIntIds)
+ catalog = get_utility(ICatalog)
+ params = Eq(catalog['parents'], intids.register(self.context))
+ if data.get('content_type'):
+ params &= Any(catalog['content_type'], data['content_type'])
+ else:
+ params &= Any(catalog['content_type'], CONTENT_TYPES.keys())
+ query = data.get('query')
+ if query:
+ sequence = get_utility(ISequentialIntIds)
+ if query.startswith('+'):
+ params &= Eq(catalog['oid'], sequence.get_full_oid(query))
+ else:
+ query_params = Eq(catalog['oid'], sequence.get_full_oid(query))
+ negotiator = get_utility(INegotiator)
+ for lang in {self.request.registry.settings.get('pyramid.default_locale_name', 'en'),
+ self.request.locale_name,
+ negotiator.server_language} | negotiator.offered_languages:
+ index_name = 'title:{0}'.format(lang)
+ if index_name in catalog:
+ index = catalog[index_name]
+ if index.check_query(query):
+ query_params |= Contains(index, ' and '.join((w+'*' for w in query.split())))
+ params &= query_params
+ if data.get('owner'):
+ params &= Eq(catalog['role:owner'], data['owner'])
+ if data.get('created_after'):
+ params &= Ge(catalog['created_date'], data['created_after'])
+ if data.get('created_before'):
+ params &= Le(catalog['created_date'], data['created_before'])
+ if data.get('modified_after'):
+ params &= Ge(catalog['modified_date'], data['modified_after'])
+ if data.get('modified_before'):
+ params &= Le(catalog['modified_date'], data['modified_before'])
+ return unique(map(lambda x: IWorkflowVersions(x).get_last_versions()[0],
+ CatalogResultSet(CatalogQuery(catalog).query(params,
+ sort_index='modified_date',
+ reverse=True))))
+
+
+@pagelet_config(name='advanced-search.html', context=ISiteRoot, layer=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION)
+class SiteRootAdvancedSearchView(SearchView):
+ """Site root advanced search view"""
+
+ search_form_factory = SiteRootAdvancedSearchForm
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootAdvancedSearchView), provides=IPageHeader)
+class SiteRootAdvancedSearchHeaderAdapter(DefaultPageHeaderAdapter):
+ """Site root advanced search header adapter"""
+
+ back_url = '#dashboard.html'
+ back_target = None
+
+ icon_class = 'fa fa-fw fa-search'
+
+
+@view_config(name='advanced-search-results.html', context=ISiteRoot, request_type=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION)
+@implementer(ISiteRootDashboardTable)
+class SiteRootAdvancedSearchResultsView(AdminView, SearchResultsView):
+ """Site root advanced search results view"""
+
+ title = _("Advanced search results")
+ search_form_factory = SiteRootAdvancedSearchForm
+
+ sortOn = None
+
+ def __init__(self, context, request):
+ super(SiteRootAdvancedSearchResultsView, self).__init__(context, request)
+ request.registry.notify(PageletCreatedEvent(self))
+
+ @property
+ def data_attributes(self):
+ attributes = super(SiteRootAdvancedSearchResultsView, self).data_attributes
+ attributes['table'] = {'data-ams-datatable-sorting': '[]',
+ 'data-ams-datatable-display-length':
+ IAdminProfile(self.request.principal).table_page_length}
+ return attributes
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootAdvancedSearchResultsView), provides=IValues)
+class SearchResultsViewValuesAdapter(ContextRequestViewAdapter):
+ """Search results view values adapter"""
+
+ @property
+ def values(self):
+ form = self.view.search_form_factory(self.context, self.request)
+ return form.get_search_results() or ()
--- a/src/pyams_content/root/zmi/sites.py Mon Dec 04 15:36:40 2017 +0100
+++ b/src/pyams_content/root/zmi/sites.py Fri Dec 08 10:41:32 2017 +0100
@@ -23,20 +23,25 @@
from pyams_content.zmi.interfaces import ISiteTreeMenu, ISiteTreeTable
from pyams_sequence.interfaces import ISequentialIdInfo
from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.container import ITableWithActions
from pyams_skin.layer import IPyAMSLayer
from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
from pyams_workflow.interfaces import IWorkflowPublicationInfo
from pyams_zmi.interfaces.menu import ISiteManagementMenu
from pyams_zmi.layer import IAdminLayer
from z3c.table.interfaces import IColumn, IValues
+from zope.intid.interfaces import IIntIds
# import packages
+from pyams_content.skin import pyams_content
from pyams_pagelet.pagelet import pagelet_config
from pyams_skin.container import ContainerView
from pyams_skin.page import DefaultPageHeaderAdapter
from pyams_skin.table import BaseTable, TrashColumn, ActionColumn, I18nColumn
from pyams_skin.viewlet.menu import MenuItem
from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.fanstatic import get_resource_path
+from pyams_utils.registry import get_utility
from pyams_utils.url import absolute_url
from pyams_viewlet.manager import viewletmanager_config
from pyams_viewlet.viewlet import viewlet_config
@@ -52,7 +57,7 @@
# Sites and blogs view
#
-@viewlet_config(name='site-tree.menu', layer=IAdminLayer, context=ISiteRoot, manager=ISiteManagementMenu,
+@viewlet_config(name='site-tree.menu', context=ISiteRoot, layer=IAdminLayer, manager=ISiteManagementMenu,
permission=VIEW_SYSTEM_PERMISSION, weight=5)
@viewletmanager_config(name='site-tree.menu', layer=IAdminLayer, context=ISiteRoot, provides=ISiteTreeMenu)
@implementer(ISiteTreeMenu)
@@ -64,7 +69,7 @@
url = '#site-tree.html'
-@implementer(IDashboardTable, ISiteTreeTable)
+@implementer(IDashboardTable, ITableWithActions, ISiteTreeTable)
class SiteTreeTable(BaseTable):
"""Site tree table"""
@@ -76,8 +81,19 @@
@property
def data_attributes(self):
attributes = super(SiteTreeTable, self).data_attributes
- attributes['table'] = {'data-ams-location': absolute_url(self.context, self.request),
- 'data-ams-delete-target': 'delete-shared-site.json'}
+ intids = get_utility(IIntIds)
+ attributes.setdefault('table', {}).update({
+ 'data-ams-plugins': 'pyams_content',
+ 'data-ams-plugin-pyams_content-src': get_resource_path(pyams_content),
+ 'data-ams-location': absolute_url(self.context, self.request),
+ 'data-ams-delete-target': 'delete-shared-site.json'
+ })
+ attributes.setdefault('tr', {}).update({
+ 'id': lambda x, col: '{0}::{1}'.format(self.id, intids.queryId(x)),
+ 'data-ams-location': lambda x, col: absolute_url(x.__parent__, self.request),
+ 'data-ams-tree-node-id': lambda x, col: intids.queryId(x),
+ 'data-ams-tree-node-parent-id': lambda x, col: intids.queryId(x.__parent__)
+ })
return attributes
@@ -123,7 +139,7 @@
return '--'
else:
try:
- return sequence.get_short_oid()
+ return sequence.get_base_oid()
except TypeError:
return '--'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/root/zmi/templates/advanced-search.pt Fri Dec 08 10:41:32 2017 +0100
@@ -0,0 +1,223 @@
+<div class="ams-widget" i18n:domain="pyams_content">
+ <header>
+ <span tal:condition="view.widget_icon_class | nothing"
+ class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
+ </span>
+ <h2 tal:content="view.legend"></h2>
+ <tal:var content="structure provider:pyams.widget_title" />
+ <tal:var content="structure provider:pyams.toolbar" />
+ </header>
+ <div class="widget-body no-padding">
+ <div tal:define="prefix provider:form_prefix"
+ tal:replace="structure prefix">Form prefix</div>
+ <tal:var content="structure provider:content_help" />
+ <form method="post"
+ data-async
+ tal:attributes="id view.id;
+ name view.name;
+ action view.get_form_action();
+ method view.method;
+ enctype view.enctype;
+ acceptCharset view.acceptCharset;
+ accept view.accept;
+ autocomplete view.autocomplete;
+ class view.css_class;
+ data-ams-data extension:object_data(view);
+ data-ams-form-handler view.get_ajax_handler() | nothing;
+ data-ams-form-options view.get_form_options() | nothing;
+ data-ams-form-submit-target view.form_target | nothing;
+ data-ams-form-download-target view.download_target | nothing;
+ data-ams-warn-on-change view.warn_on_change;">
+ <div class="modal-viewport">
+ <fieldset>
+ <div class="widgets-prefix"
+ tal:define="prefix provider:widgets_prefix"
+ tal:condition="prefix"
+ tal:content="structure prefix">Widgets prefix</div>
+ <tal:loop repeat="group view.groups">
+ <fieldset tal:define="legend group.legend"
+ tal:omit-tag="not:legend"
+ tal:attributes="class 'bordered' if group.bordered else None">
+ <tal:if condition="group.checkbox_switch">
+ <legend data-ams-checker-value="selected"
+ tal:condition="legend"
+ tal:attributes="class group.css_class;
+ data-ams-checker-fieldname '{0}:list'.format(group.checkbox_widget.name);
+ data-ams-checker-readonly 'readonly' if group.checkbox_widget.mode == 'display' else None;
+ data-ams-checker-marker '{0}-empty-marker'.format(group.checkbox_widget.name);
+ data-ams-checker-state group.checker_state;">
+ <label tal:content="legend">Legend</label>
+ </legend>
+ </tal:if>
+ <tal:if condition="not:group.checkbox_switch">
+ <legend tal:condition="legend"
+ tal:content="legend"
+ tal:attributes="class group.css_class;
+ data-ams-switcher-state group.switcher_state;">Legend</legend>
+ </tal:if>
+ <tal:var define="help group.help" condition="help">
+ <div class=""
+ tal:define="html import:pyams_utils.text.text_to_html;
+ i18n_help html(request.localizer.translate(help));"
+ tal:content="structure i18n_help"></div>
+ </tal:var>
+ <div class="form-group" tal:define="widget view.widgets['query']">
+ <label class="control-label col-md-3">
+ <span>
+ <tal:var content="widget.label" />
+ <i class="fa fa-question-circle hint" title="Input hint"
+ tal:define="description widget.field.description"
+ tal:condition="description"
+ tal:attributes="title description;
+ data-ams-hint-html '<' in description;"></i>
+ </span>
+ </label>
+ <div class="col-md-9">
+ <label class="input"
+ tal:attributes="class widget.widget_css_class | default;
+ data-ams-data extension:object_data(widget);
+ data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+ <input tal:replace="structure widget.render()" />
+ </label>
+ </div>
+ </div>
+ <div class="form-group">
+ <tal:var define="widget view.widgets['owner']">
+ <label class="control-label col-md-3">
+ <span>
+ <tal:var content="widget.label" />
+ <i class="fa fa-question-circle hint" title="Input hint"
+ tal:define="description widget.field.description"
+ tal:condition="description"
+ tal:attributes="title description;
+ data-ams-hint-html '<' in description;"></i>
+ </span>
+ </label>
+ <div class="col-md-4">
+ <label class="input"
+ tal:attributes="class widget.widget_css_class | default;
+ data-ams-data extension:object_data(widget);
+ data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+ <input tal:replace="structure widget.render()" />
+ </label>
+ </div>
+ </tal:var>
+ <tal:var define="widget view.widgets['content_type']">
+ <label class="control-label col-md-2">
+ <span>
+ <tal:var content="widget.label" />
+ <i class="fa fa-question-circle hint" title="Input hint"
+ tal:define="description widget.field.description"
+ tal:condition="description"
+ tal:attributes="title description;
+ data-ams-hint-html '<' in description;"></i>
+ </span>
+ </label>
+ <div class="col-md-3">
+ <label class="input"
+ tal:attributes="class widget.widget_css_class | default;
+ data-ams-data extension:object_data(widget);
+ data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+ <input tal:replace="structure widget.render()" />
+ </label>
+ </div>
+ </tal:var>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-md-3">
+ <span i18n:translate="">Created between</span>
+ </label>
+ <div class="col-md-4">
+ <label class="input"
+ tal:define="widget view.widgets['created_after']"
+ tal:attributes="class widget.widget_css_class | default;
+ data-ams-data extension:object_data(widget);
+ data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+ <input tal:replace="structure widget.render()" />
+ </label>
+ </div>
+ <div class="control-label col-md-1 text-align-center">
+ <i18n:var translate=""> and </i18n:var>
+ </div>
+ <div class="col-md-4">
+ <label class="input"
+ tal:define="widget view.widgets['created_before']"
+ tal:attributes="class widget.widget_css_class | default;
+ data-ams-data extension:object_data(widget);
+ data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+ <input tal:replace="structure widget.render()" />
+ </label>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-md-3">
+ <span i18n:translate="">Modified between</span>
+ </label>
+ <div class="col-md-4">
+ <label class="input"
+ tal:define="widget view.widgets['modified_after']"
+ tal:attributes="class widget.widget_css_class | default;
+ data-ams-data extension:object_data(widget);
+ data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+ <input tal:replace="structure widget.render()" />
+ </label>
+ </div>
+ <div class="control-label col-md-1 text-align-center">
+ <i18n:var translate=""> and </i18n:var>
+ </div>
+ <div class="col-md-4">
+ <label class="input"
+ tal:define="widget view.widgets['modified_before']"
+ tal:attributes="class widget.widget_css_class | default;
+ data-ams-data extension:object_data(widget);
+ data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+ <input tal:replace="structure widget.render()" />
+ </label>
+ </div>
+ </div>
+ </fieldset>
+ </tal:loop>
+ <div class="widgets-suffix"
+ tal:define="suffix provider:widgets_suffix"
+ tal:condition="suffix"
+ tal:content="structure suffix">Widgets suffix</div>
+ <div class="subforms"
+ tal:condition="view.subforms">
+ <fieldset tal:define="title view.subforms_legend"
+ tal:omit-tag="not:title">
+ <legend tal:condition="title" tal:content="title" i18n:translate="">Title</legend>
+ <tal:loop repeat="subform view.subforms">
+ <tal:var replace="structure subform.render()" />
+ </tal:loop>
+ </fieldset>
+ </div>
+ <div class="tabforms"
+ tal:condition="view.tabforms">
+ <ul class="nav nav-tabs">
+ <li tal:repeat="tabform view.tabforms"
+ tal:attributes="class 'small {active} {errors}'.format(active='active' if repeat['tabform'].start() else '',
+ errors='state-error' if tabform.widgets.errors else '')">
+ <a data-toggle="tab"
+ tal:attributes="href string:#${tabform.id}"
+ tal:content="tabform.tab_label" i18n:translate="">Tab label</a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane fade in"
+ tal:repeat="tabform view.tabforms"
+ tal:attributes="id tabform.id;
+ class 'tab-pane {active} fade in'.format(active='active' if repeat['tabform'].start() else '');"
+ tal:content="structure tabform.render()"></div>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ <footer>
+ <button tal:repeat="action view.actions.values()"
+ tal:replace="structure action.render()">Action</button>
+ </footer>
+ </form>
+ <div tal:define="prefix provider:form_suffix"
+ tal:replace="structure prefix">Form suffix</div>
+ </div>
+</div>
--- a/src/pyams_content/root/zmi/templates/dashboard.pt Mon Dec 04 15:36:40 2017 +0100
+++ b/src/pyams_content/root/zmi/templates/dashboard.pt Fri Dec 08 10:41:32 2017 +0100
@@ -13,7 +13,30 @@
<tal:var content="structure provider:pyams.toolbar" />
</header>
<div class="widget-body">
- <tal:var define="global dashboard_length 0">
+ <div class="ams-widget">
+ <header>
+ <h2 i18n:translate="">SEARCH - Between all contents</h2>
+ </header>
+ <div class="padding-10 padding-bottom-0">
+ <form class="ams-form clearfix margin-bottom-10" method="post" action="quick-search.html"
+ data-async data-ams-form-submit-target="#search_results">
+ <div class="form-group">
+ <div class="col-md-6">
+ <label class="input">
+ <button type="submit" class="icon-append fa fa-fw fa-search no-border no-padding"
+ data-ams-form-hide-loading="true"></button>
+ <input type="text" name="query" placeholder="Quick search..." i18n:attributes="placeholder" />
+ </label>
+ </div>
+ <div class="col-md-6">
+ <a class="nowrap btn-sm col-md-2" href="#advanced-search.html"
+ i18n:translate="">Advanced search...</a>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ <div id="search_results" tal:define="global dashboard_length 0">
<tal:loop repeat="table view.tables">
<tal:if condition="table.values">
<tal:var content="structure table.render()" />
@@ -23,6 +46,6 @@
<div tal:condition="not:dashboard_length" class="alert alert-info" i18n:translate="">
You are not actually concerned by any content.
</div>
- </tal:var>
+ </div>
</div>
</div>