--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,19 @@
+
+syntax: regexp
+^develop-eggs$
+syntax: regexp
+^parts$
+syntax: regexp
+^bin$
+syntax: regexp
+^\.installed\.cfg$
+syntax: regexp
+^\.settings$
+syntax: regexp
+^build$
+syntax: regexp
+^dist$
+syntax: regexp
+^\.idea$
+syntax: regexp
+.*\.pyc$
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.installed.cfg Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,103 @@
+[buildout]
+installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs/pyams-portal.egg-link
+parts = package i18n pyflakes test
+
+[package]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pcreate
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pdistreport
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/ptweens
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pshell
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pserve
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/prequest
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pviews
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/proutes
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-15.1-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+eggs = pyams_portal
+ pyramid
+ zope.component
+ zope.interface
+eggs-directory = /var/local/env/pyams/eggs
+recipe = zc.recipe.egg
+
+[i18n]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pybabel
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pot-create
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/polint
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-15.1-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+eggs = babel
+ lingua
+eggs-directory = /var/local/env/pyams/eggs
+recipe = zc.recipe.egg
+
+[pyflakes]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pyflakes
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/pyflakes
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-15.1-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+eggs = pyflakes
+eggs-directory = /var/local/env/pyams/eggs
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["src"]
+recipe = zc.recipe.egg
+scripts = pyflakes
+
+[test]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/parts/test
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/test
+__buildout_signature__ = zc.recipe.testrunner-2.0.0-py3.4.egg zc.recipe.egg-2.0.1-py3.4.egg setuptools-15.1-py3.4.egg zope.testrunner-4.4.6-py3.4.egg zc.buildout-2.3.1-py3.4.egg zope.interface-4.1.2-py3.4-linux-x86_64.egg zope.exceptions-4.0.7-py3.4.egg six-e6b62e54b4df360c40dfcbb76c1ecf1a
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs
+eggs = pyams_portal [test]
+eggs-directory = /var/local/env/pyams/eggs
+location = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/parts/test
+recipe = zc.recipe.testrunner
+script = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/bin/test
+
+[buildout]
+installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs/lingua.egg-link
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs/pyams-portal.egg-link
+
+[buildout]
+parts = i18n pyflakes test package
+
+[buildout]
+parts = pyflakes test package i18n
+
+[buildout]
+parts = test package i18n pyflakes
+
+[buildout]
+parts = package i18n pyflakes test
+
+[buildout]
+installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs/lingua.egg-link
+ /home/tflorac/Dropbox/src/PyAMS/pyams_portal/develop-eggs/pyams-portal.egg-link
+
+[buildout]
+parts = i18n pyflakes test package
+
+[buildout]
+parts = pyflakes test package i18n
+
+[buildout]
+parts = test package i18n pyflakes
+
+[buildout]
+parts = package i18n pyflakes test
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,42 @@
+Zope Public License (ZPL) Version 2.1
+=====================================
+
+A copyright notice accompanies this license document that identifies
+the copyright holders.
+
+This license has been certified as open source. It has also been designated
+as GPL compatible by the Free Software Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions in source code must retain the accompanying copyright
+ notice, this list of conditions, and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the accompanying copyright
+ notice, this list of conditions, and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Names of the copyright holders must not be used to endorse or promote
+ products derived from this software without prior written permission
+ from the copyright holders.
+ 4. The right to distribute this software or to use it for any purpose does
+ not give you the right to use Servicemarks (sm) or Trademarks (tm) of the
+ copyright holders. Use of them is covered by separate agreement with the
+ copyright holders.
+ 5. If any files are modified, you must cause the modified files to carry
+ prominent notices stating that you changed the files and the date of any
+ change.
+
+
+Disclaimer
+==========
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,5 @@
+include *.txt
+recursive-include docs *
+recursive-include src *
+global-exclude *.pyc
+global-exclude *.*~
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bootstrap.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,178 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", help="use a specific zc.buildout version")
+
+parser.add_option("-t", "--accept-buildout-test-releases",
+ dest='accept_buildout_test_releases',
+ action="store_true", default=False,
+ help=("Normally, if you do not specify a --version, the "
+ "bootstrap script and buildout gets the newest "
+ "*final* versions of zc.buildout and its recipes and "
+ "extensions for you. If you use this flag, "
+ "bootstrap and buildout will get the newest releases "
+ "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+ help=("Specify the path to the buildout configuration "
+ "file to be used."))
+parser.add_option("-f", "--find-links",
+ help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+ action="store_true", default=False,
+ help=("Let bootstrap.py use existing site packages"))
+
+
+options, args = parser.parse_args()
+
+######################################################################
+# load/install setuptools
+
+try:
+ if options.allow_site_packages:
+ import setuptools
+ import pkg_resources
+ from urllib.request import urlopen
+except ImportError:
+ from urllib2 import urlopen
+
+ez = {}
+exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+ # ez_setup imports site, which adds site packages
+ # this will remove them from the path to ensure that incompatible versions
+ # of setuptools are not in the path
+ import site
+ # inside a virtualenv, there is no 'getsitepackages'.
+ # We can't remove these reliably
+ if hasattr(site, 'getsitepackages'):
+ for sitepackage_path in site.getsitepackages():
+ sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set. We will
+# do it.
+for path in sys.path:
+ if path not in pkg_resources.working_set.entries:
+ pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+cmd = [sys.executable, '-c',
+ 'from setuptools.command.easy_install import main; main()',
+ '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+ 'bootstrap-testing-find-links',
+ options.find_links or
+ ('http://downloads.buildout.org/'
+ if options.accept_buildout_test_releases else None)
+ )
+if find_links:
+ cmd.extend(['-f', find_links])
+
+setuptools_path = ws.find(
+ pkg_resources.Requirement.parse('setuptools')).location
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+ # Figure out the most recent final version of zc.buildout.
+ import setuptools.package_index
+ _final_parts = '*final-', '*final'
+
+ def _final_version(parsed_version):
+ for part in parsed_version:
+ if (part[:1] == '*') and (part not in _final_parts):
+ return False
+ return True
+ index = setuptools.package_index.PackageIndex(
+ search_path=[setuptools_path])
+ if find_links:
+ index.add_find_links((find_links,))
+ req = pkg_resources.Requirement.parse(requirement)
+ if index.obtain(req) is not None:
+ best = []
+ bestv = None
+ for dist in index[req.project_name]:
+ distv = dist.parsed_version
+ if _final_version(distv):
+ if bestv is None or distv > bestv:
+ best = [dist]
+ bestv = distv
+ elif distv == bestv:
+ best.append(dist)
+ if best:
+ best.sort()
+ version = best[-1].version
+if version:
+ requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
+ raise Exception(
+ "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+ args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+ args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/buildout.cfg Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,61 @@
+[buildout]
+eggs-directory = /var/local/env/pyams/eggs
+
+socket-timeout = 3
+show-picked-versions = true
+newest = false
+
+allow-hosts =
+ bitbucket.org
+ *.python.org
+ *.sourceforge.net
+ github.com
+
+#extends = http://download.ztfy.org/webapp/ztfy.webapp.dev.cfg
+versions = versions
+newest = false
+#allow-picked-versions = false
+
+src = src
+develop =
+ .
+ /var/local/src/pyams/ext/lingua
+
+parts =
+ package
+ i18n
+ pyflakes
+ test
+
+[package]
+recipe = zc.recipe.egg
+eggs =
+ pyams_portal
+ pyramid
+ zope.component
+ zope.interface
+
+[i18n]
+recipe = zc.recipe.egg
+eggs =
+ babel
+ lingua
+
+[pyflakes]
+recipe = zc.recipe.egg
+eggs = pyflakes
+scripts = pyflakes
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"]
+
+[pyflakesrun]
+recipe = collective.recipe.cmd
+on_install = true
+cmds = ${buildout:develop}/bin/${pyflakes:scripts}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = pyams_portal [test]
+
+[versions]
+pyams_portal = 0.1.0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/README.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,68 @@
+
+
+Portal template configuration storage
+=====================================
+
+
+<PortalTemplate mon_template>
+ name = 'template1'
+ __annotations__['pyams_portal.template'] = PortalTemplateConfiguration:
+ __parent__ = mon_template
+ rows = 2
+ slot_names = ['slot1', 'slot2', 'slot3']
+ slot_order = {0: ['slot1', 'slot2],
+ 1: ['slot3']}
+ slots = {0: {'slot1': [1, 2],
+ 'slot2': [3, 4, 5]},
+ 1: {'slot3': []}},
+ slot_config = {'slot1': <SlotConfiguration>,
+ 'slot2': <SlotConfiguration>,
+ 'slot3': <SlotConfiguration>},
+ portlets = {1: 'portlet1',
+ 2: 'portlet2',
+ 3: 'portlet1',
+ 4: 'portlet3',
+ 5: 'portlet1'},
+ __annotations__['pyams_portal.portlets'] = PortalPortletsConfiguration:
+ portlet_config = {1: <Portlet1Configuration(portlet='portlet1')>,
+ 2: <Portlet2Configuration(portlet='portlet2')>},
+ 3: <Portlet1Configuration(portlet='portlet1')>,
+ 4: <Portlet3Configuration(portlet='portlet3')>,
+ 5: <Portlet1Configuration(portlet='portlet1')>}}
+
+
+<PortalContext mon_contexte>
+ shared_template = 'template1'
+ template = mon_template
+ __annotations__['pyams_portal.template'] = PortalTemplateConfiguration:
+ __parent__ = mon_contexte
+ slots = {0: {'slot1': [1, 2],
+ 'slot2': [3, 4, 5]},
+ 1: {'slot3': []}},
+ slot_config = {'slot1': <SlotConfiguration>,
+ 'slot2': <SlotConfiguration>,
+ 'slot3': <SlotConfiguration>},
+ __annotations__['pyams_portal.portlets'] = PortalPortletsConfiguration:
+ portlet_config = {1: <Portlet1Configuration(portlet='portlet1')>,
+ 2: <Portlet2Configuration(portlet='portlet2')>},
+ 3: <Portlet1Configuration(portlet='portlet1')>,
+ 4: <Portlet3Configuration(portlet='portlet3')>,
+ 5: <Portlet1Configuration(portlet='portlet1')>}}
+
+
+<IPortalContext context1>:
+ <PortalPage>
+ __parent__ = context1
+ inherit_parent = False
+ use_local_template = False
+ shared_template = 'template1'
+
+
+<IPortalContext context2>:
+ <PortalPage>
+ __parent__ = context2
+ inherit_parent = False
+ use_local_template = True
+ local_template = <PortalWfTemplate ++template++>
+ __annotations__['pyams_portal.portlets'] = PortalPortletsConfiguration:
+ __parent__ = ++template++
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,69 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+"""
+This module contains pyams_portal package
+"""
+import os
+from setuptools import setup, find_packages
+
+DOCS = os.path.join(os.path.dirname(__file__),
+ 'docs')
+
+README = os.path.join(DOCS, 'README.txt')
+HISTORY = os.path.join(DOCS, 'HISTORY.txt')
+
+version = '0.1.0'
+long_description = open(README).read() + '\n\n' + open(HISTORY).read()
+
+tests_require = []
+
+setup(name='pyams_portal',
+ version=version,
+ description="PyAMS portal and portlets interfaces and classes",
+ long_description=long_description,
+ classifiers=[
+ "License :: OSI Approved :: Zope Public License",
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords='Pyramid PyAMS portal portlets',
+ author='Thierry Florac',
+ author_email='tflorac@ulthar.net',
+ url='http://hg.ztfy.org/pyams/pyams_portal',
+ license='ZPL',
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ namespace_packages=[],
+ include_package_data=True,
+ package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo',
+ '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']},
+ zip_safe=False,
+ # uncomment this to be able to run tests with setup.py
+ test_suite="pyams_portal.tests.test_utilsdocs.test_suite",
+ tests_require=tests_require,
+ extras_require=dict(test=tests_require),
+ install_requires=[
+ 'setuptools',
+ # -*- Extra requirements: -*-
+ 'fanstatic',
+ 'pyramid',
+ 'zope.component',
+ 'zope.interface',
+ ],
+ entry_points={
+ 'fanstatic.libraries': [
+ 'pyams_portal = pyams_portal:library'
+ ]
+ })
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/PKG-INFO Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,86 @@
+Metadata-Version: 1.1
+Name: pyams-portal
+Version: 0.1.0
+Summary: PyAMS portal and portlets interfaces and classes
+Home-page: http://hg.ztfy.org/pyams/pyams_portal
+Author: Thierry Florac
+Author-email: tflorac@ulthar.net
+License: ZPL
+Description:
+
+ Portal template configuration storage
+ =====================================
+
+
+ <PortalTemplate mon_template>
+ name = 'template1'
+ __annotations__['pyams_portal.template'] = PortalTemplateConfiguration:
+ __parent__ = mon_template
+ rows = 2
+ slot_names = ['slot1', 'slot2', 'slot3']
+ slot_order = {0: ['slot1', 'slot2],
+ 1: ['slot3']}
+ slots = {0: {'slot1': [1, 2],
+ 'slot2': [3, 4, 5]},
+ 1: {'slot3': []}},
+ slot_config = {'slot1': <SlotConfiguration>,
+ 'slot2': <SlotConfiguration>,
+ 'slot3': <SlotConfiguration>},
+ portlets = {1: 'portlet1',
+ 2: 'portlet2',
+ 3: 'portlet1',
+ 4: 'portlet3',
+ 5: 'portlet1'},
+ __annotations__['pyams_portal.portlets'] = PortalPortletsConfiguration:
+ portlet_config = {1: <Portlet1Configuration(portlet='portlet1')>,
+ 2: <Portlet2Configuration(portlet='portlet2')>},
+ 3: <Portlet1Configuration(portlet='portlet1')>,
+ 4: <Portlet3Configuration(portlet='portlet3')>,
+ 5: <Portlet1Configuration(portlet='portlet1')>}}
+
+
+ <PortalContext mon_contexte>
+ shared_template = 'template1'
+ template = mon_template
+ __annotations__['pyams_portal.template'] = PortalTemplateConfiguration:
+ __parent__ = mon_contexte
+ slots = {0: {'slot1': [1, 2],
+ 'slot2': [3, 4, 5]},
+ 1: {'slot3': []}},
+ slot_config = {'slot1': <SlotConfiguration>,
+ 'slot2': <SlotConfiguration>,
+ 'slot3': <SlotConfiguration>},
+ __annotations__['pyams_portal.portlets'] = PortalPortletsConfiguration:
+ portlet_config = {1: <Portlet1Configuration(portlet='portlet1')>,
+ 2: <Portlet2Configuration(portlet='portlet2')>},
+ 3: <Portlet1Configuration(portlet='portlet1')>,
+ 4: <Portlet3Configuration(portlet='portlet3')>,
+ 5: <Portlet1Configuration(portlet='portlet1')>}}
+
+
+ <IPortalContext context1>:
+ <PortalPage>
+ __parent__ = context1
+ inherit_parent = False
+ use_local_template = False
+ shared_template = 'template1'
+
+
+ <IPortalContext context2>:
+ <PortalPage>
+ __parent__ = context2
+ inherit_parent = False
+ use_local_template = True
+ local_template = <PortalWfTemplate ++template++>
+ __annotations__['pyams_portal.portlets'] = PortalPortletsConfiguration:
+ __parent__ = ++template++
+
+
+
+Keywords: Pyramid PyAMS portal portlets
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: Zope Public License
+Classifier: Development Status :: 4 - Beta
+Classifier: Programming Language :: Python
+Classifier: Framework :: Pyramid
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/SOURCES.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,54 @@
+MANIFEST.in
+setup.py
+docs/HISTORY.txt
+docs/README.txt
+src/pyams_portal/__init__.py
+src/pyams_portal/include.py
+src/pyams_portal/page.py
+src/pyams_portal/portlet.py
+src/pyams_portal/site.py
+src/pyams_portal/slot.py
+src/pyams_portal/template.py
+src/pyams_portal/workflow.py
+src/pyams_portal.egg-info/PKG-INFO
+src/pyams_portal.egg-info/SOURCES.txt
+src/pyams_portal.egg-info/dependency_links.txt
+src/pyams_portal.egg-info/entry_points.txt
+src/pyams_portal.egg-info/namespace_packages.txt
+src/pyams_portal.egg-info/not-zip-safe
+src/pyams_portal.egg-info/requires.txt
+src/pyams_portal.egg-info/top_level.txt
+src/pyams_portal/doctests/README.txt
+src/pyams_portal/interfaces/__init__.py
+src/pyams_portal/locales/pyams_portal.pot
+src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.mo
+src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.po
+src/pyams_portal/portlets/__init__.py
+src/pyams_portal/portlets/context/__init__.py
+src/pyams_portal/portlets/context/context.pt
+src/pyams_portal/portlets/context/interfaces.py
+src/pyams_portal/portlets/image/__init__.py
+src/pyams_portal/portlets/image/image.pt
+src/pyams_portal/portlets/image/interfaces.py
+src/pyams_portal/resources/css/portal.css
+src/pyams_portal/resources/css/portal.min.css
+src/pyams_portal/resources/js/portal.js
+src/pyams_portal/resources/js/portal.min.js
+src/pyams_portal/resources/less/portal.less
+src/pyams_portal/tests/__init__.py
+src/pyams_portal/tests/test_utilsdocs.py
+src/pyams_portal/tests/test_utilsdocstrings.py
+src/pyams_portal/zmi/__init__.py
+src/pyams_portal/zmi/container.py
+src/pyams_portal/zmi/interfaces.py
+src/pyams_portal/zmi/portlet.py
+src/pyams_portal/zmi/portlets/__init__.py
+src/pyams_portal/zmi/portlets/context.py
+src/pyams_portal/zmi/portlets/image.py
+src/pyams_portal/zmi/portlets/templates/context-preview.pt
+src/pyams_portal/zmi/portlets/templates/image-preview.pt
+src/pyams_portal/zmi/template/__init__.py
+src/pyams_portal/zmi/template/config.py
+src/pyams_portal/zmi/template/page.py
+src/pyams_portal/zmi/template/workflow.py
+src/pyams_portal/zmi/template/templates/config.pt
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/dependency_links.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/entry_points.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,3 @@
+[fanstatic.libraries]
+pyams_portal = pyams_portal:library
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/namespace_packages.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/not-zip-safe Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/requires.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,7 @@
+setuptools
+fanstatic
+pyramid
+zope.component
+zope.interface
+
+[test]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal.egg-info/top_level.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+pyams_portal
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+from fanstatic import Library
+library = Library('pyams_portal', 'resources')
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_portal')
+
+
+def includeme(config):
+ """Pyramid include"""
+
+ from .include import include_package
+ include_package(config)
+
+ # register custom permissions
+ config.register_permission({'id': 'portal.templates.manage',
+ 'title': _("Manage portal templates")})
+
+ # register custom roles
+ config.register_role({'id': 'portal.TemplatesManager',
+ 'title': _("Portal templates manager"),
+ 'permissions': {'portal.templates.manage', 'view', 'system.view'}})
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/doctests/README.txt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,3 @@
+====================
+pyams_portal package
+====================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/include.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+def include_package(config):
+ """Pyramid include"""
+
+ # add translations
+ config.add_translation_dirs('pyams_portal:locales')
+
+ # load registry components
+ try:
+ import pyams_zmi
+ except ImportError:
+ config.scan(ignore='pyams_portal.zmi')
+ else:
+ config.scan()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/interfaces/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,357 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_workflow.interfaces import IWorkflowManagedContent
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IContainer
+from zope.contentprovider.interfaces import IContentProvider
+
+# import packages
+from pyams_security.schema import PermissionField
+from pyams_utils.schema import PersistentDict, PersistentList
+from zope.container.constraints import contains
+from zope.interface import invariant, Interface, Attribute, Invalid
+from zope.schema import List, TextLine, Object, Int, Bool, Choice
+
+from pyams_portal import _
+
+
+class IPortlet(Interface):
+ """Portlet interface"""
+
+ name = Attribute("Portlet internal name")
+
+ label = Attribute("Portlet visible name")
+
+ permission = PermissionField(title="Portlet permission",
+ description="Permission required to display permission",
+ required=False)
+
+ toolbar_image = Attribute("Porlet toolbar image")
+
+ toolbar_css_class = Attribute("Portlet toolbar CSS class")
+
+
+class IPortletAddingInfo(Interface):
+ """Portlet adding info interface"""
+
+ portlet_name = Choice(title=_("Portlet"),
+ vocabulary="PyAMS portal portlets")
+
+ slot_name = Choice(title=_("Slot name"),
+ description=_("Slot name to which this configuration applies"),
+ required=True,
+ vocabulary='PyAMS template slots')
+
+
+class IPortletConfiguration(Interface):
+ """Portlet configuration interface"""
+
+ template = Attribute("Template to which this configuration applis")
+
+ slot_name = TextLine(title=_("Slot name"),
+ description=_("Slot name to which this configuration applies"),
+ required=True)
+
+ portlet_name = Attribute("Portlet name")
+
+ position = Int(title=_("Position"),
+ description=_("Portlet position inside slot"),
+ required=True,
+ min=0)
+
+ visible = Bool(title=_("Visible portlet?"),
+ description=_("Select 'no' to hide this portlet. This will not break configuration inheritance..."),
+ required=True,
+ default=True)
+
+ can_inherit = Attribute("Can inherit parent configuration?")
+
+ inherit_parent = Bool(title=_("Inherit parent configuration?"),
+ description=_("This option is only available if context's parent is using the same template "
+ "and if this portlet is also present in the same slot..."),
+ required=True,
+ default=True)
+
+
+class IPortletContentProvider(IContentProvider):
+ """Portlet content provider"""
+
+ portlet = Object(title="Portlet utility",
+ schema=IPortlet)
+
+ configuration = Object(title="Portlet renderer configuration",
+ schema=IPortletConfiguration)
+
+
+class IPortletPreviewer(IPortletContentProvider):
+ """Portlet previewer interface
+
+ A portlet previewer should be defined as an adapter for a context,
+ a request, a view and a portlet
+ """
+
+
+class IPortletRenderer(IPortletContentProvider):
+ """Portlet renderer interface
+
+ A portlet renderer should be defined as an adapter for a context,
+ a request, a view and a portlet
+ """
+
+
+class ISlot(Interface):
+ """Portal template slot interface"""
+
+ name = TextLine(title=_("Slot name"),
+ description=_("This name must be unique in a given template"),
+ required=True)
+
+ row_id = Int(title=_("Row ID"),
+ required=False)
+
+
+class ISlotConfiguration(Interface):
+ """Portal slot configuration"""
+
+ template = Attribute("Slot template")
+
+ slot_name = TextLine(title="Slot name")
+
+ visible = Bool(title=_("Visible slot?"),
+ description=_("Select 'no' to hide this slot. This will not break configuration inheritance..."),
+ required=True,
+ default=True)
+
+ can_inherit = Attribute("Can inherit parent configuration?")
+
+ inherit_parent = Bool(title=_("Inherit parent configuration?"),
+ description=_("This option is only available if context's parent template is using a "
+ "template containing the same slot..."),
+ required=True,
+ default=True)
+
+ xs_width = Int(title=_("Extra small device width"),
+ description=_("Slot width, in columns count, on extra small devices (phones...); "
+ "set to 0 to hide the portlet"),
+ required=False,
+ min=0,
+ max=12)
+
+ sm_width = Int(title=_("Small device width"),
+ description=_("Slot width, in columns count, on small devices (tablets...); "
+ "set to 0 to hide the portlet"),
+ required=False,
+ min=0,
+ max=12)
+
+ md_width = Int(title=_("Medium devices width"),
+ description=_("Slot width, in columns count, on medium desktop devices (>= 992 pixels); "
+ "set to 0 to hide the portlet"),
+ required=False,
+ min=0,
+ max=12)
+
+ lg_width = Int(title=_("Large devices width"),
+ description=_("Slot width, in columns count, on large desktop devices (>= 1200 pixels); "
+ "set to 0 to hide the portlet"),
+ required=False,
+ min=0,
+ max=12)
+
+ css_class = TextLine(title=_("CSS class"),
+ description=_("CSS class applied to this slot"),
+ required=False)
+
+ def get_css_class(self, device=None):
+ """Get current CSS class"""
+
+ def get_width(self, device=None):
+ """Get slot width for each or given device"""
+
+ def set_width(self, width, device=None):
+ """Set width in columns count for given device"""
+
+
+class ISlotRenderer(IContentProvider):
+ """Slot renderer"""
+
+
+class IPortalTemplateConfiguration(Interface):
+ """Portal template configuration interface"""
+
+ rows = Int(title="Rows count",
+ required=True,
+ default=1,
+ min=0)
+
+ def add_row(self):
+ """Add new row"""
+
+ def set_row_order(self, order):
+ """Change template rows order"""
+
+ def delete_row(self, row_id):
+ """Delete template row"""
+
+ slot_names = PersistentList(title="Slot names",
+ value_type=TextLine())
+
+ slot_order = PersistentDict(title="Solts order",
+ key_type=Int(), # row index
+ value_type=PersistentList(value_type=TextLine())) # slot name
+
+ slots = PersistentDict(title="Slots portlets",
+ description="List of slots associated with a given template",
+ key_type=Int(), # row index
+ value_type=PersistentDict(key_type=TextLine(), # slot name
+ value_type=PersistentList(value_type=Choice(
+ vocabulary='PyAMS portal portlets')), # portlet names
+ required=False))
+
+ slot_config = PersistentDict(title="Slots configuration",
+ key_type=TextLine(), # slot name
+ value_type=Object(schema=ISlotConfiguration),
+ required=False)
+
+ def add_slot(self, slot_name, row_id=None):
+ """Add slot with given name"""
+
+ def set_slot_order(self, order):
+ """Change template slots order"""
+
+ def get_slot_row(self, slot_name):
+ """Get row containing given slot"""
+
+ def get_slots(self, row_id):
+ """Get ordered list of slots for given row ID"""
+
+ def get_slots_width(self, device=None):
+ """Get slots width for given or all device(s)"""
+
+ def set_slot_width(self, slot_name, device, width):
+ """Set slot width for given device"""
+
+ def get_slot_configuration(self, slot_name):
+ """Get slot configuration for given slot"""
+
+ def delete_slot(self, slot_name):
+ """Delete template slot"""
+
+
+# class IPortalPortletsConfiguration(Interface):
+# """Portal template portlets configuration interface"""
+#
+ portlet_config = PersistentDict(title="Portlet configuration",
+ key_type=TextLine(), # slot name
+ value_type=PersistentDict(key_type=Int(min=0), # portlet position inside slot
+ value_type=Object(schema=IPortletConfiguration)),
+ required=False)
+
+ def add_portlet(self, portlet_name, slot_name):
+ """Add portlet to givben slot"""
+
+ def set_portlet_order(self, order):
+ """Set template portlets order"""
+
+ def get_portlet_configuration(self, slot_name, position):
+ """Get portlet configuration for given slot"""
+
+ def delete_portlet(self, slot_name, position):
+ """Delete template portlet"""
+
+
+class IPortalTemplate(IAttributeAnnotatable):
+ """Portal template interface
+
+ A portal template is a named utility providing a name and a set of slots.
+ """
+
+ name = TextLine(title=_("Template name"),
+ description=_("Two registered templates can't share the same name..."),
+ required=True)
+
+
+class IPortalWfTemplate(IWorkflowManagedContent):
+ """Workflow managed portal template interface"""
+
+
+class IPortalTemplateContainer(IContainer, IAttributeAnnotatable):
+ """Portal template container interface"""
+
+ contains(IPortalTemplate)
+
+
+class IPortalTemplateContainerConfiguration(Interface):
+ """Portal templates container configuration"""
+
+ selected_portlets = List(title=_("Selected portlets"),
+ description=_("These portlets will be directly available in templates configuration "
+ "page toolbar"),
+ value_type=Choice(vocabulary="PyAMS portal portlets"),
+ required=False)
+
+
+class IPortalTemplateRenderer(IContentProvider):
+ """Portal template renderer
+
+ A portal template renderer should be implemented as an adapter for a context, a request
+ and a template
+ """
+
+
+class IPortalPage(Interface):
+ """Portal page interface
+
+ The page is the highest configuration level.
+ It defines which template is used (a shared or local one), which gives
+ the slots list.
+ """
+
+ can_inherit = Attribute("Can inherit parent template?")
+
+ inherit_parent = Bool(title=_("Inherit parent template?"),
+ description=_("Should we reuse parent template?"),
+ required=True,
+ default=True)
+
+ use_local_template = Bool(title=_("Use local template?"),
+ description=_("If 'yes', you can define a custom local template instead of "
+ "a shared template"),
+ required=True,
+ default=False)
+
+ shared_template = Choice(title=_("Page template"),
+ description=_("Template used for this page"),
+ vocabulary='PyAMS portal templates',
+ required=False)
+
+ @invariant
+ def check_template(self):
+ if not (self.use_local_template or self.shared_template):
+ raise Invalid(_("You must choose to use a local template or select a shared one!"))
+
+ local_template = Object(title=_("Local template"),
+ schema=IPortalWfTemplate,
+ required=False)
+
+ template = Attribute("Used template")
+
+
+class IPortalContext(IAttributeAnnotatable):
+ """Portal context marker interface"""
Binary file src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.mo has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.po Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,504 @@
+#
+# French translations for PACKAGE package
+# This file is distributed under the same license as the PACKAGE package.
+# Thierry Florac <tflorac@ulthar.net>, 2015.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-05-12 13:59+0200\n"
+"PO-Revision-Date: 2015-05-12 12:10+0200\n"
+"Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
+"Language-Team: French <traduc@traduc.org>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/pyams_portal/workflow.py:41
+msgid "Draft"
+msgstr "Brouillon"
+
+#: src/pyams_portal/workflow.py:42
+msgid "Published"
+msgstr "Publié"
+
+#: src/pyams_portal/workflow.py:43
+msgid "Retired"
+msgstr "Retiré"
+
+#: src/pyams_portal/workflow.py:44
+msgid "Archived"
+msgstr "Archivé"
+
+#: src/pyams_portal/workflow.py:45
+msgid "Deleted"
+msgstr "Supprimé"
+
+#: src/pyams_portal/workflow.py:103
+msgid "Initialize"
+msgstr "Initialiser"
+
+#: src/pyams_portal/workflow.py:108
+msgid "Publish..."
+msgstr "Publier..."
+
+#: src/pyams_portal/workflow.py:116
+msgid ""
+"This content is currently in DRAFT mode.\n"
+" Publishing it will make it "
+"publicly visible."
+msgstr ""
+"Ce modèle est actuellement en mode BROUILLON.\n"
+"En le publiant, vous le rendrez visible."
+
+#: src/pyams_portal/workflow.py:120
+msgid "Retire..."
+msgstr "Retirer..."
+
+#: src/pyams_portal/workflow.py:128
+msgid ""
+"This content is actually published.\n"
+" You can retire it to make "
+"it invisible, but contents using this\n"
+" template won't be visible "
+"anymore!"
+msgstr ""
+"Ce modèle est actuellement publié.\n"
+"Vous pouvez le retirer pour le rendre invisible, mais les contenus qui "
+"utilisent ce modèle ne seront plus consultables !"
+
+#: src/pyams_portal/workflow.py:133 src/pyams_portal/workflow.py:181
+msgid "Create new version..."
+msgstr "Créer une nouvelle version..."
+
+#: src/pyams_portal/workflow.py:143
+msgid "Re-publish..."
+msgstr "Re-publier..."
+
+#: src/pyams_portal/workflow.py:151
+msgid ""
+"This content was published and retired.\n"
+" You can re-publish it to "
+"make it visible again."
+msgstr ""
+"Ce modèle a été publié puis retiré.\n"
+"Vous pouvez le re-publier pour le rendre à nouveau disponible."
+
+#: src/pyams_portal/workflow.py:155 src/pyams_portal/workflow.py:168
+msgid "Archive..."
+msgstr "Archiver..."
+
+#: src/pyams_portal/workflow.py:163
+msgid ""
+"This content is currently published.\n"
+" If it is archived, it will "
+"not be possible to make it visible again\n"
+" except by creating a new "
+"version!"
+msgstr ""
+"Ce modèle est actuellement publié.\n"
+"S'il est archivé, il ne sera plus possible de le rendre à nouveau "
+"disponible, sauf en créant une nouvelle version."
+
+#: src/pyams_portal/workflow.py:176
+msgid ""
+"This content has been published but is currently retired.\n"
+" If it is archived, it will "
+"not be possible to make it visible again\n"
+" except by creating a new "
+"version!"
+msgstr ""
+"Ce contenu a été publié mais est actuellement retiré.\n"
+"S'il est archivé, il ne sera plus possible de le rendre à nouveau "
+"disponible, sauf en créant une nouvelle version."
+
+#: src/pyams_portal/workflow.py:191
+msgid "Delete..."
+msgstr "Supprimer..."
+
+#: src/pyams_portal/workflow.py:199
+msgid ""
+"This content has never been published.\n"
+" It can be removed and definitely deleted."
+msgstr ""
+"Ce modèle n'a jamais été publié.\n"
+"Vous pouvez donc le supprimer définitivement."
+
+#: src/pyams_portal/__init__.py:31
+msgid "Manage portal templates"
+msgstr "Gérer les modèles de présentation"
+
+#: src/pyams_portal/__init__.py:35
+msgid "Portal templates manager"
+msgstr "Gestionnaire des modèles"
+
+#: src/pyams_portal/zmi/portlet.py:41
+msgid "Edit portlet configuration"
+msgstr "Modifier la configuration d'un modèle"
+
+#: src/pyams_portal/zmi/portlet.py:38
+#, python-format
+msgid "« {0} » portal template - {1}"
+msgstr "Modèle de présentation « {0} » - {1}"
+
+#: src/pyams_portal/zmi/template/config.py:61
+msgid "Properties"
+msgstr "Propriétés"
+
+#: src/pyams_portal/zmi/template/config.py:72
+msgid "Portal template configuration"
+msgstr "Configuration d'un modèle"
+
+#: src/pyams_portal/zmi/template/config.py:120
+msgid "Portlets configuration"
+msgstr "Configuration des portlets"
+
+#: src/pyams_portal/zmi/template/config.py:133
+msgid "Add row..."
+msgstr "Ajouter une ligne..."
+
+#: src/pyams_portal/zmi/template/config.py:175
+msgid "Add slot..."
+msgstr "Ajouter un slot..."
+
+#: src/pyams_portal/zmi/template/config.py:191
+msgid "Add slot"
+msgstr "Ajout d'un slot"
+
+#: src/pyams_portal/zmi/template/config.py:265
+msgid "Edit slot properties"
+msgstr "Propriétés d'un slot"
+
+#: src/pyams_portal/zmi/template/config.py:333
+msgid "Add portlet..."
+msgstr "Ajouter un composant..."
+
+#: src/pyams_portal/zmi/template/config.py:349
+msgid "Add portlet"
+msgstr "Ajouter un composant"
+
+#: src/pyams_portal/zmi/template/config.py:209
+#: src/pyams_portal/zmi/template/__init__.py:269
+msgid "Specified name is already used!"
+msgstr "Le nom indiqué est déjà utilisé !"
+
+#: src/pyams_portal/zmi/template/config.py:118
+#: src/pyams_portal/zmi/template/config.py:189
+#: src/pyams_portal/zmi/template/config.py:347
+#, python-format
+msgid "« {0} » portal template"
+msgstr "Modèle de présentation « {0} »"
+
+#: src/pyams_portal/zmi/template/config.py:262
+#, python-format
+msgid "« {0} » portal template - {1} slot"
+msgstr "Modèle de présentation « {0} » - Slot {1}"
+
+#: src/pyams_portal/zmi/template/workflow.py:109
+msgid "Publish template"
+msgstr "Publier un modèle"
+
+#: src/pyams_portal/zmi/template/workflow.py:151
+msgid "Retire template"
+msgstr "Retirer un modèle"
+
+#: src/pyams_portal/zmi/template/workflow.py:180
+msgid "Archive template"
+msgstr "Archiver un modèle"
+
+#: src/pyams_portal/zmi/template/workflow.py:209
+#: src/pyams_portal/zmi/template/workflow.py:201
+msgid "Create new version"
+msgstr "Créer une nouvelle version"
+
+#: src/pyams_portal/zmi/template/workflow.py:100
+#: src/pyams_portal/zmi/template/workflow.py:142
+#: src/pyams_portal/zmi/template/workflow.py:171
+#: src/pyams_portal/zmi/template/workflow.py:200
+msgid "Close"
+msgstr "Fermer"
+
+#: src/pyams_portal/zmi/template/workflow.py:101
+msgid "Publish"
+msgstr "Publier"
+
+#: src/pyams_portal/zmi/template/workflow.py:143
+msgid "Retire"
+msgstr "Retirer"
+
+#: src/pyams_portal/zmi/template/workflow.py:172
+msgid "Archive"
+msgstr "Archiver"
+
+#: src/pyams_portal/zmi/template/__init__.py:88
+#: src/pyams_portal/zmi/template/__init__.py:196
+#: src/pyams_portal/zmi/template/__init__.py:240
+msgid "Portal templates"
+msgstr "Modèles de présentation"
+
+#: src/pyams_portal/zmi/template/__init__.py:97
+msgid "Shared portal templates"
+msgstr "Modèles de présentation partagés"
+
+#: src/pyams_portal/zmi/template/__init__.py:163
+msgid "Delete template"
+msgstr "Supprimer le modèle"
+
+#: src/pyams_portal/zmi/template/__init__.py:195
+msgid "Portal"
+msgstr "Portail"
+
+#: src/pyams_portal/zmi/template/__init__.py:229
+msgid "Add shared template..."
+msgstr "Ajouter un modèle partagé..."
+
+#: src/pyams_portal/zmi/template/__init__.py:241
+msgid "Add shared template"
+msgstr "Ajout d'un modèle de présentation"
+
+#: src/pyams_portal/zmi/template/__init__.py:153
+msgid "Older versions"
+msgstr "Versions précédentes"
+
+#: src/pyams_portal/zmi/template/__init__.py:212
+#: src/pyams_portal/zmi/template/__init__.py:146
+#, python-format
+msgid "Version {version} ({state} - last update {date})"
+msgstr "Version {version} ({state} - dernière modification {date})"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:15
+#: src/pyams_portal/zmi/template/templates/config.pt:29
+msgid "Version ${version} - ${state}"
+msgstr "Version ${version} - ${state}"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:42
+msgid "Selected display:"
+msgstr "Type de périphérique sélectionné :"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:47
+msgid "Current device"
+msgstr "Périphérique actuel"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:48
+msgid "Extra small device (phone)"
+msgstr "Très petits périphériques (téléphone)"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:49
+msgid "Small device (tablet)"
+msgstr "Petits périphériques (tablette)"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:50
+msgid "Medium desktop device (> 970px)"
+msgstr "Écrans de taille moyenne (> 970 px)"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:51
+msgid "Large desktop device (> 1170px)"
+msgstr "Écrans de grande taille (> 1170 px)"
+
+#: src/pyams_portal/zmi/template/templates/config.pt:111
+msgid "Delete row..."
+msgstr "Supprimer la ligne..."
+
+#: src/pyams_portal/zmi/template/templates/config.pt:119
+msgid "Edit slot properties..."
+msgstr "Propriétés..."
+
+#: src/pyams_portal/zmi/template/templates/config.pt:126
+msgid "Delete slot..."
+msgstr "Supprimer le slot..."
+
+#: src/pyams_portal/zmi/template/templates/config.pt:134
+msgid "Edit portlet properties..."
+msgstr "Propriétés..."
+
+#: src/pyams_portal/zmi/template/templates/config.pt:141
+msgid "Delete portlet..."
+msgstr "Supprimer le composant..."
+
+#: src/pyams_portal/portlets/context/__init__.py:43
+msgid "Context content"
+msgstr "Contenu du contexte"
+
+#: src/pyams_portal/portlets/image/__init__.py:44
+msgid "Image"
+msgstr "Image"
+
+#: src/pyams_portal/portlets/image/interfaces.py:30
+msgid "Selected image"
+msgstr "Image sélectionnée"
+
+#: src/pyams_portal/interfaces/__init__.py:49
+msgid "Portlet"
+msgstr "Composant"
+
+#: src/pyams_portal/interfaces/__init__.py:52
+#: src/pyams_portal/interfaces/__init__.py:63
+#: src/pyams_portal/interfaces/__init__.py:117
+msgid "Slot name"
+msgstr "Nom du slot"
+
+#: src/pyams_portal/interfaces/__init__.py:53
+#: src/pyams_portal/interfaces/__init__.py:64
+msgid "Slot name to which this configuration applies"
+msgstr "Nom du slot correspondant à la configuration"
+
+#: src/pyams_portal/interfaces/__init__.py:69
+msgid "Position"
+msgstr "Position"
+
+#: src/pyams_portal/interfaces/__init__.py:70
+msgid "Portlet position inside slot"
+msgstr "Position du composant au sein du slot"
+
+#: src/pyams_portal/interfaces/__init__.py:74
+msgid "Visible portlet?"
+msgstr "Composant visible ?"
+
+#: src/pyams_portal/interfaces/__init__.py:75
+msgid ""
+"Select 'no' to hide this portlet. This will not break configuration "
+"inheritance..."
+msgstr ""
+"Sélectionnez 'non' pour masquer ce composant. Ce paramètre pourra être "
+"surchargé par héritage au sein des pages qui utilisent ce composant."
+
+#: src/pyams_portal/interfaces/__init__.py:81
+#: src/pyams_portal/interfaces/__init__.py:139
+msgid "Inherit parent configuration?"
+msgstr "Hériter de la configuration du parent ?"
+
+#: src/pyams_portal/interfaces/__init__.py:82
+msgid ""
+"This option is only available if context's parent is using the same template "
+"and if this portlet is also present in the same slot..."
+msgstr ""
+"Cette option n'est disponible que si le parent utilise le même modèle de "
+"présentation et si ce composant est bien présent dans le même slot..."
+
+#: src/pyams_portal/interfaces/__init__.py:118
+msgid "This name must be unique in a given template"
+msgstr "Ce nom doit être unique au sein d'un modèle de présentation"
+
+#: src/pyams_portal/interfaces/__init__.py:121
+msgid "Row ID"
+msgstr "ID de la ligne"
+
+#: src/pyams_portal/interfaces/__init__.py:132
+msgid "Visible slot?"
+msgstr "Slot visible ?"
+
+#: src/pyams_portal/interfaces/__init__.py:133
+msgid ""
+"Select 'no' to hide this slot. This will not break configuration "
+"inheritance..."
+msgstr ""
+"Sélectionnez 'non' pour marquer ce slot. Ce paramètre pourra être surchargé "
+"par héritage..."
+
+#: src/pyams_portal/interfaces/__init__.py:140
+msgid ""
+"This option is only available if context's parent template is using a "
+"template containing the same slot..."
+msgstr ""
+"Cette option n'est disponible que si le parent utilise un modèle contenant "
+"un slot de même nom..."
+
+#: src/pyams_portal/interfaces/__init__.py:145
+msgid "Extra small device width"
+msgstr "Largeur sur très petits périphériques"
+
+#: src/pyams_portal/interfaces/__init__.py:146
+msgid ""
+"Slot width, in columns count, on extra small devices (phones...); set to 0 "
+"to hide the portlet"
+msgstr ""
+"Largeur du slot, en nombre de colonnes, sur les très petits périphériques "
+"(téléphones...) ; indiquez une valeur de 0 pour masquer ce composant"
+
+#: src/pyams_portal/interfaces/__init__.py:152
+msgid "Small device width"
+msgstr "Largeur sur petits périphériques"
+
+#: src/pyams_portal/interfaces/__init__.py:153
+msgid ""
+"Slot width, in columns count, on small devices (tablets...); set to 0 to "
+"hide the portlet"
+msgstr ""
+"Largeur du slot, en nombre de colonnes, sur les petits périphériques "
+"(tablettes...) ; indiquez une valeur de 0 pour masquer ce composant"
+
+#: src/pyams_portal/interfaces/__init__.py:159
+msgid "Medium devices width"
+msgstr "Largeur sur périphériques moyens"
+
+#: src/pyams_portal/interfaces/__init__.py:160
+msgid ""
+"Slot width, in columns count, on medium desktop devices (>= 992 pixels); set "
+"to 0 to hide the portlet"
+msgstr ""
+"Largeur du slot, en nombre de colonnes, sur les périphériques moyens (>= 992 "
+"pixels) ; indiquez une valeur de 0 pour masquer ce composant"
+
+#: src/pyams_portal/interfaces/__init__.py:166
+msgid "Large devices width"
+msgstr "Largeur sur grands périphériques"
+
+#: src/pyams_portal/interfaces/__init__.py:167
+msgid ""
+"Slot width, in columns count, on large desktop devices (>= 1200 pixels); set "
+"to 0 to hide the portlet"
+msgstr ""
+"Largeur du slot, en nombre de colonnes, sur les grands périphériques (>= "
+"1200 pixels) ; indiquez une valeur de 0 pour masquer ce composant"
+
+#: src/pyams_portal/interfaces/__init__.py:173
+msgid "CSS class"
+msgstr "Class CSS"
+
+#: src/pyams_portal/interfaces/__init__.py:174
+msgid "CSS class applied to this slot"
+msgstr "Classe CSS spécifique appliquée à ce slot"
+
+#: src/pyams_portal/interfaces/__init__.py:276
+msgid "Template name"
+msgstr "Nom du modèle"
+
+#: src/pyams_portal/interfaces/__init__.py:277
+msgid "Two registered templates can't share the same name..."
+msgstr "Deux modèles partagés ne peuvent pas utiliser le même nom..."
+
+#: src/pyams_portal/interfaces/__init__.py:309
+msgid "Inherit parent template?"
+msgstr "Hériter du modèle du parent ?"
+
+#: src/pyams_portal/interfaces/__init__.py:310
+msgid "Should we reuse parent template?"
+msgstr "Doit-on ré-utiliser le modèle du parent ?"
+
+#: src/pyams_portal/interfaces/__init__.py:314
+msgid "Use local template?"
+msgstr "Utiliser un modèle local ?"
+
+#: src/pyams_portal/interfaces/__init__.py:315
+msgid ""
+"If 'yes', you can define a custom local template instead of a shared template"
+msgstr ""
+"Si 'oui', vous pouvez définir un modèle de présentation local au lieu d'un "
+"modèle partagé"
+
+#: src/pyams_portal/interfaces/__init__.py:320
+msgid "Page template"
+msgstr "Modèle de page"
+
+#: src/pyams_portal/interfaces/__init__.py:321
+msgid "Template used for this page"
+msgstr "Modèle de présentation utilisé pour cette page"
+
+#: src/pyams_portal/interfaces/__init__.py:325
+msgid "Local template"
+msgstr "Modèle local"
+
+#~ msgid "Portlet templates"
+#~ msgstr "Modèles de présentation"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/locales/pyams_portal.pot Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,460 @@
+#
+# SOME DESCRIPTIVE TITLE
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-05-12 13:59+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+
+#: ./src/pyams_portal/workflow.py:41
+msgid "Draft"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:42
+msgid "Published"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:43
+msgid "Retired"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:44
+msgid "Archived"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:45
+msgid "Deleted"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:103
+msgid "Initialize"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:108
+msgid "Publish..."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:116
+msgid ""
+"This content is currently in DRAFT mode.\n"
+" Publishing it will make it publicly visible."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:120
+msgid "Retire..."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:128
+msgid ""
+"This content is actually published.\n"
+" You can retire it to make it invisible, but contents using this\n"
+" template won't be visible anymore!"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:133 ./src/pyams_portal/workflow.py:181
+msgid "Create new version..."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:143
+msgid "Re-publish..."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:151
+msgid ""
+"This content was published and retired.\n"
+" You can re-publish it to make it visible again."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:155 ./src/pyams_portal/workflow.py:168
+msgid "Archive..."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:163
+msgid ""
+"This content is currently published.\n"
+" If it is archived, it will not be possible to make it visible again\n"
+" except by creating a new version!"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:176
+msgid ""
+"This content has been published but is currently retired.\n"
+" If it is archived, it will not be possible to make it visible again\n"
+" except by creating a new version!"
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:191
+msgid "Delete..."
+msgstr ""
+
+#: ./src/pyams_portal/workflow.py:199
+msgid ""
+"This content has never been published.\n"
+" It can be removed and definitely deleted."
+msgstr ""
+
+#: ./src/pyams_portal/__init__.py:31
+msgid "Manage portal templates"
+msgstr ""
+
+#: ./src/pyams_portal/__init__.py:35
+msgid "Portal templates manager"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/portlet.py:41
+msgid "Edit portlet configuration"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/portlet.py:38
+#, python-format
+msgid "« {0} » portal template - {1}"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:61
+msgid "Properties"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:72
+msgid "Portal template configuration"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:120
+msgid "Portlets configuration"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:133
+msgid "Add row..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:175
+msgid "Add slot..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:191
+msgid "Add slot"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:265
+msgid "Edit slot properties"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:333
+msgid "Add portlet..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:349
+msgid "Add portlet"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:209
+#: ./src/pyams_portal/zmi/template/__init__.py:269
+msgid "Specified name is already used!"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:118
+#: ./src/pyams_portal/zmi/template/config.py:189
+#: ./src/pyams_portal/zmi/template/config.py:347
+#, python-format
+msgid "« {0} » portal template"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/config.py:262
+#, python-format
+msgid "« {0} » portal template - {1} slot"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:109
+msgid "Publish template"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:151
+msgid "Retire template"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:180
+msgid "Archive template"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:209
+#: ./src/pyams_portal/zmi/template/workflow.py:201
+msgid "Create new version"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:100
+#: ./src/pyams_portal/zmi/template/workflow.py:142
+#: ./src/pyams_portal/zmi/template/workflow.py:171
+#: ./src/pyams_portal/zmi/template/workflow.py:200
+msgid "Close"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:101
+msgid "Publish"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:143
+msgid "Retire"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/workflow.py:172
+msgid "Archive"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:88
+#: ./src/pyams_portal/zmi/template/__init__.py:196
+#: ./src/pyams_portal/zmi/template/__init__.py:240
+msgid "Portal templates"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:97
+msgid "Shared portal templates"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:163
+msgid "Delete template"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:195
+msgid "Portal"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:229
+msgid "Add shared template..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:241
+msgid "Add shared template"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:153
+msgid "Older versions"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/__init__.py:212
+#: ./src/pyams_portal/zmi/template/__init__.py:146
+#, python-format
+msgid "Version {version} ({state} - last update {date})"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:15
+#: ./src/pyams_portal/zmi/template/templates/config.pt:29
+msgid "Version ${version} - ${state}"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:42
+msgid "Selected display:"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:47
+msgid "Current device"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:48
+msgid "Extra small device (phone)"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:49
+msgid "Small device (tablet)"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:50
+msgid "Medium desktop device (> 970px)"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:51
+msgid "Large desktop device (> 1170px)"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:111
+msgid "Delete row..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:119
+msgid "Edit slot properties..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:126
+msgid "Delete slot..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:134
+msgid "Edit portlet properties..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/template/templates/config.pt:141
+msgid "Delete portlet..."
+msgstr ""
+
+#: ./src/pyams_portal/portlets/context/__init__.py:43
+msgid "Context content"
+msgstr ""
+
+#: ./src/pyams_portal/portlets/image/__init__.py:44
+msgid "Image"
+msgstr ""
+
+#: ./src/pyams_portal/portlets/image/interfaces.py:30
+msgid "Selected image"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:49
+msgid "Portlet"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:52
+#: ./src/pyams_portal/interfaces/__init__.py:63
+#: ./src/pyams_portal/interfaces/__init__.py:117
+msgid "Slot name"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:53
+#: ./src/pyams_portal/interfaces/__init__.py:64
+msgid "Slot name to which this configuration applies"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:69
+msgid "Position"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:70
+msgid "Portlet position inside slot"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:74
+msgid "Visible portlet?"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:75
+msgid ""
+"Select 'no' to hide this portlet. This will not break configuration "
+"inheritance..."
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:81
+#: ./src/pyams_portal/interfaces/__init__.py:139
+msgid "Inherit parent configuration?"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:82
+msgid ""
+"This option is only available if context's parent is using the same template "
+"and if this portlet is also present in the same slot..."
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:118
+msgid "This name must be unique in a given template"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:121
+msgid "Row ID"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:132
+msgid "Visible slot?"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:133
+msgid ""
+"Select 'no' to hide this slot. This will not break configuration "
+"inheritance..."
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:140
+msgid ""
+"This option is only available if context's parent template is using a "
+"template containing the same slot..."
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:145
+msgid "Extra small device width"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:146
+msgid ""
+"Slot width, in columns count, on extra small devices (phones...); set to 0 to"
+" hide the portlet"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:152
+msgid "Small device width"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:153
+msgid ""
+"Slot width, in columns count, on small devices (tablets...); set to 0 to hide"
+" the portlet"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:159
+msgid "Medium devices width"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:160
+msgid ""
+"Slot width, in columns count, on medium desktop devices (>= 992 pixels); set "
+"to 0 to hide the portlet"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:166
+msgid "Large devices width"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:167
+msgid ""
+"Slot width, in columns count, on large desktop devices (>= 1200 pixels); set "
+"to 0 to hide the portlet"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:173
+msgid "CSS class"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:174
+msgid "CSS class applied to this slot"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:276
+msgid "Template name"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:277
+msgid "Two registered templates can't share the same name..."
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:309
+msgid "Inherit parent template?"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:310
+msgid "Should we reuse parent template?"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:314
+msgid "Use local template?"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:315
+msgid ""
+"If 'yes', you can define a custom local template instead of a shared template"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:320
+msgid "Page template"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:321
+msgid "Template used for this page"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:325
+msgid "Local template"
+msgstr ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/page.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,151 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyramid.view import view_config
+from zope.traversing.interfaces import ITraversable
+from pyams_portal.template import PortalWfTemplate, PortalTemplate
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.registry import query_utility
+from pyams_workflow.interfaces import IWorkflowInfo, IWorkflowVersions
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortalPage, IPortalContext, IPortalTemplateRenderer, \
+ IPortalTemplateConfiguration, IPortalWfTemplate
+from zope.annotation.interfaces import IAnnotations
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent, ObjectAddedEvent
+from zope.location.location import locate
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IPortalPage)
+class PortalPage(Persistent, Contained):
+ """Portal page"""
+
+ _inherit_parent = FieldProperty(IPortalPage['inherit_parent'])
+ _use_local_template = FieldProperty(IPortalPage['use_local_template'])
+ _shared_template = FieldProperty(IPortalPage['shared_template'])
+ _local_template = FieldProperty(IPortalPage['local_template'])
+
+ @property
+ def can_inherit(self):
+ return IPortalContext.providedBy(self.__parent__.__parent__)
+
+ @property
+ def inherit_parent(self):
+ return self._inherit_parent if self.can_inherit else False
+
+ @inherit_parent.setter
+ def inherit_parent(self, value):
+ self._inherit_parent = value
+
+ @property
+ def use_local_template(self):
+ if self.inherit_parent:
+ return IPortalPage(self.__parent__.__parent__).use_local_template
+ else:
+ return self._use_local_template
+
+ @use_local_template.setter
+ def use_local_template(self, value):
+ self._use_local_template = value
+ if value and (self._local_template is None):
+ registry = get_current_registry()
+ wf_template = self._local_template = PortalWfTemplate()
+ registry.notify(ObjectCreatedEvent(wf_template))
+ locate(wf_template, self, '++template++')
+ template = PortalTemplate()
+ registry.notify(ObjectCreatedEvent(template))
+ IWorkflowVersions(wf_template).add_version(template, None)
+ IWorkflowInfo(template).fire_transition('init')
+
+ @property
+ def shared_template(self):
+ if self.inherit_parent:
+ return IPortalPage(self.__parent__.__parent__).shared_template
+ else:
+ return self._shared_template
+
+ @shared_template.setter
+ def shared_template(self, value):
+ if not self.inherit_parent:
+ if isinstance(value, IPortalWfTemplate):
+ value = value.__name__
+ self._shared_template = value
+
+ @property
+ def local_template(self):
+ if self.inherit_parent:
+ return IPortalPage(self.__parent__.__parent__).local_template
+ else:
+ return self._local_template
+
+ @local_template.setter
+ def local_template(self, value):
+ if not self.inherit_parent:
+ self._local_template = value
+
+ @property
+ def template(self):
+ if self.use_local_template:
+ return self.local_template
+ else:
+ template = self.shared_template
+ if isinstance(template, str):
+ template = query_utility(IPortalWfTemplate, name=template)
+ return template
+
+
+PORTAL_PAGE_KEY = 'pyams_portal.page'
+
+
+@adapter_config(name='template', context=IPortalContext, provides=ITraversable)
+class PortalPageTemplateTraverser(ContextAdapter):
+ """++template++ portal context traverser"""
+
+ def traverse(self, name, furtherpath=None):
+ page = IPortalPage(self.context)
+ if page.use_local_template:
+ return page.template
+
+
+@adapter_config(context=IPortalContext, provides=IPortalPage)
+def PortalPageFactory(context):
+ """Portal page factory"""
+ annotations = IAnnotations(context)
+ page = annotations.get(PORTAL_PAGE_KEY)
+ if page is None:
+ page = annotations[PORTAL_PAGE_KEY] = PortalPage()
+ get_current_registry().notify(ObjectCreatedEvent(page))
+ locate(page, context)
+ return page
+
+
+@view_config(context=IPortalContext, request_type=IPyAMSLayer)
+def PortalPageRenderer(request):
+ page = IPortalPage(request.context)
+ template = page.template
+ registry = request.registry
+ renderer = registry.queryMultiAdapter((request.context, request, template), IPortalTemplateRenderer)
+ if renderer is not None:
+ configuration = registry.queryMultiAdapter((request.context, template), IPortalTemplateConfiguration)
+ return renderer(configuration)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlet.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,160 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import logging
+logger = logging.getLogger('PyAMS (portal)')
+
+import venusian
+
+# import interfaces
+from pyams_portal.interfaces import IPortlet, IPortletRenderer, IPortletConfiguration, \
+ IPortalPage, IPortletPreviewer
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from persistent import Persistent
+from pyams_utils.request import check_request
+from pyams_viewlet.viewlet import ContentProvider
+from pyramid.exceptions import ConfigurationError
+from zope.container.contained import Contained
+from zope.interface import implementer, provider
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import getVocabularyRegistry, SimpleVocabulary, SimpleTerm
+
+
+@implementer(IPortletConfiguration)
+class PortletConfiguration(Persistent, Contained):
+ """Portlet configuration"""
+
+ template = None
+ portlet_name = None
+ slot_name = FieldProperty(IPortletConfiguration['slot_name'])
+ position = FieldProperty(IPortletConfiguration['position'])
+ visible = FieldProperty(IPortletConfiguration['visible'])
+ _inherit_parent = FieldProperty(IPortletConfiguration['inherit_parent'])
+
+ def __init__(self, portlet):
+ self.portlet_name = portlet.name
+
+ @property
+ def can_inherit(self):
+ return IPortalPage.providedBy(self.__parent__)
+
+ @property
+ def inherit_parent(self):
+ return self._inherit_parent if self.can_inherit else False
+
+ @inherit_parent.setter
+ def inherit_parent(self, value):
+ self._inherit_parent = value
+
+
+@implementer(IPortlet)
+class Portlet(object):
+ """Base portlet content provider"""
+
+ permission = FieldProperty(IPortlet['permission'])
+
+ toolbar_image = None
+ toolbar_css_class = 'fa fa-fw fa-2x fa-edit'
+
+
+@provider(IVocabularyFactory)
+class PortletVocabulary(SimpleVocabulary):
+ """Portlet vocabulary"""
+
+ def __init__(self, context):
+ request = check_request()
+ translate = request.localizer.translate
+ utils = request.registry.getUtilitiesFor(IPortlet)
+ terms = [SimpleTerm(name, title=translate(util.label))
+ for name, util in sorted(utils, key=lambda x: translate(x[1].label))]
+ super(PortletVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS portal portlets', PortletVocabulary)
+
+
+class PortletContentProvider(ContentProvider):
+ """Bae portlet content provider"""
+
+ def __init__(self, context, request, view, portlet_config):
+ super(PortletContentProvider, self).__init__(context, request, view)
+ self.__parent__ = view
+ self.configuration = portlet_config
+ self.portlet = self.request.registry.getUtility(IPortlet, name=portlet_config.portlet_name)
+
+ def __call__(self):
+ if self.portlet.permission and not self.request.has_permission(self.portlet.permission):
+ return ''
+ self.update()
+ return self.render()
+
+
+@implementer(IPortletPreviewer)
+class PortletPreviewer(PortletContentProvider):
+ """Portlet previewer adapter"""
+
+
+@implementer(IPortletRenderer)
+class PortletRenderer(PortletContentProvider):
+ """Portlet renderer adapter"""
+
+
+class portlet_config(object):
+ """Class decorator used to declare a portlet"""
+
+ venusian = venusian # for testing injection
+
+ def __init__(self, **settings):
+ self.__dict__.update(settings)
+
+ def __call__(self, wrapped):
+ settings = self.__dict__.copy()
+ depth = settings.pop('_depth', 0)
+
+ def callback(context, name, ob):
+ name = settings.get('name') or getattr(ob, 'name', None)
+ if name is None:
+ raise ConfigurationError("You must provide a name for a portlet")
+
+ permission = settings.get('permission')
+ if permission is not None:
+ ob.permission = permission
+
+ if type(ob) is type:
+ factory = ob
+ component = None
+ else:
+ factory = None
+ component = ob
+
+ config = context.config.with_package(info.module)
+ logger.debug("Registering portlet {0} named '{1}'".format(str(component) if component else str(factory),
+ name))
+ config.registry.registerUtility(component=component, factory=factory,
+ provided=IPortlet, name=name)
+
+ info = self.venusian.attach(wrapped, callback, category='pyams_portal',
+ depth=depth + 1)
+ if info.scope == 'class':
+ # if the decorator was attached to a method in a class, or
+ # otherwise executed at class scope, we need to set an
+ # 'attr' into the settings if one isn't already in there
+ if settings.get('attr') is None:
+ settings['attr'] = wrapped.__name__
+
+ settings['_info'] = info.codeinfo # fbo "action_method"
+ return wrapped
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/context/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,57 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from .interfaces import IContextPortletConfiguration
+from pyams_portal.interfaces import IPortletRenderer, IPortalContext
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_portal.portlet import Portlet, portlet_config, PortletRenderer, PortletConfiguration
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from zope.interface import implementer, Interface
+
+from pyams_portal import _
+
+
+CONTEXT_PORTLET_NAME = 'pyams_portal.portlet.context'
+
+
+@portlet_config(permission='view')
+class ContextPortlet(Portlet):
+ """Context portlet
+
+ The goal of this portlet is to provide context content
+ """
+
+ name = CONTEXT_PORTLET_NAME
+ label = _("Context content")
+
+
+@adapter_config(context=ContextPortlet, provides=IContextPortletConfiguration)
+@implementer(IContextPortletConfiguration)
+class ContextPortletConfiguration(PortletConfiguration):
+ """Context portlet configuration"""
+
+
+@adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, ContextPortlet), provides=IPortletRenderer)
+@template_config(template='context.pt', layer=IPyAMSLayer)
+class ContextPortletRenderer(PortletRenderer):
+ """Context portlet renderer"""
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/context/context.pt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+<h3>This is my context!!!</h3>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/context/interfaces.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,25 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortletConfiguration
+
+# import packages
+
+
+class IContextPortletConfiguration(IPortletConfiguration):
+ """Context portlet configuration interface"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/image/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,61 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyams_file.property import FileProperty
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from .interfaces import IImagePortletConfiguration
+from pyams_portal.interfaces import IPortalContext, IPortletRenderer
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_portal.portlet import portlet_config, Portlet, PortletConfiguration, PortletRenderer
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from zope.interface import implementer, Interface
+
+from pyams_portal import _
+
+
+IMAGE_PORTLET_NAME = 'pyams_portal.portlet.image'
+
+
+@portlet_config(permission='view')
+class ImagePortlet(Portlet):
+ """Image portlet
+
+ The goal of this portlet is to display an image
+ """
+
+ name = IMAGE_PORTLET_NAME
+ label = _("Image")
+
+ toolbar_image = None
+ toolbar_css_class = 'fa fa-fw fa-2x fa-picture-o'
+
+
+@adapter_config(context=ImagePortlet, provides=IImagePortletConfiguration)
+@implementer(IImagePortletConfiguration)
+class ImagePortletConfiguration(PortletConfiguration):
+ """Image portlet configuration"""
+
+ image = FileProperty(IImagePortletConfiguration['image'])
+
+
+@adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, ImagePortlet), provides=IPortletRenderer)
+@template_config(template='image.pt', layer=IPyAMSLayer)
+class ImagePortletRenderer(PortletRenderer):
+ """Image portlet renderer"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/image/interfaces.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyams_file.schema import ImageField
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortletConfiguration
+
+# import packages
+
+from pyams_portal import _
+
+
+class IImagePortletConfiguration(IPortletConfiguration):
+ """Image portlet configuration interface"""
+
+ image = ImageField(title=_("Selected image"),
+ required=False)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/resources/css/portal.css Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,133 @@
+#portal_config .rows {
+ min-height: 15px;
+}
+#portal_config .row {
+ position: relative;
+ margin: 5px 0;
+ padding: 2px 4px;
+ border: 1px solid rgba(199, 81, 0, 0.4);
+ border-top-width: 1.5em;
+ min-height: 20px;
+ cursor: move;
+}
+#portal_config .row > .row_id {
+ position: absolute;
+ right: 2px;
+ top: -1.6em;
+}
+#portal_config .row .slot {
+ margin: 3px 0;
+ padding: 3px;
+ border: 1px solid rgba(98, 120, 128, 0.65);
+ border-bottom-width: 6px;
+ min-height: 20px!important;
+}
+#portal_config .row .slot > .header {
+ background-color: rgba(98, 120, 128, 0.6);
+ color: white;
+}
+#portal_config .row .portlet {
+ margin: 3px 0;
+ padding: 3px;
+ border: 1px solid rgba(98, 120, 128, 0.6);
+ min-height: 20px!important;
+}
+#portal_config .row .portlet > .header {
+ background-color: rgba(92, 109, 115, 0.8);
+ color: white;
+}
+#portal_config .row-highlight {
+ margin: 5px 0;
+ border: 1px solid #c75100;
+ min-height: 40px;
+}
+#portal_config .slots {
+ min-height: 15px;
+}
+#portal_config .slot-highlight {
+ margin: 3px 0;
+ border: 1px solid #7b939c;
+ min-height: 40px;
+}
+#portal_config .portlets {
+ min-height: 15px;
+}
+#portal_config .portlets-hover {
+ background-color: silver;
+}
+#portal_config .portlets-active {
+ background-color: silver;
+}
+#portal_config .portlet-highlight {
+ margin: 0;
+ border: 1px solid #7b939c;
+ min-height: 40px;
+}
+#portal_config.container .col-12 {
+ float: left;
+ width: 100%!important;
+}
+#portal_config.container .col-11 {
+ float: left;
+ width: 91.66666667%!important;
+}
+#portal_config.container .col-10 {
+ float: left;
+ width: 83.33333333%!important;
+}
+#portal_config.container .col-9 {
+ float: left;
+ width: 75%!important;
+}
+#portal_config.container .col-8 {
+ float: left;
+ width: 66.66666667%!important;
+}
+#portal_config.container .col-7 {
+ float: left;
+ width: 58.33333333%!important;
+}
+#portal_config.container .col-6 {
+ float: left;
+ width: 50%!important;
+}
+#portal_config.container .col-5 {
+ float: left;
+ width: 41.66666667%!important;
+}
+#portal_config.container .col-4 {
+ float: left;
+ width: 33.33333333%!important;
+}
+#portal_config.container .col-3 {
+ float: left;
+ width: 25%!important;
+}
+#portal_config.container .col-2 {
+ float: left;
+ width: 16.66666667%!important;
+}
+#portal_config.container .col-1 {
+ float: left;
+ width: 8.33333333%!important;
+}
+#portal_config.container .col-0 {
+ float: left;
+ width: 100%!important;
+ opacity: 0.5;
+}
+#portal_config.container .col-0 > .portlets {
+ display: none;
+}
+#portal_config.container-xs {
+ max-width: 750px!important;
+}
+#portal_config.container-sm {
+ width: 750px!important;
+}
+#portal_config.container-md {
+ width: 970px!important;
+}
+#portal_config.container-lg {
+ width: 1170px!important;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/resources/js/portal.js Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,495 @@
+(function($) {
+
+ window.PyAMS_portal = {
+
+ /**
+ * Templates management
+ */
+ template: {
+
+ initConfig: function() {
+ var config = $('#portal_config');
+ if (config.data('ams-allowed-change')) {
+ // Init sortables and resizables
+ $('.rows', config).addClass('sortable');
+ $('.slots', config).addClass('sortable');
+ $('.slot', config).addClass('resizable');
+ $('.portlets', config).addClass('sortable');
+ MyAMS.plugins.enabled.sortable(config);
+ MyAMS.plugins.enabled.resizable(config);
+ // Init rows toolbar drag and drop
+ $('.btn-row', '.btn-toolbar').draggable({
+ cursor: 'move',
+ helper: 'clone',
+ revert: 'invalid',
+ connectToSortable: '.rows'
+ });
+ $('.rows', config).droppable({
+ accept: '.btn-row',
+ drop: function(event, ui) {
+ if (ui.draggable.hasClass('already-dropped'))
+ return;
+ ui.draggable.addClass('already-dropped');
+ MyAMS.ajax.post('add-template-row.json', {}, function(result) {
+ var row_id = result.row_id;
+ var rows = $('.rows', '#portal_config');
+ ui.draggable.removeClassPrefix('btn')
+ .removeClassPrefix('ui-')
+ .removeClass('already-dropped')
+ .removeAttr('style')
+ .addClass('row context-menu')
+ .attr('data-ams-row-id', row_id)
+ .empty()
+ .append($('<span></span>').addClass('row_id label label-success pull-right')
+ .text(row_id))
+ .append($('<div></div>').addClass('slots')
+ .sortable({
+ placeholder: 'slot-highlight',
+ connectWith: '.slots',
+ over: PyAMS_portal.template.overSlots,
+ stop: PyAMS_portal.template.sortSlots
+ }))
+ .contextMenu({
+ menuSelector: '#rowMenu',
+ menuSelected: MyAMS.helpers.contextMenuHandler
+ });
+ PyAMS_portal.template.sortRows();
+ rows.sortable('refresh');
+ });
+ }
+ });
+ // Init slot toolbar drag and drop
+ $('.btn-slot', '.btn-toolbar').draggable({
+ cursor: 'move',
+ helper: 'clone',
+ revert: 'invalid',
+ connectToSortable: '.slots'
+ });
+ $('.slots', config).droppable({
+ accept: '.btn-slot',
+ drop: function(event, ui) {
+ if (ui.draggable.hasClass('already-dropped'))
+ return;
+ ui.draggable.addClass('already-dropped');
+ var row_id = ui.helper.parents('.row:first').data('ams-row-id');
+ MyAMS.dialog.open('add-template-slot.html?form.widgets.row_id=' + row_id);
+ }
+ });
+ // Init portlets toolbar drag and drop
+ $('.btn-portlet', '.btn-toolbar').draggable({
+ cursor: 'move',
+ helper: 'clone',
+ revert: 'invalid',
+ connectToSortable: '.portlets'
+ });
+ $('.portlets', config).droppable({
+ accept: '.btn-portlet',
+ hoverClass: 'portlets-hover',
+ activeClass: 'portlets-active',
+ drop: function(event, ui) {
+ if (ui.draggable.hasClass('already-dropped'))
+ return;
+ ui.draggable.addClass('already-dropped');
+ var source = ui.draggable;
+ var target = $(this);
+ var slot = target.parents('.slot:first');
+ MyAMS.ajax.post('drag-template-portlet.json', {
+ portlet_name: source.data('ams-portlet-name'),
+ slot_name: slot.data('ams-slot-name')
+ }, function(result) {
+ MyAMS.ajax.handleJSON(result);
+ });
+ }
+ });
+ }
+ },
+
+
+ /**
+ * Display selector
+ */
+
+ selectDisplay: function() {
+ var device = $(this).val();
+ MyAMS.ajax.post('get-slots-width.json',
+ {device: device},
+ function(result) {
+ var config = $('#portal_config');
+ config.removeClassPrefix('container-');
+ if (device) {
+ config.addClass('container-' + device);
+ }
+ $('.slot', config).removeClassPrefix('col-');
+ for (var slot_name in result) {
+ var widths = result[slot_name];
+ var slot = $('.slot[data-ams-slot-name="' + slot_name + '"]', config);
+ if (device) {
+ slot.addClass('col-' + widths[device]);
+ } else {
+ for (var display in widths) {
+ slot.addClass('col-' + display + '-' + widths[display]);
+ }
+ }
+ }
+ });
+ },
+
+ /**
+ * Rows management
+ */
+
+ addRow: function() {
+ return function() {
+ $(this).parents('.btn-group').removeClass('open');
+ MyAMS.ajax.post('add-template-row.json', {}, function(result) {
+ var row_id = result.row_id;
+ var rows = $('.rows', '#portal_config');
+ $('<div></div>').addClass('row context-menu')
+ .attr('data-ams-row-id', row_id)
+ .append($('<span></span>').addClass('row_id label label-success pull-right')
+ .text(row_id))
+ .append($('<div></div>').addClass('slots')
+ .sortable({
+ placeholder: 'slot-highlight',
+ connectWith: '.slots',
+ over: PyAMS_portal.template.overSlots,
+ stop: PyAMS_portal.template.sortSlots
+ }))
+ .contextMenu({
+ menuSelector: '#rowMenu',
+ menuSelected: MyAMS.helpers.contextMenuHandler
+ })
+ .appendTo(rows);
+ rows.sortable('refresh');
+ });
+ };
+ },
+
+ overRows: function(event, ui) {
+ $(ui.placeholder).attr('class', $(ui.item).attr('class'))
+ .removeClassPrefix('ui-')
+ .addClass('row-highlight')
+ .css('height', $(ui.item).outerHeight());
+ },
+
+ sortRows: function(event, ui) {
+ if (ui && ui.item.hasClass('already-dropped'))
+ return;
+ var config = $('#portal_config');
+ var ids = $('.row', config).listattr('data-ams-row-id');
+ MyAMS.ajax.post('set-template-row-order.json',
+ {rows: JSON.stringify(ids)},
+ function(result) {
+ if (result.status == 'success') {
+ $('.row', config).each(function (index) {
+ $(this).attr('data-ams-row-id', index);
+ $('span.row_id', $(this)).text(index);
+ })
+ }
+ });
+ },
+
+ deleteRow: function() {
+ return function(row) {
+ MyAMS.skin.bigBox({
+ title: MyAMS.i18n.WARNING,
+ content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i> ' + MyAMS.i18n.DELETE_WARNING,
+ buttons: MyAMS.i18n.BTN_OK_CANCEL
+ }, function(button) {
+ if (button == MyAMS.i18n.BTN_OK) {
+ if (!row.hasClass('row'))
+ row = row.parents('.row');
+ MyAMS.ajax.post('delete-template-row.json',
+ {row_id: row.data('ams-row-id')},
+ function(result) {
+ if (result.status == 'success') {
+ row.remove();
+ $('.row', '#portal_config').each(function (index) {
+ $(this).removeData()
+ .attr('data-ams-row-id', index);
+ $('span.row_id', $(this)).text(index);
+ })
+ }
+ });
+ }
+ });
+ };
+ },
+
+
+ /**
+ * Slots management
+ */
+
+ addSlotCallback: function(result) {
+ var slots = $('.slots', '.row[data-ams-row-id="' + result.row_id + '"]');
+ var slot_name = result.slot_name;
+ var new_slot = $('<div></div>').addClass('slot context-menu col col-md-12')
+ .attr('data-ams-slot-name', slot_name)
+ .append($('<div></div>').addClass('header padding-x-5')
+ .text(slot_name))
+ .append($('<div></div>').addClass('portlets')
+ .sortable({
+ placeholder: 'portlet-highlight',
+ connectWith: '.portlets',
+ over: PyAMS_portal.template.overPortlets,
+ stop: PyAMS_portal.template.sortPortlets
+ }))
+ .append($('<div></div>').addClass('clearfix'))
+ .contextMenu({
+ menuSelector: '#slotMenu',
+ menuSelected: MyAMS.helpers.contextMenuHandler
+ });
+ var slot_button = $('.btn-slot', slots);
+ if (slot_button.exists()) { // Slot added via drag & drop
+ slot_button.replaceWith(new_slot);
+ $('.slot', slots).each(function() {
+ $(this).removeData();
+ });
+ PyAMS_portal.template.sortSlots();
+ } else {
+ new_slot.appendTo(slots);
+ }
+ slots.sortable('refresh');
+ },
+
+ startSlotResize: function(event, ui) {
+ var slot = ui.element;
+ var row = slot.parents('.slots:first');
+ var colWidth = (row.innerWidth() - 110) / 12;
+ var slotHeight = slot.height();
+ ui.element.resizable('option', 'grid', [colWidth, slotHeight]);
+ ui.element.resizable('option', 'minWidth', colWidth);
+ ui.element.resizable('option', 'minHeight', slotHeight);
+ ui.element.resizable('option', 'maxWidth', row.innerWidth());
+ ui.element.resizable('option', 'maxHeight', slotHeight);
+ },
+
+ stopSlotResize: function(event, ui) {
+ var slot = ui.element;
+ var row = slot.parents('.slots:first');
+ var colWidth = (row.innerWidth() - 10) / 12;
+ var slotCols = Math.round($(slot).width() / colWidth);
+ var device = $('#device_selector').val();
+ if (!device) {
+ var deviceWidth = $('body').width();
+ if (deviceWidth > 1170)
+ device = 'lg';
+ else if (deviceWidth > 970)
+ device = 'md';
+ else if (deviceWidth > 750)
+ device = 'sm';
+ else
+ device = 'xs';
+ }
+ MyAMS.ajax.post('set-slot-width.json',
+ {slot_name: slot.data('ams-slot-name'),
+ device: device,
+ width: slotCols},
+ function(result) {
+ slot.removeClassPrefix('col-');
+ slot.removeAttr('style');
+ var slot_name = slot.data('ams-slot-name');
+ var widths = result[slot_name];
+ if (device) {
+ slot.addClass('col-' + device + '-' + widths[device]);
+ } else {
+ slot.addClass('col-' + widths[device]);
+ }
+ });
+ },
+
+ editSlot: function() {
+ return function(slot) {
+ if (!slot.hasClass('slot'))
+ slot = slot.parents('.slot');
+ MyAMS.dialog.open('slot-properties.html?form.widgets.slot_name=' + slot.data('ams-slot-name'));
+ };
+ },
+
+ editSlotCallback: function(result) {
+ var slot = $('.slot[data-ams-slot-name="' + result.slot_name + '"]');
+ slot.attr('class', 'slot context-menu col');
+ var device = $('#device_selector').val();
+ if (device)
+ slot.addClass('col-' + result.width[device]);
+ else {
+ for (device in result.width) {
+ slot.addClass('col-' + device + '-' + result.width[device]);
+ }
+ }
+ },
+
+ overSlots: function(event, ui) {
+ $(ui.placeholder).attr('class', $(ui.item).attr('class'))
+ .removeClassPrefix('ui-')
+ .addClass('slot-highlight')
+ .css('height', $(ui.item).outerHeight());
+ },
+
+ sortSlots: function(event, ui) {
+ if (ui && ui.item.hasClass('already-dropped'))
+ return;
+ var config = $('#portal_config');
+ var order = {};
+ $('.row', config).each(function() {
+ var row = $(this);
+ var row_config = [];
+ $('.slot', row).each(function() {
+ row_config.push($(this).data('ams-slot-name'));
+ });
+ order[parseInt(row.attr('data-ams-row-id'))] = row_config;
+ });
+ MyAMS.ajax.post('set-template-slot-order.json',
+ {order: JSON.stringify(order)},
+ function(result) {
+ if (result.status == 'success') {}
+ });
+ },
+
+ deleteSlot: function() {
+ return function(slot) {
+ MyAMS.skin.bigBox({
+ title: MyAMS.i18n.WARNING,
+ content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i> ' + MyAMS.i18n.DELETE_WARNING,
+ buttons: MyAMS.i18n.BTN_OK_CANCEL
+ }, function(button) {
+ if (button == MyAMS.i18n.BTN_OK) {
+ if (!slot.hasClass('slot'))
+ slot = slot.parents('.slot');
+ MyAMS.ajax.post('delete-template-slot.json',
+ {slot_name: slot.data('ams-slot-name')},
+ function(result) {
+ if (result.status == 'success') {
+ slot.remove();
+ $('.slot', '#portal_config').each(function() {
+ $(this).removeData();
+ });
+ }
+ });
+ }
+ });
+ };
+ },
+
+
+ /**
+ * Portlets management
+ */
+
+ addPortletCallback: function(result) {
+ var portlets = $('.portlets', '.slot[data-ams-slot-name="' + result.slot_name + '"]');
+ var portlet = $('<div></div>').addClass('portlet context-menu')
+ .attr('data-ams-portlet-name', result.portlet_name)
+ .attr('data-ams-portlet-slot', result.slot_name)
+ .attr('data-ams-portlet-position', result.position)
+ .append($('<div></div>').addClass('header padding-x-5')
+ .text(result.label))
+ . append($('<div></div>').addClass('preview')
+ .html(result.preview || ''))
+ .contextMenu({
+ menuSelector: '#portletMenu',
+ menuSelected: MyAMS.helpers.contextMenuHandler
+ });
+ MyAMS.initContent($('.preview', portlet));
+ var portlet_button = $('.btn-portlet', portlets);
+ if (portlet_button.exists()) { // Portlet added via drag & drop
+ portlet_button.replaceWith(portlet);
+ $('.portlet', portlets).each(function() {
+ $(this).removeData();
+ });
+ PyAMS_portal.template.sortPortlets(null, {item: portlet});
+ } else {
+ portlet.appendTo(portlets);
+ }
+ portlets.sortable('refresh');
+ },
+
+ editPortlet: function() {
+ return function(portlet) {
+ if (!portlet.hasClass('portlet'))
+ portlet = portlet.parents('.portlet:first');
+ var slot = portlet.parents('.slot:first');
+ var row = slot.parents('.row:first');
+ MyAMS.dialog.open('portlet-properties.html?form.widgets.slot_name=' + slot.data('ams-slot-name') +
+ '&form.widgets.position=' + portlet.data('ams-portlet-position'));
+ };
+ },
+
+ editPortletCallback: function(result) {
+ if (result.preview) {
+ var config = $('#portal_config');
+ var portlet = $('.portlet[data-ams-portlet-slot="' + result.slot_name + '"]' +
+ '[data-ams-portlet-position="' + result.position + '"]', config);
+ $('.preview', portlet).html(result.preview);
+ MyAMS.initContent($('.preview', portlet));
+ }
+ },
+
+ overPortlets: function(event, ui) {
+ $(ui.placeholder).attr('class', $(ui.item).attr('class'))
+ .removeClassPrefix('ui-')
+ .addClass('portlet-highlight')
+ .css('height', $(ui.item).outerHeight());
+ },
+
+ sortPortlets: function(event, ui) {
+ if (ui.item.hasClass('already-dropped'))
+ return;
+ var portlet = ui.item;
+ var to_slot = portlet.parents('.slot');
+ var to_portlets = $('.portlet', to_slot);
+ var order = {from: {name: portlet.data('ams-portlet-name'),
+ slot: portlet.data('ams-portlet-slot'),
+ position: portlet.data('ams-portlet-position')},
+ to: {slot: to_slot.data('ams-slot-name'),
+ names: to_portlets.listattr('data-ams-portlet-name'),
+ slots: to_portlets.listattr('data-ams-portlet-slot'),
+ positions: to_portlets.listattr('data-ams-portlet-position')}};
+ MyAMS.ajax.post('set-template-portlet-order.json',
+ {order: JSON.stringify(order)},
+ function(result) {
+ if (result.status == 'success') {
+ var from_slot = $('.slot[data-ams-slot-name="' + portlet.attr('data-ams-portlet-slot') + '"]', '#portal_config');
+ $('.portlet', from_slot).each(function(index) {
+ $(this).removeData()
+ .attr('data-ams-portlet-position', index);
+ });
+ $('.portlet', to_slot).each(function(index) {
+ $(this).removeData()
+ .attr('data-ams-portlet-slot', to_slot.attr('data-ams-slot-name'))
+ .attr('data-ams-portlet-position', index);
+ });
+ }
+ });
+ },
+
+ deletePortlet: function() {
+ return function(portlet) {
+ MyAMS.skin.bigBox({
+ title: MyAMS.i18n.WARNING,
+ content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i> ' + MyAMS.i18n.DELETE_WARNING,
+ buttons: MyAMS.i18n.BTN_OK_CANCEL
+ }, function(button) {
+ if (button == MyAMS.i18n.BTN_OK) {
+ if (!portlet.hasClass('portlet'))
+ portlet = portlet.parents('.portlet');
+ MyAMS.ajax.post('delete-template-portlet.json',
+ {slot_name: portlet.data('ams-portlet-slot'),
+ position: portlet.data('ams-portlet-position')},
+ function(result) {
+ if (result.status == 'success') {
+ portlet.remove();
+ $('.portlet', '#portal_config').each(function() {
+ $(this).removeData();
+ });
+ }
+ });
+ }
+ });
+ };
+ }
+ }
+ };
+
+})(jQuery);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/resources/js/portal.min.js Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+(function(a){window.PyAMS_portal={template:{initConfig:function(){var b=a("#portal_config");if(b.data("ams-allowed-change")){a(".rows",b).addClass("sortable");a(".slots",b).addClass("sortable");a(".slot",b).addClass("resizable");a(".portlets",b).addClass("sortable");MyAMS.plugins.enabled.sortable(b);MyAMS.plugins.enabled.resizable(b);a(".btn-row",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".rows"});a(".rows",b).droppable({accept:".btn-row",drop:function(c,d){if(d.draggable.hasClass("already-dropped")){return}d.draggable.addClass("already-dropped");MyAMS.ajax.post("add-template-row.json",{},function(e){var f=e.row_id;var g=a(".rows","#portal_config");d.draggable.removeClassPrefix("btn").removeClassPrefix("ui-").removeClass("already-dropped").removeAttr("style").addClass("row context-menu").attr("data-ams-row-id",f).empty().append(a("<span></span>").addClass("row_id label label-success pull-right").text(f)).append(a("<div></div>").addClass("slots").sortable({placeholder:"slot-highlight",connectWith:".slots",over:PyAMS_portal.template.overSlots,stop:PyAMS_portal.template.sortSlots})).contextMenu({menuSelector:"#rowMenu",menuSelected:MyAMS.helpers.contextMenuHandler});PyAMS_portal.template.sortRows();g.sortable("refresh")})}});a(".btn-slot",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".slots"});a(".slots",b).droppable({accept:".btn-slot",drop:function(d,e){if(e.draggable.hasClass("already-dropped")){return}e.draggable.addClass("already-dropped");var c=e.helper.parents(".row:first").data("ams-row-id");MyAMS.dialog.open("add-template-slot.html?form.widgets.row_id="+c)}});a(".btn-portlet",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".portlets"});a(".portlets",b).droppable({accept:".btn-portlet",hoverClass:"portlets-hover",activeClass:"portlets-active",drop:function(c,e){if(e.draggable.hasClass("already-dropped")){return}e.draggable.addClass("already-dropped");var d=e.draggable;var f=a(this);var g=f.parents(".slot:first");MyAMS.ajax.post("drag-template-portlet.json",{portlet_name:d.data("ams-portlet-name"),slot_name:g.data("ams-slot-name")},function(h){MyAMS.ajax.handleJSON(h)})}})}},selectDisplay:function(){var b=a(this).val();MyAMS.ajax.post("get-slots-width.json",{device:b},function(c){var d=a("#portal_config");d.removeClassPrefix("container-");if(b){d.addClass("container-"+b)}a(".slot",d).removeClassPrefix("col-");for(var e in c){var f=c[e];var h=a('.slot[data-ams-slot-name="'+e+'"]',d);if(b){h.addClass("col-"+f[b])}else{for(var g in f){h.addClass("col-"+g+"-"+f[g])}}}})},addRow:function(){return function(){a(this).parents(".btn-group").removeClass("open");MyAMS.ajax.post("add-template-row.json",{},function(b){var c=b.row_id;var d=a(".rows","#portal_config");a("<div></div>").addClass("row context-menu").attr("data-ams-row-id",c).append(a("<span></span>").addClass("row_id label label-success pull-right").text(c)).append(a("<div></div>").addClass("slots").sortable({placeholder:"slot-highlight",connectWith:".slots",over:PyAMS_portal.template.overSlots,stop:PyAMS_portal.template.sortSlots})).contextMenu({menuSelector:"#rowMenu",menuSelected:MyAMS.helpers.contextMenuHandler}).appendTo(d);d.sortable("refresh")})}},overRows:function(b,c){a(c.placeholder).attr("class",a(c.item).attr("class")).removeClassPrefix("ui-").addClass("row-highlight").css("height",a(c.item).outerHeight())},sortRows:function(d,e){if(e&&e.item.hasClass("already-dropped")){return}var b=a("#portal_config");var c=a(".row",b).listattr("data-ams-row-id");MyAMS.ajax.post("set-template-row-order.json",{rows:JSON.stringify(c)},function(f){if(f.status=="success"){a(".row",b).each(function(g){a(this).attr("data-ams-row-id",g);a("span.row_id",a(this)).text(g)})}})},deleteRow:function(){return function(b){MyAMS.skin.bigBox({title:MyAMS.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i> '+MyAMS.i18n.DELETE_WARNING,buttons:MyAMS.i18n.BTN_OK_CANCEL},function(c){if(c==MyAMS.i18n.BTN_OK){if(!b.hasClass("row")){b=b.parents(".row")}MyAMS.ajax.post("delete-template-row.json",{row_id:b.data("ams-row-id")},function(d){if(d.status=="success"){b.remove();a(".row","#portal_config").each(function(e){a(this).removeData().attr("data-ams-row-id",e);a("span.row_id",a(this)).text(e)})}})}})}},addSlotCallback:function(b){var e=a(".slots",'.row[data-ams-row-id="'+b.row_id+'"]');var d=b.slot_name;var c=a("<div></div>").addClass("slot context-menu col col-md-12").attr("data-ams-slot-name",d).append(a("<div></div>").addClass("header padding-x-5").text(d)).append(a("<div></div>").addClass("portlets").sortable({placeholder:"portlet-highlight",connectWith:".portlets",over:PyAMS_portal.template.overPortlets,stop:PyAMS_portal.template.sortPortlets})).append(a("<div></div>").addClass("clearfix")).contextMenu({menuSelector:"#slotMenu",menuSelected:MyAMS.helpers.contextMenuHandler});var f=a(".btn-slot",e);if(f.exists()){f.replaceWith(c);a(".slot",e).each(function(){a(this).removeData()});PyAMS_portal.template.sortSlots()}else{c.appendTo(e)}e.sortable("refresh")},startSlotResize:function(c,e){var g=e.element;var f=g.parents(".slots:first");var b=(f.innerWidth()-110)/12;var d=g.height();e.element.resizable("option","grid",[b,d]);e.element.resizable("option","minWidth",b);e.element.resizable("option","minHeight",d);e.element.resizable("option","maxWidth",f.innerWidth());e.element.resizable("option","maxHeight",d)},stopSlotResize:function(e,g){var i=g.element;var h=i.parents(".slots:first");var c=(h.innerWidth()-10)/12;var f=Math.round(a(i).width()/c);var d=a("#device_selector").val();if(!d){var b=a("body").width();if(b>1170){d="lg"}else{if(b>970){d="md"}else{if(b>750){d="sm"}else{d="xs"}}}}MyAMS.ajax.post("set-slot-width.json",{slot_name:i.data("ams-slot-name"),device:d,width:f},function(j){i.removeClassPrefix("col-");i.removeAttr("style");var k=i.data("ams-slot-name");var l=j[k];if(d){i.addClass("col-"+d+"-"+l[d])}else{i.addClass("col-"+l[d])}})},editSlot:function(){return function(b){if(!b.hasClass("slot")){b=b.parents(".slot")}MyAMS.dialog.open("slot-properties.html?form.widgets.slot_name="+b.data("ams-slot-name"))}},editSlotCallback:function(b){var d=a('.slot[data-ams-slot-name="'+b.slot_name+'"]');d.attr("class","slot context-menu col");var c=a("#device_selector").val();if(c){d.addClass("col-"+b.width[c])}else{for(c in b.width){d.addClass("col-"+c+"-"+b.width[c])}}},overSlots:function(b,c){a(c.placeholder).attr("class",a(c.item).attr("class")).removeClassPrefix("ui-").addClass("slot-highlight").css("height",a(c.item).outerHeight())},sortSlots:function(d,e){if(e&&e.item.hasClass("already-dropped")){return}var c=a("#portal_config");var b={};a(".row",c).each(function(){var g=a(this);var f=[];a(".slot",g).each(function(){f.push(a(this).data("ams-slot-name"))});b[parseInt(g.attr("data-ams-row-id"))]=f});MyAMS.ajax.post("set-template-slot-order.json",{order:JSON.stringify(b)},function(f){if(f.status=="success"){}})},deleteSlot:function(){return function(b){MyAMS.skin.bigBox({title:MyAMS.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i> '+MyAMS.i18n.DELETE_WARNING,buttons:MyAMS.i18n.BTN_OK_CANCEL},function(c){if(c==MyAMS.i18n.BTN_OK){if(!b.hasClass("slot")){b=b.parents(".slot")}MyAMS.ajax.post("delete-template-slot.json",{slot_name:b.data("ams-slot-name")},function(d){if(d.status=="success"){b.remove();a(".slot","#portal_config").each(function(){a(this).removeData()})}})}})}},addPortletCallback:function(b){var c=a(".portlets",'.slot[data-ams-slot-name="'+b.slot_name+'"]');var e=a("<div></div>").addClass("portlet context-menu").attr("data-ams-portlet-name",b.portlet_name).attr("data-ams-portlet-slot",b.slot_name).attr("data-ams-portlet-position",b.position).append(a("<div></div>").addClass("header padding-x-5").text(b.label)).append(a("<div></div>").addClass("preview").html(b.preview||"")).contextMenu({menuSelector:"#portletMenu",menuSelected:MyAMS.helpers.contextMenuHandler});MyAMS.initContent(a(".preview",e));var d=a(".btn-portlet",c);if(d.exists()){d.replaceWith(e);a(".portlet",c).each(function(){a(this).removeData()});PyAMS_portal.template.sortPortlets(null,{item:e})}else{e.appendTo(c)}c.sortable("refresh")},editPortlet:function(){return function(c){if(!c.hasClass("portlet")){c=c.parents(".portlet:first")}var d=c.parents(".slot:first");var b=d.parents(".row:first");MyAMS.dialog.open("portlet-properties.html?form.widgets.slot_name="+d.data("ams-slot-name")+"&form.widgets.position="+c.data("ams-portlet-position"))}},editPortletCallback:function(b){if(b.preview){var c=a("#portal_config");var d=a('.portlet[data-ams-portlet-slot="'+b.slot_name+'"][data-ams-portlet-position="'+b.position+'"]',c);a(".preview",d).html(b.preview);MyAMS.initContent(a(".preview",d))}},overPortlets:function(b,c){a(c.placeholder).attr("class",a(c.item).attr("class")).removeClassPrefix("ui-").addClass("portlet-highlight").css("height",a(c.item).outerHeight())},sortPortlets:function(c,f){if(f.item.hasClass("already-dropped")){return}var g=f.item;var e=g.parents(".slot");var d=a(".portlet",e);var b={from:{name:g.data("ams-portlet-name"),slot:g.data("ams-portlet-slot"),position:g.data("ams-portlet-position")},to:{slot:e.data("ams-slot-name"),names:d.listattr("data-ams-portlet-name"),slots:d.listattr("data-ams-portlet-slot"),positions:d.listattr("data-ams-portlet-position")}};MyAMS.ajax.post("set-template-portlet-order.json",{order:JSON.stringify(b)},function(h){if(h.status=="success"){var i=a('.slot[data-ams-slot-name="'+g.attr("data-ams-portlet-slot")+'"]',"#portal_config");a(".portlet",i).each(function(j){a(this).removeData().attr("data-ams-portlet-position",j)});a(".portlet",e).each(function(j){a(this).removeData().attr("data-ams-portlet-slot",e.attr("data-ams-slot-name")).attr("data-ams-portlet-position",j)})}})},deletePortlet:function(){return function(b){MyAMS.skin.bigBox({title:MyAMS.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i> '+MyAMS.i18n.DELETE_WARNING,buttons:MyAMS.i18n.BTN_OK_CANCEL},function(c){if(c==MyAMS.i18n.BTN_OK){if(!b.hasClass("portlet")){b=b.parents(".portlet")}MyAMS.ajax.post("delete-template-portlet.json",{slot_name:b.data("ams-portlet-slot"),position:b.data("ams-portlet-position")},function(d){if(d.status=="success"){b.remove();a(".portlet","#portal_config").each(function(){a(this).removeData()})}})}})}}}}})(jQuery);
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/resources/less/portal.less Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,146 @@
+#portal_config {
+
+ .rows {
+ min-height: 15px;
+ }
+ .row {
+ position: relative;
+ margin: 5px 0;
+ padding: 2px 4px;
+ border: 1px solid rgba(199, 81, 0, 0.40);
+ border-top-width: 1.5em;
+ min-height: 20px;
+ cursor: move;
+
+ > .row_id {
+ position: absolute;
+ right: 2px;
+ top: -1.6em;
+ }
+ .slot {
+ margin: 3px 0;
+ padding: 3px;
+ border: 1px solid rgba(98, 120, 128, 0.65);
+ border-bottom-width: 6px;
+ min-height: 20px!important;
+
+ >.header {
+ background-color: rgba(98, 120, 128, 0.60);
+ color: white;
+ }
+ }
+ .portlet {
+ margin: 3px 0;
+ padding: 3px;
+ border: 1px solid rgba(98, 120, 128, 0.60);
+ min-height: 20px!important;
+
+ >.header {
+ background-color: rgba(92, 109, 115, 0.80);
+ color: white;
+ }
+ }
+ }
+ .row-highlight {
+ margin: 5px 0;
+ border: 1px solid #c75100;
+ min-height: 40px;
+ }
+
+ .slots {
+ min-height: 15px;
+ }
+ .slot-highlight {
+ margin: 3px 0;
+ border: 1px solid #7b939c;
+ min-height: 40px;
+ }
+
+ .portlets {
+ min-height: 15px;
+
+ &-hover {
+ background-color: silver;
+ }
+ &-active {
+ background-color: silver;
+ }
+ }
+ .portlet-highlight {
+ margin: 0;
+ border: 1px solid #7b939c;
+ min-height: 40px;
+ }
+
+ &.container {
+ .col-12 {
+ float: left;
+ width: 100%!important;
+ }
+ .col-11 {
+ float: left;
+ width: 91.66666667%!important;
+ }
+ .col-10 {
+ float: left;
+ width: 83.33333333%!important;
+ }
+ .col-9 {
+ float: left;
+ width: 75%!important;
+ }
+ .col-8 {
+ float: left;
+ width: 66.66666667%!important;
+ }
+ .col-7 {
+ float: left;
+ width: 58.33333333%!important;
+ }
+ .col-6 {
+ float: left;
+ width: 50%!important;
+ }
+ .col-5 {
+ float: left;
+ width: 41.66666667%!important;
+ }
+ .col-4 {
+ float: left;
+ width: 33.33333333%!important;
+ }
+ .col-3 {
+ float: left;
+ width: 25%!important;
+ }
+ .col-2 {
+ float: left;
+ width: 16.66666667%!important;
+ }
+ .col-1 {
+ float: left;
+ width: 8.33333333%!important;
+ }
+ .col-0 {
+ float: left;
+ width: 100%!important;
+ opacity: 0.5;
+
+ >.portlets {
+ display: none;
+ }
+ }
+ }
+ &.container-xs {
+ max-width: 750px!important;
+ }
+ &.container-sm {
+ width: 750px!important;
+ }
+ &.container-md {
+ width: 970px!important;
+ }
+ &.container-lg {
+ width: 1170px!important;
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/site.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,48 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortalTemplateContainer
+from pyams_utils.interfaces.site import ISiteGenerations
+from zope.site.interfaces import INewLocalSite
+
+# import packages
+from pyams_portal.template import PortalTemplateContainer
+from pyams_utils.registry import utility_config
+from pyams_utils.site import check_required_utilities
+from pyramid.events import subscriber
+
+
+REQUIRED_UTILITIES = ((IPortalTemplateContainer, '', PortalTemplateContainer, 'Portal templates'), )
+
+
+@subscriber(INewLocalSite)
+def handle_new_local_site(event):
+ """Create a new templates container when a site is created"""
+ site = event.manager.__parent__
+ check_required_utilities(site, REQUIRED_UTILITIES)
+
+
+@utility_config(name='PyAMS portal', provides=ISiteGenerations)
+class PortalGenerationsChecker(object):
+ """Portal generations checker"""
+
+ generation = 1
+
+ def evolve(self, site, current=None):
+ """Check for required utilities"""
+ check_required_utilities(site, REQUIRED_UTILITIES)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/slot.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,159 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import ISlotConfiguration, IPortalTemplateConfiguration, IPortalTemplate, IPortalPage
+
+# import packages
+from persistent import Persistent
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+PORTAL_SLOTS_KEY = 'pyams_portal.slots'
+
+
+@implementer(ISlotConfiguration)
+class SlotConfiguration(Persistent, Contained):
+ """Portal slot class"""
+
+ slot_name = FieldProperty(ISlotConfiguration['slot_name'])
+ visible = FieldProperty(ISlotConfiguration['visible'])
+ _inherit_parent = FieldProperty(ISlotConfiguration['inherit_parent'])
+ _xs_width = FieldProperty(ISlotConfiguration['xs_width'])
+ _sm_width = FieldProperty(ISlotConfiguration['sm_width'])
+ _md_width = FieldProperty(ISlotConfiguration['md_width'])
+ _lg_width = FieldProperty(ISlotConfiguration['lg_width'])
+ _css_class = FieldProperty(ISlotConfiguration['css_class'])
+
+ def __init__(self, slot_name, **kwargs):
+ self.slot_name = slot_name
+ self.xs_width = 12
+ self.sm_width = 12
+ self.md_width = 12
+ self.lg_width = 12
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ @property
+ def template(self):
+ if IPortalTemplate.providedBy(self.__parent__):
+ return self.__parent__
+ else:
+ return IPortalPage(self.__parent__).template
+
+ @property
+ def can_inherit(self):
+ return IPortalPage.providedBy(self.__parent__)
+
+ @property
+ def inherit_parent(self):
+ return self._inherit_parent if self.can_inherit else False
+
+ @inherit_parent.setter
+ def inherit_parent(self, value):
+ self._inherit_parent = value
+
+ @property
+ def xs_width(self):
+ if self.inherit_parent:
+ config = IPortalTemplateConfiguration(self.template)
+ return config.get_slot_configuration(self.slot_name).xs_width
+ else:
+ return self._xs_width
+
+ @xs_width.setter
+ def xs_width(self, value):
+ self._xs_width = value
+
+ @property
+ def sm_width(self):
+ if self.inherit_parent:
+ config = IPortalTemplateConfiguration(self.template)
+ return config.get_slot_configuration(self.slot_name).sm_width
+ else:
+ return self._sm_width
+
+ @sm_width.setter
+ def sm_width(self, value):
+ self._sm_width = value
+
+ @property
+ def md_width(self):
+ if self.inherit_parent:
+ config = IPortalTemplateConfiguration(self.template)
+ return config.get_slot_configuration(self.slot_name).md_width
+ else:
+ return self._md_width
+
+ @md_width.setter
+ def md_width(self, value):
+ self._md_width = value
+
+ @property
+ def lg_width(self):
+ if self.inherit_parent:
+ config = IPortalTemplateConfiguration(self.template)
+ return config.get_slot_configuration(self.slot_name).lg_width
+ else:
+ return self._lg_width
+
+ @lg_width.setter
+ def lg_width(self, value):
+ self._lg_width = value
+
+ @property
+ def css_class(self):
+ if self.inherit_parent:
+ config = IPortalTemplateConfiguration(self.template)
+ return config.get_slot_configuration(self.slot_name).css_class
+ else:
+ return self._css_class
+
+ @css_class.setter
+ def css_class(self, value):
+ self._css_class = value
+
+ def get_css_class(self, device=None):
+ if not device:
+ device = ('xs', 'sm', 'md', 'lg')
+ elif isinstance(device, str):
+ device = (device, )
+ result = [self.css_class or '']
+ for attr in device:
+ width = getattr(self, attr + '_width')
+ result.append('col-{0}-{1}'.format(attr, width))
+ return ' '.join(result)
+
+ def get_width(self, device=None):
+ if not device:
+ device = ('xs', 'sm', 'md', 'lg')
+ elif isinstance(device, str):
+ device = (device, )
+ result = {}
+ for attr in device:
+ result[attr] = getattr(self, attr + '_width')
+ return result
+
+ def set_width(self, width, device=None):
+ if not device:
+ device = ('xs', 'sm', 'md', 'lg')
+ elif isinstance(device, str):
+ device = (device, )
+ for attr in device:
+ setattr(self, attr + '_width', width)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/template.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,441 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, IPortalContext, IPortalPage, \
+ IPortletConfiguration, IPortlet, IPortalTemplateContainer, IPortalWfTemplate, IPortalTemplateContainerConfiguration
+from pyams_workflow.interfaces import IWorkflowVersions
+from zope.annotation.interfaces import IAnnotations
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from persistent.list import PersistentList
+from persistent.mapping import PersistentMapping
+from pyams_portal.slot import SlotConfiguration
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import get_local_registry
+from pyams_utils.request import check_request
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.componentvocabulary.vocabulary import UtilityVocabulary
+from zope.container.contained import Contained
+from zope.container.folder import Folder
+from zope.copy import clone
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location.location import locate
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+
+@implementer(IPortalTemplateContainer)
+class PortalTemplateContainer(Folder):
+ """Portal template container"""
+
+
+@implementer(IPortalTemplateContainerConfiguration)
+class PortalTemplateContainerConfiguration(Persistent, Contained):
+ """Portal template container configuration"""
+
+ selected_portlets = FieldProperty(IPortalTemplateContainerConfiguration['selected_portlets'])
+
+
+PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY = 'pyams_portal.container.configuration'
+
+
+@adapter_config(context=IPortalTemplateContainer, provides=IPortalTemplateContainerConfiguration)
+def PortalTemplateContainerConfigurationFactory(context):
+ """Portal template container configuration factory"""
+ annotations = IAnnotations(context)
+ config = annotations.get(PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY)
+ if config is None:
+ config = annotations[PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY] = PortalTemplateContainerConfiguration()
+ get_current_registry().notify(ObjectCreatedEvent(config))
+ locate(config, context)
+ return config
+
+
+@implementer(IPortalTemplate)
+class PortalTemplate(Persistent, Contained):
+ """Portal template persistent class"""
+
+ name = FieldProperty(IPortalTemplate['name'])
+
+
+@implementer(IPortalWfTemplate)
+class PortalWfTemplate(Persistent, Contained):
+ """Portal template workflow manager class"""
+
+ content_class = PortalTemplate
+ workflow_name = 'PyAMS portal template workflow'
+ view_permission = None
+
+
+@subscriber(IObjectAddedEvent, context_selector=IPortalWfTemplate)
+def handle_added_template(event):
+ """Register shared template"""
+ registry = get_local_registry()
+ if (registry is not None) and IPortalTemplateContainer.providedBy(event.newParent):
+ registry.registerUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IPortalWfTemplate)
+def handle_removed_template(event):
+ """Unregister removed template"""
+ registry = get_local_registry()
+ if (registry is not None) and IPortalTemplateContainer.providedBy(event.oldParent):
+ registry.unregisterUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
+
+
+class PortalTemplatesVocabulary(UtilityVocabulary):
+ """Portal templates vocabulary"""
+
+ interface = IPortalWfTemplate
+ nameOnly = True
+
+getVocabularyRegistry().register('PyAMS portal templates', PortalTemplatesVocabulary)
+
+
+@implementer(IPortalTemplateConfiguration)
+class PortalTemplateConfiguration(Persistent, Contained):
+ """Portal template configuration"""
+
+ rows = FieldProperty(IPortalTemplateConfiguration['rows'])
+ _slot_names = FieldProperty(IPortalTemplateConfiguration['slot_names'])
+ _slot_order = FieldProperty(IPortalTemplateConfiguration['slot_order'])
+ _slots = FieldProperty(IPortalTemplateConfiguration['slots'])
+ slot_config = FieldProperty(IPortalTemplateConfiguration['slot_config'])
+ portlet_config = FieldProperty(IPortalTemplateConfiguration['portlet_config'])
+
+ def __init__(self):
+ self._slot_names = PersistentList()
+ self._slot_order = PersistentMapping()
+ self._slot_order[0] = PersistentList()
+ self._slots = PersistentMapping()
+ self._slots[0] = PersistentMapping()
+ self.slot_config = PersistentMapping()
+ self.portlet_config = PersistentMapping()
+
+ def add_row(self):
+ """Add new row and return last row index (0 based)"""
+ self.rows += 1
+ last_index = self.rows - 1
+ self.slot_order[last_index] = PersistentList()
+ self.slots[last_index] = PersistentMapping()
+ return last_index
+
+ def set_row_order(self, order):
+ """Change template row order"""
+ if not isinstance(order, (list, tuple)):
+ order = list(order)
+ old_slot_order = self.slot_order
+ old_slots = self.slots
+ assert len(order) == self.rows
+ new_slot_order = PersistentMapping()
+ new_slots = PersistentMapping()
+ for index, row_id in enumerate(order):
+ new_slot_order[index] = old_slot_order.get(row_id) or PersistentList()
+ new_slots[index] = old_slots.get(row_id) or PersistentMapping()
+ if self.slot_order != new_slot_order:
+ self.slot_order = new_slot_order
+ self.slots = new_slots
+
+ def delete_row(self, row_id):
+ """Delete template row"""
+ assert row_id in self.slots
+ for slot_name in self.slots.get(row_id, {}).keys():
+ if slot_name in self.slot_names:
+ self.slot_names.remove(slot_name)
+ if slot_name in self.slot_config:
+ del self.slot_config[slot_name]
+ if slot_name in self.portlet_config:
+ del self.portlet_config[slot_name]
+ for index in range(row_id, self.rows-1):
+ self.slot_order[index] = self.slot_order[index+1]
+ self.slots[index] = self.slots[index+1]
+ if self.rows > 0:
+ del self.slot_order[self.rows-1]
+ del self.slots[self.rows-1]
+ self.rows -= 1
+
+ @property
+ def slot_names(self):
+ if IPortalTemplate.providedBy(self.__parent__):
+ return self._slot_names
+ else:
+ return IPortalTemplateConfiguration(self.__parent__).slot_names
+
+ @slot_names.setter
+ def slot_names(self, value):
+ self._slot_names = value
+
+ @property
+ def slot_order(self):
+ if IPortalTemplate.providedBy(self.__parent__):
+ return self._slot_order
+ else:
+ return IPortalTemplateConfiguration(self.__parent__).slot_order
+
+ @slot_order.setter
+ def slot_order(self, value):
+ self._slot_order = value
+
+ @property
+ def slots(self):
+ if IPortalTemplate.providedBy(self.__parent__):
+ return self._slots
+ else:
+ return IPortalTemplateConfiguration(self.__parent__).slots
+
+ @slots.setter
+ def slots(self, value):
+ self._slots = value
+
+ def add_slot(self, slot_name, row_id=None):
+ assert slot_name not in self.slot_names
+ self.slot_names.append(slot_name)
+ if row_id is None:
+ row_id = 0
+ # init slots order
+ if row_id not in self.slot_order:
+ self.slot_order[row_id] = PersistentList()
+ self.slot_order[row_id].append(slot_name)
+ # init slots portlets
+ if row_id not in self.slots:
+ self.slots[row_id] = PersistentMapping()
+ self.slots[row_id][slot_name] = PersistentList()
+ # init slots configuration
+ slot = self.slot_config[slot_name] = SlotConfiguration(slot_name)
+ locate(slot, self.__parent__)
+ return row_id, slot_name
+
+ def set_slot_order(self, order):
+ """Set slots order"""
+ old_slot_order = self.slot_order
+ old_slots = self.slots
+ new_slot_order = PersistentMapping()
+ new_slots = PersistentMapping()
+ for row_id in sorted(map(int, order.keys())):
+ new_slot_order[row_id] = PersistentList(order[row_id])
+ new_slots[row_id] = PersistentMapping()
+ for slot_name in order[row_id]:
+ old_row_id = self.get_slot_row(slot_name)
+ new_slots[row_id][slot_name] = old_slots[old_row_id][slot_name]
+ if new_slot_order != old_slot_order:
+ self.slot_order = new_slot_order
+ self.slots = new_slots
+
+ def get_slot_row(self, slot_name):
+ for row_id in self.slot_order:
+ if slot_name in self.slot_order[row_id]:
+ return row_id
+
+ def get_slots(self, row_id):
+ """Get ordered slots list"""
+ return self.slot_order.get(row_id, [])
+
+ def get_slots_width(self, device=None):
+ """Get slots width"""
+ result = {}
+ for slot_name, config in self.slot_config.items():
+ result[slot_name] = config.get_width(device)
+ return result
+
+ def set_slot_width(self, slot_name, device, width):
+ """Set slot width"""
+ self.slot_config[slot_name].set_width(width, device)
+
+ def get_slot_configuration(self, slot_name):
+ """Get slot configuration"""
+ if slot_name not in self.slot_names:
+ return None
+ config = self.slot_config.get(slot_name)
+ if config is None:
+ if IPortalTemplate.providedBy(self.__parent__):
+ config = SlotConfiguration()
+ else:
+ config = clone(IPortalTemplateConfiguration(self.__parent__).get_slot_configuration(slot_name))
+ config.inherit_parent = True
+ self.slot_config[slot_name] = config
+ locate(config, self.__parent__)
+ return config
+
+ def delete_slot(self, slot_name):
+ """Delete slot and associated portlets"""
+ assert slot_name in self.slot_names
+ row_id = self.get_slot_row(slot_name)
+ del self.portlet_config[slot_name]
+ del self.slot_config[slot_name]
+ del self.slots[row_id][slot_name]
+ self.slot_order[row_id].remove(slot_name)
+ self.slot_names.remove(slot_name)
+
+ def add_portlet(self, portlet_name, slot_name):
+ """Add portlet to given slot"""
+ assert slot_name in self.slot_names
+ row_id = self.get_slot_row(slot_name)
+ if slot_name not in self.slots.get(row_id):
+ self.slots[row_id][slot_name] = PersistentList()
+ self.slots[row_id][slot_name].append(portlet_name)
+ if slot_name not in self.portlet_config:
+ self.portlet_config[slot_name] = PersistentMapping()
+ position = len(self.slots[row_id][slot_name]) - 1
+ portlet = get_current_registry().getUtility(IPortlet, name=portlet_name)
+ config = IPortletConfiguration(portlet)
+ config.slot_name = slot_name
+ config.position = position
+ locate(config, self.__parent__, '++portlet++{0}::{1}'.format(slot_name, position))
+ self.portlet_config[slot_name][position] = config
+ return {'portlet_name': portlet_name,
+ 'slot_name': slot_name,
+ 'position': position,
+ 'label': check_request().localizer.translate(portlet.label)}
+
+ def set_portlet_order(self, order):
+ """Set portlet order"""
+ source = order['from']
+ source_slot = source['slot']
+ source_row = self.get_slot_row(source_slot)
+ target = order['to']
+ target_slot = target['slot']
+ target_row = self.get_slot_row(target_slot)
+ portlet_config = self.portlet_config
+ old_config = portlet_config[source_slot].pop(source['position'])
+ target_config = PersistentMapping()
+ for index, (slot_name, portlet_name, position) in enumerate(zip(target['slots'], target['names'],
+ target['positions'])):
+ if (slot_name == source_slot) and (position == source['position']):
+ target_config[index] = old_config
+ else:
+ target_config[index] = portlet_config[slot_name][position]
+ target_config[index].slot_name = target_slot
+ target_config[index].position = index
+ locate(target_config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
+ portlet_config[target_slot] = target_config
+ # re-order source portlets
+ config = portlet_config[source_slot]
+ for index, key in enumerate(sorted(config)):
+ if index != key:
+ config[index] = config.pop(key)
+ config[index].position = index
+ locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(source_slot, index))
+ # re-order target portlets
+ if target_slot != source_slot:
+ config = portlet_config[target_slot]
+ for index, key in enumerate(sorted(config)):
+ config[index] = config.pop(key)
+ config[index].position = index
+ locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(target_slot, index))
+ self.portlet_config = portlet_config
+ del self.slots[source_row][source_slot][source['position']]
+ self.slots[target_row][target_slot] = PersistentList(target['names'])
+
+ def get_portlet_configuration(self, slot_name, position):
+ """Get portlet configuration"""
+ if slot_name not in self.slot_names:
+ return None
+ config = self.portlet_config.get(slot_name, {}).get(position)
+ if config is None:
+ if IPortalTemplate.providedBy(self.__parent__):
+ portlet_name = self.slots[slot_name][position]
+ portlet = get_current_registry().queryUtility(IPortlet, name=portlet_name)
+ config = IPortletConfiguration(portlet)
+ else:
+ config = clone(IPortalTemplateConfiguration(self.__parent__).get_portlet_configuration(slot_name,
+ position))
+ config.inherit_parent = True
+ self.portlet_config[slot_name][position] = config
+ locate(config, self.__parent__)
+ return config
+
+ def delete_portlet(self, slot_name, position):
+ """Delete portlet"""
+ assert slot_name in self.slot_names
+ row_id = self.get_slot_row(slot_name)
+ config = self.portlet_config[slot_name]
+ del config[position]
+ if len(config) and (position < max(tuple(config.keys()))):
+ for index, key in enumerate(sorted(config)):
+ config[index] = config.pop(key)
+ config[index].position = index
+ locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
+ del self.slots[row_id][slot_name][position]
+
+
+class PortalTemplateSlotsVocabulary(SimpleVocabulary):
+ """Portal template slots vocabulary"""
+
+ def __init__(self, context):
+ config = IPortalTemplateConfiguration(context)
+ terms = [SimpleTerm(slot_name) for slot_name in sorted(config.slot_names)]
+ super(PortalTemplateSlotsVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS template slots', PortalTemplateSlotsVocabulary)
+
+
+@adapter_config(name='portlet', context=IPortalTemplate, provides=ITraversable)
+class PortalTemplatePortletTraverser(ContextAdapter):
+ """++portlet++ namespace traverser"""
+
+ def traverse(self, name, furtherpath=None):
+ config = IPortalTemplateConfiguration(self.context)
+ if name:
+ slot_name, position = name.split('::')
+ return config.get_portlet_configuration(slot_name, int(position))
+ else:
+ return config
+
+
+TEMPLATE_CONFIGURATION_KEY = 'pyams_portal.template'
+
+
+@adapter_config(context=IPortalTemplate, provides=IPortalTemplateConfiguration)
+def PortalTemplateConfigurationFactory(context):
+ """Portal template configuration factory"""
+ annotations = IAnnotations(context)
+ config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
+ if config is None:
+ config = annotations[TEMPLATE_CONFIGURATION_KEY] = PortalTemplateConfiguration()
+ get_current_registry().notify(ObjectCreatedEvent(config))
+ locate(config, context)
+ return config
+
+
+@adapter_config(context=IPortalContext, provides=IPortalTemplateConfiguration)
+def PortalContextConfigurationFactory(context):
+ """Portal context configuration factory"""
+ page = IPortalPage(context)
+ if page.use_local_template:
+ template = IWorkflowVersions(page.template).get_last_versions()[0]
+ config = IPortalTemplateConfiguration(template)
+ else:
+ annotations = IAnnotations(context)
+ config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
+ if config is None:
+ # we clone template configuration
+ config = annotations[TEMPLATE_CONFIGURATION_KEY] = clone(IPortalTemplateConfiguration(page.template))
+ get_current_registry().notify(ObjectCreatedEvent(config))
+ locate(config, context)
+ return config
+
+
+@adapter_config(context=IPortletConfiguration, provides=IPortalTemplateConfiguration)
+def PortalPortletConfigurationFactory(context):
+ """Portal portlet configuration factory"""
+ return IPortalTemplateConfiguration(context.__parent__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/tests/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/tests/test_utilsdocs.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,59 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+"""
+Generic Test case for pyams_portal doctest
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.dirname(__file__)
+
+def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):
+ """Returns a test suite, based on doctests found in /doctest."""
+ suite = []
+ if globs is None:
+ globs = globals()
+
+ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_ONLY_FIRST_FAILURE)
+
+ package_dir = os.path.split(test_dir)[0]
+ if package_dir not in sys.path:
+ sys.path.append(package_dir)
+
+ doctest_dir = os.path.join(package_dir, 'doctests')
+
+ # filtering files on extension
+ docs = [os.path.join(doctest_dir, doc) for doc in
+ os.listdir(doctest_dir) if doc.endswith('.txt')]
+
+ for test in docs:
+ suite.append(doctest.DocFileSuite(test, optionflags=flags,
+ globs=globs, setUp=setUp,
+ tearDown=tearDown,
+ module_relative=False))
+
+ return unittest.TestSuite(suite)
+
+def test_suite():
+ """returns the test suite"""
+ return doc_suite(current_dir)
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/tests/test_utilsdocstrings.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+"""
+Generic Test case for pyams_portal doc strings
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.abspath(os.path.dirname(__file__))
+
+def doc_suite(test_dir, globs=None):
+ """Returns a test suite, based on doc tests strings found in /*.py"""
+ suite = []
+ if globs is None:
+ globs = globals()
+
+ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_ONLY_FIRST_FAILURE)
+
+ package_dir = os.path.split(test_dir)[0]
+ if package_dir not in sys.path:
+ sys.path.append(package_dir)
+
+ # filtering files on extension
+ docs = [doc for doc in
+ os.listdir(package_dir) if doc.endswith('.py')]
+ docs = [doc for doc in docs if not doc.startswith('__')]
+
+ for test in docs:
+ fd = open(os.path.join(package_dir, test))
+ content = fd.read()
+ fd.close()
+ if '>>> ' not in content:
+ continue
+ test = test.replace('.py', '')
+ location = 'pyams_portal.%s' % test
+ suite.append(doctest.DocTestSuite(location, optionflags=flags,
+ globs=globs))
+
+ return unittest.TestSuite(suite)
+
+def test_suite():
+ """returns the test suite"""
+ return doc_suite(current_dir)
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/workflow.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,223 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from datetime import datetime
+
+# import interfaces
+from pyams_workflow.interfaces import IWorkflowPublicationInfo, IWorkflowState, IWorkflowVersions, IWorkflowInfo, \
+ ObjectClonedEvent, IWorkflow
+
+# import packages
+from pyams_utils.registry import utility_config
+from pyams_workflow.workflow import Transition, Workflow
+from pyramid.threadlocal import get_current_registry
+from zope.copy import copy
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+from pyams_portal import _
+
+
+DRAFT = 'draft' # new template
+PUBLISHED = 'published' # published template
+RETIRED = 'retired' # retired template
+ARCHIVED = 'archived' # archive done
+DELETED = 'deleted' # deleted template
+
+
+STATUS_IDS = ('draft', 'published', 'retired', 'archived', 'deleted')
+STATUS_LABELS = (_("Draft"),
+ _("Published"),
+ _("Retired"),
+ _("Archived"),
+ _("Deleted"))
+
+STATUS_VOCABULARY = SimpleVocabulary([SimpleTerm(STATUS_IDS[i], STATUS_IDS[i], t)
+ for i, t in enumerate(STATUS_LABELS)])
+
+
+def publish_action(wf, context):
+ """Publish template"""
+ now = datetime.utcnow()
+ wf_content = IWorkflowPublicationInfo(context)
+ if wf_content.first_publication_date is None:
+ wf_content.first_publication_date = now
+ IWorkflowPublicationInfo(context).publication_date = now
+ version_id = IWorkflowState(context).version_id
+ for version in IWorkflowVersions(context).get_versions(('published', 'retired')):
+ if version is not context:
+ IWorkflowInfo(version).fire_transition_toward('archived',
+ comment="Published version {0}".format(version_id))
+
+
+def clone_action(wf, context):
+ """Duplicate template"""
+ result = copy(context)
+ registry = get_current_registry()
+ registry.notify(ObjectClonedEvent(result, context))
+ return result
+
+
+def retire_action(wf, context):
+ """Archive template"""
+ now = datetime.utcnow()
+ IWorkflowPublicationInfo(context).publication_expiration_date = now
+
+
+def archive_action(wf, context):
+ """Archive template"""
+ now = datetime.utcnow()
+ content = IWorkflowPublicationInfo(context)
+ content.publication_expiration_date = min(content.publication_expiration_date or now, now)
+
+
+def can_delete(wf, context):
+ content = IWorkflowPublicationInfo(context)
+ return content.publication_effective_date is None
+
+
+def delete_action(wf, context):
+ """Delete draft version"""
+ parent = context.__parent__
+ name = context.__name__
+ del parent[name]
+
+
+#
+# Workflow transitions
+#
+
+init = Transition(transition_id='init',
+ title=_("Initialize"),
+ source=None,
+ destination=DRAFT)
+
+draft_to_published = Transition('draft_to_published',
+ title=_("Publish..."),
+ source=DRAFT,
+ destination=PUBLISHED,
+ permission='portal.templates.manage',
+ action=publish_action,
+ order=1,
+ menu_css_class='fa fa-fw fa-play',
+ view_name='wf-publish.html',
+ html_help=_('''This content is currently in DRAFT mode.
+ Publishing it will make it publicly visible.'''))
+
+published_to_retired = Transition('published_to_retired',
+ title=_("Retire..."),
+ source=PUBLISHED,
+ destination=RETIRED,
+ permission='portal.templates.manage',
+ action=retire_action,
+ order=2,
+ menu_css_class='fa fa-fw fa-stop',
+ view_name='wf-retire.html',
+ html_help=_('''This content is actually published.
+ You can retire it to make it invisible, but contents using this
+ template won't be visible anymore!'''))
+
+published_to_draft = Transition('published_to_draft',
+ title=_("Create new version..."),
+ source=PUBLISHED,
+ destination=DRAFT,
+ permission='portal.templates.manage',
+ action=clone_action,
+ order=99,
+ menu_css_class='fa fa-fw fa-copy',
+ view_name='wf-clone.html')
+
+retired_to_published = Transition('retired_to_published',
+ title=_("Re-publish..."),
+ source=RETIRED,
+ destination=PUBLISHED,
+ permission='portal.templates.manage',
+ action=publish_action,
+ order=1,
+ menu_css_class='fa fa-fw fa-play',
+ view_name='wf-publish.html',
+ html_help=_('''This content was published and retired.
+ You can re-publish it to make it visible again.'''))
+
+published_to_archived = Transition('published_to_archived',
+ title=_("Archive..."),
+ source=PUBLISHED,
+ destination=ARCHIVED,
+ permission='portal.templates.manage',
+ action=archive_action,
+ order=3,
+ menu_css_class='fa fa-fw fa-archive',
+ view_name='wf-archive.html',
+ html_help=_('''This content is currently published.
+ If it is archived, it will not be possible to make it visible again
+ except by creating a new version!'''))
+
+retired_to_archived = Transition('retired_to_archived',
+ title=_("Archive..."),
+ source=RETIRED,
+ destination=ARCHIVED,
+ permission='portal.templates.manage',
+ action=archive_action,
+ order=3,
+ menu_css_class='fa fa-fw fa-archive',
+ view_name='wf-archive.html',
+ html_help=_('''This content has been published but is currently retired.
+ If it is archived, it will not be possible to make it visible again
+ except by creating a new version!'''))
+
+archived_to_draft = Transition('archived_to_draft',
+ title=_("Create new version..."),
+ source=ARCHIVED,
+ destination=DRAFT,
+ permission='portal.templates.manage',
+ action=clone_action,
+ order=99,
+ menu_css_class='fa fa-fw fa-copy',
+ view_name='wf-clone.html')
+
+deleted = Transition('delete',
+ title=_("Delete..."),
+ source=DRAFT,
+ destination=DELETED,
+ condition=can_delete,
+ action=delete_action,
+ order=6,
+ menu_css_class='fa fa-fw fa-trash',
+ view_name='wf-delete.html',
+ html_help=_('''This content has never been published.
+ It can be removed and definitely deleted.'''))
+
+wf_transitions = [init,
+ draft_to_published,
+ published_to_retired,
+ published_to_draft,
+ retired_to_published,
+ published_to_archived,
+ retired_to_archived,
+ archived_to_draft,
+ deleted]
+
+
+wf = Workflow(wf_transitions,
+ states=STATUS_VOCABULARY,
+ published_states=(PUBLISHED,))
+
+
+@utility_config(name='PyAMS portal template workflow', provides=IWorkflow)
+class WorkflowUtility(object):
+ """Workflow utility registration"""
+
+ def __new__(cls):
+ return wf
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/container.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,204 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortalTemplateContainer, IPortalWfTemplate, IPortalTemplateContainerConfiguration
+from pyams_portal.zmi.interfaces import IPortalTemplateContainerMenu
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.container import ITable, ITableElementEditor
+from pyams_skin.layer import IPyAMSLayer
+from pyams_workflow.interfaces import IWorkflowVersions
+from pyams_zmi.interfaces.menu import IControlPanelMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IColumn, IValues
+from zope.component.interfaces import ISite
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import ContainerView
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import DefaultElementEditorAdapter, BaseTable, TrashColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_workflow.zmi.workflow import WorkflowContentNameColumn
+from pyams_zmi.form import AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.url import resource_url
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_portal import _
+
+
+#
+# Template container views
+#
+
+@adapter_config(context=(IPortalTemplateContainer, IAdminLayer, ITable), provides=ITableElementEditor)
+class PortalTemplateContainerTableElementEditor(DefaultElementEditorAdapter):
+ """Portal template container table element editor"""
+
+ view_name = 'portal-templates.html'
+ modal_target = False
+
+ @property
+ def url(self):
+ site = get_parent(self.context, ISite)
+ return resource_url(site, self.request, 'admin.html#{0}'.format(self.view_name))
+
+
+@viewlet_config(name='portal-templates.menu', context=ISite, layer=IAdminLayer, manager=IControlPanelMenu,
+ permission='system.view', weight=20)
+@viewletmanager_config(name='portal-templates.menu', context=ISite, layer=IAdminLayer)
+@implementer(IPortalTemplateContainerMenu)
+class PortalTemplateContainerMenuItem(MenuItem):
+ """Portal template container menu"""
+
+ label = _("Portal templates")
+ icon_class = 'fa fa-fw fa-columns'
+ url = '#portal-templates.html'
+
+
+class PortalTemplateContainerTable(BaseTable):
+ """Portal template container table"""
+
+ id = 'portal_templates_table'
+ title = _("Shared portal templates")
+
+ @property
+ def data_attributes(self):
+ manager = query_utility(IPortalTemplateContainer)
+ attributes = super(PortalTemplateContainerTable, self).data_attributes
+ table_attrs = {'data-ams-location': absolute_url(manager, self.request),
+ 'data-ams-plugins': 'pyams_workflow pyams_portal',
+ 'data-ams-plugin-pyams_workflow-src': "/--static--/pyams_workflow/js/workflow{MyAMS.devext}.js",
+ 'data-ams-plugin-pyams_portal-src': "/--static--/pyams_portal/js/portal{MyAMS.devext}.js",
+ 'data-ams-plugin-pyams_portal-css': "/--static--/pyams_portal/css/portal{MyAMS.devext}.css"}
+ if 'table' in attributes:
+ attributes['table'].update(table_attrs)
+ else:
+ attributes['table'] = table_attrs
+ return attributes
+
+
+@adapter_config(context=(IPortalWfTemplate, IAdminLayer, PortalTemplateContainerTable), provides=ITableElementEditor)
+class PortalTemplateTableElementEditor(DefaultElementEditorAdapter):
+ """Portal template table element editor"""
+
+ modal_target = False
+
+ @property
+ def url(self):
+ wf_versions = IWorkflowVersions(self.context).get_last_versions(count=1)
+ if wf_versions:
+ return resource_url(wf_versions[0], self.request, 'admin.html#{0}'.format(self.view_name))
+ else:
+ return None
+
+
+@adapter_config(name='name', context=(Interface, IAdminLayer, PortalTemplateContainerTable), provides=IColumn)
+class PortalTemplateContainerNameColumn(WorkflowContentNameColumn):
+ """Portal template container name column"""
+
+ name_field = 'name'
+
+
+@adapter_config(name='trash', context=(Interface, IAdminLayer, PortalTemplateContainerTable), provides=IColumn)
+class PortalTemplateContainerTrashColumn(TrashColumn):
+ """Portal template container trash column"""
+
+ icon_hint = _("Delete template")
+ permission = 'portal.templates.manage'
+
+
+@adapter_config(context=(ISite, IAdminLayer, PortalTemplateContainerTable), provides=IValues)
+class PortalTemplateContainerValuesAdapter(ContextRequestViewAdapter):
+ """Portal template container values adapter"""
+
+ @property
+ def values(self):
+ manager = query_utility(IPortalTemplateContainer)
+ if manager is not None:
+ return manager.values()
+ return ()
+
+
+@pagelet_config(name='portal-templates.html', context=ISite, layer=IPyAMSLayer, permission='system.view')
+@implementer(IInnerPage)
+class PortalTemplateContainerView(AdminView, ContainerView):
+ """Portal template container view"""
+
+ table_class = PortalTemplateContainerTable
+
+ def __init__(self, context, request):
+ super(PortalTemplateContainerView, self).__init__(context, request)
+
+
+@adapter_config(context=(ISite, IAdminLayer, PortalTemplateContainerView), provides=IPageHeader)
+class PortalTemplateContainerHeaderAdapter(DefaultPageHeaderAdapter):
+ """Portal template container header adapter"""
+
+ icon_class = 'fa fa-fw fa-columns'
+ title = _("Portal")
+ subtitle = _("Portal templates")
+
+
+#
+# Templates container configuration views
+#
+
+@viewlet_config(name='templates-container-configuration.menu', context=ISite, layer=IAdminLayer,
+ manager=IPortalTemplateContainerMenu, permission='system.view', weight=1)
+class PortalTemplatesContainerPropertiesMenu(MenuItem):
+ """Portal template container configuration menu"""
+
+ label = _("Selected portlets...")
+ icon_class = 'fa-thumb-tack'
+
+ url = 'properties.html'
+ modal_target = True
+
+ def get_url(self):
+ container = query_utility(IPortalTemplateContainer)
+ return absolute_url(container, self.request, self.url)
+
+
+@pagelet_config(name='properties.html', context=IPortalTemplateContainer, layer=IPyAMSLayer,
+ permission='system.view')
+class PortalTemplateContainerPropertiesEditForm(AdminDialogEditForm):
+ """Portal template container properties edit form"""
+
+ title = _("Portal templates container")
+ legend = _("Edit selected portlets")
+ icon_css_class = 'fa fa-fw fa-thumb-tack'
+
+ fields = field.Fields(IPortalTemplateContainerConfiguration)
+ ajax_handler = 'properties.json'
+ edit_permission = 'system.manage'
+
+
+@view_config(name='properties.json', context=IPortalTemplateContainer, request_type=IPyAMSLayer,
+ permission='system.manage', renderer='json', xhr=True)
+class PortalTemplateContainerPropertiesAJAXEditForm(AJAXEditForm, PortalTemplateContainerPropertiesEditForm):
+ """Portal template container properties edit form, JSON renderer"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/interfaces.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,30 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_form.interfaces.form import IForm
+from pyams_skin.interfaces.viewlet import IMenuItem
+
+# import packages
+
+
+class IPortalTemplateContainerMenu(IMenuItem):
+ """Portal template container menu interface"""
+
+
+class IPortletConfigurationEditor(IForm):
+ """Portlet configuration editor interface"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/portlet.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,63 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortlet
+from z3c.form.interfaces import HIDDEN_MODE
+
+# import packages
+from pyams_zmi.form import AdminDialogEditForm
+from pyramid.url import resource_url
+from z3c.form import field
+
+from pyams_portal import _
+
+
+class PortletConfigurationEditor(AdminDialogEditForm):
+ """Base portlet configuration editor"""
+
+ @property
+ def title(self):
+ translate = self.request.localizer.translate
+ registry = self.request.registry
+ portlet = registry.queryUtility(IPortlet, name=self.context.portlet_name)
+ return translate(_("« {0} » portal template - {1}")).format(self.context.__parent__.name,
+ translate(portlet.label))
+
+ legend = _("Edit portlet configuration")
+ dialog_class = 'modal-large'
+
+ interface = None
+ edit_permission = 'portal.templates.manage'
+
+ def get_form_action(self):
+ return resource_url(self.context.__parent__, self.request, self.request.view_name)
+
+ def get_ajax_handler(self):
+ return resource_url(self.context.__parent__, self.request, self.ajax_handler)
+
+ @property
+ def fields(self):
+ fields = field.Fields(self.interface)
+ if not self.getContent().can_inherit:
+ fields = fields.omit('inherit_parent')
+ return fields
+
+ def updateWidgets(self, prefix=None):
+ super(PortletConfigurationEditor, self).updateWidgets(prefix)
+ self.widgets['slot_name'].mode = HIDDEN_MODE
+ self.widgets['position'].mode = HIDDEN_MODE
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/portlets/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/portlets/context.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_pagelet.interfaces import IPagelet
+from pyams_portal.interfaces import IPortletPreviewer
+from pyams_portal.portlets.context.interfaces import IContextPortletConfiguration
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.portlet import PortletPreviewer
+from pyams_portal.zmi.portlet import PortletConfigurationEditor
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from zope.interface import Interface
+
+
+@pagelet_config(name='properties.html', context=IContextPortletConfiguration, request_type=IPyAMSLayer,
+ permission='system.view')
+class ContextPortletConfigurationEditor(PortletConfigurationEditor):
+ """Context portlet configuration editor"""
+
+ interface = IContextPortletConfiguration
+
+
+@adapter_config(name='properties.json', context=(IContextPortletConfiguration, IPyAMSLayer), provides=IPagelet)
+class ContextPortletConfigurationAJAXEditor(AJAXEditForm, ContextPortletConfigurationEditor):
+ """Context portlet configuration editor, AJAX renderer"""
+
+
+@adapter_config(context=(Interface, IPyAMSLayer, Interface, IContextPortletConfiguration),
+ provides=IPortletPreviewer)
+@template_config(template='templates/context-preview.pt', layer=IPyAMSLayer)
+class ContextPortletPreviewer(PortletPreviewer):
+ """Context portlet previewer"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/portlets/image.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyams_portal.interfaces import IPortletPreviewer
+from pyams_portal.portlet import PortletPreviewer
+from pyams_template.template import template_config
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_pagelet.interfaces import IPagelet
+from pyams_portal.portlets.image.interfaces import IImagePortletConfiguration
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.zmi.portlet import PortletConfigurationEditor
+from pyams_utils.adapter import adapter_config
+from zope.interface import Interface
+
+
+@pagelet_config(name='properties.html', context=IImagePortletConfiguration, request_type=IPyAMSLayer,
+ permission='system.view')
+class ImagePortletConfigurationEditor(PortletConfigurationEditor):
+ """Image portlet configuration editor"""
+
+ interface = IImagePortletConfiguration
+
+
+@adapter_config(name='properties.json', context=(IImagePortletConfiguration, IPyAMSLayer), provides=IPagelet)
+class ImagePortletConfigurationAJAXEditor(AJAXEditForm, ImagePortletConfigurationEditor):
+ """Image portlet configuration editor, AJAX renderer"""
+
+
+@adapter_config(context=(Interface, IPyAMSLayer, Interface, IImagePortletConfiguration),
+ provides=IPortletPreviewer)
+@template_config(template='templates/image-preview.pt', layer=IPyAMSLayer)
+class ImagePortletPreviewer(PortletPreviewer):
+ """Image portlet previewer"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/portlets/templates/context-preview.pt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,1 @@
+This is my preview !!
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/portlets/templates/image-preview.pt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,32 @@
+<tal:var define="config view.configuration">
+ <tal:if condition="config.visible">
+ <tal:if condition="config.image">
+ <a class="fancybox margin-left-5" data-toggle
+ data-ams-fancybox-type="image"
+ tal:define="image config.image;
+ thumbnails extension:thumbnails(image);
+ target python:thumbnails.get_thumbnail('800x600', 'jpeg');"
+ tal:attributes="href extension:absolute_url(target);">
+ <img class="thumbnail padding-5 margin-5"
+ tal:define="thumbnail python:thumbnails.get_thumbnail('128x128', 'jpeg');"
+ tal:attributes="src extension:absolute_url(thumbnail)" src="" alt="" />
+ </a>
+ </tal:if>
+ <tal:if condition="not:config.image">
+ <div class="text-center padding-y-5">
+ <span class="fa-stack fa-lg">
+ <i class="fa fa-picture-o fa-stack-1x"></i>
+ <i class="fa fa-ban fa-stack-2x text-danger"></i>
+ </span>
+ </div>
+ </tal:if>
+ </tal:if>
+ <tal:if condition="not:config.visible">
+ <div class="text-center padding-y-5">
+ <span class="fa-stack fa-lg">
+ <i class="fa fa-eye fa-stack-1x"></i>
+ <i class="fa fa-ban fa-stack-2x text-danger"></i>
+ </span>
+ </div>
+ </tal:if>
+</tal:var>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/template/__init__.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,122 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyams_skin.interfaces import IContentTitle
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortalTemplateContainer, IPortalTemplate
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowInfo
+from pyams_zmi.layer import IAdminLayer
+from z3c.form.interfaces import IDataExtractedEvent
+from zope.component.interfaces import ISite
+
+# import packages
+from pyams_form.form import AJAXAddForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.template import PortalTemplate, PortalWfTemplate
+from pyams_portal.zmi.container import PortalTemplateContainerTable
+from pyams_skin.container import delete_container_element
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_utils.registry import query_utility
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm
+from pyramid.events import subscriber
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import Interface, Invalid
+from zope.lifecycleevent import ObjectCreatedEvent
+
+from pyams_portal import _
+
+
+@adapter_config(context=(IPortalTemplate, IPyAMSLayer, Interface), provides=IContentTitle)
+class PortalTemplateTitleAdapter(ContextRequestViewAdapter):
+ """Portal template title adapter"""
+
+ @property
+ def title(self):
+ translate = self.request.localizer.translate
+ return translate(_("« {0} » portal template")).format(self.context.name)
+
+
+#
+# Template views
+#
+
+@viewlet_config(name='add-portal-template.menu', context=ISite, layer=IAdminLayer,
+ view=PortalTemplateContainerTable, manager=IToolbarAddingMenu,
+ permission='portal.templates.manage', weight=20)
+class PortalTemplateAddMenu(ToolbarMenuItem):
+ """Portal template add menu"""
+
+ label = _("Add shared template...")
+ label_css_class = 'fa fa-fw fa-columns'
+ url = 'add-portal-template.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-portal-template.html', context=ISite, layer=IPyAMSLayer,
+ permission='portal.templates.manage')
+class PortalTemplateAddForm(AdminDialogAddForm):
+ """Portal template add form"""
+
+ title = _("Portal templates")
+ legend = _("Add shared template")
+ icon_css_class = 'fa fa-fw fa-columns'
+
+ fields = field.Fields(IPortalTemplate)
+ ajax_handler = 'add-portal-template.json'
+ edit_permission = None
+
+ def create(self, data):
+ return PortalTemplate()
+
+ def add(self, template):
+ wf_template = PortalWfTemplate()
+ self.request.registry.notify(ObjectCreatedEvent(wf_template))
+ context = query_utility(IPortalTemplateContainer)
+ context[template.name] = wf_template
+ IWorkflowVersions(wf_template).add_version(template, None)
+ IWorkflowInfo(template).fire_transition('init')
+
+ def nextURL(self):
+ return absolute_url(self.context, self.request, 'portal-templates.html')
+
+
+@subscriber(IDataExtractedEvent, form_selector=PortalTemplateAddForm)
+def handle_new_template_data_extraction(event):
+ """Handle new template form data extraction"""
+ container = query_utility(IPortalTemplateContainer)
+ name = event.data.get('name')
+ if name in container:
+ event.form.widgets.errors += (Invalid(_("Specified name is already used!")),)
+
+
+@view_config(name='add-portal-template.json', context=ISite, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplateAJAXAddForm(AJAXAddForm, PortalTemplateAddForm):
+ """Portal template add form, AJAX handler"""
+
+
+@view_config(name='delete-element.json', context=IPortalTemplateContainer, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def delete_portal_template(request):
+ """Delete template from portal"""
+ return delete_container_element(request)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/template/config.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,483 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyams_skin.page import DefaultPageHeaderAdapter
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+
+# import interfaces
+from pyams_pagelet.interfaces import IPagelet, PageletCreatedEvent
+from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, ISlot, \
+ IPortletAddingInfo, IPortlet, ISlotConfiguration, IPortletPreviewer, IPortalTemplateContainer, \
+ IPortalTemplateContainerConfiguration
+from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentTitle
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_workflow.interfaces import IWorkflowState, IWorkflowVersions
+from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from transaction.interfaces import ITransactionManager
+from z3c.form.interfaces import IDataExtractedEvent, HIDDEN_MODE
+
+# import packages
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.workflow import STATUS_LABELS, STATUS_IDS, PUBLISHED, ARCHIVED
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import JsToolbarMenuItem, ToolbarMenuDivider, ToolbarMenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.registry import query_utility
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.decorator import reify
+from pyramid.events import subscriber
+from pyramid.exceptions import NotFound
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, Invalid
+
+from pyams_portal import _
+
+
+@viewlet_config(name='template-properties.menu', context=IPortalTemplate, layer=IAdminLayer,
+ manager=ISiteManagementMenu, permission='system.view', weight=1)
+@viewletmanager_config(name='template-properties.menu', layer=IAdminLayer, provides=IPropertiesMenu)
+@implementer(IPropertiesMenu)
+class PortalTemplatePropertiesMenu(MenuItem):
+ """Portal template properties menu"""
+
+ label = _("Properties")
+ icon_class = 'fa-twitch'
+ url = '#properties.html'
+
+
+@pagelet_config(name='properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission='system.view')
+@template_config(template='templates/config.pt', layer=IAdminLayer)
+@implementer(IInnerPage)
+class PortalTemplateConfigView(AdminView):
+ """Portal template configuration view"""
+
+ title = _("Shared portal template configuration")
+
+ def get_context(self):
+ return self.context
+
+ @property
+ def can_change(self):
+ return self.request.has_permission('portal.templates.manage') and \
+ IWorkflowState(self.get_context()).state not in (PUBLISHED, ARCHIVED)
+
+ @reify
+ def configuration(self):
+ return IPortalTemplateConfiguration(self.get_context())
+
+ @property
+ def selected_portlets(self):
+ container = query_utility(IPortalTemplateContainer)
+ configuration = IPortalTemplateContainerConfiguration(container)
+ return [query_utility(IPortlet, name=portlet_name) for portlet_name in configuration.selected_portlets or ()]
+
+ def get_portlet(self, name):
+ return self.request.registry.getUtility(IPortlet, name=name)
+
+ def get_portlet_label(self, name):
+ return self.request.localizer.translate(self.get_portlet(name).label)
+
+ def get_portlet_preview(self, slot_name, position):
+ portlet_config = self.configuration.get_portlet_configuration(slot_name, position)
+ previewer = self.request.registry.queryMultiAdapter((self.get_context(), self.request, self, portlet_config),
+ IPortletPreviewer)
+ if previewer is not None:
+ previewer.update()
+ return previewer.render()
+ else:
+ return ''
+
+
+@adapter_config(context=(IPortalTemplate, IAdminLayer, PortalTemplateConfigView), provides=IPageHeader)
+class PortalTemplateConfigHeaderAdapter(DefaultPageHeaderAdapter):
+ """Portal template configuration header adapter"""
+
+ back_url = '/admin.html#portal-templates.html'
+ back_target = None
+
+ icon_class = 'fa fa-fw fa-columns'
+ subtitle = _("Portlets configuration")
+
+
+#
+# Rows views
+#
+
+@viewlet_config(name='add-template-row.menu', context=IPortalTemplate, layer=IAdminLayer,
+ view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
+ permission='portal.templates.manage', weight=1)
+class PortalTemplateRowAddMenu(JsToolbarMenuItem):
+ """Portal template row add menu"""
+
+ label = _("Add row...")
+ label_css_class = 'fa fa-fw fa-indent'
+ url = 'PyAMS_portal.template.addRow'
+
+
+@view_config(name='add-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def add_template_row(request):
+ """Add template raw"""
+ config = IPortalTemplateConfiguration(request.context)
+ return {'row_id': config.add_row()}
+
+
+@view_config(name='set-template-row-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def set_template_row_order(request):
+ """Set template rows order"""
+ config = IPortalTemplateConfiguration(request.context)
+ row_ids = map(int, json.loads(request.params.get('rows')))
+ config.set_row_order(row_ids)
+ return {'status': 'success'}
+
+
+@view_config(name='delete-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def delete_template_row(request):
+ """Delete template row"""
+ config = IPortalTemplateConfiguration(request.context)
+ config.delete_row(int(request.params.get('row_id')))
+ return {'status': 'success'}
+
+
+#
+# Slots views
+#
+
+@viewlet_config(name='add-template-slot.menu', context=IPortalTemplate, layer=IAdminLayer,
+ view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
+ permission='portal.templates.manage', weight=2)
+class PortalTemplateSlotAddMenu(ToolbarMenuItem):
+ """Portal template slot add menu"""
+
+ label = _("Add slot...")
+ label_css_class = 'fa fa-fw fa-columns'
+ url = 'add-template-slot.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-template-slot.html', context=IPortalTemplate, layer=IPyAMSLayer,
+ permission='portal.templates.manage')
+class PortalTemplateSlotAddForm(AdminDialogAddForm):
+ """Portal template slot add form"""
+
+ @property
+ def title(self):
+ translate = self.request.localizer.translate
+ return translate(_("« {0} » portal template")).format(self.context.name)
+
+ legend = _("Add slot")
+ icon_css_class = 'fa fa-fw fa-columns'
+
+ fields = field.Fields(ISlot)
+ ajax_handler = 'add-template-slot.json'
+ edit_permission = None
+
+ def updateWidgets(self, prefix=None):
+ super(PortalTemplateSlotAddForm, self).updateWidgets()
+ self.widgets['row_id'].value = self.request.params.get('form.widgets.row_id')
+
+ def createAndAdd(self, data):
+ config = IPortalTemplateConfiguration(self.context)
+ return config.add_slot(data.get('name'), data.get('row_id'))
+
+
+@subscriber(IDataExtractedEvent, form_selector=PortalTemplateSlotAddForm)
+def handle_new_slot_data_extraction(event):
+ """Handle new slot form data extraction"""
+ config = IPortalTemplateConfiguration(event.form.context)
+ name = event.data.get('name')
+ if name in config.slot_names:
+ event.form.widgets.errors += (Invalid(_("Specified name is already used!")),)
+
+
+@view_config(name='add-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplateSlotAJAXAddForm(AJAXAddForm, PortalTemplateSlotAddForm):
+ """Portal template slot add form, AJAX handler"""
+
+ def get_ajax_output(self, changes):
+ return {'status': 'callback',
+ 'callback': 'PyAMS_portal.template.addSlotCallback',
+ 'options': {'row_id': changes[0],
+ 'slot_name': changes[1]}}
+
+
+@view_config(name='set-template-slot-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def set_template_slot_order(request):
+ """Set template slots order"""
+ config = IPortalTemplateConfiguration(request.context)
+ order = json.loads(request.params.get('order'))
+ for key in order.copy().keys():
+ order[int(key)] = order.pop(key)
+ config.set_slot_order(order)
+ return {'status': 'success'}
+
+
+@view_config(name='get-slots-width.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='system.view', renderer='json', xhr=True)
+def get_template_slots_width(request):
+ """Get template slots width"""
+ config = IPortalTemplateConfiguration(request.context)
+ return config.get_slots_width(request.params.get('device'))
+
+
+@view_config(name='set-slot-width.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def set_template_slot_width(request):
+ """Set template slot width"""
+ config = IPortalTemplateConfiguration(request.context)
+ config.set_slot_width(request.params.get('slot_name'),
+ request.params.get('device'),
+ int(request.params.get('width')))
+ return config.get_slots_width(request.params.get('device'))
+
+
+@pagelet_config(name='slot-properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission='system.view')
+class PortalTemplateSlotPropertiesEditForm(AdminDialogEditForm):
+ """Slot properties edit form"""
+
+ @property
+ def title(self):
+ translate = self.request.localizer.translate
+ return translate(_("« {0} » portal template - {1} slot")).format(self.context.name,
+ self.getContent().slot_name)
+
+ legend = _("Edit slot properties")
+
+ label_css_class = 'control-label col-md-5'
+ input_css_class = 'col-md-7'
+
+ @property
+ def fields(self):
+ fields = field.Fields(ISlotConfiguration)
+ if not self.getContent().can_inherit:
+ fields = fields.omit('inherit_parent')
+ return fields
+
+ ajax_handler = 'slot-properties.json'
+ edit_permission = 'portal.templates.manage'
+
+ def __init__(self, context, request):
+ super(PortalTemplateSlotPropertiesEditForm, self).__init__(context, request)
+ self.config = IPortalTemplateConfiguration(context)
+
+ def getContent(self):
+ slot_name = self.request.params.get('form.widgets.slot_name')
+ return self.config.slot_config[slot_name]
+
+ def updateWidgets(self, prefix=None):
+ super(PortalTemplateSlotPropertiesEditForm, self).updateWidgets(prefix)
+ self.widgets['slot_name'].mode = HIDDEN_MODE
+
+
+@view_config(name='slot-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplateSlotPropertiesAJAXEditForm(AJAXEditForm, PortalTemplateSlotPropertiesEditForm):
+ """Slot properties edit form, AJAX renderer"""
+
+ def get_ajax_output(self, changes):
+ if changes:
+ slot_name = self.widgets['slot_name'].value
+ slot_config = self.config.slot_config[slot_name]
+ return {'status': 'success',
+ 'callback': 'PyAMS_portal.template.editSlotCallback',
+ 'options': {'slot_name': slot_name,
+ 'width': slot_config.get_width()}}
+ else:
+ return super(PortalTemplateSlotPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+@view_config(name='delete-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def delete_template_slot(request):
+ """Delete template slot"""
+ config = IPortalTemplateConfiguration(request.context)
+ config.delete_slot(request.params.get('slot_name'))
+ return {'status': 'success'}
+
+
+#
+# Portlet views
+#
+
+@viewlet_config(name='add-template-portlet.divider', context=IPortalTemplate, layer=IAdminLayer,
+ view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
+ permission='portal.templates.manage', weight=10)
+class PortalTemplateAddMenuDivider(ToolbarMenuDivider):
+ """Portal template menu divider"""
+
+
+@viewlet_config(name='add-template-portlet.menu', context=IPortalTemplate, layer=IAdminLayer,
+ view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
+ permission='portal.templates.manage', weight=20)
+class PortalTemplatePortletAddMenu(ToolbarMenuItem):
+ """Portal template portlet add menu"""
+
+ label = _("Add portlet...")
+ label_css_class = 'fa fa-fw fa-columns'
+ url = 'add-template-portlet.html'
+ modal_target = True
+
+
+@pagelet_config(name='add-template-portlet.html', context=IPortalTemplate, layer=IPyAMSLayer,
+ permission='portal.templates.manage')
+class PortalTemplatePortletAddForm(AdminDialogAddForm):
+ """Portal template portlet add form"""
+
+ @property
+ def title(self):
+ translate = self.request.localizer.translate
+ return translate(_("« {0} » portal template")).format(self.context.name)
+
+ legend = _("Add portlet")
+ icon_css_class = 'fa fa-fw fa-columns'
+
+ fields = field.Fields(IPortletAddingInfo)
+ ajax_handler = 'add-template-portlet.json'
+ edit_permission = None
+
+ def createAndAdd(self, data):
+ config = IPortalTemplateConfiguration(self.context)
+ return config.add_portlet(data.get('portlet_name'), data.get('slot_name'))
+
+
+@view_config(name='add-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplatePortletAJAXAddForm(AJAXAddForm, PortalTemplatePortletAddForm):
+ """Portal template portlet add form, AJAX handler"""
+
+ def get_ajax_output(self, changes):
+ config = IPortalTemplateConfiguration(self.context)
+ portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position'])
+ previewer = self.request.registry.queryMultiAdapter((self.context, self.request, self, portlet_config),
+ IPortletPreviewer)
+ if previewer is not None:
+ previewer.update()
+ changes['preview'] = previewer.render()
+ return {'status': 'callback',
+ 'callback': 'PyAMS_portal.template.addPortletCallback',
+ 'options': changes}
+
+
+@view_config(name='drag-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def drag_template_portlet(request):
+ """Drag portlet icon to slot"""
+ config = IPortalTemplateConfiguration(request.context)
+ portlet_name = request.params.get('portlet_name')
+ slot_name = request.params.get('slot_name')
+ changes = config.add_portlet(portlet_name, slot_name)
+ portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position'])
+ previewer = request.registry.queryMultiAdapter((request.context, request, request, portlet_config),
+ IPortletPreviewer)
+ if previewer is not None:
+ previewer.update()
+ changes['preview'] = previewer.render()
+ return {'status': 'callback',
+ 'close_form': False,
+ 'callback': 'PyAMS_portal.template.addPortletCallback',
+ 'options': changes}
+
+
+@view_config(name='set-template-portlet-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def set_template_portlet_order(request):
+ """Set template portlet order"""
+ config = IPortalTemplateConfiguration(request.context)
+ order = json.loads(request.params.get('order'))
+ order['from']['position'] = int(order['from']['position'])
+ order['to']['positions'] = list(map(int, order['to']['positions']))
+ config.set_portlet_order(order)
+ return {'status': 'success'}
+
+
+@view_config(name='portlet-properties.html', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='system.view')
+class PortalTemplatePortletEditForm(AdminDialogEditForm):
+ """Portal template portlet edit form"""
+
+ dialog_class = 'modal-large'
+
+ def __call__(self):
+ request = self.request
+ request.registry.notify(PageletCreatedEvent(self))
+ slot_name = request.params.get('form.widgets.slot_name')
+ position = int(request.params.get('form.widgets.position'))
+ config = IPortalTemplateConfiguration(self.context)
+ portlet_config = config.get_portlet_configuration(slot_name, position)
+ if portlet_config is None:
+ raise NotFound()
+ editor = self.request.registry.queryMultiAdapter((portlet_config, request),
+ IPagelet, name='properties.html')
+ if editor is None:
+ raise NotFound()
+ editor.ajax_handler = 'portlet-properties.json'
+ editor.update()
+ return editor()
+
+
+@view_config(name='portlet-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplatePortletAJAXEditForm(AJAXEditForm, PortalTemplatePortletEditForm):
+ """Portal template portlet edit form, AJAX renderer"""
+
+ def __call__(self):
+ request = self.request
+ request.registry.notify(PageletCreatedEvent(self))
+ slot_name = request.params.get('form.widgets.slot_name')
+ position = int(request.params.get('form.widgets.position'))
+ config = IPortalTemplateConfiguration(self.context)
+ portlet_config = config.get_portlet_configuration(slot_name, position)
+ if portlet_config is None:
+ raise NotFound()
+ editor = request.registry.queryMultiAdapter((portlet_config, request),
+ IPagelet, name='properties.json')
+ if editor is None:
+ raise NotFound()
+ changes = editor()
+ if changes:
+ # we commit before loading previewer to avoid BLOBs "uncommited changes" error
+ ITransactionManager(self.context).commit()
+ previewer = request.registry.queryMultiAdapter((self.context, request, self, portlet_config),
+ IPortletPreviewer)
+ if previewer is not None:
+ previewer.update()
+ changes.update({'status': 'callback',
+ 'callback': 'PyAMS_portal.template.editPortletCallback',
+ 'options': {'slot_name': slot_name,
+ 'position': position,
+ 'preview': previewer.render()}})
+ return changes
+
+
+@view_config(name='delete-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+def delete_template_portlet(request):
+ """Delete template portlet"""
+ config = IPortalTemplateConfiguration(request.context)
+ config.delete_portlet(request.params.get('slot_name'), int(request.params.get('position')))
+ return {'status': 'success'}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/template/page.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,153 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyramid.exceptions import NotFound
+from pyramid.view import view_config
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.interfaces import PageletCreatedEvent, IPagelet
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.interfaces import IPortalContext, IPortalPage, IPortalTemplateConfiguration
+from pyams_portal.workflow import PUBLISHED, ARCHIVED
+from pyams_portal.zmi.template.config import PortalTemplateConfigView
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowState
+from pyams_zmi.form import AdminDialogEditForm
+from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from pyams_zmi.view import AdminView
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from z3c.form import field
+from zope.interface import implementer
+
+from pyams_portal import _
+
+
+
+@viewlet_config(name='template-properties.menu', context=IPortalContext, layer=IAdminLayer,
+ manager=IPropertiesMenu, permission='manage', weight=5)
+class PortalContextTemplatePropertiesMenu(MenuItem):
+ """Portal context template properties menu"""
+
+ label = _("Presentation template...")
+ icon_class = 'fa-columns'
+
+ url = 'template-properties.html'
+ modal_target = True
+
+
+@pagelet_config(name='template-properties.html', context=IPortalContext, layer=IPyAMSLayer, permission='manage')
+class PortalContextTemplatePropertiesEditForm(AdminDialogEditForm):
+ """Portal context template properties edit form"""
+
+ @property
+ def title(self):
+ return self.context.title
+
+ legend = _("Edit template configuration")
+
+ @property
+ def fields(self):
+ fields = field.Fields(IPortalPage).select('inherit_parent', 'use_local_template', 'shared_template')
+ if not self.getContent().can_inherit:
+ fields = fields.omit('inherit_parent')
+ return fields
+
+ ajax_handler = 'template-properties.json'
+ edit_permission = 'manage'
+
+ def getContent(self):
+ return IPortalPage(self.context)
+
+
+@view_config(name='template-properties.json', context=IPortalContext, request_type=IPyAMSLayer,
+ permission='manage', renderer='json', xhr=True)
+class PortalContextTemplatePropertiesAJAXEditForm(AJAXEditForm, PortalContextTemplatePropertiesEditForm):
+ """Portal context template properties edit form, JSON renderer"""
+
+
+@viewlet_config(name='template-config.menu', context=IPortalContext, layer=IAdminLayer,
+ manager=ISiteManagementMenu, permission='manage', weight=20)
+class PortalContextTemplateConfigMenu(MenuItem):
+ """Portal context template configuration menu"""
+
+ label = _("Template properties")
+ icon_class = 'fa-columns'
+
+ url = '#template-config.html'
+
+ def __new__(cls, context, request, view, manager=None):
+ page = IPortalPage(context)
+ if page.template is None:
+ return None
+ return MenuItem.__new__(cls)
+
+ def get_url(self):
+ page = IPortalPage(self.context)
+ if page.use_local_template:
+ template = IWorkflowVersions(page.template).get_last_versions()[0]
+ return absolute_url(template, self.request, 'admin.html#properties.html')
+ else:
+ return super(PortalContextTemplateConfigMenu, self).get_url()
+
+
+@pagelet_config(name='template-config.html', context=IPortalContext, layer=IPyAMSLayer, permission='manage')
+class PortalContextTemplateConfigView(PortalTemplateConfigView):
+ """Portal context template configuration view"""
+
+ title = _("Local portal template configuration")
+
+ def get_context(self):
+ template = IPortalPage(self.context).template
+ return IWorkflowVersions(template).get_last_versions()[0]
+
+ @property
+ def can_change(self):
+ if not IPortalPage(self.context).use_local_template:
+ return False
+ return self.request.has_permission('manage') and \
+ IWorkflowState(self.get_context()).state not in (PUBLISHED, ARCHIVED)
+
+
+@view_config(name='portlet-properties.html', context=IPortalContext, request_type=IPyAMSLayer, permission='manage')
+class PortalContextTemplatePortletEditForm(AdminDialogEditForm):
+ """Portal context template portlet edit form"""
+
+ dialog_class = 'modal-large'
+
+ def __call__(self):
+ request = self.request
+ request.registry.notify(PageletCreatedEvent(self))
+ slot_name = request.params.get('form.widgets.slot_name')
+ position = int(request.params.get('form.widgets.position'))
+ config = IPortalTemplateConfiguration(self.context)
+ portlet_config = config.get_portlet_configuration(slot_name, position)
+ if portlet_config is None:
+ raise NotFound()
+ editor = self.request.registry.queryMultiAdapter((portlet_config, request),
+ IPagelet, name='properties.html')
+ if editor is None:
+ raise NotFound()
+ editor.ajax_handler = 'portlet-properties.json'
+ editor.update()
+ return editor()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/template/templates/config.pt Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,160 @@
+<tal:var define="config view.configuration" i18n:domain="pyams_portal">
+ <div class="ams-widget"
+ data-ams-plugins="pyams_portal"
+ data-ams-plugin-pyams_portal-src="/--static--/pyams_portal/js/portal{MyAMS.devext}.js"
+ data-ams-plugin-pyams_portal-css="/--static--/pyams_portal/css/portal{MyAMS.devext}.css"
+ data-ams-plugin-pyams_portal-callback="PyAMS_portal.template.initConfig">
+ <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.title">Title</h2>
+ <tal:var content="structure provider:pyams.widget_title" />
+ <tal:var content="structure provider:pyams.toolbar" />
+ </header>
+ <div class="widget-body" tal:define="can_change view.can_change">
+ <div class="btn-toolbar" role="toolbar"
+ tal:condition="can_change">
+ <div class="btn-group" role="group">
+ <div class="btn btn-default btn-row hint" title="Add row" i18n:attributes="title"
+ data-ams-hint-gravity="n">
+ <i class="fa fa-fw fa-2x fa-indent"></i>
+ </div>
+ <div class="btn btn-default btn-slot hint" title="Add slot" i18n:attributes="title"
+ data-ams-hint-gravity="n">
+ <i class="fa fa-fw fa-2x fa-columns"></i>
+ </div>
+ </div>
+ <div class="btn-group" role="group">
+ <div tal:repeat="portlet view.selected_portlets"
+ class="btn btn-default btn-portlet hint"
+ data-ams-hint-gravity="n"
+ tal:attributes="data-ams-portlet-name portlet.name;
+ title portlet.label;">
+ <img tal:condition="portlet.toolbar_image"
+ tal:attributes="src portlet.toolbar_image" />
+ <i tal:condition="portlet.toolbar_css_class"
+ tal:attributes="class portlet.toolbar_css_class"></i>
+ </div>
+ </div>
+ <div class="btn-group" role="group">
+ <div class="btn btn-default hint" data-ams-url="add-template-portlet.html" data-toggle="modal"
+ data-ams-hint-gravity="n"
+ title="Add another portlet..." i18n:attributes="title">
+ <i class="fa fa-fw fa-2x fa-plus"></i>
+ </div>
+ </div>
+ </div>
+ <div class="clearfix">
+ <div class="ams-form form-horizontal margin-bottom-10">
+ <label class="control-label col-md-6 padding-right-5" i18n:translate="">Selected display:</label>
+ <div class="col-md-5">
+ <select id="device_selector" class="select2"
+ data-ams-select2-width="300px"
+ data-ams-change-handler="PyAMS_portal.template.selectDisplay">
+ <option value="" selected i18n:translate="">Current device</option>
+ <option value="xs" i18n:translate="">Extra small device (phone)</option>
+ <option value="sm" i18n:translate="">Small device (tablet)</option>
+ <option value="md" i18n:translate="">Medium desktop device (> 970px)</option>
+ <option value="lg" i18n:translate="">Large desktop device (> 1170px)</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div id="portal_config" class="container"
+ tal:attributes="data-ams-allowed-change can_change">
+ <div class="rows"
+ data-ams-sortable-placeholder="row-highlight"
+ data-ams-sortable-items="> .row"
+ data-ams-sortable-over="PyAMS_portal.template.overRows"
+ data-ams-sortable-stop="PyAMS_portal.template.sortRows">
+ <div class="row context-menu"
+ data-ams-contextmenu-selector="#rowMenu"
+ tal:repeat="row range(config.rows)"
+ tal:attributes="data-ams-row-id row;">
+ <span class="row_id label label-success pull-right"
+ tal:content="row"></span>
+ <div class="slots"
+ data-ams-sortable-placeholder="slot-highlight"
+ data-ams-sortable-connectwith=".slots"
+ data-ams-sortable-over="PyAMS_portal.template.overSlots"
+ data-ams-sortable-stop="PyAMS_portal.template.sortSlots">
+ <div class="slot context-menu col col-md-12 no-padding"
+ data-ams-contextmenu-selector="#slotMenu"
+ data-ams-resizable-start="PyAMS_portal.template.startSlotResize"
+ data-ams-resizable-stop="PyAMS_portal.template.stopSlotResize"
+ data-ams-resizable-handles="e"
+ tal:repeat="slot_name config.get_slots(row)"
+ tal:attributes="class string:slot context-menu col ${config.get_slot_configuration(slot_name).get_css_class()};
+ data-ams-slot-name slot_name;">
+ <div class="header padding-x-5"
+ tal:content="slot_name"></div>
+ <div class="portlets"
+ data-ams-sortable-placeholder="portlet-highlight"
+ data-ams-sortable-connectwith=".portlets"
+ data-ams-sortable-over="PyAMS_portal.template.overPortlets"
+ data-ams-sortable-stop="PyAMS_portal.template.sortPortlets">
+ <div class="portlet context-menu"
+ data-ams-contextmenu-selector="#portletMenu"
+ tal:repeat="portlet_name config.slots.get(row,{}).get(slot_name, ())"
+ tal:attributes="data-ams-portlet-name portlet_name;
+ data-ams-portlet-slot slot_name;
+ data-ams-portlet-position repeat['portlet_name'].index();">
+ <div class="header padding-x-5"
+ tal:content="string:${view.get_portlet_label(portlet_name)}"></div>
+ <div class="preview"
+ tal:content="structure view.get_portlet_preview(slot_name, repeat['portlet_name'].index())"></div>
+ </div>
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <ul id="rowMenu" class="dropdown-menu" role="menu" style="display:none;"
+ tal:condition="can_change">
+ <li class="small">
+ <a tabindex="-1" data-ams-url="PyAMS_portal.template.deleteRow">
+ <i class="fa fa-fw fa-trash"></i>
+ <i18n:var translate="">Delete row...</i18n:var>
+ </a>
+ </li>
+ </ul>
+ <ul id="slotMenu" class="dropdown-menu" role="menu" style="display:none;" >
+ <li class="small">
+ <a tabindex="-1" data-ams-url="PyAMS_portal.template.editSlot">
+ <i class="fa fa-fw fa-edit"></i>
+ <i18n:var translate="">Edit slot properties...</i18n:var>
+ </a>
+ </li>
+ <tal:if condition="can_change">
+ <li class="divider"></li>
+ <li class="small" tal:condition="can_change">
+ <a tabindex="-1" data-ams-url="PyAMS_portal.template.deleteSlot">
+ <i class="fa fa-fw fa-trash"></i>
+ <i18n:var translate="">Delete slot...</i18n:var>
+ </a>
+ </li>
+ </tal:if>
+ </ul>
+ <ul id="portletMenu" class="dropdown-menu" role="menu" style="display:none;" >
+ <li class="small">
+ <a tabindex="-1" data-ams-url="PyAMS_portal.template.editPortlet">
+ <i class="fa fa-fw fa-edit"></i>
+ <i18n:var translate="">Edit portlet properties...</i18n:var>
+ </a>
+ </li>
+ <tal:if condition="can_change">
+ <li class="divider"></li>
+ <li class="small">
+ <a tabindex="-1" data-ams-url="PyAMS_portal.template.deletePortlet">
+ <i class="fa fa-fw fa-trash"></i>
+ <i18n:var translate="">Delete portlet...</i18n:var>
+ </a>
+ </li>
+ </tal:if>
+ </ul>
+ </div>
+ </div>
+</tal:var>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/template/workflow.py Wed Jun 17 09:58:33 2015 +0200
@@ -0,0 +1,176 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortalTemplate
+from pyams_skin.layer import IPyAMSLayer
+from pyams_workflow.interfaces import IWorkflowPublicationInfo, IWorkflowCommentInfo, IWorkflowInfo, \
+ IWorkflowTransitionInfo
+
+# import packages
+from pyams_form.form import AJAXAddForm
+from pyams_form.schema import CloseButton
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_utils.url import absolute_url
+from pyams_workflow.zmi.transition import WorkflowContentTransitionForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import Interface
+from zope.lifecycleevent import ObjectModifiedEvent
+
+from pyams_portal import _
+
+
+#
+# Base workflow form
+#
+
+class PortalTemplateWorkflowForm(WorkflowContentTransitionForm):
+ """Base portal template workflow form"""
+
+
+#
+# Publish forms
+#
+
+class IPortalTemplatePublishButtons(Interface):
+ """Portal template publish buttons"""
+
+ close = CloseButton(name='close', title=_("Close"))
+ action = button.Button(name='action', title=_("Publish"))
+
+
+@pagelet_config(name='wf-publish.html', context=IPortalTemplate, layer=IPyAMSLayer,
+ permission='portal.templates.manage')
+class PortalTemplatePublishForm(PortalTemplateWorkflowForm):
+ """Portal template publish form"""
+
+ legend = _("Publish template")
+
+ fields = field.Fields(IWorkflowTransitionInfo) + \
+ field.Fields(IWorkflowPublicationInfo).select('publication_effective_date',
+ 'publication_expiration_date') + \
+ field.Fields(IWorkflowCommentInfo)
+ buttons = button.Buttons(IPortalTemplatePublishButtons)
+ ajax_handler = 'wf-publish.json'
+
+ def createAndAdd(self, data):
+ pub_info = IWorkflowPublicationInfo(self.context)
+ pub_info.publication_effective_date = data.get('publication_effective_date')
+ pub_info.publication_expiration_date = data.get('publication_expiration_date')
+ info = IWorkflowInfo(self.context)
+ info.fire_transition_toward('published', comment=data.get('comment'))
+ self.request.registry.notify(ObjectModifiedEvent(self.context))
+ return info
+
+
+@view_config(name='wf-publish.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplateAJAXPublishForm(AJAXAddForm, PortalTemplatePublishForm):
+ """Portal template publish form, AJAX renderer"""
+
+
+#
+# Retire form
+#
+
+class IPortalTemplateRetireButtons(Interface):
+ """Portal template retire buttons"""
+
+ close = CloseButton(name='close', title=_("Close"))
+ action = button.Button(name='action', title=_("Retire"))
+
+
+@pagelet_config(name='wf-retire.html', context=IPortalTemplate, layer=IPyAMSLayer,
+ permission='portal.templates.manage')
+class PortalTemplateRetireForm(PortalTemplateWorkflowForm):
+ """Portal template retire form"""
+
+ legend = _("Retire template")
+
+ buttons = button.Buttons(IPortalTemplateRetireButtons)
+ ajax_handler = 'wf-retire.json'
+
+
+@view_config(name='wf-retire.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplateAJAXRetireForm(AJAXAddForm, PortalTemplateRetireForm):
+ """Portal template retire form, AJAX renderer"""
+
+
+#
+# Archive form
+#
+
+class IPortalTemplateArchiveButtons(Interface):
+ """Portal template archive buttons"""
+
+ close = CloseButton(name='close', title=_("Close"))
+ action = button.Button(name='action', title=_("Archive"))
+
+
+@pagelet_config(name='wf-archive.html', context=IPortalTemplate, layer=IPyAMSLayer,
+ permission='portal.templates.manage')
+class PortalTemplateArchiveForm(PortalTemplateWorkflowForm):
+ """Portal template archive form"""
+
+ legend = _("Archive template")
+
+ buttons = button.Buttons(IPortalTemplateArchiveButtons)
+ ajax_handler = 'wf-archive.json'
+
+
+@view_config(name='wf-archive.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplateAJAXArchiveForm(AJAXAddForm, PortalTemplateArchiveForm):
+ """Portal template archive form, AJAX renderer"""
+
+
+#
+# Clone forms
+#
+
+class IPortalTemplateCloneButtons(Interface):
+ """Portal template clone buttons"""
+
+ close = CloseButton(name='close', title=_("Close"))
+ action = button.Button(name='action', title=_("Create new version"))
+
+
+@pagelet_config(name='wf-clone.html', context=IPortalTemplate, layer=IPyAMSLayer,
+ permission='portal.templates.manage')
+class PortalTemplateCloneForm(PortalTemplateWorkflowForm):
+ """Portal template clone form"""
+
+ legend = _("Create new version")
+
+ buttons = button.Buttons(IPortalTemplateCloneButtons)
+ ajax_handler = 'wf-clone.json'
+
+ def createAndAdd(self, data):
+ info = IWorkflowInfo(self.context)
+ return info.fire_transition_toward('draft', comment=data.get('comment'))
+
+
+@view_config(name='wf-clone.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+ permission='portal.templates.manage', renderer='json', xhr=True)
+class PortalTemplateAJAXCloneForm(AJAXAddForm, PortalTemplateCloneForm):
+ """Portal template clone form, AJAX renderer"""
+
+ def get_ajax_output(self, changes):
+ return {'status': 'redirect',
+ 'location': absolute_url(changes, self.request, 'admin.html#properties.html')}