Repository: collective/collective.cover (language: python)

Issue:
Fix tests in Plone 6



Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 000000000..f66a2b6ce
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[report]
+include =
+    src/collective/*
+omit =
+    */test*
+    */upgrades/*
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..62af94f89
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+[*]
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+[{*.py,*.cfg,*.yaml}]
+indent_size = 4
+
+[{*.html,*.dtml,*.pt,*.zpt,*.xml,*.zcml,*.js}]
+indent_size = 2
+
+[{package.json,.travis.yml}]
+indent_size = 2
+
+[Makefile]
+indent_style = tab
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..6f9ff673b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,1 @@
+CHANGES.rst merge=union
diff --git a/.github/workflows/plone-package.yml b/.github/workflows/plone-package.yml
index b4d9b9050..86211f5c9 100644
--- a/.github/workflows/plone-package.yml
+++ b/.github/workflows/plone-package.yml
@@ -12,10 +12,7 @@ jobs:
             fail-fast: false
             matrix:
                 plone-version:
-                  - 'Plone43'
-                  - 'Plone43_DEXTERITY_ONLY'
-                  - 'Plone43_PLONE_APP_JQUERY_1.9.2'
-                  - 'Plone51'
+                  - 'Plone52'
                 python-version: [2.7]
 
         steps:
diff --git a/CHANGES.rst b/CHANGES.rst
index 15f9d929e..d04103ab0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,11 +3,35 @@ Changelog
 
 There's a frood who really knows where his towel is.
 
-2.2.4 (unreleased)
+3.0.0 (unreleased)
 ^^^^^^^^^^^^^^^^^^
 
-- Nothing changed yet.
+- Add suport to Python 3.6, 3.7 and 3.8.
+  [pbauer]
 
+- Drop support for Plone 4.x
+  [cleberjsantos]
+
+- Add support to Plone 5.2.
+  [cleberjsantos]
+
+- Drop PFG support
+  [cleberjsantos]
+
+- Fix plone.namedfile scales
+  [cleberjsantos]
+
+- Correction of the visualization of the image scales
+  [cleberjsantos]
+
+- Fix Js registry with new concepts in Plone 5
+  [cleberjsantos]
+
+- Drop Deco grid
+  [cleberjsantos]
+
+- Refactor tests
+  [cleberjsantos]
 
 2.2.3 (2021-02-02)
 ^^^^^^^^^^^^^^^^^^
diff --git a/base.cfg b/base.cfg
index f95b9b6eb..d33ca2cda 100644
--- a/base.cfg
+++ b/base.cfg
@@ -5,6 +5,7 @@ package-extras = [relations, test]
 
 parts +=
     checkversions
+    createcoverage
     dependencychecker
     i18ndude
     node
@@ -31,17 +32,13 @@ pre-commit-hook = True
 prefer-single-quotes = True
 return-status-codes = True
 
+[code-analysis:python27]
+flake8-ignore += I004
+
 [dependencychecker]
 recipe = zc.recipe.egg
 eggs = z3c.dependencychecker
 
-[instance]
-deprecation-warnings = on
-zope-conf-additional +=
-    publisher-profile-file ${buildout:directory}/var/instance/profile.dat
-environment-vars +=
-    PROFILE_PUBLISHER 1
-
 [i18ndude]
 recipe = zc.recipe.egg
 eggs = i18ndude
@@ -80,11 +77,3 @@ initialization +=
     import time
     time.tzset()
     os.environ["ROBOT_SELENIUM2LIBRARY_RUN_ON_FAILURE"] = "Capture Page Screenshot"
-
-[versions]
-# Keep the same as in requirements.txt:
-setuptools = 42.0.2
-zc.buildout = 2.13.3
-
-# tzlocal >= 3.0.0 is not compatiblem with Python 2
-tzlocal = 2.1
diff --git a/buildout.cfg b/buildout.cfg
index e63615ae3..f29aa9cb6 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -1,4 +1,4 @@
 [buildout]
 
 extends =
-    test_plone51.cfg
+    test_plone52.cfg
diff --git a/constraints.txt b/constraints.txt
index 9ef3c62be..24cbf8771 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -1,1 +1,1 @@
--c constraints_plone51.txt
+-c constraints_plone52.txt
diff --git a/constraints_plone43.txt b/constraints_plone43.txt
deleted file mode 100644
index b7518d6fb..000000000
--- a/constraints_plone43.txt
+++ /dev/null
@@ -1,1 +0,0 @@
--c https://dist.plone.org/release/5.1-latest/requirements.txt
diff --git a/constraints_plone51.txt b/constraints_plone51.txt
deleted file mode 100644
index b7518d6fb..000000000
--- a/constraints_plone51.txt
+++ /dev/null
@@ -1,1 +0,0 @@
--c https://dist.plone.org/release/5.1-latest/requirements.txt
diff --git a/constraints_plone52.txt b/constraints_plone52.txt
new file mode 100644
index 000000000..848b28f9d
--- /dev/null
+++ b/constraints_plone52.txt
@@ -0,0 +1,1 @@
+-c https://dist.plone.org/release/5.2-latest/requirements.txt
diff --git a/docs/end-user.rst b/docs/end-user.rst
index da4353860..5749718e3 100644
--- a/docs/end-user.rst
+++ b/docs/end-user.rst
@@ -109,11 +109,6 @@ Right now, you can not edit individual items metadata in the list, but you can
 remove or reorder them. The title and image fields will include a link to the
 original object location.
 
-FormGen
-+++++++
-
-A tile to show a 'FormFolder' is also included in Plone 4 but is now deprecated; do not use it.
-
 Rich Text
 +++++++++
 
diff --git a/heroku.cfg b/heroku.cfg
index e6cea32a6..21f1f97c2 100644
--- a/heroku.cfg
+++ b/heroku.cfg
@@ -1,7 +1,7 @@
 [buildout]
 extends =
-    http://dist.plone.org/release/4.3-latest/versions.cfg
-    versions-4.3.x.cfg
+    http://dist.plone.org/release/5.2-latest/versions.cfg
+    versions-5.2.x.cfg
 unzip = true
 newest = false
 versions = versions
diff --git a/pyproject.toml b/pyproject.toml
index b77bfbe50..6442d4c88 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,3 @@
 [tool.dependencychecker]
 Zope2 = ['App', 'OFS','Products.Five']
-ignore-packages = ['plone.app.relationfield', 'Products.PloneFormGen']
+ignore-packages = ['plone.app.relationfield']
diff --git a/setup.py b/setup.py
index 7a1d11185..1ef637567 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
 from setuptools import setup
 
 
-version = "2.2.4.dev0"
+version = "3.0.0.dev0"
 description = "A sane, working, editor-friendly way of creating front pages and other composite pages. Working now, for mere mortals."
 long_description = "\n\n".join(
     [
@@ -23,8 +23,7 @@
         "Development Status :: 5 - Production/Stable",
         "Environment :: Web Environment",
         "Framework :: Plone",
-        "Framework :: Plone :: 4.3",
-        "Framework :: Plone :: 5.1",
+        "Framework :: Plone :: 5.2",
         "Intended Audience :: End Users/Desktop",
         "Intended Audience :: System Administrators",
         "License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
@@ -32,6 +31,10 @@
         "Programming Language :: JavaScript",
         "Programming Language :: Python",
         "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
         "Topic :: Office/Business :: News/Diary",
         "Topic :: Software Development :: Libraries :: Python Modules",
     ],
@@ -39,6 +42,11 @@
     author="Carlos de la Guardia et al.",
     author_email="cguardia@yahoo.com",
     url="https://github.com/collective/collective.cover",
+    project_urls={
+        "PyPI": "https://pypi.python.org/pypi/collective.cover",
+        "Source": "https://github.com/collective/collective.cover",
+        "Tracker": "https://github.com/collective/collective.cover/issues",
+    },
     license="GPLv2",
     packages=find_packages("src"),
     package_dir={"": "src"},
@@ -51,10 +59,9 @@
         "collective.js.bootstrap",
         "collective.js.galleria",
         "collective.js.jqueryui",
-        "collective.z3cform.datetimewidget",
         "Missing",
-        "plone.api >=1.4.11",
-        "plone.app.blocks >=2.2.1",
+        "plone.api",
+        "plone.app.blocks",
         "plone.app.content",
         "plone.app.contentmenu",
         "plone.app.dexterity",
@@ -66,7 +73,7 @@
         "plone.app.lockingbehavior",
         "plone.app.registry",
         "plone.app.textfield",
-        "plone.app.tiles >=1.1.0",
+        "plone.app.tiles",
         "plone.app.uuid",
         "plone.app.vocabularies",
         "plone.autoform",
@@ -84,7 +91,7 @@
         "plone.uuid",
         "plone.z3cform",
         "Products.CMFCore",
-        "Products.CMFPlone >=4.3",
+        "Products.CMFPlone >=5.2",
         "Products.GenericSetup",
         "setuptools",
         "six",
@@ -110,7 +117,7 @@
             "cssselect",
             "lxml",
             "mock",
-            "plone.api >=1.8.5",
+            "plone.api",
             "plone.app.robotframework",
             "plone.app.testing [robot]",
             "plone.browserlayer",
@@ -119,7 +126,6 @@
             "robotsuite",
             "testfixtures",
             "transaction",
-            "tzlocal",
         ],
     },
     entry_points="""
diff --git a/src/collective/cover/Extensions/Install.py b/src/collective/cover/Extensions/Install.py
deleted file mode 100644
index a86dee9bf..000000000
--- a/src/collective/cover/Extensions/Install.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from collective.cover.config import PROJECTNAME
-from plone import api
-
-
-def uninstall(portal, reinstall=False):
-    if not reinstall:
-        profile = "profile-{0}:uninstall".format(PROJECTNAME)
-        setup_tool = api.portal.get_tool("portal_setup")
-        setup_tool.runAllImportStepsFromProfile(profile)
-        return "Ran all uninstall steps."
diff --git a/src/collective/cover/Extensions/__init__.py b/src/collective/cover/Extensions/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/collective/cover/browser/contentchooser.py b/src/collective/cover/browser/contentchooser.py
index 4d3f287c0..368dea355 100644
--- a/src/collective/cover/browser/contentchooser.py
+++ b/src/collective/cover/browser/contentchooser.py
@@ -11,7 +11,7 @@
 from Products.CMFCore.interfaces import IFolderish
 from Products.CMFPlone.browser.navtree import SitemapNavtreeStrategy
 from Products.CMFPlone.PloneBatch import Batch
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser import BrowserView
 from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
 from zope.component import getUtility
@@ -75,7 +75,7 @@ def search(self, query=None, page=1, b_size=ITEMS_BY_REQUEST, uuids=None):
         catalog_query = {"sort_on": "effective", "sort_order": "descending"}
         catalog_query["portal_type"] = searchable_types
         if query:
-            catalog_query["Title"] = u"{0}*".format(safe_unicode(query))
+            catalog_query["Title"] = u"{0}*".format(safe_text(query))
         results = catalog(**catalog_query)
         self.total_results = len(results)
         start = (page - 1) * b_size
@@ -121,7 +121,7 @@ def _getCurrentValues(self):
         # the vocabulary returns the values sorted by their translated title
         for term in vocab._terms:
             value = portal_types[term.value].id  # portal_type
-            title = safe_unicode(term.title)  # already translated title
+            title = safe_text(term.title)  # already translated title
             result.append((value, title))
 
         return result
@@ -216,7 +216,6 @@ def jsonByType(self, rooted, document_base_url, searchtext, page="1"):
                         normalizer.normalize(brain.review_state or "")
                     ),
                     "title": brain.Title == "" and brain.id or brain.Title,
-                    "icon": self.getIcon(brain).url or "",
                     "is_folderish": brain.is_folderish,
                     "description": brain.Description or "",
                 }
diff --git a/src/collective/cover/browser/cover.py b/src/collective/cover/browser/cover.py
index ad37ce3fa..dd0a33ece 100644
--- a/src/collective/cover/browser/cover.py
+++ b/src/collective/cover/browser/cover.py
@@ -1,10 +1,17 @@
 # -*- coding: utf-8 -*-
 from plone.app.blocks.interfaces import IBlocksTransformEnabled
+from plone.registry.interfaces import IRegistry
+from Products.CMFPlone.interfaces.controlpanel import IImagingSchema
 from Products.Five.browser import BrowserView
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from zope.component import getUtility
 from zope.interface import implementer
-from zope.schema.interfaces import IVocabularyFactory
+
+
+def get_settings():
+    registry = getUtility(IRegistry)
+    settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)
+    return settings
 
 
 @implementer(IBlocksTransformEnabled)
@@ -60,7 +67,5 @@ class Helper(BrowserView):
     @staticmethod
     def get_image_scales():
         """List all image scales which are available on the site."""
-        factory = getUtility(IVocabularyFactory, "plone.app.vocabularies.ImagesScales")
-        vocabulary = factory(None)
-        # TODO: fix scales order upsteam in plone.app.vocabularies
-        return [term.title for term in vocabulary]
+        settings = get_settings()
+        return settings.allowed_sizes
diff --git a/src/collective/cover/browser/layout.py b/src/collective/cover/browser/layout.py
index e02d4575c..0a43796b0 100644
--- a/src/collective/cover/browser/layout.py
+++ b/src/collective/cover/browser/layout.py
@@ -20,6 +20,7 @@
 from zope.schema.interfaces import IVocabularyFactory
 
 import json
+import six
 
 
 # TODO: implement EditCancelledEvent and EditFinishedEvent
@@ -71,8 +72,9 @@ def render(self):
                 # Store name and layout as unicode.  Note that the
                 # name must only contain ascii because it is used as
                 # value for a vocabulary.
-                name = name.decode("ascii", "ignore")
-                layout = layout.decode("utf-8")
+                if six.PY2:
+                    name = name.decode("ascii", "ignore")
+                    layout = layout.decode("utf-8")
                 settings.layouts[name] = layout
 
         return self.index()
@@ -148,7 +150,9 @@ def render_section(self, section, mode):
             return self.group(section=section, mode=mode)
         elif section["type"] == u"tile":
             tile_url = "@@{0}/{1}".format(section.get("tile-type"), section.get("id"))
-            tile = self.context.restrictedTraverse(tile_url.encode(), None)
+            if six.PY2:
+                tile_url = tile_url.encode()
+            tile = self.context.restrictedTraverse(tile_url, None)
             if tile is None:
                 return '<div class="tileNotFound">Could not find tile</div>'
             if mode == "layout_edit":
diff --git a/src/collective/cover/browser/scaling.py b/src/collective/cover/browser/scaling.py
index 192c9ecf3..f0f8d1b5f 100644
--- a/src/collective/cover/browser/scaling.py
+++ b/src/collective/cover/browser/scaling.py
@@ -4,6 +4,7 @@
 from persistent.dict import PersistentDict
 from plone.app.uuid.utils import uuidToObject
 from plone.namedfile.interfaces import INamedImage
+from plone.namedfile.interfaces import IStableImageScale
 from plone.namedfile.scaling import ImageScale as BaseImageScale
 from plone.namedfile.scaling import ImageScaling as BaseImageScaling
 from plone.namedfile.utils import set_headers
@@ -15,6 +16,7 @@
 from ZODB.POSException import ConflictError
 from zope.annotation import IAnnotations
 from zope.component import queryMultiAdapter
+from zope.interface import alsoProvides
 from zope.publisher.interfaces import NotFound
 
 import logging
@@ -94,9 +96,13 @@ def publishTraverse(self, request, name):
                 name, _ = name.rsplit(".", 1)
             storage = AnnotationStorage(self.context)
             info = storage.get(name)
-            if info is not None:
-                scale_view = ImageScale(self.context, self.request, **info)
-                return scale_view.__of__(self.context)
+
+            if info is None:
+                raise NotFound(self, name, self.request)
+            scale_view = ImageScale(self.context, self.request, **info)
+            alsoProvides(scale_view, IStableImageScale)
+            return scale_view
+
         else:
             # otherwise `name` must refer to a field...
             if "." in name:
@@ -188,7 +194,7 @@ def scale(self, fieldname=None, scale=None, height=None, width=None, **parameter
         if fieldname is None:
             fieldname = IPrimaryFieldInfo(self.context).fieldname
         if scale is not None:
-            available = self.getAvailableSizes(fieldname)
+            available = self.available_sizes
             if scale not in available:
                 return None
             width, height = available[scale]
@@ -200,7 +206,8 @@ def scale(self, fieldname=None, scale=None, height=None, width=None, **parameter
             width=width,
             **parameters
         )
+
         if info is not None:
-            info["fieldname"] = fieldname
             scale_view = ImageScale(self.context, self.request, **info)
-            return scale_view.__of__(self.context)
+            alsoProvides(scale_view, IStableImageScale)
+            return scale_view
diff --git a/src/collective/cover/browser/templates/content_contentchooser.pt b/src/collective/cover/browser/templates/content_contentchooser.pt
index 9ec1ef80c..c602b68e3 100644
--- a/src/collective/cover/browser/templates/content_contentchooser.pt
+++ b/src/collective/cover/browser/templates/content_contentchooser.pt
@@ -13,10 +13,6 @@
     <div class="close">
         <a href="#" class="hiddenStructure" title="Close this box">Close this box.</a>
     </div>
-    <ul class="formTabs" tal:condition="not:view/is_plone_5">
-      <li class="formTab"><a href="#recent" i18n:translate="">Recent items</a></li>
-      <li class="formTab"><a href="#content-tree" i18n:translate="">Content tree</a></li>
-    </ul>
 
       <fieldset id="recent" class="tab-pane active">
         <legend>Recent Items</legend>
diff --git a/src/collective/cover/browser/templates/search_list.pt b/src/collective/cover/browser/templates/search_list.pt
index 0aae5d5ad..0f5e74790 100644
--- a/src/collective/cover/browser/templates/search_list.pt
+++ b/src/collective/cover/browser/templates/search_list.pt
@@ -19,7 +19,7 @@
                     <tal:block>
                         <a tal:attributes="rel level;
                                            class string:$item_class;
-                                           title string:${node/Description} : ${content_path}">
+                                           title python:'{0} : {1}'.format(node['Description'],content_path)">
                             <img tal:replace="structure item_icon/html_tag|item_icon" />
                             <span tal:content="node/Title">Selected Item Title</span>
                         </a>
diff --git a/src/collective/cover/config.py b/src/collective/cover/config.py
index 1046e7deb..da13ef207 100644
--- a/src/collective/cover/config.py
+++ b/src/collective/cover/config.py
@@ -10,6 +10,7 @@
 
 
 PROJECTNAME = "collective.cover"
+IS_PLONE_5 = api.env.plone_version().startswith("5")
 
 # by default, all cover tiles will be available on layouts
 DEFAULT_AVAILABLE_TILES = [
@@ -35,12 +36,10 @@
     "News Item",
 ]
 
-IS_PLONE_5 = api.env.plone_version().startswith("5")
-
-DEFAULT_GRID_SYSTEM = "bootstrap3" if IS_PLONE_5 else "deco16_grid"
+DEFAULT_GRID_SYSTEM = "bootstrap3"
 
 _ = u'[{{"type": "row", "children": [{{"type": "group", "column-size": {size}, "roles": ["Manager"]}}]}}]'
-_SIZE = 12 if IS_PLONE_5 else 16
+_SIZE = 12
 EMPTY_LAYOUT = _.format(size=_SIZE)
 
 # z3c.form.widget.SequenceWidget used for css_class field
diff --git a/src/collective/cover/content.py b/src/collective/cover/content.py
index 962628546..4ab463491 100644
--- a/src/collective/cover/content.py
+++ b/src/collective/cover/content.py
@@ -12,7 +12,7 @@
 from plone.dexterity.content import Item
 from plone.indexer import indexer
 from plone.tiles.interfaces import ITileDataManager
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from zope.annotation import IAnnotations
 from zope.component import queryAdapter
 
@@ -45,8 +45,8 @@ def get_tiles(self, types=None, layout=None):
         :type layout: list
         :returns: a list of tiles; each tile is described as {id, type}
         """
-        filter = types is not None
-        if filter and isinstance(types, str):
+        filter_tiles = types is not None
+        if filter_tiles and isinstance(types, str):
             types = [types]
 
         if layout is None:
@@ -65,13 +65,13 @@ def get_tiles(self, types=None, layout=None):
             assert isinstance(layout, list)  # nosec
 
         tiles = []
-        for e in layout:
-            if e["type"] == "tile":
-                if filter and e["tile-type"] not in types:
+        for item in layout:
+            if item["type"] == "tile":
+                if filter_tiles and item.get("tile-type", False) not in types:
                     continue
-                tiles.append(dict(id=e["id"], type=e["tile-type"]))
-            if "children" in e:
-                tiles.extend(self.get_tiles(types, e["children"]))
+                tiles.append(dict(id=item["id"], type=item["tile-type"]))
+            if "children" in item:
+                tiles.extend(self.get_tiles(types, item["children"]))
         return tiles
 
     def list_tiles(self, types=None):
@@ -157,7 +157,7 @@ def purge_deleted_tiles(self):
         layout_tiles = self.list_tiles()
         annotations = IAnnotations(self)
 
-        for key in annotations:
+        for key in annotations.keys():
             if not key.startswith(ANNOTATION_PREFIXES):
                 continue
 
@@ -184,7 +184,7 @@ def searchableText(obj):
             text_list.append(searchable.SearchableText())
     tiles_text = u" ".join(text_list)
     searchable_text = [
-        safe_unicode(entry)
+        safe_text(entry)
         for entry in (
             obj.id,
             obj.Title(),
diff --git a/src/collective/cover/grids.py b/src/collective/cover/grids.py
index 3df695f0f..67f521214 100644
--- a/src/collective/cover/grids.py
+++ b/src/collective/cover/grids.py
@@ -69,27 +69,3 @@ def columns_formatter(self, columns):
                 column["class"] += " {0}".format(column["css-class"])
 
         return columns
-
-
-@implementer(IGridSystem)
-class Deco16Grid(BaseGrid):
-
-    """Deco grid system (16 columns)."""
-
-    title = _(u"Deco (16 columns)")
-    ncolumns = 16
-    column_class = "cell"
-
-    def columns_formatter(self, columns):
-        w = "width-"
-        p = "position-"
-        offset = 0
-        for column in columns:
-            width = column.get("column-size", 1)
-            column["class"] = (
-                self.column_class + " " + (w + str(width)) + " " + (p + str(offset))
-            )
-            if "css-class" in column:
-                column["class"] += " {0}".format(column["css-class"])
-            offset = offset + width
-        return columns
diff --git a/src/collective/cover/grids.zcml b/src/collective/cover/grids.zcml
index 8ec29a34f..740844106 100644
--- a/src/collective/cover/grids.zcml
+++ b/src/collective/cover/grids.zcml
@@ -2,6 +2,5 @@
 
   <utility name="bootstrap3" factory=".grids.Bootstrap3" />
   <utility name="bootstrap2" factory=".grids.Bootstrap2" />
-  <utility name="deco16_grid" factory=".grids.Deco16Grid" />
 
 </configure>
diff --git a/src/collective/cover/locales/ca/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/ca/LC_MESSAGES/collective.cover.po
index 68fadcec9..6fe952948 100644
--- a/src/collective/cover/locales/ca/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/ca/LC_MESSAGES/collective.cover.po
@@ -164,10 +164,6 @@ msgstr "Configuracions de portada"
 msgid "Date"
 msgstr "Data"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/collective.cover.pot b/src/collective/cover/locales/collective.cover.pot
index cb1550f14..4fdc48120 100644
--- a/src/collective/cover/locales/collective.cover.pot
+++ b/src/collective/cover/locales/collective.cover.pot
@@ -38,10 +38,6 @@ msgstr ""
 msgid "A tile that shows an Calendar for the site."
 msgstr ""
 
-#: collective/cover/tiles/tiles.zcml:127
-msgid "A tile that shows an embedded Form Folder from Plone FormGen."
-msgstr ""
-
 #: collective/cover/tiles/tiles.zcml:18
 msgid "A tile that shows an image with a link."
 msgstr ""
@@ -163,10 +159,6 @@ msgstr ""
 msgid "Date"
 msgstr ""
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
@@ -229,10 +221,6 @@ msgstr ""
 msgid "Footer"
 msgstr ""
 
-#: collective/cover/tiles/tiles.zcml:127
-msgid "FormGen Tile"
-msgstr ""
-
 #: collective/cover/tiles/configuration_widgets/datetime.pt:50
 msgid "Format"
 msgstr ""
@@ -609,11 +597,6 @@ msgstr ""
 msgid "msg_short_name_list"
 msgstr ""
 
-#. Default: "FormGen"
-#: collective/cover/tiles/pfg.py:41
-msgid "msg_short_name_pfg"
-msgstr ""
-
 #. Default: "Rich Text"
 #: collective/cover/tiles/richtext.py:39
 msgid "msg_short_name_richtext"
diff --git a/src/collective/cover/locales/de/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/de/LC_MESSAGES/collective.cover.po
index 98c9ce03d..80bcaecdf 100644
--- a/src/collective/cover/locales/de/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/de/LC_MESSAGES/collective.cover.po
@@ -166,10 +166,6 @@ msgstr "Cover Konfiguration"
 msgid "Date"
 msgstr "Datum"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/es/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/es/LC_MESSAGES/collective.cover.po
index 74f2bf094..371cfd737 100644
--- a/src/collective/cover/locales/es/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/es/LC_MESSAGES/collective.cover.po
@@ -167,10 +167,6 @@ msgstr "Configuraciones de portada"
 msgid "Date"
 msgstr "Fecha"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr "Deco (16 columnas)"
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/fi/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/fi/LC_MESSAGES/collective.cover.po
index 8325d496c..72430464e 100644
--- a/src/collective/cover/locales/fi/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/fi/LC_MESSAGES/collective.cover.po
@@ -163,10 +163,6 @@ msgstr "Kansisivun asetukset"
 msgid "Date"
 msgstr "Päiväys"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/fr/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/fr/LC_MESSAGES/collective.cover.po
index 678e08248..b00dc1ab0 100644
--- a/src/collective/cover/locales/fr/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/fr/LC_MESSAGES/collective.cover.po
@@ -166,10 +166,6 @@ msgstr "Page de couverture"
 msgid "Date"
 msgstr "Date"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/it/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/it/LC_MESSAGES/collective.cover.po
index d332989bb..87a259dc8 100644
--- a/src/collective/cover/locales/it/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/it/LC_MESSAGES/collective.cover.po
@@ -166,10 +166,6 @@ msgstr "Impostazioni di collective.cover"
 msgid "Date"
 msgstr "Data"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/ja/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/ja/LC_MESSAGES/collective.cover.po
index bf3826aac..abbbed7e9 100644
--- a/src/collective/cover/locales/ja/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/ja/LC_MESSAGES/collective.cover.po
@@ -166,10 +166,6 @@ msgstr "カバー設定"
 msgid "Date"
 msgstr "日付"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/manual.pot b/src/collective/cover/locales/manual.pot
index a06961ca7..9c4e6975a 100644
--- a/src/collective/cover/locales/manual.pot
+++ b/src/collective/cover/locales/manual.pot
@@ -117,11 +117,3 @@ msgstr ""
 #: tiles/configure.zcml
 msgid "A tile that shows the body of a content type."
 msgstr ""
-
-#: tiles/configure.zcml
-msgid "FormGen Tile"
-msgstr ""
-
-#: tiles/configure.zcml
-msgid "A tile that shows an embedded Form Folder from Plone FormGen."
-msgstr ""
diff --git a/src/collective/cover/locales/nl/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/nl/LC_MESSAGES/collective.cover.po
index e34cfeb60..71e384413 100644
--- a/src/collective/cover/locales/nl/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/nl/LC_MESSAGES/collective.cover.po
@@ -167,10 +167,6 @@ msgstr "Voorpagina instellingen"
 msgid "Date"
 msgstr "Datum"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/no/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/no/LC_MESSAGES/collective.cover.po
index dfa87e304..0a1f5229b 100644
--- a/src/collective/cover/locales/no/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/no/LC_MESSAGES/collective.cover.po
@@ -168,10 +168,6 @@ msgstr "Forsideinnstillinger"
 msgid "Date"
 msgstr "Dato"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/pt_BR/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/pt_BR/LC_MESSAGES/collective.cover.po
index 9a9c02a85..3aa944d01 100644
--- a/src/collective/cover/locales/pt_BR/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/pt_BR/LC_MESSAGES/collective.cover.po
@@ -165,10 +165,6 @@ msgstr "Configurações da capa"
 msgid "Date"
 msgstr "Data"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr "Deco (16 colunas)"
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/zh_CN/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/zh_CN/LC_MESSAGES/collective.cover.po
index 2e9db9730..d22e0735b 100644
--- a/src/collective/cover/locales/zh_CN/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/zh_CN/LC_MESSAGES/collective.cover.po
@@ -165,10 +165,6 @@ msgstr "组合页设置"
 msgid "Date"
 msgstr "日期"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/locales/zh_TW/LC_MESSAGES/collective.cover.po b/src/collective/cover/locales/zh_TW/LC_MESSAGES/collective.cover.po
index 3a48e497d..3741450ab 100644
--- a/src/collective/cover/locales/zh_TW/LC_MESSAGES/collective.cover.po
+++ b/src/collective/cover/locales/zh_TW/LC_MESSAGES/collective.cover.po
@@ -163,10 +163,6 @@ msgstr "封面設定"
 msgid "Date"
 msgstr "日期"
 
-#: collective/cover/grids.py:82
-msgid "Deco (16 columns)"
-msgstr ""
-
 #: collective/cover/tiles/basic.py:28
 #: collective/cover/tiles/collection.py:40
 #: collective/cover/tiles/embed.py:26
diff --git a/src/collective/cover/profiles/default/jsregistry.xml b/src/collective/cover/profiles/default/jsregistry.xml
deleted file mode 100644
index 9ee702977..000000000
--- a/src/collective/cover/profiles/default/jsregistry.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0"?>
-<object name="portal_javascripts">
-  <javascript id="++resource++collective.js.bootstrap/js/bootstrap.min.js"
-      cacheable="True" compression="none" cookable="True" enabled="True"
-      expression="" inline="False" />
-</object>
diff --git a/src/collective/cover/profiles/default/metadata.xml b/src/collective/cover/profiles/default/metadata.xml
index 4e5e19412..d0671ef89 100644
--- a/src/collective/cover/profiles/default/metadata.xml
+++ b/src/collective/cover/profiles/default/metadata.xml
@@ -1,11 +1,10 @@
 <?xml version="1.0"?>
 <metadata>
-  <version>24</version>
+  <version>30</version>
   <dependencies>
     <dependency>profile-collective.js.galleria:default</dependency>
     <dependency>profile-collective.js.jqueryui:default</dependency>
     <dependency>profile-plone.app.blocks:default</dependency>
-    <dependency>profile-plone.app.dexterity:default</dependency>
     <dependency>profile-plone.app.iterate:plone.app.iterate</dependency>
     <dependency>profile-plone.app.tiles:default</dependency>
   </dependencies>
diff --git a/src/collective/cover/profiles/default/registry/bootstrapjs.xml b/src/collective/cover/profiles/default/registry/bootstrapjs.xml
new file mode 100644
index 000000000..d1a807228
--- /dev/null
+++ b/src/collective/cover/profiles/default/registry/bootstrapjs.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<registry
+    xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+    i18n:domain="collective.cover">
+
+  <records prefix="plone.bundles/bootstrap"
+           interface='Products.CMFPlone.interfaces.IBundleRegistry'>
+
+    <value key="merge_with">default</value>
+    <value key="enabled">True</value>
+    <value key="depends">plone</value>
+    <value key="jscompilation">++resource++collective.js.bootstrap/js/bootstrap.min.js</value>
+
+    <!-- Please keep this in sync with upgrades.update_last_compilation -->
+    <value key="last_compilation">2021-03-31 15:00:00</value>
+    <value key="compile">False</value>
+  </records>
+
+</registry>
diff --git a/src/collective/cover/profiles/default/registry.xml b/src/collective/cover/profiles/default/registry/collective.cover.xml
similarity index 96%
rename from src/collective/cover/profiles/default/registry.xml
rename to src/collective/cover/profiles/default/registry/collective.cover.xml
index b59d3601a..02079a104 100644
--- a/src/collective/cover/profiles/default/registry.xml
+++ b/src/collective/cover/profiles/default/registry/collective.cover.xml
@@ -1,5 +1,7 @@
 <?xml version="1.0"?>
-<registry>
+<registry
+    xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+
   <records interface="collective.cover.controlpanel.ICoverSettings" />
 
   <record field="layouts"
@@ -40,7 +42,6 @@
       <element>collective.cover.embed</element>
       <element>collective.cover.file</element>
       <element>collective.cover.list</element>
-      <element>collective.cover.pfg</element>
       <element>collective.cover.richtext</element>
     </value>
   </record>
diff --git a/src/collective/cover/profiles/default/registry/main.xml b/src/collective/cover/profiles/default/registry/main.xml
new file mode 100644
index 000000000..bdaf8b2a7
--- /dev/null
+++ b/src/collective/cover/profiles/default/registry/main.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<registry
+    xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+    i18n:domain="collective.cover">
+
+  <records interface="Products.CMFPlone.interfaces.controlpanel.ITypesSchema" prefix="plone">
+    <value purge="false" key="default_page_types">
+      <element>collective.cover.content</element>
+    </value>
+
+  </records>
+
+</registry>
diff --git a/src/collective/cover/profiles/default/types/collective.cover.content.xml b/src/collective/cover/profiles/default/types/collective.cover.content.xml
index 4bc1cfc01..d71b16248 100644
--- a/src/collective/cover/profiles/default/types/collective.cover.content.xml
+++ b/src/collective/cover/profiles/default/types/collective.cover.content.xml
@@ -18,10 +18,11 @@
   <property name="klass">collective.cover.content.Cover</property>
 
   <property name="behaviors">
-    <element value="plone.app.content.interfaces.INameFromTitle" />
-    <element value="plone.app.dexterity.behaviors.metadata.IBasic" />
-    <element value="plone.app.dexterity.behaviors.exclfromnav.IExcludeFromNavigation" />
-    <element value="plone.app.lockingbehavior.behaviors.ILocking" />
+    <element value="plone.namefromtitle" />
+    <element value="plone.basic" />
+    <element value="plone.excludefromnavigation" />
+    <element value="plone.locking" />
+    <element value="plone.relateditems" />
   </property>
 
   <!-- View information -->
diff --git a/src/collective/cover/profiles/uninstall/cssregistry.xml b/src/collective/cover/profiles/uninstall/cssregistry.xml
deleted file mode 100644
index 040bea49e..000000000
--- a/src/collective/cover/profiles/uninstall/cssregistry.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0"?>
-<object name="portal_css">
-  <stylesheet remove="True" id="++resource++collective.cover/css/contentchooser.css" />
-  <stylesheet remove="True" id="++resource++collective.cover/css/cover.css" />
-</object>
diff --git a/src/collective/cover/profiles/uninstall/jsregistry.xml b/src/collective/cover/profiles/uninstall/jsregistry.xml
deleted file mode 100644
index 0a82629dc..000000000
--- a/src/collective/cover/profiles/uninstall/jsregistry.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0"?>
-<object name="portal_javascripts">
-  <javascript remove="True" id="++resource++collective.cover/js/contentchooser.js" />
-  <javascript remove="True" id="++resource++collective.js.bootstrap/js/bootstrap.min.js" />
-  <javascript remove="True" id="++resource++collective.cover/js/vendor/jquery.endless-scroll.js" />
-  <javascript remove="True" id="++resource++collective.cover/js/layout_edit.js" />
-  <javascript remove="True" id="++resource++collective.cover/js/main.js" />
-</object>
diff --git a/src/collective/cover/profiles/uninstall/registry/bootstrapjs.xml b/src/collective/cover/profiles/uninstall/registry/bootstrapjs.xml
new file mode 100644
index 000000000..e3dcfc013
--- /dev/null
+++ b/src/collective/cover/profiles/uninstall/registry/bootstrapjs.xml
@@ -0,0 +1,7 @@
+<registry
+    xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+    i18n:domain="collective.cover">
+
+  <records remove="True" prefix="plone.bundles/bootstrap" interface='Products.CMFPlone.interfaces.IBundleRegistry' />
+
+</registry>
diff --git a/src/collective/cover/profiles/uninstall/registry.xml b/src/collective/cover/profiles/uninstall/registry/main.xml
similarity index 100%
rename from src/collective/cover/profiles/uninstall/registry.xml
rename to src/collective/cover/profiles/uninstall/registry/main.xml
diff --git a/src/collective/cover/setuphandlers.py b/src/collective/cover/setuphandlers.py
index d100fda35..27ba390f0 100644
--- a/src/collective/cover/setuphandlers.py
+++ b/src/collective/cover/setuphandlers.py
@@ -2,16 +2,6 @@
 from Products.CMFPlone.interfaces import INonInstallable
 from zope.interface import implementer
 
-import pkg_resources
-
-
-try:
-    pkg_resources.get_distribution("plone.app.relationfield")
-except pkg_resources.DistributionNotFound:
-    HAS_RELATIONFIELD = False
-else:
-    HAS_RELATIONFIELD = True
-
 
 @implementer(INonInstallable)
 class HiddenProfiles(object):  # pragma: no cover
@@ -37,18 +27,5 @@ def add_default_layout():
         settings.layouts[default] = EMPTY_LAYOUT
 
 
-def install_relationfield():
-    """Install plone.app.relationfield."""
-    from plone import api
-
-    qi = api.portal.get_tool("portal_quickinstaller")
-    qi.installProduct("plone.app.relationfield")
-    fti = api.portal.get_tool("portal_types")["collective.cover.content"]
-    fti.behaviors += ("plone.app.relationfield.behavior.IRelatedItems",)
-
-
 def run_after(portal_setup):
     add_default_layout()
-
-    if HAS_RELATIONFIELD:
-        install_relationfield()
diff --git a/src/collective/cover/subscribers.py b/src/collective/cover/subscribers.py
index 68725f428..24c696650 100644
--- a/src/collective/cover/subscribers.py
+++ b/src/collective/cover/subscribers.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
-from collective.cover.config import IS_PLONE_5
 from collective.cover.controlpanel import ICoverSettings
 from collective.cover.utils import assign_tile_ids
+from plone.app.linkintegrity.handlers import updateReferences
 from plone.registry.interfaces import IRegistry
 from zope.annotation.interfaces import IAnnotations
 from zope.component import getUtility
@@ -9,14 +9,6 @@
 import json
 
 
-if IS_PLONE_5:
-    from plone.app.linkintegrity.handlers import updateReferences
-else:
-    from plone.app.linkintegrity.handlers import referencedRelationship
-    from plone.app.linkintegrity.references import updateReferences
-    from Products.Archetypes.interfaces import IReferenceable
-
-
 def override_object_annotations(cover, event):
     """
     We need to override the annotations stored in the old object with the
@@ -50,14 +42,7 @@ def update_link_integrity(obj, event):
     """
     refs = obj.get_referenced_objects()
 
-    if IS_PLONE_5:
-        updateReferences(obj, refs)
-    else:
-        # needed by plone.app.linkintegrity under Plone 4.x
-        adapted = IReferenceable(obj, None)
-        if adapted is None:
-            return
-        updateReferences(adapted, referencedRelationship, refs)
+    updateReferences(obj, refs)
 
 
 def assign_id_for_tiles(cover, event):
diff --git a/src/collective/cover/subscribers.zcml b/src/collective/cover/subscribers.zcml
index 81424542d..0c696afb1 100644
--- a/src/collective/cover/subscribers.zcml
+++ b/src/collective/cover/subscribers.zcml
@@ -30,10 +30,4 @@
       handler=".subscribers.assign_id_for_tiles"
       />
 
-  <subscriber zcml:condition="not-have plone-5"
-      for="collective.cover.tiles.base.IPersistentCoverTile
-           zope.lifecycleevent.interfaces.IObjectRemovedEvent"
-      handler="plone.app.linkintegrity.handlers.referencedObjectRemoved"
-      />
-
 </configure>
diff --git a/src/collective/cover/tiles/banner.py b/src/collective/cover/tiles/banner.py
index 91b6ee123..dc596f2a6 100644
--- a/src/collective/cover/tiles/banner.py
+++ b/src/collective/cover/tiles/banner.py
@@ -9,7 +9,7 @@
 from plone.namedfile import field
 from plone.tiles.interfaces import ITileDataManager
 from plone.uuid.interfaces import IUUID
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from zope import schema
 from zope.interface import implementer
@@ -87,8 +87,8 @@ def populate_with_object(self, obj):
             self.clear_scales()
 
         obj = aq_base(obj)  # avoid acquisition
-        title = safe_unicode(obj.Title())
-        description = safe_unicode(obj.Description())
+        title = safe_text(obj.Title())
+        description = safe_text(obj.Description())
 
         data_mgr = ITileDataManager(self)
         data_mgr.set(
diff --git a/src/collective/cover/tiles/base.py b/src/collective/cover/tiles/base.py
index 144a89765..b85e39be2 100644
--- a/src/collective/cover/tiles/base.py
+++ b/src/collective/cover/tiles/base.py
@@ -449,7 +449,7 @@ def getRelativePaths(self):
                 scales = parent.unrestrictedTraverse(
                     "{0}/{1}".format(prefix.strip("/"), "@@images")
                 )
-                for size in scales.getAvailableSizes().keys():
+                for size in scales.available_sizes.keys():
                     yield "{0}/@@images/{1}".format(prefix, size)
 
     def getAbsolutePaths(self):
diff --git a/src/collective/cover/tiles/basic.py b/src/collective/cover/tiles/basic.py
index 1d7d1ad71..f058f5eb4 100644
--- a/src/collective/cover/tiles/basic.py
+++ b/src/collective/cover/tiles/basic.py
@@ -11,7 +11,7 @@
 from plone.namedfile.field import NamedBlobImage as NamedImage
 from plone.tiles.interfaces import ITileDataManager
 from plone.uuid.interfaces import IUUID
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from zope import schema
 from zope.interface import implementer
@@ -138,8 +138,8 @@ def Subject(self):
     def populate_with_object(self, obj):
         super(BasicTile, self).populate_with_object(obj)
 
-        title = safe_unicode(obj.Title())
-        description = safe_unicode(obj.Description())
+        title = safe_text(obj.Title())
+        description = safe_text(obj.Description())
 
         image = self.get_image_data(obj)
         if image:
diff --git a/src/collective/cover/tiles/calendar.py b/src/collective/cover/tiles/calendar.py
index 92b004a0f..c3e224624 100644
--- a/src/collective/cover/tiles/calendar.py
+++ b/src/collective/cover/tiles/calendar.py
@@ -4,8 +4,9 @@
 from collective.cover.tiles.base import IPersistentCoverTile
 from collective.cover.tiles.base import PersistentCoverTile
 from DateTime import DateTime
+from plone import api
 from Products.CMFCore.utils import getToolByName
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from six.moves.urllib.parse import quote_plus
 from time import localtime
@@ -13,6 +14,8 @@
 from zope.i18nmessageid import MessageFactory
 from zope.interface import implementer
 
+import calendar
+
 
 PLMF = MessageFactory("plonelocales")
 
@@ -31,6 +34,8 @@ class CalendarTile(PersistentCoverTile):
     is_editable = False
     is_droppable = False
     short_name = _(u"msg_short_name_calendar", default=u"Calendar")
+    calendar_states = ("published",)
+    calendar_types = ("Event",)
 
     def __init__(self, context, request):
         super(CalendarTile, self).__init__(context, request)
@@ -38,7 +43,6 @@ def __init__(self, context, request):
 
     def _setup(self):
         context = aq_inner(self.context)
-        self.calendar = getToolByName(context, "portal_calendar")
         self._ts = getToolByName(context, "translation_service")
         self.url_quote_plus = quote_plus
 
@@ -69,9 +73,8 @@ def getEventsForCalendar(self):
             (self.context, self.request), name="plone_portal_state"
         )
         navigation_root_path = portal_state.navigation_root_path()
-        weeks = self.calendar.getEventsForCalendar(
-            month, year, path=navigation_root_path
-        )
+        weeks = self.old_getEventsForCalendar(month, year, path=navigation_root_path)
+
         for week in weeks:
             for day in week:
                 daynumber = day["day"]
@@ -96,10 +99,146 @@ def getEventsForCalendar(self):
 
         return weeks
 
+    def old_getEventsForCalendar(self, month="1", year="2002", **kw):
+        """recreates a sequence of weeks, by days each day is a mapping.
+        {'day': #, 'url': None}
+        """
+        year = int(year)
+        month = int(month)
+        # daysByWeek is a list of days inside a list of weeks, like so:
+        # [[0, 1, 2, 3, 4, 5, 6],
+        #  [7, 8, 9, 10, 11, 12, 13],
+        #  [14, 15, 16, 17, 18, 19, 20],
+        #  [21, 22, 23, 24, 25, 26, 27],
+        #  [28, 29, 30, 31, 0, 0, 0]]
+        first_weekday = api.portal.get_registry_record("plone.first_weekday") + 1
+
+        calendar.setfirstweekday(first_weekday)
+        daysByWeek = calendar.monthcalendar(year, month)
+        weeks = []
+
+        events = self.old_catalog_getevents(year, month, **kw)
+
+        for week in daysByWeek:
+            days = []
+            for day in week:
+                if day in events:
+                    days.append(events[day])
+                else:
+                    days.append({"day": day, "event": 0, "eventslist": []})
+
+            weeks.append(days)
+
+        return weeks
+
+    def old_catalog_getevents(self, year, month, **kw):
+        """given a year and month return a list of days that have events"""
+        # XXX: this method violates the rules for tools/utilities:
+        # it depends on a non-utility tool
+        year = int(year)
+        month = int(month)
+        last_day = calendar.monthrange(year, month)[1]
+        first_date = self.old_getBeginAndEndTimes(1, month, year)[0]
+        last_date = self.old_getBeginAndEndTimes(last_day, month, year)[1]
+
+        query_args = {
+            "portal_type": self.calendar_types,
+            "review_state": self.calendar_states,
+            "start": {"query": last_date, "range": "max"},
+            "end": {"query": first_date, "range": "min"},
+            "sort_on": "start",
+        }
+        query_args.update(kw)
+
+        ctool = getToolByName(self, "portal_catalog")
+        query = ctool(**query_args)
+
+        # compile a list of the days that have events
+        eventDays = {}
+        for daynumber in range(1, 32):  # 1 to 31
+            eventDays[daynumber] = {"eventslist": [], "event": 0, "day": daynumber}
+        includedevents = []
+        for result in query:
+            if result.getRID() in includedevents:
+                break
+            else:
+                includedevents.append(result.getRID())
+            event = {}
+            # we need to deal with events that end next month
+            if result.end.month != month:
+                # doesn't work for events that last ~12 months
+                # fix it if it's a problem, otherwise ignore
+                eventEndDay = last_day
+                event["end"] = None
+            else:
+                eventEndDay = result.end.day
+                event["end"] = result.end.time()
+            # and events that started last month
+            if result.start.month != month:  # same as above (12 month thing)
+                eventStartDay = 1
+                event["start"] = None
+            else:
+                eventStartDay = result.start.day
+                event["start"] = result.start.time()
+
+            event["title"] = result.Title or result.getId
+
+            if eventStartDay != eventEndDay:
+                allEventDays = range(eventStartDay, eventEndDay + 1)
+                eventDays[eventStartDay]["eventslist"].append(
+                    {"end": None, "start": result.start.time(), "title": event["title"]}
+                )
+                eventDays[eventStartDay]["event"] = 1
+
+                for eventday in allEventDays[1:-1]:
+                    eventDays[eventday]["eventslist"].append(
+                        {"end": None, "start": None, "title": event["title"]}
+                    )
+                    eventDays[eventday]["event"] = 1
+
+                if result.end == result.end.replace(hour=0, minute=0, second=0):
+                    last_day_data = eventDays[allEventDays[-2]]
+                    last_days_event = last_day_data["eventslist"][-1]
+                    last_days_event["end"] = (
+                        (result.end - 1).replace(hour=23, minute=59, second=59).time()
+                    )
+                else:
+                    eventDays[eventEndDay]["eventslist"].append(
+                        {
+                            "end": result.end.time(),
+                            "start": None,
+                            "title": event["title"],
+                        }
+                    )
+                    eventDays[eventEndDay]["event"] = 1
+            else:
+                eventDays[eventStartDay]["eventslist"].append(event)
+                eventDays[eventStartDay]["event"] = 1
+            # This list is not uniqued and isn't sorted
+            # uniquing and sorting only wastes time
+            # and in this example we don't need to because
+            # later we are going to do an 'if 2 in eventDays'
+            # so the order is not important.
+            # example:  [23, 28, 29, 30, 31, 23]
+        return eventDays
+
+    def old_getBeginAndEndTimes(self, day, month, year):
+        """Get two DateTime objects representing the beginning and end
+        of the given day
+        """
+        day = int(day)
+        month = int(month)
+        year = int(year)
+
+        begin = DateTime("{0}/{1:02d}/{2:02d} 00:00:00".format(year, month, day))
+        end = DateTime("{0}/{1:02d}/{2:02d} 23:59:59".format(year, month, day))
+
+        return (begin, end)
+
     def getEventString(self, event):
-        start = event["start"] and ":".join(event["start"].split(":")[:2]) or ""
-        end = event["end"] and ":".join(event["end"].split(":")[:2]) or ""
-        title = safe_unicode(event["title"]) or u"event"
+        start = event["start"] and ":".join(str(event["start"]).split(":")[:2]) or ""
+        end = event["end"] and ":".join(str(event["end"]).split(":")[:2]) or ""
+        title = safe_text(event["title"]) or u"event"
 
         if start and end:
             eventstring = "{0}-{1} {2}".format(start, end, title)
@@ -119,9 +258,10 @@ def getYearAndMonthToDisplay(self):
         # First priority goes to the data in the REQUEST
         year = request.get("year", None)
         month = request.get("month", None)
+        use_session = False
 
         # Next get the data from the SESSION
-        if self.calendar.getUseSession():
+        if use_session:
             session = request.get("SESSION", None)
             if session:
                 if not year:
@@ -167,15 +307,17 @@ def getNextMonth(self, year, month):
     def getWeekdays(self):
         """Returns a list of Messages for the weekday names."""
         weekdays = []
+        first_weekday = api.portal.get_registry_record("plone.first_weekday") + 1
+        day_numbers = [i % 7 for i in range(first_weekday, first_weekday + 7)]
+
         # list of ordered weekdays as numbers
-        for day in self.calendar.getDayNumbers():
+        for day in day_numbers:
             weekdays.append(
                 PLMF(
                     self._ts.day_msgid(day, format="s"),
                     default=self._ts.weekday_english(day, format="a"),
                 )
             )
-
         return weekdays
 
     def isToday(self, day):
@@ -189,7 +331,7 @@ def isToday(self, day):
         )
 
     def getReviewStateString(self):
-        states = self.calendar.getCalendarStates()
+        states = self.calendar_states
         return "".join(
             map(
                 lambda x: "review_state={0}&amp;".format(self.url_quote_plus(x)), states
@@ -197,7 +339,7 @@ def getReviewStateString(self):
         )
 
     def getEventTypes(self):
-        types = self.calendar.getCalendarTypes()
+        types = self.calendar_types
         return "".join(
             map(lambda x: "Type={0}&amp;".format(self.url_quote_plus(x)), types)
         )
diff --git a/src/collective/cover/tiles/carousel.py b/src/collective/cover/tiles/carousel.py
index 18a069b73..5c5dcd020 100644
--- a/src/collective/cover/tiles/carousel.py
+++ b/src/collective/cover/tiles/carousel.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 from __future__ import division
+
 from collective.cover import _
 from collective.cover.interfaces import ITileEditForm
 from collective.cover.tiles.list import IListTile
diff --git a/src/collective/cover/tiles/collection.py b/src/collective/cover/tiles/collection.py
index 61ee49d13..e5746fb1b 100644
--- a/src/collective/cover/tiles/collection.py
+++ b/src/collective/cover/tiles/collection.py
@@ -10,7 +10,7 @@
 from plone.tiles.interfaces import ITileDataManager
 from plone.tiles.interfaces import ITileType
 from plone.uuid.interfaces import IUUID
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from zope import schema
 from zope.component import queryUtility
@@ -128,7 +128,7 @@ def results(self):
             if self.data.get("random", False):
                 if size > len(results):
                     size = len(results)
-                return random.sample(results, size)
+                return random.sample(list(results), size)
 
             return results[offset : offset + size]
         else:
@@ -145,7 +145,7 @@ def populate_with_object(self, obj):
         super(CollectionTile, self).populate_with_object(obj)  # check permission
 
         if obj.portal_type in self.accepted_ct():
-            header = safe_unicode(obj.Title())  # use collection's title as header
+            header = safe_text(obj.Title())  # use collection's title as header
             footer = _(u"More…")  # XXX: can we use field's default?
 
             data_mgr = ITileDataManager(self)
diff --git a/src/collective/cover/tiles/configuration.py b/src/collective/cover/tiles/configuration.py
index cae3177b6..79465d022 100644
--- a/src/collective/cover/tiles/configuration.py
+++ b/src/collective/cover/tiles/configuration.py
@@ -2,6 +2,7 @@
 from persistent.dict import PersistentDict
 from plone.namedfile.interfaces import INamedBlobImageField
 from plone.tiles.interfaces import ITileType
+from Products.CMFPlone.utils import safe_text
 from zope.annotation.interfaces import IAnnotations
 from zope.component import getUtility
 from zope.interface import implementer
@@ -12,8 +13,6 @@
 from zope.schema.interfaces import IInt
 from zope.schema.interfaces import ITextLine
 
-import six
-
 
 ANNOTATIONS_KEY_PREFIX = u"plone.tiles.configuration"
 
@@ -105,7 +104,7 @@ def _set_default_configuration(self):
         fields = getFieldNamesInOrder(tile_type.schema)
 
         for name, field in getFieldsInOrder(tile_type.schema):
-            order = six.text_type(fields.index(name))
+            order = safe_text(fields.index(name))
             # default configuration attributes for all fields
             defaults[name] = {"order": order, "visibility": u"on"}
             if name == "css_class":
diff --git a/src/collective/cover/tiles/configuration_widgets/configure.zcml b/src/collective/cover/tiles/configuration_widgets/configure.zcml
index 4a0ad2c86..c26aef749 100644
--- a/src/collective/cover/tiles/configuration_widgets/configure.zcml
+++ b/src/collective/cover/tiles/configuration_widgets/configure.zcml
@@ -38,28 +38,12 @@
         template="namedimage.pt"
         />
 
-    <!--BEGIN IDatetimeWidget customization
-        https://github.com/plone/plone.formwidget.datetime/blob/1.3.1/plone/formwidget/datetime/z3cform/configure.zcml#L77
-        This package is used instead of collective.z3cform.datetimewidget if you're
-        using plone.app.contenttypes >= 1.1.1.
-        TODO: Review this IDatetimeWidget customization, check if it's possible to have just on
-        package in all situations. Check https://github.com/collective/collective.cover/issues/747-->
-    <z3c:widgetTemplate zcml:condition="installed plone.formwidget.datetime"
-        mode="configure"
-        widget="plone.formwidget.datetime.z3cform.interfaces.IDatetimeWidget"
-        layer="z3c.form.interfaces.IFormLayer"
-        template="datetime.pt"
-        />
-
-    <!--If you don't have plone.app.contenttypes >= 1.1.1 installed, this is used-->
-    <!--instead.-->
     <z3c:widgetTemplate
         mode="configure"
-        widget="collective.z3cform.datetimewidget.interfaces.IDatetimeWidget"
+        widget="plone.app.z3cform.interfaces.IDatetimeWidget"
         layer="z3c.form.interfaces.IFormLayer"
         template="datetime.pt"
         />
-    <!--END IDatetimeWidget customization-->
 
     <class class=".cssclasswidget.CSSClassWidget">
         <require permission="zope.Public"
diff --git a/src/collective/cover/tiles/configuration_widgets/cssclasswidget.py b/src/collective/cover/tiles/configuration_widgets/cssclasswidget.py
index 71c43f296..27aec0545 100644
--- a/src/collective/cover/tiles/configuration_widgets/cssclasswidget.py
+++ b/src/collective/cover/tiles/configuration_widgets/cssclasswidget.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-from interfaces import ICSSClassWidget
+from collective.cover.tiles.configuration_widgets.interfaces import ICSSClassWidget
 from z3c.form import interfaces
 from z3c.form.browser import widget
 from z3c.form.browser.select import SelectWidget
diff --git a/src/collective/cover/tiles/data.py b/src/collective/cover/tiles/data.py
index 1cc1bd991..55bee05ec 100644
--- a/src/collective/cover/tiles/data.py
+++ b/src/collective/cover/tiles/data.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 from collective.cover.tiles.base import IPersistentCoverTile
+from copy import copy
 from persistent.dict import PersistentDict
 from plone.namedfile.interfaces import INamedImage
 from plone.tiles.data import PersistentTileDataManager
@@ -47,7 +48,8 @@ def set(self, data):
         except KeyError:
             pass
 
-        for k, v in data.items():
+        data_copy = copy(data)
+        for k, v in data_copy.items():
             if INamedImage.providedBy(v):
                 mtime_key = "{0}_mtime".format(k)
                 if (
diff --git a/src/collective/cover/tiles/edit_widgets/more_link.py b/src/collective/cover/tiles/edit_widgets/more_link.py
index 60bc9b7a3..e2d7428b6 100644
--- a/src/collective/cover/tiles/edit_widgets/more_link.py
+++ b/src/collective/cover/tiles/edit_widgets/more_link.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-from interfaces import IMoreLinkWidget
+from collective.cover.tiles.edit_widgets.interfaces import IMoreLinkWidget
 from Products.CMFCore.utils import getToolByName
 from z3c.form import interfaces
 from z3c.form.browser.text import TextWidget
diff --git a/src/collective/cover/tiles/file.py b/src/collective/cover/tiles/file.py
index bbc5a5209..bd824dd52 100644
--- a/src/collective/cover/tiles/file.py
+++ b/src/collective/cover/tiles/file.py
@@ -6,9 +6,9 @@
 from plone.app.uuid.utils import uuidToObject
 from plone.tiles.interfaces import ITileDataManager
 from plone.uuid.interfaces import IUUID
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
-from Products.MimetypesRegistry.common import MimeTypeException
+from Products.MimetypesRegistry.interfaces import MimeTypeException
 from zope import schema
 from zope.interface import implementer
 
@@ -151,8 +151,8 @@ def populate_with_object(self, obj):
         if obj.portal_type not in self.accepted_ct():
             return
         data = {
-            "title": safe_unicode(obj.Title()),
-            "description": safe_unicode(obj.Description()),
+            "title": safe_text(obj.Title()),
+            "description": safe_text(obj.Description()),
             "download": True,
             "uuid": IUUID(obj),
         }
diff --git a/src/collective/cover/tiles/list.py b/src/collective/cover/tiles/list.py
index a42360b2b..6fddfbf3a 100644
--- a/src/collective/cover/tiles/list.py
+++ b/src/collective/cover/tiles/list.py
@@ -14,6 +14,7 @@
 from plone.tiles.interfaces import ITileDataManager
 from plone.tiles.interfaces import ITileType
 from plone.uuid.interfaces import IUUID
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from zope import schema
 from zope.component import queryUtility
@@ -21,7 +22,6 @@
 from zope.schema import getFieldsInOrder
 
 import logging
-import six
 
 
 logger = logging.getLogger(PROJECTNAME)
@@ -135,7 +135,6 @@ def results(self):
                         logger.debug(
                             "Non-existent object {0} removed from tile".format(uuid)
                         )  # noqa: E501
-
         return results[: self.limit]
 
     def is_empty(self):
@@ -209,7 +208,7 @@ def populate_with_uuids(self, uuids):
         for uuid in uuids:
             if uuid not in uuids_dict:
                 entry = dict()
-                entry[u"order"] = six.text_type(order)
+                entry[u"order"] = safe_text(order)
                 uuids_dict[uuid] = entry
                 order += 1
 
diff --git a/src/collective/cover/tiles/pfg.py b/src/collective/cover/tiles/pfg.py
deleted file mode 100644
index c70ccf43a..000000000
--- a/src/collective/cover/tiles/pfg.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# -*- coding: utf-8 -*-
-"""PFG tile is deprecated and will be removed in collective.cover 3"""
-from AccessControl import Unauthorized
-from collective.cover import _
-from collective.cover.tiles.base import IPersistentCoverTile
-from collective.cover.tiles.base import PersistentCoverTile
-from plone.app.uuid.utils import uuidToObject
-from plone.tiles.interfaces import ITileDataManager
-from plone.uuid.interfaces import IUUID
-from Products.CMFPlone.utils import safe_unicode
-from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
-from zope import schema
-from zope.interface import implementer
-
-import warnings
-
-
-warnings.warn(__doc__, DeprecationWarning)
-
-
-class IPFGTile(IPersistentCoverTile):
-
-    title = schema.TextLine(
-        title=_(u"Title"),
-        required=False,
-    )
-
-    description = schema.Text(
-        title=_(u"Description"),
-        required=False,
-    )
-
-    uuid = schema.TextLine(
-        title=_(u"UUID"),
-        required=False,
-        readonly=True,
-    )
-
-
-@implementer(IPFGTile)
-class PFGTile(PersistentCoverTile):
-
-    index = ViewPageTemplateFile("templates/pfg.pt")
-
-    is_editable = True
-    is_configurable = True
-    short_name = _(u"msg_short_name_pfg", default=u"FormGen")
-
-    def body(self):
-        body = ""
-        uuid = self.data.get("uuid", None)
-        try:
-            obj = uuid and uuidToObject(uuid)
-            if obj is not None:
-                body = obj.restrictedTraverse("fg_embedded_view_p3")()
-        except Unauthorized:
-            body = ""
-        return body
-
-    def populate_with_object(self, obj):
-        super(PFGTile, self).populate_with_object(obj)
-
-        data = {
-            "title": safe_unicode(obj.Title()),
-            "description": safe_unicode(obj.Description()),
-            "uuid": IUUID(obj),
-        }
-
-        data_mgr = ITileDataManager(self)
-        data_mgr.set(data)
-
-    def accepted_ct(self):
-        """Return 'FormFolder' as the only content type accepted in the tile."""
-        return ["FormFolder"]
diff --git a/src/collective/cover/tiles/richtext.py b/src/collective/cover/tiles/richtext.py
index 834ee8b2d..2b74ff24b 100644
--- a/src/collective/cover/tiles/richtext.py
+++ b/src/collective/cover/tiles/richtext.py
@@ -12,6 +12,7 @@
 from plone.tiles.interfaces import ITileDataManager
 from plone.uuid.interfaces import IUUID
 from Products.CMFPlone.utils import safe_hasattr
+from Products.CMFPlone.utils import safe_text
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from zope import schema
 from zope.interface import implementer
@@ -99,5 +100,5 @@ def SearchableText(self):
             encoding="utf-8" if isinstance(value, six.text_type) else value.encoding,
         )
 
-        searchable_text = six.text_type(data.getData(), "utf-8")
+        searchable_text = safe_text(data.getData())
         return searchable_text.strip()  # remove leading and trailing spaces
diff --git a/src/collective/cover/tiles/templates/pfg.pt b/src/collective/cover/tiles/templates/pfg.pt
deleted file mode 100644
index 4c5bba33f..000000000
--- a/src/collective/cover/tiles/templates/pfg.pt
+++ /dev/null
@@ -1,46 +0,0 @@
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:metal="http://xml.zope.org/namespaces/metal"
-      xmlns:tal="http://xml.zope.org/namespaces/tal"
-      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
-      i18n:domain="collective.cover">
-
-<body tal:define="body view/body">
-  <p tal:condition="python: not body and view.is_compose_mode()" i18n:translate="">
-    Please drag&amp;drop a Form Folder here to populate the tile.
-  </p>
-  <div class="cover-pfg-tile tile-content">
-  <tal:fields
-     tal:condition="body"
-     repeat="field view/get_configured_fields">
-
-       <tal:title define="htmltag python:field.get('htmltag', 'h1')"
-               condition="python:field['id'] == 'title'">
-
-        <h1 tal:condition="python:htmltag == 'h1'"
-          tal:content="python:field['content']" >
-        </h1>
-
-        <h2 tal:condition="python:htmltag == 'h2'"
-          tal:content="python:field['content']" >
-        </h2>
-        <h3 tal:condition="python:htmltag == 'h3'"
-          tal:content="python:field['content']" >
-        </h3>
-        <h4 tal:condition="python:htmltag == 'h4'"
-          tal:content="python:field['content']" >
-        </h4>
-    </tal:title>
-
-    <tal:description condition="python:field['id'] == 'description'">
-      <p tal:content="python:field['content']">Description</p>
-    </tal:description>
-
-  </tal:fields>
-  <div tal:condition="body" class="contentbody">
-    <div class="content"
-         tal:content="structure body">Form Body</div>
-    <div class="visualClear"><!-- --></div>
-  </div>
-  </div>
-</body>
-</html>
diff --git a/src/collective/cover/tiles/tiles.zcml b/src/collective/cover/tiles/tiles.zcml
index b3105e297..af2477913 100644
--- a/src/collective/cover/tiles/tiles.zcml
+++ b/src/collective/cover/tiles/tiles.zcml
@@ -113,19 +113,6 @@
       for="*"
       />
 
-  <plone:tile
-      zcml:condition="installed Products.PloneFormGen"
-      name="collective.cover.pfg"
-      title="FormGen Tile"
-      description="A tile that shows an embedded Form Folder from Plone FormGen."
-      icon="++resource++collective.cover/img/tile-pfg.png"
-      add_permission="cmf.ModifyPortalContent"
-      schema=".pfg.IPFGTile"
-      class=".pfg.PFGTile"
-      permission="zope2.View"
-      for="*"
-      />
-
   <plone:tile
       name="collective.cover.calendar"
       title="Calendar Tile"
diff --git a/src/collective/cover/utils.py b/src/collective/cover/utils.py
index 422ca3e0f..0070c867c 100644
--- a/src/collective/cover/utils.py
+++ b/src/collective/cover/utils.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from collective.cover.config import IS_PLONE_5
 from plone import api
 
 import uuid
@@ -52,9 +51,4 @@ def uuidToCatalogBrain(uuid):
 
 def get_types_use_view_action_in_listings():
     """Helper funtion to deal with API inconsistencies."""
-    if IS_PLONE_5:
-        return api.portal.get_registry_record("plone.types_use_view_action_in_listings")
-    else:
-        portal_properties = api.portal.get_tool(name="portal_properties")
-        site_properties = portal_properties.site_properties
-        return site_properties.getProperty("typesUseViewActionInListings", ())
+    return api.portal.get_registry_record("plone.types_use_view_action_in_listings")
diff --git a/src/collective/cover/vocabularies.py b/src/collective/cover/vocabularies.py
index 1d69a81f0..51a3588f6 100644
--- a/src/collective/cover/vocabularies.py
+++ b/src/collective/cover/vocabularies.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from collective.cover.config import IS_PLONE_5
 from collective.cover.controlpanel import ICoverSettings
 from collective.cover.interfaces import IGridSystem
 from collective.cover.tiles.base import IPersistentCoverTile
@@ -29,19 +28,9 @@ def __call__(self, context):
 @implementer(IVocabularyFactory)
 class AvailableTilesVocabulary(object):
     def __call__(self, context):
-
         registry = getUtility(IRegistry)
         settings = registry.forInterface(ICoverSettings)
         tiles = settings.available_tiles
-
-        # FIXME: https://github.com/collective/collective.cover/issues/633
-        if IS_PLONE_5 and "collective.cover.calendar" in tiles:
-            tiles.remove("collective.cover.calendar")
-
-        # XXX: PFG tile is deprecated and will be removed in collective.cover 3
-        if IS_PLONE_5 and "collective.cover.pfg" in tiles:
-            tiles.remove("collective.cover.pfg")
-
         items = [SimpleTerm(value=i, title=i) for i in tiles]
         return SimpleVocabulary(items)
 
@@ -63,13 +52,6 @@ class EnabledTilesVocabulary(object):
 
     @staticmethod
     def enabled(name):
-        # FIXME: https://github.com/collective/collective.cover/issues/633
-        if IS_PLONE_5 and name == "collective.cover.calendar":
-            return False
-
-        # XXX: PFG tile is deprecated and will be removed in collective.cover 3
-        if IS_PLONE_5 and name == "collective.cover.pfg":
-            return False
 
         tile_type = queryUtility(ITileType, name)
         if tile_type and tile_type.schema:
diff --git a/src/collective/cover/widgets/selectpreview.py b/src/collective/cover/widgets/selectpreview.py
index d190683c4..90d314f63 100644
--- a/src/collective/cover/widgets/selectpreview.py
+++ b/src/collective/cover/widgets/selectpreview.py
@@ -13,12 +13,12 @@
 import zope.interface
 
 
+@zope.interface.implementer(ISelectPreviewWidget)
 class SelectPreviewWidget(select.SelectWidget):
     """Widget for adding new keywords and autocomplete with the ones in the
     system.
     """
 
-    zope.interface.implementsOnly(ISelectPreviewWidget)
     klass = u"keyword-widget"
     display_template = ViewPageTemplateFile("selectpreview_display.pt")
     input_template = ViewPageTemplateFile("selectpreview_input.pt")
diff --git a/src/collective/cover/widgets/textlinessortable.py b/src/collective/cover/widgets/textlinessortable.py
index e8b176fcf..85c3690e9 100644
--- a/src/collective/cover/widgets/textlinessortable.py
+++ b/src/collective/cover/widgets/textlinessortable.py
@@ -3,22 +3,20 @@
 from collective.cover.utils import uuidToObject
 from collective.cover.widgets.interfaces import ITextLinesSortableWidget
 from Products.CMFPlone.utils import base_hasattr
-from Products.CMFPlone.utils import safe_unicode
+from Products.CMFPlone.utils import safe_text
 from z3c.form import interfaces
 from z3c.form import widget
 from z3c.form.browser import textlines
 from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
-
-import six
-import zope.interface
+from zope.interface import implementer
 
 
+@implementer(ITextLinesSortableWidget)
 class TextLinesSortableWidget(textlines.TextLinesWidget):
     """Widget for adding new keywords and autocomplete with the ones in the
     system.
     """
 
-    zope.interface.implementsOnly(ITextLinesSortableWidget)
     klass = u"textlines-sortable-widget"
     configure_template = ViewPageTemplateFile("textlines_sortable_configure.pt")
     display_template = ViewPageTemplateFile("textlines_sortable_display.pt")
@@ -80,7 +78,7 @@ def get_custom_title(self, uuid):
             return title
         # If didn't find, get object title
         obj = uuidToObject(uuid)
-        return safe_unicode(obj.Title())
+        return safe_text(obj.Title())
 
     def get_custom_description(self, uuid):
         """Returns the custom Description assigned to a specific item
@@ -99,7 +97,7 @@ def get_custom_description(self, uuid):
             return description
         # If didn't find, get object description
         obj = uuidToObject(uuid)
-        return safe_unicode(obj.Description())
+        return safe_text(obj.Description())
 
     def get_custom_url(self, uuid):
         """Returns the custom URL assigned to a specific item
@@ -133,19 +131,19 @@ def extract(self):
         results = dict()
         for index, uuid in enumerate(uuids):
             obj = uuidToObject(uuid)
-            results[uuid] = {u"order": six.text_type(index)}
+            results[uuid] = {u"order": safe_text(index)}
             custom_title = self.request.get(
                 "{0}.custom_title.{1}".format(self.name, uuid), ""
             )
-            if custom_title != u"" and custom_title != safe_unicode(obj.Title()):
-                results[uuid][u"custom_title"] = six.text_type(custom_title)
+            if custom_title != u"" and custom_title != safe_text(obj.Title()):
+                results[uuid][u"custom_title"] = safe_text(custom_title)
             custom_description = self.request.get(
                 "{0}.custom_description.{1}".format(self.name, uuid), ""
             )
-            if custom_description != u"" and custom_description != safe_unicode(
+            if custom_description != u"" and custom_description != safe_text(
                 obj.Description()
             ):
-                results[uuid][u"custom_description"] = six.text_type(custom_description)
+                results[uuid][u"custom_description"] = safe_text(custom_description)
             custom_url = self.request.get(
                 "{0}.custom_url.{1}".format(self.name, uuid), ""
             )
@@ -153,11 +151,11 @@ def extract(self):
             if obj.portal_type in get_types_use_view_action_in_listings():
                 url += "/view"
             if custom_url != u"" and custom_url != url:
-                results[uuid][u"custom_url"] = six.text_type(custom_url)
+                results[uuid][u"custom_url"] = safe_text(custom_url)
         return results
 
 
-@zope.interface.implementer(interfaces.IFieldWidget)
+@implementer(interfaces.IFieldWidget)
 def TextLinesSortableFieldWidget(field, request):
     """IFieldWidget factory for TextLinesWidget."""
     return widget.FieldWidget(field, TextLinesSortableWidget(request))
diff --git a/tox.ini b/tox.ini
index 382a51e51..8984da569 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,9 +2,7 @@
 envlist =
     py27-lint,
 #    black-check,
-    py27-Plone{43,51},
-    py27-Plone43_DEXTERITY_ONLY,
-    py27-Plone43_PLONE_APP_JQUERY_1.9.2
+    py27-Plone52,
 #    docs,
     coverage-report,
 
@@ -18,10 +16,7 @@ python =
 
 [gh-actions:env]
 PLONE-VERSION =
-    Plone43: Plone43
-    Plone43_DEXTERITY_ONLY: Plone43_DEXTERITY_ONLY
-    Plone43_PLONE_APP_JQUERY_1.9.2: Plone43_PLONE_APP_JQUERY_1.9.2
-    Plone51: Plone51
+    Plone52: Plone52
 
 
 [testenv]
@@ -34,28 +29,21 @@ extras =
 commands =
     {envbindir}/buildout -q -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} bootstrap
 #    {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} annotate
-    {envbindir}/buildout -n -qq -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} {env:VERSIONS} staticresources:directory={toxinidir}/webpack install test node staticresources
+    {envbindir}/buildout -n -qq -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} staticresources:directory={toxinidir}/webpack install test node staticresources
     coverage run {envbindir}/test -v1 --auto-color {posargs}
     # coverage run {envbindir}/test -v --all -t robot {posargs}
 
 setenv =
     COVERAGE_FILE=.coverage.{envname}
-    version_file=test_plone51.cfg
-    VERSIONS=
-    Plone43: version_file=test_plone43.cfg
-    Plone51: version_file=test_plone51.cfg
-    Plone43_DEXTERITY_ONLY: DEXTERITY_ONLY=true
-    Plone43_DEXTERITY_ONLY: version_file=test_plone43.cfg
-    Plone43_PLONE_APP_JQUERY_1.9.2: VERSIONS=versions:plone.app.jquery=1.9.2
-    Plone43_PLONE_APP_JQUERY_1.9.2: version_file=test_plone43.cfg
+    version_file=test_plone52.cfg
+    Plone52: version_file=test_plone52.cfg
 
 passenv =
     DISPLAY
 
 deps =
     -rrequirements.txt
-    Plone43: -cconstraints_plone43.txt
-    Plone51: -cconstraints_plone51.txt
+    Plone52: -cconstraints_plone52.txt
     coverage
 
 [testenv:coverage-report]
@@ -65,7 +53,7 @@ basepython = python2.7
 
 deps =
     coverage
-    -cconstraints_plone51.txt
+    -cconstraints_plone52.txt
 
 setenv =
     COVERAGE_FILE=.coverage
diff --git a/webpack/app/galleria/theme.js b/webpack/app/galleria/theme.js
index 8a6caafb1..294b12a62 100644
--- a/webpack/app/galleria/theme.js
+++ b/webpack/app/galleria/theme.js
@@ -12,12 +12,14 @@ var rm_button = function(data) {
   return "<i class='tile-remove-item' data-uuid='" + uuid + "'><span class='text'>remove</span></i>";
 };
 
-(function($) {
+!(function($) {
+  "use strict";
 
   /*global jQuery, Galleria */
 
   Galleria.addTheme({
     name: 'cover_theme',
+    version: 1.6,
     author: 'Simples',
     css: '++resource++collective.cover/galleria/theme.css',
     defaults: {
@@ -32,7 +34,7 @@ var rm_button = function(data) {
     },
     init: function(options) {
 
-      Galleria.requires(1.28, 'This version of Classic theme requires Galleria 1.2.8 or later');
+      Galleria.requires(1.4, 'This version of cover_theme theme requires Galleria 1.4 or later');
 
       // cache some stuff
       var touch = Galleria.TOUCH,
@@ -114,4 +116,6 @@ var rm_button = function(data) {
     }
   });
 
+  return Galleria;
+
 }(jQuery));
diff --git a/webpack/app/js/contentchooser.js b/webpack/app/js/contentchooser.js
index 16f80bbdb..ae597cc70 100644
--- a/webpack/app/js/contentchooser.js
+++ b/webpack/app/js/contentchooser.js
@@ -428,9 +428,6 @@ export default class ContentChooser {
             html += `<li data-is-folderish="${data.items[i].is_folderish}" data-content-type="${data.items[i].portal_type}" data-content-uuid="${data.items[i].uuid}" class="ui-draggable">`;
 
             if (data.items[i].is_folderish) {
-              if (data.items[i].icon.length) {
-                html += '<img src="' + data.items[i].icon + '" /> ';
-              }
               html += '<a class="' +
                 data.items[i].classicon + ' ' + data.items[i].r_state + '" ';
               html += 'title="' + data.items[i].description + '" ';
@@ -438,9 +435,6 @@ export default class ContentChooser {
               html += '<span>' + data.items[i].title + '</span>';
               html += '</a>';
             } else {
-              if (data.items[i].portal_type == 'Image') {
-                html += '<img src="' + this.call_context + '/image.png" border="0"/> ';
-              }
               html += '<a data-ct-type="' +
                 data.items[i].portal_type + '" class="' +
                 data.items[i].classicon + ' ' +
diff --git a/webpack/app/js/layout.js b/webpack/app/js/layout.js
index 7c464e2a8..63f56c91b 100644
--- a/webpack/app/js/layout.js
+++ b/webpack/app/js/layout.js
@@ -552,6 +552,7 @@ export default class LayoutView {
     $(document).on("click", ".config-tile-link", function(e) {
       e.preventDefault();
       let url = $(this).attr("href");
+      $("#tile-configure").removeClass('hide');
       $('#tile-configure').modal();
       $.ajax({
         type: 'GET',
diff --git a/webpack/app/scss/_layout.scss b/webpack/app/scss/_layout.scss
index f4ea19a80..a23b69951 100644
--- a/webpack/app/scss/_layout.scss
+++ b/webpack/app/scss/_layout.scss
@@ -3,6 +3,18 @@
   clear: both;
   min-height: 130px;
   margin: 30px 0;
+  .cover-row {
+    padding: 25px 1px !important;
+    border-top: 1px dashed #7a7a7a !important;
+    background-color: #fff !important;
+  }
+  .cover-column {
+
+  }
+  .cover-tile {
+      margin: 25px 20px !important;
+      text-align: center;
+  }
 }
 
 /* bootstrap classes*/
@@ -69,6 +81,14 @@ button.close {
   text-shadow: 0 1px 0 rgb(255, 255, 255);
 }
 
+.close:hover {
+  color: #fff;
+  opacity: 1;
+  text-shadow: none;
+  font-size: 2rem;
+}
+
+
 .modal {
   position: fixed;
   z-index: 1050;
@@ -383,9 +403,16 @@ button.btn::-moz-focus-inner, input[type='submit'].btn::-moz-focus-inner {
     background-color: #000;
   }
   .to-delete {
-    outline: 1px solid red;
+    outline: 2px red dashed;
   }
 }
+.cover-row.to-delete {
+  border-color: transparent !important;
+}
+
+.ui-sortable-helper {
+  opacity: .9;
+}
 
 .add-column {
   position: absolute;
@@ -481,10 +508,51 @@ button.btn::-moz-focus-inner, input[type='submit'].btn::-moz-focus-inner {
 }
 
 #sidebar {
-  float: left;
+  display: flex;
+  justify-content: center;
   clear: both;
   width: 100%;
   margin-bottom: 20px;
+    ul {
+      display: flex;
+      padding-left: 0;
+    }
+
+  li {
+    
+      display: flex !important;
+      margin: 2px;
+      display: inline-block;
+      text-align: center;
+      
+      a {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: center;
+        padding: 0;
+        border-radius: 5px;
+        text-decoration: none;
+        color: #000;
+        border: 1px solid #cecece;
+        box-shadow: 0 3px 6px 0 #00000029;
+        border-radius: 4px;
+        width: 60px;
+        height: 60px;
+        line-height: 1rem;
+        background-color: #fff;
+      }
+      img {
+        height: 32px;
+        width: 32px;
+        margin: 3px;
+      }
+  } 
+  .layout-actions {
+    a {
+      color: #fff;
+      background-color: #000;
+    }
+  }
 }
 
 /* tiles config */
@@ -555,6 +623,9 @@ button.btn::-moz-focus-inner, input[type='submit'].btn::-moz-focus-inner {
     cursor: move;
   }
 }
+#class-chooser {
+  min-height: 200px !important;
+}
 
 body {
   .cssclasswidget {
@@ -576,13 +647,19 @@ body {
     display: flex;
     overflow-y: scroll;
     flex-direction: column;
-    max-height: 4rem;
+    max-height: 16rem;
+    width: 200px;
     padding: .4rem;
     list-style-type: none !important;
     border: 1px solid #A9A9A9;
     background: #FFF;
     li {
       margin-bottom: .2rem;
+      display: flex;
+      input {
+        height: 19px;
+        margin-right: 5px;
+      }
     }
     input {
       vertical-align: bottom;
diff --git a/webpack/package.json b/webpack/package.json
index 9db8d0945..f79f7c172 100644
--- a/webpack/package.json
+++ b/webpack/package.json
@@ -1,6 +1,6 @@
 {
   "name": "cover",
-  "version": "2.1.2",
+  "version": "3.0.0",
   "main": "app/cover.js",
   "scripts": {
     "build": "./node_modules/.bin/webpack -p",
@@ -25,6 +25,7 @@
   "repository": {},
   "license": "GPL-2.0",
   "dependencies": {
-    "sc-recipe-staticresources": "simplesconsultoria/sc.recipe.staticresources#1.1b5"
+    "sc-recipe-staticresources": "simplesconsultoria/sc.recipe.staticresources#1.1b5",
+    "css-loader": "^1.0.0"
   }
 }
diff --git a/webpack/webpack.config.js b/webpack/webpack.config.js
index e28b577dd..7a2d1c1e6 100644
--- a/webpack/webpack.config.js
+++ b/webpack/webpack.config.js
@@ -30,7 +30,6 @@ module.exports = makeConfig(
       './app/img/tile-file.png',
       './app/img/tile-generic.png',
       './app/img/tile-list.png',
-      './app/img/tile-pfg.png',
       './app/img/tile-richtext.png',
     );
     config.plugins.push(


Test patch:
diff --git a/src/collective/cover/testing.py b/src/collective/cover/testing.py
index 2ff99931c..9dae5c741 100644
--- a/src/collective/cover/testing.py
+++ b/src/collective/cover/testing.py
@@ -1,49 +1,20 @@
 # -*- coding: utf-8 -*-
-"""Setup test fixtures.
-
-We have to set different test fixtures depending on Plone versions and
-features we want to test:
-
-plone.app.contenttypes:
-    installed under Plone 4.3, if requested; installed under Plone 5
-
-Products.PloneFormGen
-    installed under Plone 4 only (now deprecated)
-"""
-from collective.cover.config import IS_PLONE_5
 from collective.cover.tests.utils import create_standard_content_for_tests
 from collective.cover.tests.utils import set_file_field
 from collective.cover.tests.utils import set_image_field
+from PIL import Image
 from plone.app.robotframework.testing import AUTOLOGIN_LIBRARY_FIXTURE
 from plone.app.testing import FunctionalTesting
 from plone.app.testing import IntegrationTesting
 from plone.app.testing import PLONE_FIXTURE
 from plone.app.testing import PloneSandboxLayer
-from plone.testing import z2
+from plone.testing.zope import WSGI_SERVER
 
 import os
-import pkg_resources
 import random
+import six
 
 
-try:
-    pkg_resources.get_distribution("plone.app.contenttypes")
-except pkg_resources.DistributionNotFound:
-    DEXTERITY_ONLY = False
-else:
-    # this environment variable is set in .travis.yml test matrix
-    DEXTERITY_ONLY = os.environ.get("DEXTERITY_ONLY") is not None
-
-
-# XXX: PFG tile is deprecated and will be removed in collective.cover 3
-try:
-    pkg_resources.get_distribution("Products.PloneFormGen")
-except pkg_resources.DistributionNotFound:
-    HAS_PFG = False
-else:
-    # XXX: even if product is present, PFG tile must not be available
-    HAS_PFG = True if not IS_PLONE_5 else False
-
 ALL_CONTENT_TYPES = [
     "Collection",
     "Document",
@@ -54,23 +25,23 @@
 ]
 
 zptlogo = (
-    "GIF89a\x10\x00\x10\x00\xd5\x00\x00\xff\xff\xff\xff\xff\xfe\xfc\xfd\xfd"
-    "\xfa\xfb\xfc\xf7\xf9\xfa\xf5\xf8\xf9\xf3\xf6\xf8\xf2\xf5\xf7\xf0\xf4\xf6"
-    "\xeb\xf1\xf3\xe5\xed\xef\xde\xe8\xeb\xdc\xe6\xea\xd9\xe4\xe8\xd7\xe2\xe6"
-    "\xd2\xdf\xe3\xd0\xdd\xe3\xcd\xdc\xe1\xcb\xda\xdf\xc9\xd9\xdf\xc8\xd8\xdd"
-    "\xc6\xd7\xdc\xc4\xd6\xdc\xc3\xd4\xda\xc2\xd3\xd9\xc1\xd3\xd9\xc0\xd2\xd9"
-    "\xbd\xd1\xd8\xbd\xd0\xd7\xbc\xcf\xd7\xbb\xcf\xd6\xbb\xce\xd5\xb9\xcd\xd4"
-    "\xb6\xcc\xd4\xb6\xcb\xd3\xb5\xcb\xd2\xb4\xca\xd1\xb2\xc8\xd0\xb1\xc7\xd0"
-    "\xb0\xc7\xcf\xaf\xc6\xce\xae\xc4\xce\xad\xc4\xcd\xab\xc3\xcc\xa9\xc2\xcb"
-    "\xa8\xc1\xca\xa6\xc0\xc9\xa4\xbe\xc8\xa2\xbd\xc7\xa0\xbb\xc5\x9e\xba\xc4"
-    "\x9b\xbf\xcc\x98\xb6\xc1\x8d\xae\xbaFgs\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00,\x00\x00\x00\x00\x10\x00\x10\x00\x00\x06z@\x80pH,\x12k\xc8$\xd2f\x04"
-    "\xd4\x84\x01\x01\xe1\xf0d\x16\x9f\x80A\x01\x91\xc0ZmL\xb0\xcd\x00V\xd4"
-    "\xc4a\x87z\xed\xb0-\x1a\xb3\xb8\x95\xbdf8\x1e\x11\xca,MoC$\x15\x18{"
-    "\x006}m\x13\x16\x1a\x1f\x83\x85}6\x17\x1b $\x83\x00\x86\x19\x1d!%)\x8c"
-    "\x866#'+.\x8ca`\x1c`(,/1\x94B5\x19\x1e\"&*-024\xacNq\xba\xbb\xb8h\xbeb"
-    "\x00A\x00;"
+    b"GIF89a\x10\x00\x10\x00\xd5\x00\x00\xff\xff\xff\xff\xff\xfe\xfc\xfd\xfd"
+    b"\xfa\xfb\xfc\xf7\xf9\xfa\xf5\xf8\xf9\xf3\xf6\xf8\xf2\xf5\xf7\xf0\xf4\xf6"
+    b"\xeb\xf1\xf3\xe5\xed\xef\xde\xe8\xeb\xdc\xe6\xea\xd9\xe4\xe8\xd7\xe2\xe6"
+    b"\xd2\xdf\xe3\xd0\xdd\xe3\xcd\xdc\xe1\xcb\xda\xdf\xc9\xd9\xdf\xc8\xd8\xdd"
+    b"\xc6\xd7\xdc\xc4\xd6\xdc\xc3\xd4\xda\xc2\xd3\xd9\xc1\xd3\xd9\xc0\xd2\xd9"
+    b"\xbd\xd1\xd8\xbd\xd0\xd7\xbc\xcf\xd7\xbb\xcf\xd6\xbb\xce\xd5\xb9\xcd\xd4"
+    b"\xb6\xcc\xd4\xb6\xcb\xd3\xb5\xcb\xd2\xb4\xca\xd1\xb2\xc8\xd0\xb1\xc7\xd0"
+    b"\xb0\xc7\xcf\xaf\xc6\xce\xae\xc4\xce\xad\xc4\xcd\xab\xc3\xcc\xa9\xc2\xcb"
+    b"\xa8\xc1\xca\xa6\xc0\xc9\xa4\xbe\xc8\xa2\xbd\xc7\xa0\xbb\xc5\x9e\xba\xc4"
+    b"\x9b\xbf\xcc\x98\xb6\xc1\x8d\xae\xbaFgs\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    b"\x00,\x00\x00\x00\x00\x10\x00\x10\x00\x00\x06z@\x80pH,\x12k\xc8$\xd2f\x04"
+    b"\xd4\x84\x01\x01\xe1\xf0d\x16\x9f\x80A\x01\x91\xc0ZmL\xb0\xcd\x00V\xd4"
+    b"\xc4a\x87z\xed\xb0-\x1a\xb3\xb8\x95\xbdf8\x1e\x11\xca,MoC$\x15\x18{"
+    b"\x006}m\x13\x16\x1a\x1f\x83\x85}6\x17\x1b $\x83\x00\x86\x19\x1d!%)\x8c"
+    b"\x866#'+.\x8ca`\x1c`(,/1\x94B5\x19\x1e\"&*-024\xacNq\xba\xbb\xb8h\xbeb"
+    b"\x00A\x00;"
 )
 
 
@@ -84,8 +55,6 @@ def load_file(name):
 
 
 def generate_jpeg(width, height):
-    from PIL import Image
-    from StringIO import StringIO
 
     # Mandelbrot fractal
     # FB - 201003254
@@ -113,47 +82,19 @@ def generate_jpeg(width, height):
             b = i % 16 * 16
             image.putpixel((x, y), b * 65536 + g * 256 + r)
 
-    output = StringIO()
+    output = six.BytesIO()
     image.save(output, format="PNG")
     return output
 
 
-# FIXME: workaround for https://github.com/plone/plone.app.testing/issues/39
-#        Products.TinyMCE is used only in Plone 4
-if not IS_PLONE_5:
-    autoform = ("plone.autoform", {"loadZCML": True})
-    tinymce = ("Products.TinyMCE", {"loadZCML": True})
-    products = list(PLONE_FIXTURE.products)
-    products.insert(products.index(tinymce), autoform)
-    PLONE_FIXTURE.products = tuple(products)
-
-
 class Fixture(PloneSandboxLayer):
 
     defaultBases = (PLONE_FIXTURE,)
 
     def setUpZope(self, app, configurationContext):
-        if IS_PLONE_5:
-            import plone.app.contenttypes
-
-            self.loadZCML(package=plone.app.contenttypes)
-        else:
-            # needed by plone.app.linkintegrity under Plone 4.x
-            import plone.app.referenceablebehavior
-
-            self.loadZCML(package=plone.app.referenceablebehavior)
-
-            if DEXTERITY_ONLY:
-                import plone.app.contenttypes
+        import plone.app.contenttypes
 
-                self.loadZCML(package=plone.app.contenttypes)
-                z2.installProduct(app, "Products.DateRecurringIndex")
-
-        if HAS_PFG:
-            import Products.PloneFormGen
-
-            self.loadZCML(package=Products.PloneFormGen)
-            z2.installProduct(app, "Products.PloneFormGen")
+        self.loadZCML(package=plone.app.contenttypes)
 
         import collective.cover
 
@@ -169,14 +110,7 @@ def setUpZope(self, app, configurationContext):
             manage_addVirtualHostMonster(app, "virtual_hosting")
 
     def setUpPloneSite(self, portal):
-        if IS_PLONE_5:
-            self.applyProfile(portal, "plone.app.contenttypes:default")
-        else:
-            if DEXTERITY_ONLY:
-                self.applyProfile(portal, "plone.app.contenttypes:default")
-
-        if HAS_PFG:
-            self.applyProfile(portal, "Products.PloneFormGen:default")
+        self.applyProfile(portal, "plone.app.contenttypes:default")
 
         self.applyProfile(portal, "collective.cover:default")
         self.applyProfile(portal, "collective.cover:testfixture")
@@ -203,11 +137,11 @@ def setUpPloneSite(self, portal):
 )
 
 FUNCTIONAL_TESTING = FunctionalTesting(
-    bases=(FIXTURE, z2.ZSERVER_FIXTURE),
+    bases=(FIXTURE, WSGI_SERVER),
     name="collective.cover:Functional",
 )
 
 ROBOT_TESTING = FunctionalTesting(
-    bases=(FIXTURE, AUTOLOGIN_LIBRARY_FIXTURE, z2.ZSERVER_FIXTURE),
+    bases=(FIXTURE, AUTOLOGIN_LIBRARY_FIXTURE, WSGI_SERVER),
     name="collective.cover:Robot",
 )
diff --git a/src/collective/cover/tests/LinkIntegrityUtils.py b/src/collective/cover/tests/LinkIntegrityUtils.py
index b8bd530ba..b6066dbb2 100644
--- a/src/collective/cover/tests/LinkIntegrityUtils.py
+++ b/src/collective/cover/tests/LinkIntegrityUtils.py
@@ -1,33 +1,6 @@
 # -*- coding: utf-8 -*-
 """Helper function to test link integrity feature."""
-from collective.cover.config import IS_PLONE_5
 from plone import api
-from plone.app.referenceablebehavior.referenceable import IReferenceable
-from plone.dexterity.interfaces import IDexterityFTI
-from zope.component import queryUtility
-
-import transaction
-
-
-NEEDS_REFERENCEABLE_BEHAVIOR = not IS_PLONE_5
-
-
-def setup_link_integrity():
-    if NEEDS_REFERENCEABLE_BEHAVIOR:
-        fti = queryUtility(IDexterityFTI, name="collective.cover.content")
-        behaviors = list(fti.behaviors)
-        behaviors.append(IReferenceable.__identifier__)
-        fti.behaviors = tuple(behaviors)
-        transaction.commit()
-
-
-def teardown_link_integrity():
-    if NEEDS_REFERENCEABLE_BEHAVIOR:
-        fti = queryUtility(IDexterityFTI, name="collective.cover.content")
-        behaviors = list(fti.behaviors)
-        behaviors.remove(IReferenceable.__identifier__)
-        fti.behaviors = tuple(behaviors)
-        transaction.commit()
 
 
 def get_internal_link_html_code():
diff --git a/src/collective/cover/tests/test_banner_tile.py b/src/collective/cover/tests/test_banner_tile.py
index ba0e4a548..032b5eea1 100644
--- a/src/collective/cover/tests/test_banner_tile.py
+++ b/src/collective/cover/tests/test_banner_tile.py
@@ -6,6 +6,7 @@
 from lxml import etree  # nosec
 from mock import Mock
 from plone.tiles.interfaces import ITileDataManager
+from Products.CMFPlone.utils import safe_text
 
 import six
 import unittest
@@ -62,7 +63,7 @@ def test_populate_tile_with_image_object_string(self):
         obj.setTitle(title)
         obj.reindexObject()
         self.tile.populate_with_object(obj)
-        self.assertEqual(six.text_type(title, "utf-8"), self.tile.data.get("title"))
+        self.assertEqual(safe_text(title), self.tile.data.get("title"))
         self.assertTrue(self.tile.has_image)
         self.assertIsNotNone(self.tile.getRemoteUrl())
 
@@ -89,7 +90,7 @@ def test_populate_tile_with_link_object_string(self):
         obj.setTitle(title)
         obj.reindexObject()
         self.tile.populate_with_object(obj)
-        self.assertEqual(six.text_type(title, "utf-8"), self.tile.data.get("title"))
+        self.assertEqual(safe_text(title), self.tile.data.get("title"))
         self.assertFalse(self.tile.has_image)
         self.assertEqual(self.tile.getRemoteUrl(), "http://plone.org")
 
diff --git a/src/collective/cover/tests/test_base_tile.py b/src/collective/cover/tests/test_base_tile.py
index cbbdb6df7..204a5c0dc 100644
--- a/src/collective/cover/tests/test_base_tile.py
+++ b/src/collective/cover/tests/test_base_tile.py
@@ -5,8 +5,8 @@
 from collective.cover.tiles.base import PersistentCoverTile
 from collective.cover.tiles.configuration import ITilesConfigurationScreen
 from collective.cover.tiles.permissions import ITilesPermissions
-from io import BytesIO
 from plone.tiles.interfaces import ITileDataManager
+from six import StringIO
 from zope.component import getMultiAdapter
 from zope.configuration.xmlconfig import xmlconfig
 
@@ -42,7 +42,7 @@
 
 class BaseTileTestCase(TestTileMixin, unittest.TestCase):
     def _register_tile(self):
-        xmlconfig(BytesIO(ZCML))
+        xmlconfig(StringIO(ZCML))
 
     def setUp(self):
         super(BaseTileTestCase, self).setUp()
diff --git a/src/collective/cover/tests/test_basic_tile.py b/src/collective/cover/tests/test_basic_tile.py
index 58b8eaa04..0f81ffab4 100644
--- a/src/collective/cover/tests/test_basic_tile.py
+++ b/src/collective/cover/tests/test_basic_tile.py
@@ -1,19 +1,20 @@
 # -*- coding: utf-8 -*-
 from collective.cover.testing import ALL_CONTENT_TYPES
 from collective.cover.tests.base import TestTileMixin
-from collective.cover.tests.utils import today
 from collective.cover.tiles.basic import BasicTile
 from collective.cover.tiles.basic import IBasicTile
 from DateTime import DateTime
-from lxml import etree  # nosec
+from lxml import etree
 from mock import Mock
 from plone import api
+from plone.app.event.base import localized_today
 from plone.app.testing import logout
 from plone.cachepurging.hooks import queuePurge
 from plone.cachepurging.interfaces import ICachePurgingSettings
 from plone.namedfile.file import NamedBlobImage
 from plone.registry.interfaces import IRegistry
 from plone.tiles.interfaces import ITileDataManager
+from Products.CMFPlone.utils import safe_text
 from zope.annotation.interfaces import IAnnotations
 from zope.annotation.interfaces import IAttributeAnnotatable
 from zope.component import provideUtility
@@ -128,10 +129,8 @@ def test_populate_with_object_string(self):
         obj.setDescription(description)
         obj.reindexObject()
         self.tile.populate_with_object(obj)
-        self.assertEqual(six.text_type(title, "utf-8"), self.tile.data.get("title"))
-        self.assertEqual(
-            six.text_type(description, "utf-8"), self.tile.data.get("description")
-        )
+        self.assertEqual(safe_text(title), self.tile.data.get("title"))
+        self.assertEqual(safe_text(description), self.tile.data.get("description"))
 
     def test_render_empty(self):
         msg = "Please drag&amp;drop some content here to populate the tile."
@@ -281,7 +280,7 @@ def test_show_start_date_on_events(self):
         event = self.portal["my-event"]
         self.tile.populate_with_object(event)
         rendered = self.tile()
-        start_date = api.portal.get_localized_time(today, long_format=True)
+        start_date = api.portal.get_localized_time(localized_today(), long_format=True)
         self.assertIn(start_date, rendered)
 
     def test_localized_time_is_rendered(self):
@@ -289,7 +288,7 @@ def test_localized_time_is_rendered(self):
         self.tile.populate_with_object(event)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=True, time_only=False
+            localized_today(), long_format=True, time_only=False
         )
         self.assertIn(expected, rendered)  # u'Jul 15, 2015 01:23 PM'
 
@@ -298,7 +297,7 @@ def test_localized_time_is_rendered(self):
         self.tile.set_tile_configuration(tile_conf)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=False, time_only=False
+            localized_today(), long_format=False, time_only=False
         )
         self.assertIn(expected, rendered)  # u'Jul 15, 2015
 
@@ -307,7 +306,7 @@ def test_localized_time_is_rendered(self):
         self.tile.set_tile_configuration(tile_conf)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=False, time_only=True
+            localized_today(), long_format=False, time_only=True
         )
         self.assertIn(expected, rendered)  # u'01:23 PM'
 
diff --git a/src/collective/cover/tests/test_calendar_tile.py b/src/collective/cover/tests/test_calendar_tile.py
index ec46fdaac..590246ab4 100644
--- a/src/collective/cover/tests/test_calendar_tile.py
+++ b/src/collective/cover/tests/test_calendar_tile.py
@@ -7,6 +7,7 @@
 
 
 class CalendarTileTestCase(TestTileMixin, unittest.TestCase):
+
     def setUp(self):
         super(CalendarTileTestCase, self).setUp()
         self.tile = CalendarTile(self.cover, self.request)
@@ -26,14 +27,3 @@ def test_default_configuration(self):
 
     def test_accepted_content_types(self):
         self.assertEqual(self.tile.accepted_ct(), [])
-
-
-# load tests only in Plone < 5
-def test_suite():
-    # FIXME: https://github.com/collective/collective.cover/issues/633
-    from collective.cover.config import IS_PLONE_5
-
-    if IS_PLONE_5:
-        return unittest.TestSuite()
-
-    return unittest.defaultTestLoader.loadTestsFromName(__name__)
diff --git a/src/collective/cover/tests/test_carousel_tile.py b/src/collective/cover/tests/test_carousel_tile.py
index 79e549258..788ef0899 100644
--- a/src/collective/cover/tests/test_carousel_tile.py
+++ b/src/collective/cover/tests/test_carousel_tile.py
@@ -106,7 +106,7 @@ def test_internal_structure(self):
 
         self.assertTrue(isinstance(uuids, dict))
         self.assertTrue(len(uuids) == 1)
-        self.assertTrue(uuids[obj1.UID()]["order"] == u"0")
+        self.assertTrue(uuids[obj1.UID()]["order"] == 0)
 
         # now we add a second image
         obj2 = self.portal["my-image1"]
@@ -117,8 +117,8 @@ def test_internal_structure(self):
 
         self.assertTrue(isinstance(uuids, dict))
         self.assertTrue(len(uuids) == 2)
-        self.assertTrue(uuids[obj1.UID()]["order"] == u"0")
-        self.assertTrue(uuids[obj2.UID()]["order"] == u"1")
+        self.assertTrue(uuids[obj1.UID()]["order"] == 0)
+        self.assertTrue(uuids[obj2.UID()]["order"] == 1)
 
     def test_custom_title(self):
         # we start with an empty tile
@@ -202,9 +202,9 @@ def test_data_converter(self):
         conv = UUIDSFieldDataConverter(field, widget)
 
         value = {
-            u"uuid1": {u"order": u"0"},
-            u"uuid2": {u"order": u"2"},
-            u"uuid3": {u"order": u"1"},
+            u"uuid1": {u"order": 0},
+            u"uuid2": {u"order": 2},
+            u"uuid3": {u"order": 1},
         }
 
         to_widget = conv.toWidgetValue(value)
diff --git a/src/collective/cover/tests/test_collection_tile.py b/src/collective/cover/tests/test_collection_tile.py
index 3c2f601eb..91d280feb 100644
--- a/src/collective/cover/tests/test_collection_tile.py
+++ b/src/collective/cover/tests/test_collection_tile.py
@@ -1,11 +1,12 @@
 # -*- coding: utf-8 -*-
 from collective.cover.tests.base import TestTileMixin
-from collective.cover.tests.utils import today
 from collective.cover.tiles.collection import CollectionTile
 from collective.cover.tiles.collection import ICollectionTile
 from mock import Mock
 from plone import api
+from plone.app.event.base import localized_today
 from plone.uuid.interfaces import IUUID
+from Products.CMFPlone.utils import safe_text
 
 import six
 import unittest
@@ -27,6 +28,14 @@
     )
 ]
 
+COLLECTION_FILTER = [
+    dict(
+        i="portal_type",
+        o="plone.app.querystring.operation.selection.is",
+        v=["Image"],
+    )
+]
+
 
 class CollectionTileTestCase(TestTileMixin, unittest.TestCase):
     def setUp(self):
@@ -34,6 +43,8 @@ def setUp(self):
         self.tile = CollectionTile(self.cover, self.request)
         self.tile.__name__ = u"collective.cover.collection"
         self.tile.id = u"test"
+        self.collection = self.portal["mandelbrot-set"]
+        self.collection.setQuery(COLLECTION_FILTER)
 
     @unittest.expectedFailure  # FIXME: raises BrokenImplementation
     def test_interface(self):
@@ -57,7 +68,7 @@ def test_populate_tile_with_object_text(self):
         fields to avoid UnicodeDecodeError.
         """
         title = u"El veloz murciélago hindú comía feliz cardillo y kiwi"
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         obj.setTitle(title)
         obj.reindexObject()
         self.tile.populate_with_object(obj)
@@ -72,11 +83,11 @@ def test_populate_tile_with_object_string(self):
         using strings instead of unicode objects.
         """
         title = "The quick brown fox jumps over the lazy dog"
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         obj.setTitle(title)
         obj.reindexObject()
         self.tile.populate_with_object(obj)
-        self.assertEqual(six.text_type(title, "utf-8"), self.tile.data.get("header"))
+        self.assertEqual(safe_text(title), self.tile.data.get("header"))
         self.assertTrue(self.tile.data.get("footer"))
         self.assertEqual(self.tile.data.get("uuid"), IUUID(obj))
 
@@ -86,14 +97,14 @@ def test_populate_tile_with_invalid_object(self):
         self.assertTrue(self.tile.is_empty())
 
     def test_render_empty_collection(self):
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         obj.setQuery(EMPTY)
         self.tile.populate_with_object(obj)
         rendered = self.tile()
         self.assertIn("The collection doesn't have any results.", rendered)
 
     def test_render_deleted_collection(self):
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         self.tile.populate_with_object(obj)
 
         with api.env.adopt_roles(["Manager"]):
@@ -137,7 +148,7 @@ def test_thumbnail_original_image(self):
         self.assertIsInstance(self.tile(), six.text_type)
 
     def test_number_of_items(self):
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         self.tile.populate_with_object(obj)
 
         # Collection has three images and shows them all.
@@ -154,7 +165,7 @@ def test_number_of_items(self):
         self.assertEqual(items[1].getId(), "my-image1")
 
     def test_offset(self):
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         self.tile.populate_with_object(obj)
 
         tile_conf = self.tile.get_tile_configuration()
@@ -176,7 +187,7 @@ def test_offset(self):
         self.assertEqual(items[0].getId(), "my-image1")
 
     def test_random_items(self):
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         self.tile.populate_with_object(obj)
 
         # we need to compare lists of objects
@@ -207,18 +218,17 @@ def test_show_start_date_on_events(self):
         obj = self._create_events_collection()
         self.tile.populate_with_object(obj)
         rendered = self.tile()
-        start_date = api.portal.get_localized_time(today, long_format=True)
+        start_date = api.portal.get_localized_time(localized_today(), long_format=True)
         self.assertIn(start_date, rendered)
 
     def test_date_on_items(self):
-        collection = self.portal["mandelbrot-set"]
-        self.tile.populate_with_object(collection)
+        self.tile.populate_with_object(self.collection)
 
         tile_config = self.tile.get_tile_configuration()
         self.assertEqual(tile_config["date"]["visibility"], u"on")
 
         # Get the first news item from the collection
-        content_listing_obj = collection.results()[0]
+        content_listing_obj = self.collection.results()[0]
 
         date = self.tile.Date(content_listing_obj)
         self.assertFalse(callable(date), "Date should not be calleable")
@@ -226,6 +236,7 @@ def test_date_on_items(self):
         fmt_date = self.portal.toLocalizedTime(date, True)
 
         rendered = self.tile()
+
         self.assertIn(fmt_date, rendered)
 
     def test_localized_time_is_rendered(self):
@@ -233,7 +244,7 @@ def test_localized_time_is_rendered(self):
         self.tile.populate_with_object(obj)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=True, time_only=False
+            localized_today(), long_format=True, time_only=False
         )
         self.assertIn(expected, rendered)  # u'Jul 15, 2015 01:23 PM'
 
@@ -242,7 +253,7 @@ def test_localized_time_is_rendered(self):
         self.tile.set_tile_configuration(tile_conf)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=False, time_only=False
+            localized_today(), long_format=False, time_only=False
         )
         self.assertIn(expected, rendered)  # u'Jul 15, 2015
 
@@ -251,12 +262,12 @@ def test_localized_time_is_rendered(self):
         self.tile.set_tile_configuration(tile_conf)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=False, time_only=True
+            localized_today(), long_format=False, time_only=True
         )
         self.assertIn(expected, rendered)  # u'01:23 PM'
 
     def test_get_alt(self):
-        obj = self.portal["mandelbrot-set"]
+        obj = self.collection
         self.tile.populate_with_object(obj)
         rendered = self.tile()
         # the image is there and the alt attribute is set
diff --git a/src/collective/cover/tests/test_contentchooser.py b/src/collective/cover/tests/test_contentchooser.py
index 1166272e4..3b6b7d525 100644
--- a/src/collective/cover/tests/test_contentchooser.py
+++ b/src/collective/cover/tests/test_contentchooser.py
@@ -3,8 +3,11 @@
 from io import StringIO
 from lxml import etree  # nosec
 from plone import api
+from zope.component import queryUtility
+from zope.schema.interfaces import IVocabularyFactory
 
 import json
+import six
 import unittest
 
 
@@ -26,7 +29,10 @@ def setUp(self):
 
     def test_jsonbytype(self):
         catalog = self.portal["portal_catalog"]
-        results = catalog()
+        VOCAB_ID = u"plone.app.vocabularies.ReallyUserFriendlyTypes"
+        vocab = queryUtility(IVocabularyFactory, name=VOCAB_ID)(self.portal)
+        types = [i.value for i in vocab]
+        results = catalog(portal_type=types)
         portal_objects_ids = [i.id for i in results]
 
         view = self.portal.unrestrictedTraverse("@@jsonbytype")
@@ -36,7 +42,10 @@ def test_jsonbytype(self):
         self.assertIn("items", json_response)
         json_objects_ids = [i["id"] for i in json_response["items"]]
 
-        self.assertItemsEqual(json_objects_ids, portal_objects_ids)
+        if six.PY2:
+            self.assertItemsEqual(json_objects_ids, portal_objects_ids)
+        else:
+            self.assertCountEqual(json_objects_ids, portal_objects_ids)
 
         paths = json_response["path"]
         self.assertEqual(len(paths), 1)
@@ -98,3 +107,15 @@ def test_update(self):
         self.request.set("b_size", 1)
         view()
         self.assertEqual(view.nextpage, 2)
+
+    def test_content_with_unicode_description(self):
+        with api.env.adopt_roles(["Manager"]):
+            api.content.create(
+                container=self.portal,
+                type="News Item",
+                id="noticia",
+                title=u"Noticia",
+                description=u"Isto é uma notícias",
+            )
+        view = api.content.get_view(u"content-search", self.cover, self.request)
+        self.assertIn(u"Isto é uma notícias", view())
diff --git a/src/collective/cover/tests/test_controlpanel.py b/src/collective/cover/tests/test_controlpanel.py
index 33bd2fdf3..d05713c48 100644
--- a/src/collective/cover/tests/test_controlpanel.py
+++ b/src/collective/cover/tests/test_controlpanel.py
@@ -8,7 +8,10 @@
 from plone import api
 from plone.app.testing import logout
 from plone.registry.interfaces import IRegistry
+from Products.CMFCore.interfaces import ISiteRoot
+from Products.CMFPlone.utils import get_installer
 from zope.component import getUtility
+from zope.interface import alsoProvides
 
 import unittest
 
@@ -19,12 +22,14 @@ class ControlPanelTestCase(unittest.TestCase):
 
     def setUp(self):
         self.portal = self.layer["portal"]
+        self.request = self.layer["request"]
         self.controlpanel = self.portal["portal_controlpanel"]
+        self.installer = get_installer(self.portal, self.request)
 
     def test_controlpanel_has_view(self):
         request = self.layer["request"]
         view = api.content.get_view(u"cover-settings", self.portal, request)
-        view = view.__of__(self.portal)
+        alsoProvides(view, ISiteRoot)
         self.assertTrue(view())
 
     def test_controlpanel_view_is_protected(self):
@@ -39,9 +44,9 @@ def test_controlpanel_installed(self):
         self.assertIn("cover", actions)
 
     def test_controlpanel_removed_on_uninstall(self):
-        qi = self.portal["portal_quickinstaller"]
+
         with api.env.adopt_roles(["Manager"]):
-            qi.uninstallProducts(products=[PROJECTNAME])
+            self.installer.uninstall_product(PROJECTNAME)
         actions = [a.id for a in self.controlpanel.listActions()]
         self.assertNotIn("cover", actions)
 
@@ -60,8 +65,10 @@ class RegistryTestCase(unittest.TestCase):
 
     def setUp(self):
         self.portal = self.layer["portal"]
+        self.request = self.layer["request"]
         self.registry = getUtility(IRegistry)
         self.settings = self.registry.forInterface(ICoverSettings)
+        self.installer = get_installer(self.portal, self.request)
 
     def test_sections_record_in_registry(self):
         self.assertTrue(hasattr(self.settings, "layouts"))
@@ -96,9 +103,8 @@ def test_grid_system_record_in_registry(self):
         self.assertEqual(self.settings.grid_system, DEFAULT_GRID_SYSTEM)
 
     def test_records_removed_on_uninstall(self):
-        qi = self.portal["portal_quickinstaller"]
         with api.env.adopt_roles(["Manager"]):
-            qi.uninstallProducts(products=[PROJECTNAME])
+            self.installer.uninstall_product(PROJECTNAME)
 
         BASE_REGISTRY = "collective.cover.controlpanel.ICoverSettings."
         records = [
@@ -123,6 +129,7 @@ def test_records_not_overwriten_on_reinstall(self):
         self.assertEqual(len(self.settings.layouts), 3)
 
         qi = self.portal["portal_quickinstaller"]
+        qi.installProducts(products=[PROJECTNAME])
         with api.env.adopt_roles(["Manager"]):
             qi.reinstallProducts(products=[PROJECTNAME])
 
diff --git a/src/collective/cover/tests/test_cover.py b/src/collective/cover/tests/test_cover.py
index 3402c4f59..806d626d7 100644
--- a/src/collective/cover/tests/test_cover.py
+++ b/src/collective/cover/tests/test_cover.py
@@ -3,7 +3,6 @@
 from collective.cover.config import DEFAULT_GRID_SYSTEM
 from collective.cover.controlpanel import ICoverSettings
 from collective.cover.interfaces import ICover
-from collective.cover.setuphandlers import HAS_RELATIONFIELD
 from collective.cover.testing import INTEGRATION_TESTING
 from plone import api
 from plone.app.dexterity.behaviors.exclfromnav import IExcludeFromNavigation
@@ -56,7 +55,6 @@ def test_exclude_from_navigation_behavior(self):
     def test_locking_behavior(self):
         self.assertTrue(ILocking.providedBy(self.cover))
 
-    @unittest.skipUnless(HAS_RELATIONFIELD, "Needs plone.app.relationfield")
     def test_relateditems_behavior(self):
         from plone.app.relationfield.behavior import IRelatedItems
 
@@ -89,27 +87,9 @@ def test_layoutmanager_settings(self):
         setRoles(self.portal, TEST_USER_ID, ["Manager"])
         layout_edit = self.cover.restrictedTraverse("layoutedit")
         settings = json.loads(layout_edit.layoutmanager_settings())
-        if DEFAULT_GRID_SYSTEM == "deco16_grid":
-            self.assertEqual(settings, {"ncolumns": 16})
-        elif DEFAULT_GRID_SYSTEM == "bootstrap3":
+        if DEFAULT_GRID_SYSTEM == "bootstrap3":
             self.assertEqual(settings, {"ncolumns": 12})
 
-        # Choose different grid.
-        registry = getUtility(IRegistry)
-        cover_settings = registry.forInterface(ICoverSettings)
-        if DEFAULT_GRID_SYSTEM == "deco16_grid":
-            cover_settings.grid_system = "bootstrap3"
-
-            # The number of columns should be different now.
-            settings = json.loads(layout_edit.layoutmanager_settings())
-            self.assertEqual(settings, {"ncolumns": 12})
-        elif DEFAULT_GRID_SYSTEM == "bootstrap3":
-            cover_settings.grid_system = "deco16_grid"
-
-            # The number of columns should be different now.
-            settings = json.loads(layout_edit.layoutmanager_settings())
-            self.assertEqual(settings, {"ncolumns": 16})
-
         # Choose different grid.
         registry = getUtility(IRegistry)
         cover_settings = registry.forInterface(ICoverSettings)
diff --git a/src/collective/cover/tests/test_file_tile.py b/src/collective/cover/tests/test_file_tile.py
index 04cfad9f0..f992ccda3 100644
--- a/src/collective/cover/tests/test_file_tile.py
+++ b/src/collective/cover/tests/test_file_tile.py
@@ -5,6 +5,7 @@
 from collective.cover.tiles.file import IFileTile
 from plone import api
 from plone.uuid.interfaces import IUUID
+from Products.CMFPlone.utils import safe_text
 
 import six
 import unittest
@@ -62,10 +63,8 @@ def test_populate_tile_with_object_string(self):
         obj.setDescription(description)
         obj.reindexObject()
         self.tile.populate_with_object(obj)
-        self.assertEqual(six.text_type(title, "utf-8"), self.tile.data.get("title"))
-        self.assertEqual(
-            six.text_type(description, "utf-8"), self.tile.data.get("description")
-        )
+        self.assertEqual(safe_text(title), self.tile.data.get("title"))
+        self.assertEqual(safe_text(description), self.tile.data.get("description"))
         self.assertEqual(self.tile.data.get("uuid"), IUUID(obj))
 
     def test_populate_tile_with_invalid_object(self):
diff --git a/src/collective/cover/tests/test_grid.py b/src/collective/cover/tests/test_grid.py
index 723ec908e..8274f0d94 100644
--- a/src/collective/cover/tests/test_grid.py
+++ b/src/collective/cover/tests/test_grid.py
@@ -1,6 +1,5 @@
 # -*- coding: utf-8 -*-
 from collective.cover.config import DEFAULT_GRID_SYSTEM
-from collective.cover.config import IS_PLONE_5
 from collective.cover.controlpanel import ICoverSettings
 from collective.cover.testing import INTEGRATION_TESTING
 from plone import api
@@ -43,32 +42,13 @@ def setUp(self):
         self.view = folder.cover.restrictedTraverse("view")
 
     def test_default_grid(self):
-        if IS_PLONE_5:
-            self.assertEqual(DEFAULT_GRID_SYSTEM, "bootstrap3")
-        else:
-            self.assertEqual(DEFAULT_GRID_SYSTEM, "deco16_grid")
+        self.assertEqual(DEFAULT_GRID_SYSTEM, "bootstrap3")
 
     def _switch_grid_system(self, grid):
         registry = getUtility(IRegistry)
         settings = registry.forInterface(ICoverSettings)
         settings.grid_system = grid
 
-    def test_deco16_grid(self):
-        self._switch_grid_system("deco16_grid")
-
-        document = lxml.html.fromstring(self.view())
-        rows = document.cssselect("#content div.row")
-        cells0 = rows[0].cssselect("div.cell")
-        cells1 = rows[1].cssselect("div.cell")
-        cells2 = rows[2].cssselect("div.cell")
-
-        self.assertTrue(_has_classes(cells0[0], ("width-12", "position-0")))
-        self.assertTrue(_has_classes(cells1[0], ("width-6", "position-0")))
-        self.assertTrue(_has_classes(cells1[1], ("width-6", "position-6")))
-        self.assertTrue(_has_classes(cells2[0], ("width-4", "position-0")))
-        self.assertTrue(_has_classes(cells2[1], ("width-4", "position-4")))
-        self.assertTrue(_has_classes(cells2[2], ("width-4", "position-8")))
-
     def test_bootstrap3_grid(self):
         self._switch_grid_system("bootstrap3")
 
diff --git a/src/collective/cover/tests/test_link_integrity.robot b/src/collective/cover/tests/test_link_integrity.robot
index c40187f18..6c7fc47f9 100644
--- a/src/collective/cover/tests/test_link_integrity.robot
+++ b/src/collective/cover/tests/test_link_integrity.robot
@@ -6,8 +6,6 @@ Library  ${CURDIR}/LinkIntegrityUtils.py
 
 Suite Setup  Open Test Browser
 Suite Teardown  Close all browsers
-Test Setup  Setup Link Integrity
-Test Teardown  Teardown Link Integrity
 
 *** Variables ***
 
diff --git a/src/collective/cover/tests/test_list_tile.py b/src/collective/cover/tests/test_list_tile.py
index d18b9fead..0c425ced5 100644
--- a/src/collective/cover/tests/test_list_tile.py
+++ b/src/collective/cover/tests/test_list_tile.py
@@ -3,11 +3,11 @@
 from collective.cover.testing import zptlogo
 from collective.cover.tests.base import TestTileMixin
 from collective.cover.tests.utils import set_image_field
-from collective.cover.tests.utils import today
 from collective.cover.tiles.list import IListTile
 from collective.cover.tiles.list import ListTile
 from mock import Mock
 from plone import api
+from plone.app.event.base import localized_today
 from plone.app.testing import login
 from plone.app.testing import logout
 from plone.app.testing import TEST_USER_NAME
@@ -164,9 +164,11 @@ def test_results(self):
             self.assertIn(folder[str(i)], results)
 
         # for an anonymous user, no content is returned
+        from AccessControl import Unauthorized
+
         logout()
-        results = self.tile.results()
-        self.assertEqual(len(results), 0)
+        with self.assertRaises(Unauthorized):
+            results = self.tile.results()
 
         # for the test user, the first 5 objects should be still there
         login(self.portal, TEST_USER_NAME)
@@ -207,7 +209,7 @@ def test_show_start_date_on_events(self):
         event = self.portal["my-event"]
         self.tile.populate_with_object(event)
         rendered = self.tile()
-        start_date = api.portal.get_localized_time(today, long_format=True)
+        start_date = api.portal.get_localized_time(localized_today(), long_format=True)
         self.assertIn(start_date, rendered)
 
     def test_localized_time_is_rendered(self):
@@ -215,7 +217,7 @@ def test_localized_time_is_rendered(self):
         self.tile.populate_with_object(event)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=True, time_only=False
+            localized_today(), long_format=True, time_only=False
         )
         self.assertIn(expected, rendered)  # u'Jul 15, 2015 01:23 PM'
 
@@ -224,7 +226,7 @@ def test_localized_time_is_rendered(self):
         self.tile.set_tile_configuration(tile_conf)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=False, time_only=False
+            localized_today(), long_format=False, time_only=False
         )
         self.assertIn(expected, rendered)  # u'Jul 15, 2015
 
@@ -233,7 +235,7 @@ def test_localized_time_is_rendered(self):
         self.tile.set_tile_configuration(tile_conf)
         rendered = self.tile()
         expected = api.portal.get_localized_time(
-            today, long_format=False, time_only=True
+            localized_today(), long_format=False, time_only=True
         )
         self.assertIn(expected, rendered)  # u'01:23 PM'
 
diff --git a/src/collective/cover/tests/test_pfg_tile.py b/src/collective/cover/tests/test_pfg_tile.py
deleted file mode 100644
index c6c9ea7c2..000000000
--- a/src/collective/cover/tests/test_pfg_tile.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Test for the PloneFormGen tile.
-
-They run only if Products.PloneFormGen is installed.
-
-XXX: PFG tile is deprecated and will be removed in collective.cover 3
-"""
-from collective.cover.tests.base import TestTileMixin
-from collective.cover.tiles.pfg import IPFGTile
-from collective.cover.tiles.pfg import PFGTile
-from mock import Mock
-from plone import api
-
-import unittest
-
-
-class PFGTileTestCase(TestTileMixin, unittest.TestCase):
-    def setUp(self):
-        super(PFGTileTestCase, self).setUp()
-        self.tile = PFGTile(self.cover, self.request)
-        self.tile.__name__ = u"collective.cover.pfg"
-        self.tile.id = u"test"
-
-        with api.env.adopt_roles(["Manager"]):
-            self.pfg = api.content.create(
-                self.portal,
-                "FormFolder",
-                id="my-form",
-                title="My Form",
-                description="A form form FormGen",
-            )
-
-    @unittest.expectedFailure  # FIXME: raises BrokenImplementation
-    def test_interface(self):
-        self.interface = IPFGTile
-        self.klass = PFGTile
-        super(PFGTileTestCase, self).test_interface()
-
-    def test_default_configuration(self):
-        self.assertTrue(self.tile.is_configurable)
-        self.assertTrue(self.tile.is_editable)
-        self.assertTrue(self.tile.is_droppable)
-
-    def test_accepted_content_types(self):
-        self.assertEqual(self.tile.accepted_ct(), ["FormFolder"])
-
-    def test_empty_body(self):
-        self.assertFalse(self.tile.body())
-
-    def test_body(self):
-        obj = self.pfg
-        self.tile.populate_with_object(obj)
-        self.assertIn('<label class="formQuestion" for="replyto">', self.tile.body())
-
-    def test_render_empty(self):
-        msg = "Please drag&amp;drop a Form Folder here to populate the tile."
-
-        self.tile.is_compose_mode = Mock(return_value=True)
-        self.assertIn(msg, self.tile())
-
-        self.tile.is_compose_mode = Mock(return_value=False)
-        self.assertNotIn(msg, self.tile())
-
-    def test_render(self):
-        obj = self.pfg
-
-        self.tile.populate_with_object(obj)
-        rendered = self.tile()
-
-        self.assertIn("Your E-Mail Address", rendered)
-
-    def test_render_deleted_object(self):
-        obj = self.pfg
-
-        self.tile.populate_with_object(obj)
-        # Delete original object
-        self.portal.manage_delObjects(["my-form"])
-
-        self.tile.is_compose_mode = Mock(return_value=True)
-        self.assertIn("Please drag&amp;drop", self.tile())
-
-    def test_render_restricted_object(self):
-        obj = self.pfg
-
-        self.tile.populate_with_object(obj)
-        obj.manage_permission("View", [], 0)
-
-        self.tile.is_compose_mode = Mock(return_value=True)
-        self.assertIn("Please drag&amp;drop", self.tile())
-
-
-def test_suite():
-    """Run tests only if Products.PloneFormGen is installed."""
-    from collective.cover.testing import HAS_PFG
-
-    if HAS_PFG:
-        return unittest.defaultTestLoader.loadTestsFromName(__name__)
-    else:
-        return unittest.TestSuite()
diff --git a/src/collective/cover/tests/test_robot.py b/src/collective/cover/tests/test_robot.py
index 8d0879ce7..2ab4ed1d3 100644
--- a/src/collective/cover/tests/test_robot.py
+++ b/src/collective/cover/tests/test_robot.py
@@ -1,8 +1,6 @@
 # -*- coding: utf-8 -*-
 """Setup RobotFramework tests.
-
 We have 3 kinds of expected failures:
-
 Expected Failure
     We know they are failing and need some work to be fixed.
 Mandelbug
@@ -10,8 +8,6 @@
 Issue related
     They are failing under certain reproducible circunstances.
 """
-from collective.cover.config import IS_PLONE_5
-from collective.cover.testing import DEXTERITY_ONLY
 from collective.cover.testing import ROBOT_TESTING
 from plone.testing import layered
 
@@ -24,16 +20,10 @@
 files = os.listdir(dirname)
 tests = [f for f in files if f.startswith("test_") and f.endswith(".robot")]
 
-noncritical = ["Expected Failure", "Mandelbug"]
-
-# FIXME: under Plone 4.3 with plone.app.contenttypes installed
-#        https://github.com/collective/collective.cover/issues/615
-if not IS_PLONE_5 and DEXTERITY_ONLY:
-    noncritical.append("issue_615")
-
 # FIXME: skip RobotFramework tests in Plone 5
-if IS_PLONE_5:
-    tests = []
+tests = []
+
+noncritical = ["Expected Failure", "Mandelbug"]
 
 
 def test_suite():
diff --git a/src/collective/cover/tests/test_setup.py b/src/collective/cover/tests/test_setup.py
index ce4c2da06..8204ee45a 100644
--- a/src/collective/cover/tests/test_setup.py
+++ b/src/collective/cover/tests/test_setup.py
@@ -1,16 +1,17 @@
 # -*- coding: utf-8 -*-
-from collective.cover.config import IS_PLONE_5
 from collective.cover.config import PROJECTNAME
 from collective.cover.testing import INTEGRATION_TESTING
 from plone import api
 from plone.browserlayer.utils import registered_layers
+from plone.registry.interfaces import IRegistry
+from Products.CMFPlone.interfaces import IBundleRegistry
+from Products.CMFPlone.utils import get_installer
+from zope.component import getUtility
 
 import unittest
 
 
-JS = [
-    "++resource++collective.js.bootstrap/js/bootstrap.min.js",
-]
+JS = "++resource++collective.js.bootstrap/js/bootstrap.min.js"
 
 
 class InstallTestCase(unittest.TestCase):
@@ -18,27 +19,29 @@ class InstallTestCase(unittest.TestCase):
     layer = INTEGRATION_TESTING
 
     def setUp(self):
+        self.request = self.layer["request"]
         self.portal = self.layer["portal"]
-        self.qi = self.portal["portal_quickinstaller"]
+        self.registry = getUtility(IRegistry)
+        self.installer = get_installer(self.portal, self.request)
 
     def test_installed(self):
-        self.assertTrue(self.qi.isProductInstalled(PROJECTNAME))
+        self.assertTrue(self.installer.is_product_installed(PROJECTNAME))
 
     def test_addon_layer(self):
         layers = [layer.getName() for layer in registered_layers()]
         self.assertIn("ICoverLayer", layers)
 
-    @unittest.skipIf(IS_PLONE_5, "No easy way to test this under Plone 5")
+    def test_resources_available(self):
+        res = self.portal.restrictedTraverse(JS)
+        self.assertTrue(res)
+
     def test_jsregistry(self):
-        resource_ids = self.portal.portal_javascripts.getResourceIds()
-        for id_ in JS:
-            self.assertIn(id_, resource_ids, "{0} not installed".format(id))
+        bundles = self.registry.collectionOfInterface(
+            IBundleRegistry, prefix="plone.bundles"
+        )
+        bundle = bundles["bootstrap"]
 
-    def test_resources_available(self):
-        resources = JS
-        for id_ in resources:
-            res = self.portal.restrictedTraverse(id_)
-            self.assertTrue(res)
+        self.assertEqual(bundle.jscompilation, JS)
 
     def test_reinstall_with_changed_registry(self):
         ps = getattr(self.portal, "portal_setup")
@@ -52,45 +55,36 @@ def test_versioning_policy(self):
         policy_map = repository.getPolicyMap()["collective.cover.content"]
         self.assertEqual(policy_map, [u"version_on_revert"])
 
-    @unittest.skipIf(IS_PLONE_5, "Plone 4.3 only")
-    def test_tinymce_linkable(self):
-        tinymce = self.portal["portal_tinymce"]
-        linkable = tinymce.linkable.split("\n")
-        self.assertIn("collective.cover.content", linkable)
-
 
 class UninstallTestCase(unittest.TestCase):
 
     layer = INTEGRATION_TESTING
 
     def setUp(self):
+        self.request = self.layer["request"]
         self.portal = self.layer["portal"]
-        self.qi = self.portal["portal_quickinstaller"]
+        self.registry = getUtility(IRegistry)
+        self.installer = get_installer(self.portal, self.request)
 
         with api.env.adopt_roles(["Manager"]):
-            self.qi.uninstallProducts(products=[PROJECTNAME])
+            self.installer.uninstall_product(PROJECTNAME)
 
     def test_uninstalled(self):
-        self.assertFalse(self.qi.isProductInstalled(PROJECTNAME))
+        self.assertFalse(self.installer.is_product_installed(PROJECTNAME))
 
     def test_addon_layer_removed(self):
         layers = [layer.getName() for layer in registered_layers()]
         self.assertNotIn("ICoverLayer", layers)
 
-    @unittest.skipIf(IS_PLONE_5, "No easy way to test this under Plone 5")
     def test_jsregistry_removed(self):
-        resource_ids = self.portal.portal_javascripts.getResourceIds()
-        for id_ in JS:
-            self.assertNotIn(id_, resource_ids, "{0} not removed".format(id))
+        bundles = self.registry.collectionOfInterface(
+            IBundleRegistry, prefix="plone.bundles"
+        )
+
+        self.assertNotIn("bootstrap", bundles.keys())
 
     @unittest.expectedFailure  # XXX: not pretty sure how to test this
     def test_versioning_policy_removed(self):
         repository = self.portal["portal_repository"]
         policy_map = repository.getPolicyMap()
         self.assertNotIn("collective.cover.content", policy_map)
-
-    @unittest.skipIf(IS_PLONE_5, "Plone 4.3 only")
-    def test_tinymce_linkable_removed(self):
-        tinymce = self.portal["portal_tinymce"]
-        linkable = tinymce.linkable.split("\n")
-        self.assertNotIn("collective.cover.content", linkable)
diff --git a/src/collective/cover/tests/test_textlinessortable_widget.py b/src/collective/cover/tests/test_textlinessortable_widget.py
index 8a05c05e8..7be7d2c4e 100644
--- a/src/collective/cover/tests/test_textlinessortable_widget.py
+++ b/src/collective/cover/tests/test_textlinessortable_widget.py
@@ -143,10 +143,10 @@ def test_extract(self):
         expected = {
             obj1.UID(): {
                 u"custom_url": u"custom_url",
-                u"order": u"0",
+                u"order": 0,
             },
-            obj2.UID(): {u"order": u"2"},
-            obj3.UID(): {u"order": u"1"},
+            obj2.UID(): {u"order": 2},
+            obj3.UID(): {u"order": 1},
         }
 
         extracted_value = widget.extract()
@@ -181,14 +181,14 @@ def test_utf8_custom_data(self):
         widget.name = name
         widget.context = {
             "uuids": {
-                obj1.UID(): {u"order": u"0", u"custom_description": u"áéíóú"},
-                obj2.UID(): {u"order": u"1", u"custom_description": u""},
+                obj1.UID(): {u"order": 0, u"custom_description": u"áéíóú"},
+                obj2.UID(): {u"order": 1, u"custom_description": u""},
             },
         }
 
         expected = {
-            obj1.UID(): {u"order": u"0"},
-            obj2.UID(): {u"order": u"1"},
+            obj1.UID(): {u"order": 0},
+            obj2.UID(): {u"order": 1},
         }
 
         extracted_value = widget.extract()
diff --git a/src/collective/cover/tests/test_tiles.py b/src/collective/cover/tests/test_tiles.py
index d679d9882..86ba49fca 100644
--- a/src/collective/cover/tests/test_tiles.py
+++ b/src/collective/cover/tests/test_tiles.py
@@ -23,7 +23,7 @@ def setUp(self):
     @unittest.expectedFailure  # FIXME: remove tiles on uninstall
     def test_tiles_removed_on_uninstall(self):
         qi = self.portal["portal_quickinstaller"]
-        qi.uninstallProducts(products=[PROJECTNAME])
+        qi.uninstall_product(PROJECTNAME)
         tiles = self.registry["plone.app.tiles"]
         self.assertNotIn(u"collective.cover.basic", tiles)
         self.assertNotIn(u"collective.cover.carousel", tiles)
diff --git a/src/collective/cover/tests/test_upgrades.py b/src/collective/cover/tests/test_upgrades.py
index cec1afb6d..b83df6c9a 100644
--- a/src/collective/cover/tests/test_upgrades.py
+++ b/src/collective/cover/tests/test_upgrades.py
@@ -1,9 +1,6 @@
 # -*- coding: utf-8 -*-
-from collective.cover.config import IS_PLONE_5
-from collective.cover.controlpanel import ICoverSettings
 from collective.cover.testing import INTEGRATION_TESTING
 from plone import api
-from plone.tiles.interfaces import ITileDataManager
 
 import unittest
 
@@ -51,314 +48,22 @@ def _how_many_upgrades_to_do(self):
         return len(upgrades[0])
 
 
-class Upgrade13to14TestCase(UpgradeTestCaseBase):
+# FIXME: This class is just an example of an upgrade step test.
+# Must be removed when a first test is created.
+class Upgrade31to30TestCase(UpgradeTestCaseBase):
     def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"13", u"14")
+        UpgradeTestCaseBase.setUp(self, u"31", u"30")
 
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 4)
+    def test_upgrade_step_1(self):
+        pass
 
-    # FIXME: https://github.com/collective/collective.cover/issues/633
-    @unittest.skipIf(IS_PLONE_5, "Upgrade step not supported under Plone 5")
-    def test_register_calendar_tile(self):
-        # address also an issue with Setup permission
-        title = u"Register calendar tile"
-        step = self._get_upgrade_step(title)
-        self.assertIsNotNone(step)
 
-        # simulate state on previous version
-        tile = u"collective.cover.calendar"
+#    def test_registrations(self):
+#        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
+#        self.assertGreaterEqual(int(version), int(self.to_version))
+#        self.assertEqual(self._how_many_upgrades_to_do(), 1)
 
-        record = dict(name="plone.app.tiles")
-        registered_tiles = api.portal.get_registry_record(**record)
-        registered_tiles.remove(tile)
-        api.portal.set_registry_record(value=registered_tiles, **record)
-        registered_tiles = api.portal.get_registry_record(**record)
-        self.assertNotIn(tile, registered_tiles)
-
-        record = dict(interface=ICoverSettings, name="available_tiles")
-        available_tiles = api.portal.get_registry_record(**record)
-        available_tiles.remove(tile)
-        api.portal.set_registry_record(value=available_tiles, **record)
-        available_tiles = api.portal.get_registry_record(**record)
-        self.assertNotIn(tile, available_tiles)
-
-        # run the upgrade step to validate the update
-        self._do_upgrade_step(step)
-
-        registered_tiles = api.portal.get_registry_record(name="plone.app.tiles")
-        self.assertIn(tile, registered_tiles)
-
-        available_tiles = api.portal.get_registry_record(
-            interface=ICoverSettings, name="available_tiles"
-        )
-        self.assertIn(tile, available_tiles)
-
-    def test_register_calendar_script(self):
-        # address also an issue with Setup permission
-        title = u"Register calendar script"
-        step = self._get_upgrade_step(title)
-        self.assertIsNotNone(step)
-
-        # simulate state on previous version
-        js_tool = api.portal.get_tool("portal_javascripts")
-        js_tool.unregisterResource("++resource++collective.cover/js/main.js")
-
-        script = "++resource++collective.cover/js/main.js"
-        self.assertNotIn(script, js_tool.getResourceIds())
-
-        # run the upgrade step to validate the update
-        self._do_upgrade_step(step)
-
-        self.assertIn(script, js_tool.getResourceIds())
-
-
-class Upgrade14to15TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"14", u"15")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 2)
-
-    def test_fix_image_field_modification_time(self):
-        from persistent.dict import PersistentDict
-
-        title = u"Fix image field modification time"
-        step = self._get_upgrade_step(title)
-        self.assertIsNotNone(step)
-
-        # simulate state on previous version
-        cover = self._create_cover(id="test-cover", layout="Empty layout")
-        cover.cover_layout = (
-            '[{"type": "row", "children": [{"column-size": 16, "type": '
-            '"group", "children": [{"tile-type": '
-            '"collective.cover.basic", "type": "tile", "id": '
-            '"ca6ba6675ef145e4a569c5e410af7511"}], "roles": ["Manager"]}]}]'
-        )
-
-        tile = cover.get_tile("ca6ba6675ef145e4a569c5e410af7511")
-        obj = self.portal["my-image"]
-        tile.populate_with_object(obj)
-
-        dmgr = ITileDataManager(tile)
-        old_data = dmgr.get()
-        old_data["image_mtime"] = repr(old_data["image_mtime"])
-        dmgr.annotations[dmgr.key] = PersistentDict(old_data)
-
-        data = dmgr.get()
-        self.assertIsInstance(data["image_mtime"], str)
-
-        # run the upgrade step to validate the update
-        self._do_upgrade_step(step)
-
-        data = dmgr.get()
-        self.assertIsInstance(data["image_mtime"], float)
-
-
-class Upgrade15to16TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"15", u"16")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 1)
-
-
-class Upgrade16to17TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"16", u"17")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 1)
-
-
-class Upgrade17to18TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"17", u"18")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 2)
-
-
-class Upgrade18to19TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"18", u"19")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 3)
-
-    def test_purge_deleted_tiles(self):
-        from zope.annotation import IAnnotations
-
-        title = u"Purge Deleted Tiles"
-        step = self._get_upgrade_step(title)
-        self.assertIsNotNone(step)
-
-        # simulate state on previous version
-        cover = self._create_cover(id="test-cover", layout="Empty layout")
-
-        annotations = IAnnotations(cover)
-        key = u"plone.tiles.data.abc123"
-        annotations[key] = u"Plone"
-        self.assertEqual(annotations[key], u"Plone")
-
-        # run the upgrade step to validate the update
-        self._do_upgrade_step(step)
-        self.assertNotIn(key, annotations)
-
-    @unittest.skipIf(IS_PLONE_5, "Upgrade step not supported under Plone 5")
-    def test_register_resource(self):
-        # address also an issue with Setup permission
-        title = u"Register resource"
-        step = self._get_upgrade_step(title)
-        self.assertIsNotNone(step)
-
-        # simulate state on previous version
-        from collective.cover.upgrades.v19 import JS
-
-        js_tool = api.portal.get_tool("portal_javascripts")
-        js_tool.unregisterResource(JS)
-        self.assertNotIn(JS, js_tool.getResourceIds())
-
-        # run the upgrade step to validate the update
-        self._do_upgrade_step(step)
-        self.assertIn(JS, js_tool.getResourceIds())
-
-
-class Upgrade19to20TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"19", u"20")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 1)
-
-
-class Upgrade20to21TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"20", u"21")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 1)
-
-
-class Upgrade21to22TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"21", u"22")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 1)
-
-
-class Upgrade22to23TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"22", u"23")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 1)
-
-    @staticmethod
-    def _set_remote_url_field(tile, remote_url):
-        data_mgr = ITileDataManager(tile)
-        data = data_mgr.get()
-        data.update(remote_url=remote_url)
-        data_mgr.set(data)
-
-    def test_add_remote_url_field(self):
-        title = u"Show remote_url field on Basic tiles"
-        step = self._get_upgrade_step(title)
-        self.assertIsNotNone(step)
-
-        # simulate state on previous version
-        layout = (
-            '[{"type": "row", "children": [{"column-size": 12, "type": '
-            '"group", "children": [{"tile-type": '
-            '"collective.cover.basic", "type": "tile", "id": '
-            '"test"}], "roles": ["Manager"]}]}]'
-        )
-        obj1 = self._create_cover(id="foo", layout="Empty layout")
-        obj2 = self._create_cover(id="bar", layout="Empty layout")
-        obj1.cover_layout = obj2.cover_layout = layout
-
-        with api.env.adopt_roles(["Manager"]):
-            news = api.content.create(self.portal, "News Item", title=u"Baz")
-
-        tile = obj1.restrictedTraverse("@@collective.cover.basic/test")
-        tile.populate_with_object(news)
-        self._set_remote_url_field(tile, None)
-        tile = obj1.restrictedTraverse("@@collective.cover.basic/test")
-        self.assertIsNone(tile.data["remote_url"])
-
-        tile = obj2.restrictedTraverse("@@collective.cover.basic/test")
-        tile.populate_with_object(news)
-        remote_url = "http://example.org/"
-        self._set_remote_url_field(tile, remote_url)
-        tile = obj2.restrictedTraverse("@@collective.cover.basic/test")
-        self.assertEqual(tile.data["remote_url"], remote_url)
-
-        # run the upgrade step to validate the update
-        from logging import INFO
-        from testfixtures import LogCapture
-
-        config = {"level": INFO, "attributes": ["getMessage"]}
-        with LogCapture("collective.cover", **config) as log:
-            self._do_upgrade_step(step)
-
-        log.check_present(
-            "Found 2 objects in the catalog",
-            '/plone/bar ("test"): remote_url=http://example.org/',
-        )
-
-
-class Upgrade23to24TestCase(UpgradeTestCaseBase):
-    def setUp(self):
-        UpgradeTestCaseBase.setUp(self, u"23", u"24")
-
-    def test_registrations(self):
-        version = self.setup.getLastVersionForProfile(self.profile_id)[0]
-        self.assertGreaterEqual(int(version), int(self.to_version))
-        self.assertEqual(self._how_many_upgrades_to_do(), 3)
-
-    def test_deprecate_resource_registries(self):
-        title = u"Deprecate resource registries"
-        step = self._get_upgrade_step(title)
-        self.assertIsNotNone(step)
-
-        # simulate state on previous version
-        from collective.cover.upgrades.v24 import JS
-        from collective.cover.upgrades.v24 import CSS
-
-        js_tool = api.portal.get_tool("portal_javascripts")
-        for js in JS:
-            js_tool.registerResource(id=js)
-            self.assertIn(js, js_tool.getResourceIds())
-
-        css_tool = api.portal.get_tool("portal_css")
-        for css in CSS:
-            css_tool.registerResource(id=css)
-            self.assertIn(css, css_tool.getResourceIds())
-
-        # run the upgrade step to validate the update
-        self._do_upgrade_step(step)
-        for js in JS:
-            self.assertNotIn(js, js_tool.getResourceIds())
-        for css in CSS:
-            self.assertNotIn(css, css_tool.getResourceIds())
+#    def test_upgrade_step_2(self):
+#        title = u"Upgrade"
+#        step = self._get_upgrade_step(title)
+#        self.assertIsNotNone(step)
diff --git a/src/collective/cover/tests/test_views.py b/src/collective/cover/tests/test_views.py
index 7595d994b..fc38caea7 100644
--- a/src/collective/cover/tests/test_views.py
+++ b/src/collective/cover/tests/test_views.py
@@ -64,6 +64,7 @@ def test_alternate_view_render(self):
         html = etree.HTML(view())
         # alternate view should show title, description and viewlets
         self.assertEqual(len(html.xpath('.//h1[contains(text(),"My Title")]')), 1)
+        # FIXME: html in Plone 6 is different (<p class="lead">My Description</p>)
         self.assertEqual(
             len(html.xpath('.//div[contains(text(),"My Description")]')), 1
         )
diff --git a/src/collective/cover/tests/test_vocabularies.py b/src/collective/cover/tests/test_vocabularies.py
index aaca3195b..d2c1a6c2d 100644
--- a/src/collective/cover/tests/test_vocabularies.py
+++ b/src/collective/cover/tests/test_vocabularies.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from collective.cover.config import IS_PLONE_5
 from collective.cover.controlpanel import ICoverSettings
 from collective.cover.testing import INTEGRATION_TESTING
 from plone.registry.interfaces import IRegistry
@@ -45,11 +44,6 @@ def test_available_tiles_vocabulary(self):
             "collective.cover.list",
             "collective.cover.richtext",
         ]
-
-        # FIXME: https://github.com/collective/collective.cover/issues/633
-        if IS_PLONE_5:
-            expected.remove("collective.cover.calendar")
-
         self.assertEqual(len(tiles), len(expected))
         for i in expected:
             self.assertIn(i, tiles)
@@ -71,17 +65,6 @@ def test_enabled_tiles_vocabulary(self):
             "collective.cover.list",
             "collective.cover.richtext",
         ]
-
-        # FIXME: https://github.com/collective/collective.cover/issues/633
-        if IS_PLONE_5:
-            expected.remove("collective.cover.calendar")
-
-        # XXX: PFG tile is deprecated and will be removed in collective.cover 3
-        from collective.cover.testing import HAS_PFG
-
-        if HAS_PFG and "collective.cover.pfg" not in expected:
-            expected.append("collective.cover.pfg")
-
         self.assertEqual(len(tiles), len(expected))
         for i in expected:
             self.assertIn(i, tiles)
@@ -145,13 +128,11 @@ def test_grid_systems(self):
 
         # Our default grid system must be in the vocabulary.
         grids = vocabulary(self.portal)
-        self.assertEqual(len(grids), 3)
+        self.assertEqual(len(grids), 2)
         self.assertIn(u"bootstrap3", grids)
         self.assertIn(u"bootstrap2", grids)
-        self.assertIn(u"deco16_grid", grids)
         self.assertEqual(grids.getTerm("bootstrap3").title, u"Bootstrap 3")
         self.assertEqual(grids.getTerm("bootstrap2").title, u"Bootstrap 2")
-        self.assertEqual(grids.getTerm("deco16_grid").title, u"Deco (16 columns)")
 
     def test_image_scales(self):
         from collective.cover.browser.cover import Helper
@@ -160,6 +141,6 @@ def test_image_scales(self):
 
         self.assertGreater(len(vocabulary), 0)
         # test against some expected values
-        self.assertIn(u"imagescale_mini", vocabulary)
-        self.assertIn(u"imagescale_preview", vocabulary)
-        self.assertIn(u"imagescale_large", vocabulary)
+        self.assertIn(u"mini 200:200", vocabulary)
+        self.assertIn(u"preview 400:400", vocabulary)
+        self.assertIn(u"large 768:768", vocabulary)
diff --git a/src/collective/cover/tests/utils.py b/src/collective/cover/tests/utils.py
index 8475d4d8d..6ed2d3429 100644
--- a/src/collective/cover/tests/utils.py
+++ b/src/collective/cover/tests/utils.py
@@ -1,23 +1,17 @@
 # -*- coding: utf-8 -*-
-"""Helper functions to create test content and work around API
-inconsistencies among Archetypes and Dexterity.
-"""
-from datetime import datetime
+"""Helper functions to create test content."""
 from datetime import timedelta
-from tzlocal import get_localzone
+from plone import api
+from plone.app.event.base import localized_now
+from plone.app.textfield.value import RichTextValue
+from plone.namedfile.file import NamedBlobFile
+from plone.namedfile.file import NamedBlobImage
 
-
-today = datetime.today()
-tomorrow = today + timedelta(days=1)
-
-TZNAME = get_localzone().zone
+import six
 
 
 def create_standard_content_for_tests(portal):
     """Create one instance of each standard content type, at least."""
-    from DateTime import DateTime
-    from plone import api
-
     with api.env.adopt_roles(["Manager"]):
         api.content.create(
             container=portal,
@@ -40,18 +34,16 @@ def create_standard_content_for_tests(portal):
             description=u"This document was created for testing purposes",
         )
 
-        # XXX: handle setting text field for both, Archetypes and Dexterity
         set_text_field(obj, u"<p>The quick brown fox jumps over the lazy dog</p>")
+        now = localized_now()
+        tomorrow = now + timedelta(days=1)
 
         api.content.create(
             container=portal,
             type="Event",
             title=u"My event",
-            startDate=DateTime(today),  # Archetypes
-            endDate=DateTime(tomorrow),
-            start=today,  # Dexterity
+            start=now,
             end=tomorrow,
-            timezone=TZNAME,
         )
 
         api.content.create(
@@ -87,39 +79,19 @@ def create_standard_content_for_tests(portal):
 
 
 def set_image_field(obj, image):
-    """Set image field in object on both, Archetypes and Dexterity."""
-    from plone.namedfile.file import NamedBlobImage
-
-    try:
-        obj.setImage(image)  # Archetypes
-    except AttributeError:
-        # Dexterity
-        data = image if type(image) == str else image.getvalue()
-        obj.image = NamedBlobImage(data=data, contentType="image/jpeg")
-    finally:
-        obj.reindexObject()
-
+    """Set image field in object."""
+    data = image if isinstance(image, six.binary_type) else image.getvalue()
+    obj.image = NamedBlobImage(data=data, contentType="image/jpeg")
+    obj.reindexObject()
 
-def set_file_field(obj, file):
-    """Set file field in object on both, Archetypes and Dexterity."""
-    from plone.namedfile.file import NamedBlobFile
 
-    try:
-        obj.setFile(file)  # Archetypes
-    except AttributeError:
-        # Dexterity
-        obj.file = NamedBlobFile(data=file, contentType="text/plain")
-    finally:
-        obj.reindexObject()
+def set_file_field(obj, _file):
+    """Set file field in object."""
+    obj.file = NamedBlobFile(data=_file, contentType="text/plain")
+    obj.reindexObject()
 
 
 def set_text_field(obj, text):
-    """Set text field in object on both, Archetypes and Dexterity."""
-    from plone.app.textfield.value import RichTextValue
-
-    try:
-        obj.setText(text)  # Archetypes
-    except AttributeError:
-        obj.text = RichTextValue(text, "text/html", "text/html")  # Dexterity
-    finally:
-        obj.reindexObject()
+    """Set text field in object."""
+    obj.text = RichTextValue(text, "text/html", "text/html")
+    obj.reindexObject()
diff --git a/test_plone43.cfg b/test_plone43.cfg
deleted file mode 100644
index 35a6dc8cb..000000000
--- a/test_plone43.cfg
+++ /dev/null
@@ -1,24 +0,0 @@
-[buildout]
-
-extends =
-    https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-4.3.x.cfg
-    https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg
-    base.cfg
-
-test-eggs =
-    plone.app.contenttypes
-    plone.app.referenceablebehavior
-    Products.PloneFormGen
-
-[versions]
-# XXX: https://github.com/collective/collective.cover/issues/492
-collective.js.bootstrap = 2.3.1.1
-plone.app.blocks = 4.2.0
-plone.app.tiles = 3.1.0
-plone.tiles = 2.1
-
-# XXX: PFG tile is deprecated and will be removed in collective.cover 3
-Products.PloneFormGen = <1.8.0.alpha1
-
-# Required to use new versions of Firefox
-selenium = 3.14.1
diff --git a/test_plone51.cfg b/test_plone51.cfg
deleted file mode 100644
index 81fbc6033..000000000
--- a/test_plone51.cfg
+++ /dev/null
@@ -1,16 +0,0 @@
-[buildout]
-
-extends =
-    https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-5.1.x.cfg
-    https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg
-    base.cfg
-
-# XXX: even if product is present, PFG tile must not be available
-test-eggs =
-    Products.PloneFormGen
-
-[versions]
-# XXX: PFG tile is deprecated and will be removed in collective.cover 3
-Products.PloneFormGen = >=1.8.0.alpha1
-
-Products.PortalTransforms = >=3.1.5
diff --git a/test_plone52.cfg b/test_plone52.cfg
new file mode 100644
index 000000000..9cf600f66
--- /dev/null
+++ b/test_plone52.cfg
@@ -0,0 +1,9 @@
+[buildout]
+
+extends =
+    https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-5.2.x.cfg
+    https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg
+    base.cfg
+
+[versions:python27]
+lazy-object-proxy = 1.6.0