diff --git a/.gitignore b/.gitignore
index f7cb6f57b4..bec5382a9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,8 @@
.coverage.*
.installed*.cfg
.mr.developer.cfg
+.project
+.pydevproject
.tox
/bin/
/build/
diff --git a/buildout.cfg b/buildout.cfg
index f165596598..f633905bec 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -1,6 +1,5 @@
[buildout]
extensions = mr.developer
-
allow-picked-versions = false
develop = .
extends =
@@ -20,6 +19,17 @@ parts =
requirements
sources-dir = develop
auto-checkout =
+ zmi.styles
+ Persistence
+ AccessControl
+ Products.BTreeFolder2
+ Products.ZCatalog
+ RestrictedPython
+ Products.MailHost
+ Products.PythonScripts
+ Products.Sessions
+ Products.TemporaryFolder
+ Products.SiteErrorLog
[testenv]
PYTHONHASHSEED = random
diff --git a/docs/WHATSNEW.rst b/docs/WHATSNEW.rst
index abeb7a261e..44fa1242e3 100644
--- a/docs/WHATSNEW.rst
+++ b/docs/WHATSNEW.rst
@@ -143,3 +143,11 @@ Zope 4 depends on a new DateTime release. The new release has been optimized
for better memory use. Applications using a lot of DateTime values like the
Plone CMS have seen total memory usage to decrease by 10% to 20% for medium
to large deployments.
+
+
+ZMI overhaul
+------------
+
+The ZMI (Zope Management Interface) is now styled with Bootstrap.
+See :ref:`ZMI-label` for details how to adapt Zope add-on packages to the new
+styling.
diff --git a/docs/ZMI.rst b/docs/ZMI.rst
new file mode 100644
index 0000000000..63d9e605c4
--- /dev/null
+++ b/docs/ZMI.rst
@@ -0,0 +1,100 @@
+.. _ZMI-label:
+
+ZMI
+===
+
+ZMI is an abbreviation for `Zope Management Interface`. This is the user
+interface rendered when accessing Zope`s management screens using a web
+browser.
+
+Bootstrap ZMI
+-------------
+
+Since Zope 4.0b6 the ZMI is styled using Bootstrap. The previously used
+GIF icons were replaced by font glyphs which are stored in the package
+`zmi.styles`_ together with the CSS and JavaScript needed by Bootstrap.
+
+The free Font Awesome glyphs are used as icons, see the table of
+`available icons`_.
+
+Update packages
++++++++++++++++
+
+If you have a Product or package which contains types, which can be added via
+the ZMI, the default icon will be shown.
+
+To use one of the new icons add an attribute named ``zmi_icon`` to the class.
+As value use a name listed on `available icons`_ prefixed by ``fas fa-``.
+Example to use the info icon (i in a circle)::
+
+ zmi_icon = 'fas fa-info-circle'
+
+.. _`zmi.styles` : https://github.com/zopefoundation/zmi.styles
+.. _`available icons` : https://fontawesome.com/icons?d=gallery&m=free
+
+Use custom icons and resources
+++++++++++++++++++++++++++++++
+
+To use custom icons (which are not part of `zmi.styles`) or load custom CSS resp. JavaScript, you have to:
+
+1. create a directory and fill it with your assets
+2. register this directory as resource directory
+3. register a subscription adapter for :class:`App.interfaces.ICSSPaths` resp.
+ :class:`App.interfaces.IJSPaths`. This adapter has to return an iterable of
+ paths resp. URLs which should be loaded when rendering the ZMI.
+
+Example taken from `zmi.styles`_:
+
+* Register the resource directory via ZCML:
+
+ .. code-block:: XML
+
+
+
+* Create a subscription adapter returning the path to the CSS file
+ (`zmi.styles`_ has this code in `subscriber.py`.):
+
+
+ .. code-block:: Python
+
+ import zope.component
+ import zope.interface
+
+
+ @zope.component.adapter(zope.interface.Interface)
+ def css_paths(context):
+ """Return paths to CSS files needed for the Zope 4 ZMI."""
+ return (
+ '/++resource++zmi/bootstrap-4.1.1/bootstrap.min.css',
+ '/++resource++zmi/fontawesome-free-5.1.0/css/all.css',
+ '/++resource++zmi/zmi_base.css',
+ )
+
+* Register the subscriber via ZCML:
+
+ .. code-block:: XML
+
+
+
+
+Use custom resources via ZMI
+++++++++++++++++++++++++++++
+
+To add custom CSS or JavaScript resources via ZMI you have to add a property:
+
+* ``zmi_additional_css_paths`` for additional CSS
+* ``zmi_additional_js_paths`` for additional JavaScript
+
+The properties can have one of the following types:
+
+* ``string``
+* ``ustring``
+* ``ulines``
+
+The value of the property has to be one or more paths/URLs to CSS resp.
+JavaScript which will be included in the HTML of the ZMI. (Paths have to be
+resolvable by the browser aka not simple file system paths.)
diff --git a/docs/index.rst b/docs/index.rst
index b541ec3c9a..9d1ca2af5e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,6 +11,7 @@ Contents:
INSTALL-virtualenv
operation
USERS
+ ZMI
SECURITY
maintenance
changes
diff --git a/docs/zdgbook/Products.rst b/docs/zdgbook/Products.rst
index b551314e94..46eb2f6be1 100644
--- a/docs/zdgbook/Products.rst
+++ b/docs/zdgbook/Products.rst
@@ -833,31 +833,8 @@ When creating management views you should include the DTML variables
``manage_page_footer`` at the bottom. These variables are acquired
by your product and draw a standard management view header, tabs
widgets, and footer. The management header also includes CSS
-information which you can take advantage of if you wish to add CSS
-style information to your management views. The management CSS
-information is defined in the
-`lib/python/App/dtml/manage_page_style.css.dtml`` file. Here are the
-CSS classes defined in this file and conventions for their use.
-
-- 'form-help' -- Explanatory text related to forms. In the future,
- users may have the option to hide this text.
-
-- 'std-text' -- Declarative text unrelated to forms. You should
- rarely use this class.
-
-- 'form-title' -- Form titles.
-
-- 'form-label' -- Form labels for required form elements.
-
-- 'form-optional' -- Form labels for optional form elements.
-
-- 'form-element' -- Form elements. Note, because of a Netscape bug,
- you should not use this class on 'textarea' elements.
-
-- 'form-text' -- Declarative text in forms.
-
-- 'form-mono' -- Fixed width text in forms. You should rarely use
- this class.
+information which you can take advantage of. You can use any of the styles
+Bootstrap 4 provides. (See http://getbootstrap.com/docs/4.1/)
Here's an example management view for your poll class. It allows you
to edit the poll question and responses (see ``editPollForm.dtml``)::
diff --git a/requirements-full.txt b/requirements-full.txt
index fd99349ddd..bdaa4a3856 100644
--- a/requirements-full.txt
+++ b/requirements-full.txt
@@ -3,7 +3,7 @@ AccessControl==4.0b4
Acquisition==4.4.4
AuthEncoding==4.0.0
BTrees==4.5.0
-Chameleon==3.2
+Chameleon==3.4
DateTime==4.2
DocumentTemplate==3.0b3
ExtensionClass==4.3.0
@@ -32,6 +32,7 @@ mock==2.0.0
pbr==4.0.3
persistent==4.2.4.2
pytz==2018.4
+shutilwhich==1.1.0
six==1.11.0
transaction==2.2.1
waitress==1.1.0
diff --git a/setup.py b/setup.py
index 2a5d957266..6ca7cb57c6 100644
--- a/setup.py
+++ b/setup.py
@@ -77,6 +77,7 @@ def _read_file(filename):
'waitress',
'zExceptions >= 3.4',
'z3c.pt',
+ 'zmi.styles',
'zope.browser',
'zope.browsermenu',
'zope.browserpage >= 4.0',
diff --git a/sources.cfg b/sources.cfg
index c7b9d89f6b..951f6d00a7 100644
--- a/sources.cfg
+++ b/sources.cfg
@@ -4,7 +4,7 @@ github_push = git@github.com:zopefoundation
[sources]
# Zope-specific
-AccessControl = git ${remotes:github}/AccessControl pushurl=${remotes:github_push}/AccessControl branch=master
+AccessControl = git ${remotes:github}/AccessControl pushurl=${remotes:github_push}/AccessControl
Acquisition = git ${remotes:github}/Acquisition pushurl=${remotes:github_push}/Acquisition
AuthEncoding = git ${remotes:github}/AuthEncoding pushurl=${remotes:github_push}/AuthEncoding
DateTime = git ${remotes:github}/DateTime pushurl=${remotes:github_push}/DateTime
@@ -15,14 +15,19 @@ Persistence = git ${remotes:github}/Persistence pushurl=${remotes:github_push}/P
RestrictedPython = git ${remotes:github}/RestrictedPython pushurl=${remotes:github_push}/RestrictedPython
zExceptions = git ${remotes:github}/zExceptions pushurl=${remotes:github_push}/zExceptions
zope.globalrequest = git ${remotes:github}/zope.globalrequest pushurl=${remotes:github_push}/zope.globalrequest
+zmi.styles = git ${remotes:github}/zmi.styles pushurl=${remotes:github_push}/zmi.styles
# Optional dependencies
five.localsitemanager = git ${remotes:github}/five.localsitemanager pushurl=${remotes:github_push}/five.localsitemanager
Missing = git ${remotes:github}/Missing pushurl=${remotes:github_push}/Missing
Products.BTreeFolder2 = git ${remotes:github}/Products.BTreeFolder2 pushurl=${remotes:github_push}/Products.BTreeFolder2
Products.MailHost = git ${remotes:github}/Products.MailHost pushurl=${remotes:github_push}/Products.MailHost
+Products.PythonScripts = git ${remotes:github}/Products.PythonScripts pushurl=${remotes:github_push}/Products.PythonScripts
Products.ZCatalog = git ${remotes:github}/Products.ZCatalog pushurl=${remotes:github_push}/Products.ZCatalog
Products.ZCTextIndex = git ${remotes:github}/Products.ZCTextIndex pushurl=${remotes:github_push}/Products.ZCTextIndex
+Products.Sessions = git ${remotes:github}/Products.Sessions pushurl=${remotes:github_push}/Products.Sessions
+Products.TemporaryFolder = git ${remotes:github}/Products.TemporaryFolder pushurl=${remotes:github_push}/Products.TemporaryFolder
+Products.SiteErrorLog = git ${remotes:github}/Products.SiteErrorLog pushurl=${remotes:github_push}/Products.SiteErrorLog
Record = git ${remotes:github}/Record pushurl=${remotes:github_push}/Record
ZServer = git ${remotes:github}/ZServer pushurl=${remotes:github_push}/ZServer
diff --git a/src/App/Management.py b/src/App/Management.py
index 4cfb4aa5de..10ef4d72ec 100644
--- a/src/App/Management.py
+++ b/src/App/Management.py
@@ -18,11 +18,16 @@
from AccessControl.class_init import InitializeClass
from AccessControl.Permissions import view_management_screens
from App.interfaces import INavigation
+from App.interfaces import ICSSPaths
+from App.interfaces import IJSPaths
from App.special_dtml import DTMLFile
from ExtensionClass import Base
from six.moves.urllib.parse import quote, unquote
from zExceptions import Redirect
from zope.interface import implementer
+import itertools
+import six
+import zope.event
try:
from html import escape
@@ -87,20 +92,22 @@ def manage_workspace(self, REQUEST):
def tabs_path_default(self, REQUEST):
steps = REQUEST._steps[:-1]
script = REQUEST['BASEPATH1']
- linkpat = '%s'
- out = []
- url = linkpat % (escape(script, True), ' /')
+ linkpat = '{}/manage_workspace'
+ yield {'url': linkpat.format(escape(script, True)),
+ 'title': 'Root',
+ 'last': not bool(steps)}
if not steps:
- return url
+ return
last = steps.pop()
for step in steps:
script = '%s/%s' % (script, step)
- out.append(linkpat % (escape(script, True), escape(unquote(step))))
+ yield {'url': linkpat.format(escape(script, True)),
+ 'title': escape(unquote(step)),
+ 'last': False}
script = '%s/%s' % (script, last)
- out.append(
- '%s' %
- (escape(script, True), escape(unquote(last), False)))
- return '%s%s' % (url, '/'.join(out))
+ yield {'url': linkpat.format(escape(script, True)),
+ 'title': escape(unquote(last)),
+ 'last': True}
def tabs_path_info(self, script, path):
out = []
@@ -141,9 +148,6 @@ class Navigation(Base):
security.declareProtected(view_management_screens, 'manage_menu')
manage_menu = DTMLFile('dtml/menu', globals())
- security.declareProtected(view_management_screens, 'manage_page_header')
- manage_page_header = DTMLFile('dtml/manage_page_header', globals())
-
security.declareProtected(view_management_screens, 'manage_page_footer')
manage_page_footer = DTMLFile('dtml/manage_page_footer', globals())
@@ -155,6 +159,21 @@ class Navigation(Base):
manage_form_title._setFuncSignature(
varnames=('form_title', 'help_product', 'help_topic'))
+ _manage_page_header = DTMLFile('dtml/manage_page_header', globals())
+ security.declareProtected(view_management_screens, 'manage_page_header')
+ def manage_page_header(self, *args, **kw):
+ """manage_page_header."""
+ kw['css_urls'] = itertools.chain(
+ itertools.chain(*zope.component.subscribers((self,), ICSSPaths)),
+ self._get_zmi_additionals('zmi_additional_css_paths'))
+ kw['js_urls'] = itertools.chain(
+ itertools.chain(*zope.component.subscribers((self,), IJSPaths)),
+ self._get_zmi_additionals('zmi_additional_js_paths'))
+ return self._manage_page_header(*args, **kw)
+
+ security.declareProtected(view_management_screens, 'manage_navbar')
+ manage_navbar = DTMLFile('dtml/manage_navbar', globals())
+
security.declarePublic('manage_zmi_logout')
def manage_zmi_logout(self, REQUEST, RESPONSE):
"""Logout current user"""
@@ -175,11 +194,14 @@ def manage_zmi_logout(self, REQUEST, RESPONSE):