+
+ —
+
+
+ by
+
+ Bob Dobalina
+
+
+
+
+
+ —
+ last modified
+
+ August 16, 2001 at 23:35:59
+
+
+
+
+
+ Place custom listing info for custom types here
+
+
+
+
+
+
+
+ description
+
+
+
+
+
+
+
+
+
+
+
+
+
+ There are currently no items in this folder.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/dexterity/folder_listing.py b/src/plone/app/layout/dexterity/folder_listing.py
new file mode 100644
index 00000000..00e30e70
--- /dev/null
+++ b/src/plone/app/layout/dexterity/folder_listing.py
@@ -0,0 +1,93 @@
+from Acquisition import aq_inner
+from plone.app.dexterity import _
+from plone.base.batch import Batch
+from plone.base.interfaces import ISecuritySchema
+from plone.registry.interfaces import IRegistry
+from Products.Five import BrowserView
+from zope.component import getMultiAdapter
+from zope.component import getUtility
+
+
+class FolderView(BrowserView):
+ def __init__(self, context, request):
+ super().__init__(context, request)
+
+ self.plone_view = getMultiAdapter((context, request), name="plone")
+ self.portal_state = getMultiAdapter(
+ (context, request), name="plone_portal_state"
+ )
+ self.pas_member = getMultiAdapter((context, request), name="pas_member")
+
+ limit_display = getattr(self.request, "limit_display", None)
+ limit_display = int(limit_display) if limit_display is not None else 20
+ b_size = getattr(self.request, "b_size", None)
+ self.b_size = int(b_size) if b_size is not None else limit_display
+ b_start = getattr(self.request, "b_start", None)
+ self.b_start = int(b_start) if b_start is not None else 0
+
+ def results(self, **kwargs):
+ """Return a content listing based result set with contents of the
+ folder.
+
+ :param **kwargs: Any keyword argument, which can be used for catalog
+ queries.
+ :type **kwargs: keyword argument
+
+ :returns: plone.app.contentlisting based result set.
+ :rtype: ``plone.app.contentlisting.interfaces.IContentListing`` based
+ sequence.
+ """
+ # Extra filter
+ kwargs.update(self.request.get("contentFilter", {}))
+ if "object_provides" not in kwargs: # object_provides is more specific
+ kwargs.setdefault("portal_type", self.friendly_types)
+ kwargs.setdefault("batch", True)
+ kwargs.setdefault("b_size", self.b_size)
+ kwargs.setdefault("b_start", self.b_start)
+
+ listing = aq_inner(self.context).restrictedTraverse("@@folderListing", None)
+ if listing is None:
+ return []
+ results = listing(**kwargs)
+ return results
+
+ def batch(self):
+ batch = Batch(self.results(), size=self.b_size, start=self.b_start, orphan=1)
+ return batch
+
+ def normalizeString(self, text):
+ return self.plone_view.normalizeString(text)
+
+ def toLocalizedTime(self, time, long_format=None, time_only=None):
+ return self.plone_view.toLocalizedTime(time, long_format, time_only)
+
+ @property
+ def friendly_types(self):
+ return self.portal_state.friendly_types()
+
+ @property
+ def isAnon(self):
+ return self.portal_state.anonymous()
+
+ @property
+ def navigation_root_url(self):
+ return self.portal_state.navigation_root_url()
+
+ @property
+ def use_view_action(self):
+ registry = getUtility(IRegistry)
+ return registry.get("plone.types_use_view_action_in_listings", [])
+
+ @property
+ def show_about(self):
+ registry = getUtility(IRegistry)
+ settings = registry.forInterface(ISecuritySchema, prefix="plone")
+ show_about = getattr(settings, "allow_anon_views_about", False)
+ return show_about or not self.isAnon
+
+ @property
+ def no_items_message(self):
+ return _(
+ "description_no_items_in_folder",
+ default="There are currently no items in this folder.",
+ )
diff --git a/src/plone/app/layout/dexterity/import_types.py b/src/plone/app/layout/dexterity/import_types.py
new file mode 100644
index 00000000..a228dff2
--- /dev/null
+++ b/src/plone/app/layout/dexterity/import_types.py
@@ -0,0 +1,191 @@
+"""Support for importing Dexterity types from GS zip file."""
+
+# XXX: need to make exceptions more specific, shorten messages
+from DateTime.DateTime import DateTime
+from io import BytesIO
+from lxml import etree
+from plone.app.dexterity import _
+from plone.namedfile.field import NamedFile
+from plone.z3cform.layout import wrap_form
+from Products.CMFCore.utils import getToolByName
+from Products.GenericSetup.context import BaseContext
+from Products.GenericSetup.interfaces import IImportContext
+from z3c.form import field
+from z3c.form import form
+from zipfile import BadZipfile
+from zipfile import ZipFile
+from zope.component.hooks import getSite
+from zope.interface import implementer
+from zope.interface import Interface
+from zope.interface import Invalid
+from zope.interface import invariant
+
+import os.path
+import zope.schema
+
+
+class ITypeProfileImport(Interface):
+ """Fields for a zip import form"""
+
+ profile_file = NamedFile(
+ title=_("Type profiles archive file"),
+ required=True,
+ )
+
+ @invariant
+ def isGoodImportFile(data):
+ nfile = getattr(data, "profile_file", None)
+ if nfile is None:
+ # let required validator handle this
+ return None
+ try:
+ archive = ZipFile(BytesIO(data.profile_file.data), "r")
+ except BadZipfile:
+ raise Invalid(
+ _("Error: The file submitted must be a zip archive."),
+ )
+ name_list = archive.namelist()
+ for fname in name_list:
+ if fname == "types.xml":
+ continue
+ if (
+ os.path.dirname(fname) != "types"
+ or os.path.splitext(fname)[1] != ".xml"
+ ):
+ raise Invalid(
+ _(
+ "Error: The file submitted must be a zip archive "
+ "containing only type profile information."
+ )
+ )
+
+ # check XML for basic integrity
+ with archive.open("types.xml", "r") as f:
+ source = f.read()
+ root = etree.fromstring(source)
+ if root.tag != "object":
+ raise Invalid(_("types.xml in archive is invalid."))
+
+ # check against existing types; don't allow overwrites
+ site = getSite()
+ existing_types = getToolByName(site, "portal_types").listContentTypes()
+ for element in root.getchildren():
+ if element.tag == "object":
+ attribs = element.attrib
+ if not attribs["meta_type"] == "Dexterity FTI":
+ raise Invalid(
+ _("Types in archive must be only Dexterity types."),
+ )
+ if attribs["name"] in existing_types:
+ msg = (
+ "One or more types in the import archive is an "
+ 'existing type. Delete "%s" if you '
+ "really wish to replace it."
+ )
+ raise Invalid(
+ _(msg, attribs["name"]),
+ )
+
+
+@implementer(ITypeProfileImport)
+class TypeProfileImport:
+ form_fields = field.Fields(ITypeProfileImport)
+ profile_file = zope.schema.fieldproperty.FieldProperty(
+ ITypeProfileImport["profile_file"]
+ )
+
+ def __init__(self, profile_file):
+ self.profile_file = profile_file
+
+
+class TypeProfileImportForm(form.AddForm):
+ label = _("Import Content Types")
+ description = _(
+ "You may import types by uploading a zip archive containing type "
+ "profiles. The import archive should contain a types.xml file and a "
+ "types directory containing one or more Dexterity type information "
+ "files. For a sample, create a content type and export it from the "
+ "Dexterity Content Types page."
+ )
+ fields = field.Fields(ITypeProfileImport)
+ id = "import-types-form"
+
+ def create(self, data):
+ return TypeProfileImport(**data)
+
+ def add(self, profile_import):
+ # initialize import context
+ types_tool = getToolByName(self.context, "portal_types")
+ import_context = ZipFileImportContext(
+ types_tool, BytesIO(profile_import.profile_file.data)
+ )
+ # run the profile
+ setup_tool = getToolByName(self.context, "portal_setup")
+ handler = setup_tool.getImportStep("typeinfo")
+ handler(import_context)
+ self.status = _("Imported successfully.")
+
+ def nextURL(self):
+ url = self.context.absolute_url()
+ return url
+
+
+TypeProfileImportFormPage = wrap_form(TypeProfileImportForm)
+
+
+@implementer(IImportContext)
+class ZipFileImportContext(BaseContext):
+ """GS Import context for a ZipFile"""
+
+ def __init__(self, tool, archive_bits, encoding=None, should_purge=False):
+ super().__init__(tool, encoding)
+ self._archive = ZipFile(archive_bits, "r")
+ self._should_purge = bool(should_purge)
+ self.name_list = self._archive.namelist()
+
+ def readDataFile(self, filename, subdir=None):
+ if subdir is not None:
+ filename = "/".join((subdir, filename))
+
+ try:
+ file = self._archive.open(filename, "r")
+ except KeyError:
+ return None
+
+ return file.read()
+
+ def getLastModified(self, path):
+ try:
+ zip_info = self._archive.getinfo(path)
+ except KeyError:
+ return None
+ return DateTime(*zip_info.date_time)
+
+ def isDirectory(self, path):
+ """See IImportContext"""
+
+ # namelist only includes full filenames, not directories
+ return path not in self.name_list
+
+ def listDirectory(self, path, skip=[]):
+ """See IImportContext"""
+
+ # namelist contains only full path/filenames, not
+ # directories. But we need to include directories.
+
+ if path is None:
+ path = ""
+ path_parts = path.split("/")
+ res = set()
+ for pn in self.name_list:
+ dn, bn = os.path.split(pn)
+ dn_parts = dn.split("/")
+ if dn == path:
+ if bn not in skip:
+ res.add(bn)
+ continue
+ if dn.startswith(path) and (
+ path == "" or len(dn_parts) == len(path_parts) + 1
+ ):
+ res.add(dn_parts[-1])
+ return list(res)
diff --git a/src/plone/app/layout/dexterity/item.pt b/src/plone/app/layout/dexterity/item.pt
new file mode 100644
index 00000000..1069ef26
--- /dev/null
+++ b/src/plone/app/layout/dexterity/item.pt
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/dexterity/layout.py b/src/plone/app/layout/dexterity/layout.py
new file mode 100644
index 00000000..4f776a83
--- /dev/null
+++ b/src/plone/app/layout/dexterity/layout.py
@@ -0,0 +1,15 @@
+from plone.app.dexterity import _
+from plone.z3cform.layout import FormWrapper
+from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
+
+
+class TypeFormLayout(FormWrapper):
+ index = ViewPageTemplateFile("tabbed_forms.pt")
+
+ @property
+ def tabs(self):
+ return (
+ (_("Overview"), "@@overview"),
+ (_("Fields"), "@@fields"),
+ (_("Behaviors"), "@@behaviors"),
+ )
diff --git a/src/plone/app/layout/dexterity/modeleditor.pt b/src/plone/app/layout/dexterity/modeleditor.pt
new file mode 100644
index 00000000..afd95b60
--- /dev/null
+++ b/src/plone/app/layout/dexterity/modeleditor.pt
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+ Back to the schema editor
+
+
+
Edit field model for
+ name
+
+
+
+ This form allows you to directly edit the XML representation
+ of the field list. This makes it possible to add annotations for fieldsets,
+ validation and widgets. See the
+ Dexterity XML
+ section of the
+ Dexterity Developer Manual
+ for details.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/dexterity/modeleditor.py b/src/plone/app/layout/dexterity/modeleditor.py
new file mode 100644
index 00000000..a7fb5f7c
--- /dev/null
+++ b/src/plone/app/layout/dexterity/modeleditor.py
@@ -0,0 +1,149 @@
+from AccessControl import Unauthorized
+from lxml import etree
+from plone.app.dexterity import _
+from plone.base.utils import safe_bytes
+from plone.base.utils import safe_text
+from plone.supermodel import loadString
+from plone.supermodel import serializeModel
+from plone.supermodel.parser import SupermodelParseError
+from Products.Five import BrowserView
+from Products.statusmessages.interfaces import IStatusMessage
+from zope.component import queryMultiAdapter
+from zope.deprecation import deprecate
+
+import html
+
+NAMESPACE = "{http://namespaces.plone.org/supermodel/schema}"
+
+
+class ModelEditorView(BrowserView):
+ """Editor view."""
+
+ @property
+ @deprecate("""`ModelEditorView.escaped_model_source` is deprecated will be removed
+in Plone 7. Use `model_source` without the `structure` TALES expression
+instead.""")
+ def escaped_model_source(self):
+ # Return the HTML escaped model source.
+ return html.escape(self.model_source, False)
+
+ @deprecate("""`ModelEditorView.modelSource` is deprecated and will be removed in
+Plone 7. Use `model_source` instead.
+""")
+ def modelSource(self):
+ # BBB
+ return self.model_source
+
+ @property
+ def model_source(self):
+ # Return modified source from textarea or the original FTI source.
+ source = self._unescaped_source_from_request() or self.context.fti.model_source
+ if source:
+ return source
+
+ # or serialize the model file
+ model = self.context.fti.lookupModel()
+ return serializeModel(model)
+
+ def _unescaped_source_from_request(self):
+ """Unescape the source from the request.
+
+ We expect that the source we get from the request is escaped html.
+ If we pass this directly to the lxml parser, we get:
+ Error: XMLSyntaxError: Start tag expected
+ See https://github.com/plone/Products.CMFPlone/issues/3695
+ So we need to unescape it.
+
+ There is a danger that we unescape too much. If we somehow get already
+ unescaped xml, this may contain escaped html. If we then call html.unescape,
+ this html gets unescaped, which is not what we want.
+ The source likely starts with one of these strings:
+
+ <?xml
+ <model
+
+ We check if it starts with '<' and we only unescape then.
+ """
+ source = self.request.form.get("source")
+ if not source:
+ return
+ # If you let the source start with spaces, it actually becomes invisible
+ # in the code editor. So strip it to be safe.
+ source = source.strip()
+ if source.startswith("<"):
+ source = html.unescape(source)
+ return source
+
+ def authorized(self, context, request):
+ authenticator = queryMultiAdapter((context, request), name="authenticator")
+ return authenticator and authenticator.verify()
+
+ def __call__(self):
+ """View and eventually save the form."""
+
+ save = "form.button.save" in self.request.form
+ source = self._unescaped_source_from_request()
+ if save and source:
+ # First, check for authenticator
+ if not self.authorized(self.context, self.request):
+ raise Unauthorized
+
+ # Is it valid XML?
+ # Some safety measures.
+ # We do not want to load entities, especially file:/// entities.
+ # Also discard processing instructions.
+ #
+ source = safe_bytes(source)
+ parser = etree.XMLParser(resolve_entities=False, remove_pis=True)
+ try:
+ root = etree.fromstring(source, parser=parser)
+ except etree.XMLSyntaxError as e:
+ IStatusMessage(self.request).addStatusMessage(
+ f"XMLSyntaxError: {html.escape(safe_text(e.args[0]))}",
+ "error",
+ )
+ return super().__call__()
+
+ # a little more sanity checking, look at first two element levels
+ if root.tag != NAMESPACE + "model":
+ IStatusMessage(self.request).addStatusMessage(
+ _("Error: root tag must be 'model'"),
+ "error",
+ )
+ return super().__call__()
+
+ for element in root.getchildren():
+ if element.tag != NAMESPACE + "schema":
+ IStatusMessage(self.request).addStatusMessage(
+ _("Error: all model elements must be 'schema'"),
+ "error",
+ )
+ return super().__call__()
+
+ # can supermodel parse it?
+ # This is mainly good for catching bad dotted names.
+ try:
+ loadString(source, policy="dexterity")
+ except SupermodelParseError as e:
+ message = e.args[0].replace('\n File ""', "")
+ IStatusMessage(self.request).addStatusMessage(
+ f"SuperModelParseError: {html.escape(message)}",
+ "error",
+ )
+ return super().__call__()
+
+ # clean up formatting sins
+ source = etree.tostring(
+ root, pretty_print=True, xml_declaration=True, encoding="utf8"
+ )
+
+ # Save to FTI and also allow to clear the source
+ fti = self.context.fti
+ fti.manage_changeProperties(model_source=source)
+
+ IStatusMessage(self.request).addStatusMessage(
+ _("Changes saved."),
+ "info",
+ )
+
+ return super().__call__()
diff --git a/src/plone/app/layout/dexterity/overview.pt b/src/plone/app/layout/dexterity/overview.pt
new file mode 100644
index 00000000..27c9b8f9
--- /dev/null
+++ b/src/plone/app/layout/dexterity/overview.pt
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/dexterity/overview.py b/src/plone/app/layout/dexterity/overview.py
new file mode 100644
index 00000000..806b04fe
--- /dev/null
+++ b/src/plone/app/layout/dexterity/overview.py
@@ -0,0 +1,34 @@
+from plone.app.dexterity import _
+from plone.app.layout.dexterity.layout import TypeFormLayout
+from plone.app.dexterity.interfaces import ITypeSettings
+from Products.CMFCore.interfaces import IFolderish
+from z3c.form import field
+from z3c.form import form
+from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
+from zope.dottedname.resolve import resolve as resolveDottedName
+
+
+class TypeOverviewForm(form.EditForm):
+ enableCSRFProtection = True
+ template = ViewPageTemplateFile("overview.pt")
+
+ @property
+ def fields(self):
+ # if this type's class is not a container,
+ # remove the field for filtering contained content types
+ klass = resolveDottedName(self.context.fti.klass)
+ fields = field.Fields(ITypeSettings)
+ filtered = fields.select(
+ "title", "description", "allowed_content_types", "filter_content_types"
+ )
+ if not IFolderish.implementedBy(klass):
+ del filtered["filter_content_types"]
+ return filtered
+
+ def getContent(self):
+ return self.context.fti
+
+
+class TypeOverviewPage(TypeFormLayout):
+ form = TypeOverviewForm
+ label = _("Overview")
diff --git a/src/plone/app/layout/dexterity/tabbed_forms.pt b/src/plone/app/layout/dexterity/tabbed_forms.pt
new file mode 100644
index 00000000..d631c3b0
--- /dev/null
+++ b/src/plone/app/layout/dexterity/tabbed_forms.pt
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+ Portal status message
+
+
+
+ ()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/dexterity/types.py b/src/plone/app/layout/dexterity/types.py
new file mode 100644
index 00000000..77d19a15
--- /dev/null
+++ b/src/plone/app/layout/dexterity/types.py
@@ -0,0 +1,302 @@
+from OFS.SimpleItem import SimpleItem
+from plone.app.dexterity import _
+from plone.app.layout.dexterity.utils import UTF8Property
+from plone.app.dexterity.interfaces import ITypeSchemaContext
+from plone.app.dexterity.interfaces import ITypesContext
+from plone.app.dexterity.interfaces import ITypeSettings
+from plone.app.dexterity.interfaces import ITypeStats
+from plone.dexterity.interfaces import IDexterityFTI
+from plone.dexterity.utils import getAdditionalSchemata
+from plone.schemaeditor.browser.schema.traversal import SchemaContext
+from plone.z3cform import layout
+from plone.z3cform.crud import crud
+from plone.z3cform.layout import FormWrapper
+from Products.CMFCore.utils import getToolByName
+from Products.Five.browser.pagetemplatefile import (
+ ViewPageTemplateFile as FiveViewPageTemplateFile,
+)
+from z3c.form import button
+from z3c.form import field
+from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
+from zope.cachedescriptors.property import Lazy as lazy_property
+from zope.component import adapter
+from zope.component import ComponentLookupError
+from zope.component import getAllUtilitiesRegisteredFor
+from zope.component import getUtility
+from zope.interface import implementer
+from zope.publisher.interfaces.browser import IBrowserPublisher
+from ZPublisher.BaseRequest import DefaultPublishTraverse
+
+import urllib
+
+ALLOWED_FIELDS = [
+ "plone.app.textfield.RichText",
+ "plone.namedfile.field.NamedBlobImage",
+ "plone.namedfile.field.NamedBlobFile",
+ "plone.schema.email.Email",
+ "z3c.relationfield.schema.RelationChoice",
+ "z3c.relationfield.schema.RelationList",
+ "zope.schema._bootstrapfields.Bool",
+ "zope.schema._bootstrapfields.Int",
+ "plone.schema.jsonfield.JSONField",
+ "zope.schema._bootstrapfields.Password",
+ "zope.schema._bootstrapfields.Text",
+ "zope.schema._bootstrapfields.TextLine",
+ "zope.schema._field.Choice",
+ "zope.schema._field.Date",
+ "zope.schema._field.Datetime",
+ "zope.schema._field.Float",
+ "zope.schema._field.Set",
+ "zope.schema._field.URI",
+]
+
+
+class TypeEditSubForm(crud.EditSubForm):
+ """Content type edit subform. Just here to use a custom template."""
+
+ template = ViewPageTemplateFile("types_listing_row.pt")
+
+
+class TypeEditForm(crud.EditForm):
+ """Content type edit form.
+
+ Just a normal CRUD form without the form title or edit button.
+ """
+
+ label = None
+ editsubform_factory = TypeEditSubForm
+
+ buttons = crud.EditForm.buttons.copy().omit("edit")
+ handlers = crud.EditForm.handlers.copy()
+
+ @button.buttonAndHandler(_("Clone"))
+ def handleClone(self, action):
+ selected = self.selected_items()
+
+ if len(selected) > 1:
+ self.status = _("Please select a single type to clone.")
+ elif len(selected) == 1:
+ id = selected[0][0]
+ url = f"{self.context.context.absolute_url()}/{id}/@@clone"
+ self.request.response.redirect(url)
+ else:
+ self.status = _("Please select a type to clone.")
+
+ @button.buttonAndHandler(_("Export Type Profiles"))
+ def handleExport(self, action):
+ selected = ",".join([items[0] for items in self.selected_items()])
+
+ if len(selected) == 0:
+ self.status = _("Please select types to export.")
+ elif len(selected) > 0:
+ url = "{}/@@types-export?selected={}".format(
+ self.context.context.absolute_url(),
+ urllib.parse.quote(selected),
+ )
+ self.request.response.redirect(url)
+
+ @button.buttonAndHandler(_("Export Schema Models"))
+ def handleExportModels(self, action):
+ selected = ",".join([items[0] for items in self.selected_items()])
+
+ if len(selected) == 0:
+ self.status = _("Please select types to export.")
+ elif len(selected) > 0:
+ url = "{}/@@models-export?selected={}".format(
+ self.context.context.absolute_url(), urllib.parse.quote(selected)
+ )
+ self.request.response.redirect(url)
+
+
+class TypesEditFormWrapper(FormWrapper):
+ """Render Plone frame around our form with little modifications"""
+
+ form = TypeEditForm
+ index = FiveViewPageTemplateFile("typesformwrapper.pt")
+
+
+@adapter(IDexterityFTI)
+@implementer(ITypeSettings)
+class TypeSettingsAdapter:
+ def __init__(self, context):
+ self.context = context
+
+ @property
+ def id(self):
+ return self.context.getId()
+
+ title = UTF8Property("title")
+ description = UTF8Property("description")
+
+ @property
+ def container(self):
+ return self.context.container
+
+ def _get_allowed_content_types(self):
+ return set(self.context.allowed_content_types)
+
+ def _set_allowed_content_types(self, value):
+ if not value:
+ value = ()
+ self.context.allowed_content_types = tuple(value)
+ if value:
+ self.context.filter_content_types = True
+
+ allowed_content_types = property(
+ _get_allowed_content_types, _set_allowed_content_types
+ )
+
+ def _get_filter_content_types(self):
+ value = self.context.filter_content_types
+ if not value:
+ return "all"
+ elif value and not self.allowed_content_types:
+ return "none"
+ else:
+ return "some"
+
+ def _set_filter_content_types(self, value):
+ if value == "none":
+ self.context.filter_content_types = True
+ self.context.allowed_content_types = ()
+ elif value == "all":
+ self.context.filter_content_types = False
+ elif value == "some":
+ self.context.filter_content_types = True
+
+ filter_content_types = property(
+ _get_filter_content_types, _set_filter_content_types
+ )
+
+
+@adapter(IDexterityFTI)
+@implementer(ITypeStats)
+class TypeStatsAdapter:
+ def __init__(self, context):
+ self.context = context
+
+ @property
+ def item_count(self):
+ catalog = getToolByName(self.context, "portal_catalog")
+ lengths = dict(catalog.Indexes["portal_type"].uniqueValues(withLengths=True))
+ return lengths.get(self.context.getId(), 0)
+
+
+class TypesListing(crud.CrudForm):
+ """The combined content type edit + add forms."""
+
+ @lazy_property
+ def description(self):
+ if self.get_items():
+ return _(
+ "The following custom content types are available for your " "site."
+ )
+ return _(
+ "help_addcontenttype_button",
+ default='Content types show up on Plone\'s "Add Item" menu and '
+ "allow you to store custom data in your site. Click the "
+ '"Add Content Type" button to begin creating a new '
+ "content type with its own fields.",
+ )
+
+ template = ViewPageTemplateFile("types_listing.pt")
+ view_schema = field.Fields(ITypeSettings).select("title", "description")
+ view_schema += field.Fields(ITypeStats)
+ addform_factory = crud.NullForm
+ editform_factory = TypeEditForm
+
+ def get_items(self):
+ """Look up all Dexterity FTIs via the component registry.
+
+ (These utilities are created via an IObjectCreated handler for the
+ DexterityFTI class, configured in plone.dexterity.)
+ """
+ ftis = getAllUtilitiesRegisteredFor(IDexterityFTI)
+ return [(fti.__name__, fti) for fti in ftis]
+
+ def remove(self, id_and_item):
+ """Remove a content type."""
+ ttool = getToolByName(self.context, "portal_types")
+ ttool.manage_delObjects([id_and_item[0]])
+
+ def link(self, item, field):
+ """Generate links to the edit page for each type.
+
+ (But only for types with schemata that can be edited through the web.)
+ """
+ if field == "title":
+ return "{}/{}".format(
+ self.context.absolute_url(), urllib.parse.quote(item.__name__)
+ )
+
+
+# Create a form wrapper so the form gets layout.
+TypesListingPage = layout.wrap_form(
+ TypesListing, __wrapper_class=TypesEditFormWrapper, label=_("Content Types")
+)
+
+
+@implementer(ITypeSchemaContext)
+class TypeSchemaContext(SchemaContext):
+ fti = None
+ schemaName = ""
+ schemaEditorView = "fields"
+ allowedFields = ALLOWED_FIELDS
+
+ def browserDefault(self, request):
+ return self, ("@@overview",)
+
+ @property
+ def additionalSchemata(self):
+ return getAdditionalSchemata(portal_type=self.fti.getId())
+
+
+# IBrowserPublisher tells the Zope 2 traverser to pay attention to the
+# publishTraverse and browserDefault methods.
+@implementer(ITypesContext, IBrowserPublisher)
+class TypesContext(SimpleItem):
+ """This class represents the types configlet.
+
+ It allows us to traverse through it to (a wrapper of) the schema
+ of a particular type.
+ """
+
+ def __init__(self, context, request):
+ super().__init__(context, request)
+
+ # make sure that breadcrumbs will be correct
+ self.id = None
+ self.Title = lambda: _("Content Types")
+
+ # turn off green edit border for anything in the type control panel
+ request.set("disable_border", 1)
+
+ def publishTraverse(self, request, name):
+ """Traverse to a schema context.
+
+ 1. Try to find a content type whose name matches the next URL path
+ element.
+ 2. Look up its schema.
+ 3. Return a schema context (an acquisition-aware wrapper of the
+ schema).
+ """
+ try:
+ fti = getUtility(IDexterityFTI, name=name)
+ except ComponentLookupError:
+ return DefaultPublishTraverse(self, request).publishTraverse(request, name)
+
+ schema = fti.lookupSchema()
+ schema_context = TypeSchemaContext(
+ schema, request, name=name, title=fti.title
+ ).__of__(self)
+ schema_context.fti = fti
+ schema_context.schemaName = ""
+ return schema_context
+
+ def browserDefault(self, request):
+ """Show the 'edit' view by default.
+
+ If we aren't traversing to a schema beneath the types configlet,
+ we actually want to see the TypesListingPage.
+ """
+ return self, ("@@edit",)
diff --git a/src/plone/app/layout/dexterity/types_listing.pt b/src/plone/app/layout/dexterity/types_listing.pt
new file mode 100644
index 00000000..2cfa3675
--- /dev/null
+++ b/src/plone/app/layout/dexterity/types_listing.pt
@@ -0,0 +1,48 @@
+
+
+