diff --git a/CHANGES.rst b/CHANGES.rst
index b770825..c9f5e71 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,7 +5,9 @@ Changelog
1.0rc2 (unreleased)
-------------------
-- Nothing changed yet.
+- Added option to show action globally, regardless of blocked portlets.
+ The timeout is now always the time since the first visit of a page with this portlet.
+ [maurits]
1.0rc1 (2016-04-20)
diff --git a/README.rst b/README.rst
index b99f696..cd6a088 100644
--- a/README.rst
+++ b/README.rst
@@ -18,7 +18,7 @@ or button.
Compatibility
-------------
-Works on Plone 4.3.x, tested explicitly on Plone 4.3.8.
+Works on Plone 4.3.x, tested explicitly on Plone 4.3.8 and 4.3.11.
Not yet compatible with Plone 5: the javascript and css need work.
@@ -29,9 +29,10 @@ Features
This is basically a copy of the static text portlet with a few extra options.
- In the portlet you can set the number of milliseconds before the overlay is shown.
+ This can be several minutes and go over multiple pages:
+ using a cookie, we keep track of how long you have been on the site.
-- When the overlay is shown, a cookie is set.
- We use this to show the overlay only once.
+- When the overlay is shown, the cookie is updated so that we show the overlay only once.
The cookie is specific for this portlet:
a new call to action portlet will be shown once too.
@@ -47,6 +48,9 @@ Features
but only one overlay is shown on a page.
If there are three portlets, and the user has already seen the first one but not the others, then the second one will be shown.
+- There is a control panel where you can say that the action is global across the site.
+ This can help if parts of your site block the portlets and you still want to see the action there.
+
Examples
--------
diff --git a/src/collective/calltoaction/__init__.py b/src/collective/calltoaction/__init__.py
index 3a1f1bc..e1884a8 100644
--- a/src/collective/calltoaction/__init__.py
+++ b/src/collective/calltoaction/__init__.py
@@ -3,4 +3,5 @@
from zope.i18nmessageid import MessageFactory
+
_ = MessageFactory('collective.calltoaction')
diff --git a/src/collective/calltoaction/browser/configure.zcml b/src/collective/calltoaction/browser/configure.zcml
index d3b5444..b80603e 100644
--- a/src/collective/calltoaction/browser/configure.zcml
+++ b/src/collective/calltoaction/browser/configure.zcml
@@ -14,4 +14,11 @@
permission="zope2.View"
/>
+
+
diff --git a/src/collective/calltoaction/browser/controlpanel.py b/src/collective/calltoaction/browser/controlpanel.py
new file mode 100644
index 0000000..f863718
--- /dev/null
+++ b/src/collective/calltoaction/browser/controlpanel.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+from collective.calltoaction import _
+from collective.calltoaction.interfaces import ICollectiveCalltoactionSettings
+from plone.app.registry.browser.controlpanel import ControlPanelFormWrapper
+from plone.app.registry.browser.controlpanel import RegistryEditForm
+from plone.z3cform import layout
+from z3c.form import form
+
+
+class CalltoactionControlPanelForm(RegistryEditForm):
+ form.extends(RegistryEditForm)
+ schema = ICollectiveCalltoactionSettings
+
+CalltoactionControlPanelView = layout.wrap_form(
+ CalltoactionControlPanelForm, ControlPanelFormWrapper)
+CalltoactionControlPanelView.label = _(u'Call to action settings')
diff --git a/src/collective/calltoaction/browser/viewlet.py b/src/collective/calltoaction/browser/viewlet.py
index 257cadd..46b999c 100644
--- a/src/collective/calltoaction/browser/viewlet.py
+++ b/src/collective/calltoaction/browser/viewlet.py
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
+from collective.calltoaction.interfaces import ICollectiveCalltoactionSettings
from collective.calltoaction.portlets.calltoactionportlet import ICallToActionPortlet # noqa
+from plone import api
from plone.app.layout.viewlets.common import ViewletBase
from plone.portlets.interfaces import IPortletManager
from plone.portlets.interfaces import IPortletRenderer
from plone.portlets.interfaces import IPortletRetriever
+from plone.registry.interfaces import IRegistry
+from Products.Five import BrowserView
from zope.component import getMultiAdapter
from zope.component import getUtility
@@ -16,11 +20,18 @@ def update(self):
# footer = getUtility(IPortletManager, name='plone.footerportlets')
# But portlets in Plone 5 need to be based on z3c.form, so it may be
# tricky to support Plone 4 and 5 with the same code base.
-
+ registry = getUtility(IRegistry)
+ settings = registry.forInterface(
+ ICollectiveCalltoactionSettings, check=False)
+ self.show_global = settings.show_global
+ if self.show_global:
+ target = api.portal.get_navigation_root(self.context)
+ else:
+ target = self.context
for name in ('plone.leftcolumn', 'plone.rightcolumn'):
manager = getUtility(IPortletManager, name=name)
retriever = getMultiAdapter(
- (self.context, manager), IPortletRetriever)
+ (target, manager), IPortletRetriever)
portlets = retriever.getPortlets()
for portlet in portlets:
assignment = portlet['assignment']
@@ -48,5 +59,12 @@ def _data_to_portlet(self, manager, data):
Adapted from plone.portlets/manager.py _dataToPortlet.
"""
- return getMultiAdapter((self.context, self.request, self.view,
+ if self.show_global:
+ target = api.portal.get_navigation_root(self.context)
+ # Use dummy view for the target context.
+ view = BrowserView(target, self.request)
+ else:
+ target = self.context
+ view = self.view
+ return getMultiAdapter((target, self.request, view,
manager, data, ), IPortletRenderer)
diff --git a/src/collective/calltoaction/configure.zcml b/src/collective/calltoaction/configure.zcml
index 86db8a3..5e05ee7 100644
--- a/src/collective/calltoaction/configure.zcml
+++ b/src/collective/calltoaction/configure.zcml
@@ -53,4 +53,12 @@
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
+
+
diff --git a/src/collective/calltoaction/interfaces.py b/src/collective/calltoaction/interfaces.py
index d3dfdbc..f203af8 100644
--- a/src/collective/calltoaction/interfaces.py
+++ b/src/collective/calltoaction/interfaces.py
@@ -1,8 +1,27 @@
# -*- coding: utf-8 -*-
"""Module where all interfaces, events and exceptions live."""
+from collective.calltoaction import _
+from zope import schema
+from zope.interface import Interface
from zope.publisher.interfaces.browser import IDefaultBrowserLayer
class ICollectiveCalltoactionLayer(IDefaultBrowserLayer):
"""Marker interface that defines a browser layer."""
+
+
+class ICollectiveCalltoactionSettings(Interface):
+ show_global = schema.Bool(
+ title=_(u'Show global action'),
+ description=_(
+ u'description_global_action',
+ default=(
+ u'This looks for a portlet in the navigation root, '
+ u'which usually is the portal root. '
+ u'It shows it on every page. '
+ u'The timeout before showing the call to action, '
+ u'is calculated from the first page visit. '
+ u'The time spent on the site so far is stored on a cookie.')),
+ default=False,
+ )
diff --git a/src/collective/calltoaction/profiles/default/controlpanel.xml b/src/collective/calltoaction/profiles/default/controlpanel.xml
new file mode 100644
index 0000000..c483784
--- /dev/null
+++ b/src/collective/calltoaction/profiles/default/controlpanel.xml
@@ -0,0 +1,20 @@
+
+
diff --git a/src/collective/calltoaction/profiles/default/metadata.xml b/src/collective/calltoaction/profiles/default/metadata.xml
index b62c756..e0fbdef 100644
--- a/src/collective/calltoaction/profiles/default/metadata.xml
+++ b/src/collective/calltoaction/profiles/default/metadata.xml
@@ -1,6 +1,6 @@
- 1000
+ 1001
diff --git a/src/collective/calltoaction/profiles/default/registry.xml b/src/collective/calltoaction/profiles/default/registry.xml
new file mode 100644
index 0000000..6ac7ac3
--- /dev/null
+++ b/src/collective/calltoaction/profiles/default/registry.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/collective/calltoaction/profiles/testfixture/portlets.xml b/src/collective/calltoaction/profiles/testfixture/portlets.xml
index 623d866..b7788fb 100644
--- a/src/collective/calltoaction/profiles/testfixture/portlets.xml
+++ b/src/collective/calltoaction/profiles/testfixture/portlets.xml
@@ -13,4 +13,11 @@
1000
+
+
diff --git a/src/collective/calltoaction/resources/calltoaction.js b/src/collective/calltoaction/resources/calltoaction.js
index f5a92fb..c28d7a3 100644
--- a/src/collective/calltoaction/resources/calltoaction.js
+++ b/src/collective/calltoaction/resources/calltoaction.js
@@ -3,68 +3,109 @@
$(document).ready(function() {
var overlay_set = false;
$('.calltoaction-portlet-wrapper').each( function() {
+ var time_on_site;
+ var cookie_value;
+ var el;
+ var cookiename;
+ var start;
+ var timeout;
+ if (overlay_set) {
+ return;
+ }
// Check if the user has already seen this overlay.
- var cookiename = $(this).attr('data-cookiename');
+ cookiename = $(this).attr('data-cookiename');
// Note: readCookie and createCookie are defined in
// Products/CMFPlone/skins/plone_ecmascript/cookie_functions.js
- if (!overlay_set && !readCookie(cookiename)) {
- var timeout = $(this).attr('data-timeout');
- var el = $(this);
- setTimeout(
- function(){
- // Overlay adapted from http://jquerytools.github.io/demos/overlay/trigger.html
- el.overlay({
- // custom top position
- top: "center",
- fixed: true,
- // Before the overlay is gone be active place it correctly
- onBeforeLoad: function() {
+ cookie_value = readCookie(cookiename);
+ if (cookie_value === 'y') {
+ // already seen
+ return;
+ }
+ timeout = parseInt($(this).attr('data-timeout'));
+ if (isNaN(timeout)) {
+ timeout = 5000;
+ }
+ cookie_value = parseInt(cookie_value);
+ start = cookie_value;
+ if (isNaN(start)) {
+ start = new Date().getTime();
+ } else {
+ /*
+ Say start is 12:00 (but then in milliseconds).
+ And current time is 12:02.
+ And timeout is 3 minutes.
+ Then the new timeout is 1 minute.
+ */
+ time_on_site = new Date().getTime() - start;
+ if (time_on_site > 3600000) {
+ // Reset time on site after an hour.
+ start = new Date().getTime();
+ time_on_site = 0;
+ }
+ timeout = timeout - time_on_site;
+ }
+ if (cookie_value !== start) {
+ // Remember the session start time in a cookie.
+ createCookie(cookiename, start, 365);
+ }
+ el = $(this);
+ setTimeout(
+ function(){
+ // Overlay adapted from http://jquerytools.github.io/demos/overlay/trigger.html
+ el.overlay({
+ // custom top position
+ top: "center",
+ fixed: true,
+ // Before the overlay is gone be active place it correctly
+ onBeforeLoad: function() {
- if (el.hasClass("manager_right")){
- el.animate({right: -1000});
- }else{
- el.animate({left: -1000});
- };
+ if (el.hasClass("manager_right")){
+ el.animate({right: -1000});
+ }else{
+ el.animate({left: -1000});
+ }
- },
- // when the overlay is opened, animate our portlet
- onLoad: function() {
+ },
+ // when the overlay is opened, animate our portlet
+ onLoad: function() {
- if (el.hasClass("manager_right")){
- el.animate({right: 15});
- }
- else {
- el.animate({left: 15});
- };
+ if (el.hasClass("manager_right")){
+ el.animate({right: 15});
+ }
+ else {
+ el.animate({left: 15});
+ }
- },
- // some mask tweaks suitable for facebox-looking dialogs
- mask: {
- // you might also consider a "transparent" color for the mask
- color: '#fff',
- // load mask a little faster
- loadSpeed: 200,
- // very transparent
- opacity: 0.5
- },
- // disable this for modal dialog-type of overlays
- closeOnClick: true,
- // load it immediately after the construction
- load: true,
+ },
+ // some mask tweaks suitable for facebox-looking dialogs
+ mask: {
+ // you might also consider a "transparent" color for the mask
+ color: '#fff',
+ // load mask a little faster
+ loadSpeed: 200,
+ // very transparent
+ opacity: 0.5
+ },
+ // disable this for modal dialog-type of overlays
+ closeOnClick: true,
+ // load it immediately after the construction
+ load: true
- });
-
- // Set cookie to avoid showing overlay twice to the same
- // user. We could do this on certain events, but you have
- // to catch them all: onClose of the overlay, clicking on
- // a link in the overlay, etcetera. Much easier to simply
- // set the cookie at the moment we show the overlay.
- createCookie(cookiename, 'y', 365);
- },
- timeout);
- // We setup only one overlay, otherwise it gets a bit crazy.
- overlay_set = true;
- };
+ });
+
+ /*
+ Set cookie to avoid showing overlay twice to the same
+ user. We could do this on certain events, but you have
+ to catch them all: onClose of the overlay, clicking on
+ a link in the overlay, etcetera. Much easier to simply
+ set the cookie at the moment we show the overlay.
+ The 'y' value means: yes, the user has seen it.
+ */
+ createCookie(cookiename, 'y', 365);
+ },
+ timeout);
+ // We setup only one overlay, otherwise it gets a bit crazy.
+ overlay_set = true;
});
});
})(jQuery);
diff --git a/src/collective/calltoaction/testing.py b/src/collective/calltoaction/testing.py
index e2205ad..e910ffc 100644
--- a/src/collective/calltoaction/testing.py
+++ b/src/collective/calltoaction/testing.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+from plone import api
from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE
from plone.app.testing import applyProfile
from plone.app.testing import FunctionalTesting
@@ -6,6 +7,9 @@
from plone.app.testing import PloneSandboxLayer
from plone.testing import z2
+import collective.calltoaction
+
+
try:
# Plone 5 (or maybe Plone 4 with plone.app.contenttypes)
from plone.app.contenttypes.testing import (
@@ -14,8 +18,6 @@
# Plone 4
from plone.app.testing import PLONE_FIXTURE
-import collective.calltoaction
-
class CollectiveCalltoactionLayer(PloneSandboxLayer):
@@ -29,6 +31,10 @@ def setUpZope(self, app, configurationContext):
def setUpPloneSite(self, portal):
applyProfile(portal, 'collective.calltoaction:default')
+ # We have blacklisted context portlets in folder2.
+ # See the testfixture.
+ api.content.create(
+ container=portal, type='Folder', title='Folder2')
applyProfile(portal, 'collective.calltoaction:testfixture')
diff --git a/src/collective/calltoaction/tests/test_viewlet.py b/src/collective/calltoaction/tests/test_viewlet.py
index ce478e6..a2acb07 100644
--- a/src/collective/calltoaction/tests/test_viewlet.py
+++ b/src/collective/calltoaction/tests/test_viewlet.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+from collective.calltoaction.interfaces import ICollectiveCalltoactionSettings
from collective.calltoaction.testing import COLLECTIVE_CALLTOACTION_INTEGRATION_TESTING # noqa
from plone import api
from plone.app.layout.globals.interfaces import IViewView
@@ -6,6 +7,8 @@
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
+from plone.registry.interfaces import IRegistry
+from zope.component import getUtility
from zope.component import queryMultiAdapter
from zope.contentprovider.interfaces import IContentProvider
from zope.interface import alsoProvides
@@ -71,6 +74,37 @@ def test_viewlet(self):
self.assertIn(portlet_html, viewlet_html)
self.assertIn('data-timeout="1000"', viewlet_html)
+ def test_portlet_inheritance(self):
+ portal = self.layer['portal']
+ folder = portal.folder2
+ request = folder.REQUEST
+ view = folder.restrictedTraverse('@@plone')
+ alsoProvides(view, IViewView)
+ self.assertTrue(IViewView.providedBy(view))
+ viewlet_manager = queryMultiAdapter(
+ (folder, request, view),
+ IContentProvider,
+ 'plone.abovecontentbody')
+ viewlet = queryMultiAdapter(
+ (folder, request, view, viewlet_manager),
+ IViewlet,
+ 'collective.calltoaction')
+ self.assertTrue(viewlet is not None)
+ viewlet.update()
+ # The portlet from the test fixture is blocked.
+ self.assertEqual(len(viewlet.data), 0)
+
+ # Show the action globally. This ignores portlet inheritance.
+ registry = getUtility(IRegistry)
+ settings = registry.forInterface(
+ ICollectiveCalltoactionSettings, check=False)
+ self.assertFalse(settings.show_global)
+ settings.show_global = True
+
+ # Now we should see the portlet
+ viewlet.update()
+ self.assertEqual(len(viewlet.data), 1)
+
def test_suite():
from unittest import TestSuite, makeSuite