From 13255ba88caba09e39a2c9f99d75c13da3e75e32 Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Wed, 27 May 2026 16:38:25 +0200 Subject: [PATCH 01/12] delete unused code --- src/plone/app/users/browser/schemaeditor.py | 71 --------------------- 1 file changed, 71 deletions(-) diff --git a/src/plone/app/users/browser/schemaeditor.py b/src/plone/app/users/browser/schemaeditor.py index bda7fa3..f6fcfae 100644 --- a/src/plone/app/users/browser/schemaeditor.py +++ b/src/plone/app/users/browser/schemaeditor.py @@ -10,9 +10,7 @@ from plone.supermodel.model import finalizeSchemas from plone.supermodel.model import Model from plone.supermodel.model import SchemaClass -from plone.supermodel.parser import IFieldMetadataHandler from plone.supermodel.serializer import serialize -from plone.supermodel.utils import ns from plone.z3cform.layout import FormWrapper from Products.CMFCore.utils import getToolByName from Products.CMFPlone.utils import get_portal @@ -28,7 +26,6 @@ USERS_NAMESPACE = "http://namespaces.plone.org/supermodel/users" USERS_PREFIX = "users" -SPLITTER = "_//_" ALLOWED_FIELDS = [ "zope.schema._bootstrapfields.TextLine", @@ -74,10 +71,6 @@ def log(message, level="info", id="plone.app.users.browser.schemaeditor"): getattr(logger, level)(message) -class IMemberFieldValidator(Interface): - """Base marker for field validators""" - - class IMemberSchemaContext(Interface): """ """ @@ -175,70 +168,6 @@ def get_ttw_edited_schema(): return "" -@implementer(IFieldMetadataHandler) -class UsersMetadataSchemaExporter: - """Support the security: namespace in model definitions.""" - - namespace = ns = USERS_NAMESPACE - prefix = USERS_PREFIX - if_attrs = ( - "min", - "max", - "order", - "min_length", - "max_length", - "required", - ) - - def read(self, fieldNode, schema, field): - for attr in self.if_attrs: - value = self.load(fieldNode.get(ns(attr, self.ns), None)) - if value is not None: - setattr(field, attr, value) - - def write(self, fieldNode, schema, field): - for attr in self.if_attrs: - value = getattr(field, attr, None) - if value is not None: - v = self.serialize(value) - fieldNode.set(ns(attr, self.ns), v) - - def load(self, value): - listre = re.compile("(?Plist|set|tuple):(?P.*)", re_flags) - ltypes = { - "list": list, - "set": set, - "tuple": tuple, - } - if isinstance(value, str): - listm = listre.search(value) - if value.startswith("int:"): - value = int(value.split("int:")[1]) - elif listm: - i = listm.groupdict() - try: - tp = i["type"] - value = i["list"].split(SPLITTER) - if tp not in ["list"]: - value = ltypes[tp](value) - except Exception: - value = [] - else: - value = {"bool:true": True, "bool:false": False}.get( - value.lower(), value - ) - return value - - def serialize(self, value): - if isinstance(value, bool): - value = value and "bool:true" or "bool:false" - elif isinstance(value, (list, set, tuple)): - value = f"{type(value).__name__}:{SPLITTER.join(value)}" - elif value is not None: - value = f"int:{value}" - return value - - def is_serialisable_field(field): ret = False if field.__class__.__name__ in field_type_mapping: From a9ac919adb6c0c9144c9bc651642f1b7f47199b5 Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Wed, 27 May 2026 16:45:52 +0200 Subject: [PATCH 02/12] move constant definitions out of browser subpackage --- src/plone/app/users/browser/schemaeditor.py | 3 --- src/plone/app/users/field_extender.py | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/plone/app/users/browser/schemaeditor.py b/src/plone/app/users/browser/schemaeditor.py index f6fcfae..b753c14 100644 --- a/src/plone/app/users/browser/schemaeditor.py +++ b/src/plone/app/users/browser/schemaeditor.py @@ -24,9 +24,6 @@ import logging import re -USERS_NAMESPACE = "http://namespaces.plone.org/supermodel/users" -USERS_PREFIX = "users" - ALLOWED_FIELDS = [ "zope.schema._bootstrapfields.TextLine", "zope.schema._bootstrapfields.Text", diff --git a/src/plone/app/users/field_extender.py b/src/plone/app/users/field_extender.py index 0c7b262..077be2c 100644 --- a/src/plone/app/users/field_extender.py +++ b/src/plone/app/users/field_extender.py @@ -1,5 +1,3 @@ -from plone.app.users.browser.schemaeditor import USERS_NAMESPACE -from plone.app.users.browser.schemaeditor import USERS_PREFIX from plone.autoform import directives as form from plone.base import PloneMessageFactory as _ from plone.supermodel.interfaces import IFieldMetadataHandler @@ -14,6 +12,9 @@ import zope.schema +USERS_NAMESPACE = "http://namespaces.plone.org/supermodel/users" +USERS_PREFIX = "users" + form_vocab = SimpleVocabulary( [ SimpleTerm(value="On Registration", title=_("On Registration")), From 5bebf819c1bdc5a0ef3ca784b7911296081c6f72 Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Wed, 27 May 2026 17:26:16 +0200 Subject: [PATCH 03/12] move user schema editor views over to plone.app.layout --- pyproject.toml | 2 +- setup.py | 1 - src/plone/app/users/__init__.py | 3 + src/plone/app/users/browser/configure.zcml | 15 ----- src/plone/app/users/browser/schema_layout.pt | 69 -------------------- src/plone/app/users/browser/schemaeditor.py | 41 ++++-------- 6 files changed, 16 insertions(+), 115 deletions(-) delete mode 100644 src/plone/app/users/browser/schema_layout.pt diff --git a/pyproject.toml b/pyproject.toml index d3781be..15b06c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ Zope = [ 'zope.sendmail', 'Zope' ] 'plone.base' = [ - 'plone.batching', 'plone.registry', 'plone.schema','plone.z3cform', + 'plone.batching', 'plone.registry', 'plone.schema', 'Products.CMFCore', 'Products.CMFDynamicViewFTI', ] python-dateutil = ['dateutil'] diff --git a/setup.py b/setup.py index 7727305..0e03c59 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,6 @@ "plone.schemaeditor", "plone.supermodel", "plone.uuid", - "plone.z3cform", "z3c.form", "zope.annotation", "zope.cachedescriptors", diff --git a/src/plone/app/users/__init__.py b/src/plone/app/users/__init__.py index e69de29..b219755 100644 --- a/src/plone/app/users/__init__.py +++ b/src/plone/app/users/__init__.py @@ -0,0 +1,3 @@ +jbot_deprecations = { + "plone.app.users.browser.schema_layout.pt": "plone.app.layout.users.schema_layout.pt", +} diff --git a/src/plone/app/users/browser/configure.zcml b/src/plone/app/users/browser/configure.zcml index 2e8677d..75d53a1 100644 --- a/src/plone/app/users/browser/configure.zcml +++ b/src/plone/app/users/browser/configure.zcml @@ -68,21 +68,6 @@ permission="plone.app.controlpanel.UsersAndGroups" /> - - - - - - - -
- - - Site Setup - - -

Edit Member Form Fields

- -
- Portal status message -
- -
- - - - - -
- -
- - - diff --git a/src/plone/app/users/browser/schemaeditor.py b/src/plone/app/users/browser/schemaeditor.py index b753c14..71b8a9d 100644 --- a/src/plone/app/users/browser/schemaeditor.py +++ b/src/plone/app/users/browser/schemaeditor.py @@ -4,25 +4,32 @@ from plone.app.users.schema import SCHEMATA_KEY from plone.base import PloneMessageFactory as _ from plone.base.interfaces import IPloneSiteRoot -from plone.schemaeditor.browser.schema.listing import SchemaListing -from plone.schemaeditor.browser.schema.traversal import SchemaContext from plone.supermodel import loadString from plone.supermodel.model import finalizeSchemas from plone.supermodel.model import Model from plone.supermodel.model import SchemaClass from plone.supermodel.serializer import serialize -from plone.z3cform.layout import FormWrapper from Products.CMFCore.utils import getToolByName from Products.CMFPlone.utils import get_portal -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from zope.annotation.interfaces import IAnnotations from zope.component import getGlobalSiteManager -from zope.interface import implementer from zope.interface import Interface import copy import logging import re +import zope.deferredimport + +zope.deferredimport.initialize() + +zope.deferredimport.deprecated( + "Please use from plone.app.layout.users.schemaeditor import MemberSchemaContext instead.", + MemberSchemaContext="plone.app.layout.users.schemaeditor:MemberSchemaContext", +) +zope.deferredimport.deprecated( + "Please use from plone.app.layout.users.schemaeditor import SchemaListingPage instead.", + SchemaListingPage="plone.app.layout.users.schemaeditor:SchemaListingPage", +) ALLOWED_FIELDS = [ "zope.schema._bootstrapfields.TextLine", @@ -72,30 +79,6 @@ class IMemberSchemaContext(Interface): """ """ -class SchemaListingPage(FormWrapper): - form = SchemaListing - index = ViewPageTemplateFile("schema_layout.pt") - - -@implementer(IMemberSchemaContext) -class MemberSchemaContext(SchemaContext): - label = _("Edit Member Form Fields") - - def __init__(self, context, request): - self.fieldsWhichCannotBeDeleted = ["fullname", "email"] - self.showSaveDefaults = False - self.enableFieldsets = False - self.allowedFields = ALLOWED_FIELDS - - schema = getFromBaseSchema(IUserDataSchema) - super().__init__( - schema, - request, - name=SCHEMATA_KEY, - title=_("Member Fields"), - ) - - def updateSchema(object, event): snew_schema = serialize_ttw_schema(object.schema) applySchema(snew_schema) From 0bf9e0ee07f94337032ad5c134ecd2916256a23f Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Wed, 27 May 2026 17:27:17 +0200 Subject: [PATCH 04/12] move IMemberSchemaContext out of browser subpackage --- src/plone/app/users/browser/configure.zcml | 2 +- src/plone/app/users/browser/schemaeditor.py | 5 ----- src/plone/app/users/configure.zcml | 4 ++-- src/plone/app/users/schema.py | 4 ++++ 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/plone/app/users/browser/configure.zcml b/src/plone/app/users/browser/configure.zcml index 75d53a1..3f35a80 100644 --- a/src/plone/app/users/browser/configure.zcml +++ b/src/plone/app/users/browser/configure.zcml @@ -88,7 +88,7 @@ /> diff --git a/src/plone/app/users/browser/schemaeditor.py b/src/plone/app/users/browser/schemaeditor.py index 71b8a9d..085c8e7 100644 --- a/src/plone/app/users/browser/schemaeditor.py +++ b/src/plone/app/users/browser/schemaeditor.py @@ -13,7 +13,6 @@ from Products.CMFPlone.utils import get_portal from zope.annotation.interfaces import IAnnotations from zope.component import getGlobalSiteManager -from zope.interface import Interface import copy import logging @@ -75,10 +74,6 @@ def log(message, level="info", id="plone.app.users.browser.schemaeditor"): getattr(logger, level)(message) -class IMemberSchemaContext(Interface): - """ """ - - def updateSchema(object, event): snew_schema = serialize_ttw_schema(object.schema) applySchema(snew_schema) diff --git a/src/plone/app/users/configure.zcml b/src/plone/app/users/configure.zcml index 720ad29..cec2bd5 100644 --- a/src/plone/app/users/configure.zcml +++ b/src/plone/app/users/configure.zcml @@ -56,7 +56,7 @@ @@ -64,7 +64,7 @@ diff --git a/src/plone/app/users/schema.py b/src/plone/app/users/schema.py index d5d35a7..f9dd896 100644 --- a/src/plone/app/users/schema.py +++ b/src/plone/app/users/schema.py @@ -85,6 +85,10 @@ class IUserDataSchema(Interface): ) +class IMemberSchemaContext(Interface): + """ """ + + class IRegisterSchema(Interface): username = schema.ASCIILine( title=_("label_user_name", default="User Name"), From 266040662fe68f459b349ac128ca1d6699999884 Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Thu, 28 May 2026 16:42:26 +0200 Subject: [PATCH 05/12] move AccountPanelForm to plone.app.layout --- src/plone/app/users/__init__.py | 1 + src/plone/app/users/browser/account-panel.pt | 46 ----- src/plone/app/users/browser/account.py | 191 ++----------------- 3 files changed, 14 insertions(+), 224 deletions(-) delete mode 100644 src/plone/app/users/browser/account-panel.pt diff --git a/src/plone/app/users/__init__.py b/src/plone/app/users/__init__.py index b219755..2506efc 100644 --- a/src/plone/app/users/__init__.py +++ b/src/plone/app/users/__init__.py @@ -1,3 +1,4 @@ jbot_deprecations = { + "plone.app.users.browser.account-panel.pt": "plone.app.layout.users.account-panel.pt", "plone.app.users.browser.schema_layout.pt": "plone.app.layout.users.schema_layout.pt", } diff --git a/src/plone/app/users/browser/account-panel.pt b/src/plone/app/users/browser/account-panel.pt deleted file mode 100644 index ab4de86..0000000 --- a/src/plone/app/users/browser/account-panel.pt +++ /dev/null @@ -1,46 +0,0 @@ - - - -

${view/label}

-
- - -

${view/description}

-
- - -
- - - -
-
- - diff --git a/src/plone/app/users/browser/account.py b/src/plone/app/users/browser/account.py index 292333e..522bfec 100644 --- a/src/plone/app/users/browser/account.py +++ b/src/plone/app/users/browser/account.py @@ -4,33 +4,29 @@ from PIL import UnidentifiedImageError from plone.app.users.browser.interfaces import IAccountPanelForm from plone.app.users.browser.schemaeditor import getFromBaseSchema -from plone.app.users.utils import notifyWidgetActionExecutionError -from plone.autoform.form import AutoExtensibleForm from plone.base import PloneMessageFactory as _ from plone.base.interfaces import INavigationRoot from plone.base.interfaces import IPloneSiteRoot from plone.base.interfaces import ISecuritySchema from plone.base.utils import safe_text from plone.namedfile.file import NamedBlobImage -from plone.protect import CheckAuthenticator from plone.registry.interfaces import IRegistry from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.controlpanel.events import ConfigurationChangedEvent -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.PlonePAS.tools.membership import default_portrait -from Products.statusmessages.interfaces import IStatusMessage -from z3c.form import button -from z3c.form import form from z3c.form.interfaces import NOT_CHANGED from zope import schema -from zope.cachedescriptors.property import Lazy as lazy_property -from zope.component import getMultiAdapter from zope.component import getUtility from zope.component import provideAdapter -from zope.event import notify from zope.globalrequest import getRequest -from zope.interface import implementer -from ZTUtils import make_query + +import zope.deferredimport + +zope.deferredimport.initialize() + +zope.deferredimport.deprecated( + "Please use from plone.app.layout.users.account import AccountPanelForm instead.", + AccountPanelForm="plone.app.layout.users.account:AccountPanelForm", +) MESSAGE_EMAIL_CANNOT_CHANGE = _( "message_email_cannot_change", @@ -199,70 +195,10 @@ def timezone(self, value): return self._setProperty("timezone", value) -@implementer(IAccountPanelForm) -class AccountPanelForm(AutoExtensibleForm, form.Form): - """A simple form to be used as a basis for account panel screens.""" - - schema = IAccountPanelForm - template = ViewPageTemplateFile("account-panel.pt") - enableCSRFProtection = True - - hidden_widgets = [] - successMessage = _("Changes saved.") - noChangesMessage = _("No changes made.") - - @lazy_property - def member(self): - mtool = getToolByName(self.context, "portal_membership") - if self.request.get("userid"): - return mtool.getMemberById(self.request.get("userid")) - return mtool.getAuthenticatedMember() - - @property - def label(self): - return self.member.getProperty("fullname") or self.member.getUserName() - - def _differentEmail(self, email): - """Check if the submitted form email address differs from the existing - one. +class AccountPanelValidation: - Keeping your email the same (which happens when you change something - else on the personalize form) or changing it back to your login name, - is fine. - - So: we only return True if it is *really* a different email. - """ - membership = getToolByName(self.context, "portal_membership") - if self.request.get("userid"): - member = membership.getMemberById(self.request.get("userid")) - else: - member = membership.getAuthenticatedMember() - if email in (member.getId(), member.getUserName()): - return False - # By default, PAS transforms login names to lowercase, at least when - # email-as-login is used. So compare the transformed/normalized names. - pas = getToolByName(self.context, "acl_users") - email_normalized = pas.applyTransform(email) - # The user name should already have been normalized, but let's make sure. - login_normalized = pas.applyTransform(member.getUserName()) - return email_normalized != login_normalized - - def makeQuery(self): - userid = self.request.form.get("userid", None) - if userid is not None: - return "?{}".format(make_query({"userid": userid})) - return "" - - def action(self): - return self.request.getURL() + self.makeQuery() - - def validate_email(self, action, data): + def validate_email(self, data): context = aq_inner(self.context) - error_keys = [error.field.getName() for error in action.form.widgets.errors] - if "email" in error_keys: - # There is already a validation error for email, - # so there is no need for further validation. - return # We only need an extra check if email is used as login. registry = getUtility(IRegistry) security_settings = registry.forInterface(ISecuritySchema, prefix="plone") @@ -291,17 +227,13 @@ def validate_email(self, action, data): # Only allow if unchanged: then that member is us! if self._differentEmail(email): err_str = MESSAGE_EMAIL_IN_USE - if err_str: - notifyWidgetActionExecutionError(action, "email", err_str) + return err_str def validate_portrait(self, action, data): """Portrait validation. Checks if image is supported by Pillow. SVG files are not yet supported. """ - error_keys = [error.field.getName() for error in action.form.widgets.errors] - if "portrait" in error_keys: - return portrait_file = data["portrait"] if portrait_file is None or portrait_file is NOT_CHANGED: return @@ -309,103 +241,6 @@ def validate_portrait(self, action, data): try: Image.open(portrait) except UnidentifiedImageError: - notifyWidgetActionExecutionError( - action, "portrait", MESSAGE_IMAGE_NOT_SUPPORTED - ) + return MESSAGE_IMAGE_NOT_SUPPORTED except Exception as exc: raise exc - - @button.buttonAndHandler(_("Save")) - def handleSave(self, action): - CheckAuthenticator(self.request) - data, errors = self.extractData() - - # Extra validation for email, when it is there. email is not in the - # data when you are at the personal-preferences page. - if "email" in data: - self.validate_email(action, data) - - # Validate portrait, upload image could be not supported - # by PIL what raises an exception when scaling image. - if "portrait" in data: - self.validate_portrait(action, data) - - if action.form.widgets.errors: - self.status = self.formErrorsMessage - return - if self.applyChanges(data): - IStatusMessage(self.request).addStatusMessage( - self.successMessage, type="info" - ) - notify(ConfigurationChangedEvent(self, data)) - self._on_save(data) - else: - IStatusMessage(self.request).addStatusMessage( - self.noChangesMessage, type="info" - ) - self.request.response.redirect(self.action()) - - def updateActions(self): - super().updateActions() - if self.actions and "save" in self.actions: - self.actions["save"].addClass("btn btn-primary") - - @button.buttonAndHandler(_("Cancel")) - def cancel(self, action): - IStatusMessage(self.request).addStatusMessage( - _("Changes canceled."), type="info" - ) - self.request.response.redirect( - "{}{}".format(self.request["ACTUAL_URL"], self.makeQuery()) - ) - - def _on_save(self, data=None): - pass - - def prepareObjectTabs(self, default_tab="view", sort_first=["folderContents"]): - context = self.context - mt = getToolByName(context, "portal_membership") - tabs = [] - navigation_root_url = context.absolute_url() - - def _check_allowed(context, request, name): - """Check, if user has required permissions on view.""" - view = getMultiAdapter((context, request), name=name) - allowed = True - for perm in view.__ac_permissions__: - allowed = allowed and mt.checkPermission(perm[0], context) - return allowed - - if _check_allowed(context, self.request, "personal-information"): - tabs.append( - { - "title": _( - "title_personal_information_form", "Personal Information" - ), - "url": navigation_root_url + "/@@personal-information", - "selected": (self.__name__ == "personal-information"), - "id": "user_data-personal-information", - } - ) - - if _check_allowed(context, self.request, "personal-preferences"): - tabs.append( - { - "title": _("Personal Preferences"), - "url": navigation_root_url + "/@@personal-preferences", - "selected": (self.__name__ == "personal-preferences"), - "id": "user_data-personal-preferences", - } - ) - - member = mt.getAuthenticatedMember() - if member.canPasswordSet(): - tabs.append( - { - "title": _("label_password", "Password"), - "url": navigation_root_url + "/@@change-password", - "selected": (self.__name__ == "change-password"), - "id": "user_data-change-password", - } - ) - return tabs From 30d118a99e199710d8e8b35852a4ef9c29c5bf82 Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Fri, 29 May 2026 14:53:19 +0200 Subject: [PATCH 06/12] don't require form to test underlying implementation of its methods --- .../users/tests/test_loginname_generator.py | 14 +++---- .../app/users/tests/test_userid_generator.py | 40 +++++++++---------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/plone/app/users/tests/test_loginname_generator.py b/src/plone/app/users/tests/test_loginname_generator.py index 563bdf1..8e566da 100644 --- a/src/plone/app/users/tests/test_loginname_generator.py +++ b/src/plone/app/users/tests/test_loginname_generator.py @@ -1,7 +1,7 @@ # Note: test setup somehow fails when only tests from this file are run. from plone.app.users.browser.interfaces import ILoginNameGenerator -from plone.app.users.browser.register import BaseRegistrationForm from plone.app.users.tests.base import BaseTestCase +from plone.app.users.utils import generate_login_name from zope.component import getSiteManager @@ -11,12 +11,11 @@ def test_custom_generator(self): behavior. """ sm = getSiteManager(context=self.portal) - form = BaseRegistrationForm(self.portal, {}) data = {"useme": "me me me", "username": "frank"} sm.registerUtility(lambda data: data["useme"], provided=ILoginNameGenerator) - self.assertEqual(form.generate_login_name(data), "me me me") + self.assertEqual(generate_login_name(self.portal, data), "me me me") self.assertEqual(data.get("login_name"), "me me me") def test_custom_generator_empty(self): @@ -24,32 +23,29 @@ def test_custom_generator_empty(self): returns an empty value. """ sm = getSiteManager(context=self.portal) - form = BaseRegistrationForm(self.portal, {}) data = {"useme": "", "username": "Frank"} sm.registerUtility(lambda data: data["useme"], provided=ILoginNameGenerator) - self.assertEqual(form.generate_login_name(data), "Frank") + self.assertEqual(generate_login_name(self.portal, data), "Frank") self.assertEqual(data.get("login_name"), "Frank") def test_use_email_as_login_disabled(self): """Test generating user_id with no custom login name generator and with the use_email_as_login security setting disabled. """ - form = BaseRegistrationForm(self.portal, {}) data = {"username": "Frank"} self.security_settings.use_email_as_login = False - self.assertEqual(form.generate_login_name(data), "Frank") + self.assertEqual(generate_login_name(self.portal, data), "Frank") self.assertEqual(data.get("login_name"), "Frank") def test_use_email_as_login_enabled(self): """Test generating user_id with no custom login name generator and with the use_email_as_login security setting enabled. """ - form = BaseRegistrationForm(self.portal, {}) data = {"username": "Frank", "email": "Frank@Test.com"} self.security_settings.use_email_as_login = True - self.assertEqual(form.generate_login_name(data), "frank@test.com") + self.assertEqual(generate_login_name(self.portal, data), "frank@test.com") self.assertEqual(data.get("login_name"), "frank@test.com") diff --git a/src/plone/app/users/tests/test_userid_generator.py b/src/plone/app/users/tests/test_userid_generator.py index 43e2237..bfbad78 100644 --- a/src/plone/app/users/tests/test_userid_generator.py +++ b/src/plone/app/users/tests/test_userid_generator.py @@ -1,7 +1,7 @@ # Note: test setup somehow fails when only tests from this file are run. from plone.app.users.browser.interfaces import IUserIdGenerator -from plone.app.users.browser.register import BaseRegistrationForm from plone.app.users.tests.base import BaseTestCase +from plone.app.users.utils import generate_user_id from plone.app.users.utils import uuid_userid_generator from zope.component import getSiteManager @@ -17,38 +17,36 @@ def one_generator(data): sm = getSiteManager(context=self.portal) sm.registerUtility(one_generator, provided=IUserIdGenerator) - form = BaseRegistrationForm(self.portal, {}) data = {} - self.assertEqual(form.generate_user_id(data), "one") + self.assertEqual(generate_user_id(self.portal, data), "one") self.assertEqual(data.get("user_id"), "one") data = {"username": "joe", "fullname": "Joe User", "email": "joe@example.org"} - self.assertEqual(form.generate_user_id(data), "one") + self.assertEqual(generate_user_id(self.portal, data), "one") self.assertEqual(data.get("user_id"), "one") def test_default(self): """Test generating user_id with no custom user id generator and the default security settings. """ - form = BaseRegistrationForm(self.portal, {}) data = {} - self.assertEqual(form.generate_user_id(data), "") + self.assertEqual(generate_user_id(self.portal, data), "") self.assertEqual(data.get("user_id"), "") # An explicit username is taken by default. data = {"username": "joe", "fullname": "Joe User", "email": "joe@example.org"} - self.assertEqual(form.generate_user_id(data), "joe") + self.assertEqual(generate_user_id(self.portal, data), "joe") self.assertEqual(data.get("user_id"), "joe") # When no username is there, we try a normalized fullname. data = {"fullname": "Joe User", "email": "joe@example.org"} - self.assertEqual(form.generate_user_id(data), "joe-user") + self.assertEqual(generate_user_id(self.portal, data), "joe-user") self.assertEqual(data.get("user_id"), "joe-user") # With no fullname, we take the email. data = {"email": "Joe@example.org"} - self.assertEqual(form.generate_user_id(data), "Joe@example.org") + self.assertEqual(generate_user_id(self.portal, data), "Joe@example.org") self.assertEqual(data.get("user_id"), "Joe@example.org") def test_use_email_as_login_has_fullname(self): @@ -56,14 +54,13 @@ def test_use_email_as_login_has_fullname(self): enabled and full name is provided. """ self.security_settings.use_email_as_login = True - form = BaseRegistrationForm(self.portal, {}) data = {} - self.assertEqual(form.generate_user_id(data), "") + self.assertEqual(generate_user_id(self.portal, data), "") self.assertEqual(data.get("user_id"), "") data = {"fullname": "Joe User", "email": "joe@example.org"} - self.assertEqual(form.generate_user_id(data), "joe-user") + self.assertEqual(generate_user_id(self.portal, data), "joe-user") self.assertEqual(data.get("user_id"), "joe-user") def test_use_email_as_login_no_fullname(self): @@ -71,14 +68,13 @@ def test_use_email_as_login_no_fullname(self): enabled and full name is not provided. """ self.security_settings.use_email_as_login = True - form = BaseRegistrationForm(self.portal, {}) data = {} - self.assertEqual(form.generate_user_id(data), "") + self.assertEqual(generate_user_id(self.portal, data), "") self.assertEqual(data.get("user_id"), "") data = {"email": "joe@example.org"} - self.assertEqual(form.generate_user_id(data), "joe@example.org") + self.assertEqual(generate_user_id(self.portal, data), "joe@example.org") self.assertEqual(data.get("user_id"), "joe@example.org") def test_use_email_as_login_no_fullname_uppercase_email(self): @@ -86,14 +82,13 @@ def test_use_email_as_login_no_fullname_uppercase_email(self): enabled and full name is not provided, with an uppercase e-mail. """ self.security_settings.use_email_as_login = True - form = BaseRegistrationForm(self.portal, {}) data = {} - self.assertEqual(form.generate_user_id(data), "") + self.assertEqual(generate_user_id(self.portal, data), "") self.assertEqual(data.get("user_id"), "") data = {"email": "Joe@Example.org"} - self.assertEqual(form.generate_user_id(data), "joe@example.org") + self.assertEqual(generate_user_id(self.portal, data), "joe@example.org") self.assertEqual(data.get("user_id"), "joe@example.org") def test_use_uuid_as_userid_enabled(self): @@ -101,19 +96,20 @@ def test_use_uuid_as_userid_enabled(self): enabled. """ self.security_settings.use_uuid_as_userid = True - form = BaseRegistrationForm(self.portal, {}) data = {} - user_id = form.generate_user_id(data) + user_id = generate_user_id(self.portal, data) self.assertEqual(data.get("user_id"), user_id) self.assertEqual(len(data.get("user_id")), len(uuid_userid_generator())) data = {"username": "joe", "fullname": "Joe User", "email": "joe@example.org"} - user_id = form.generate_user_id(data) + user_id = generate_user_id(self.portal, data) self.assertNotEqual(user_id, "joe") self.assertEqual(data.get("user_id"), user_id) self.assertEqual(len(user_id), len(uuid_userid_generator())) # Calling it twice should give a different result, as every # call to the uuid generator should be unique. - self.assertNotEqual(form.generate_user_id(data), form.generate_user_id(data)) + self.assertNotEqual( + generate_user_id(self.portal, data), generate_user_id(self.portal, data) + ) From 9f5c23e1146b571fc580d12f6dd0c474b848d3ec Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Fri, 29 May 2026 14:54:57 +0200 Subject: [PATCH 07/12] move user registration forms to plone.app.layout --- setup.py | 1 - src/plone/app/users/__init__.py | 3 + src/plone/app/users/browser/configure.zcml | 22 - src/plone/app/users/browser/newuser_form.pt | 33 -- src/plone/app/users/browser/register.py | 575 +------------------ src/plone/app/users/browser/register_form.pt | 61 -- src/plone/app/users/browser/registered.pt | 157 ----- src/plone/app/users/browser/registered.py | 18 +- 8 files changed, 22 insertions(+), 848 deletions(-) delete mode 100644 src/plone/app/users/browser/newuser_form.pt delete mode 100644 src/plone/app/users/browser/register_form.pt delete mode 100644 src/plone/app/users/browser/registered.pt diff --git a/setup.py b/setup.py index 0e03c59..964213f 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ "plone.i18n", "plone.memoize", "plone.namedfile", - "plone.protect", "plone.registry", "plone.schema", "plone.schemaeditor", diff --git a/src/plone/app/users/__init__.py b/src/plone/app/users/__init__.py index 2506efc..e55c8b7 100644 --- a/src/plone/app/users/__init__.py +++ b/src/plone/app/users/__init__.py @@ -1,4 +1,7 @@ jbot_deprecations = { "plone.app.users.browser.account-panel.pt": "plone.app.layout.users.account-panel.pt", + "plone.app.users.browser.newuser_form.pt": "plone.app.layout.users.newuser_form.pt", + "plone.app.users.browser.registered.pt": "plone.app.layout.users.registered.pt", + "plone.app.users.browser.register_form.pt": "plone.app.layout.users.register_form.pt", "plone.app.users.browser.schema_layout.pt": "plone.app.layout.users.schema_layout.pt", } diff --git a/src/plone/app/users/browser/configure.zcml b/src/plone/app/users/browser/configure.zcml index 3f35a80..4704aab 100644 --- a/src/plone/app/users/browser/configure.zcml +++ b/src/plone/app/users/browser/configure.zcml @@ -11,28 +11,6 @@ file="permissions.zcml" /> - - - - - - - - - - - - - -
-

-
- - -
-
- -
- - diff --git a/src/plone/app/users/browser/register.py b/src/plone/app/users/browser/register.py index 806c2b4..3e90774 100644 --- a/src/plone/app/users/browser/register.py +++ b/src/plone/app/users/browser/register.py @@ -1,43 +1,26 @@ -from AccessControl import getSecurityManager from plone.app.users.browser.account import AccountPanelSchemaAdapter from plone.app.users.browser.account import getSchema -from plone.app.users.schema import IAddUserSchema from plone.app.users.schema import ICombinedRegisterSchema -from plone.app.users.schema import IRegisterSchema -from plone.app.users.utils import generate_login_name as _generate_login_name -from plone.app.users.utils import generate_user_id as _generate_user_id -from plone.app.users.utils import notifyWidgetActionExecutionError -from plone.autoform.form import AutoExtensibleForm -from plone.base import PloneMessageFactory as _ -from plone.base.interfaces import ISecuritySchema -from plone.base.interfaces import IUserGroupsSettingsSchema -from plone.protect import CheckAuthenticator -from plone.registry.interfaces import IRegistry -from Products.CMFCore.interfaces import ISiteRoot -from Products.CMFCore.permissions import ManagePortal -from Products.CMFCore.utils import getToolByName -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile -from Products.statusmessages.interfaces import IStatusMessage -from z3c.form import button -from z3c.form import field -from z3c.form import form -from z3c.form.browser.checkbox import CheckBoxFieldWidget -from z3c.form.interfaces import DISPLAY_MODE -from zExceptions import Forbidden -from ZODB.POSException import ConflictError -from zope.component import getAdapter -from zope.component import getMultiAdapter -from zope.component import getUtility from zope.deferredimport import deprecated -from zope.schema import getFieldNames - -import logging deprecated( "Import from plone.app.users.utils instead.", RENAME_AFTER_CREATION_ATTEMPTS="plone.app.users.utils:RENAME_AFTER_CREATION_ATTEMPTS", ) +deprecated( + "Please use from plone.app.layout.users.register import BaseRegistrationForm instead.", + BaseRegistrationForm="plone.app.layout.users.register:BaseRegistrationForm", +) +deprecated( + "Please use from plone.app.layout.users.register import RegistrationForm instead.", + RegistrationForm="plone.app.layout.users.register:RegistrationForm", +) +deprecated( + "Please use from plone.app.layout.users.register import AddUserForm instead.", + AddUserForm="plone.app.layout.users.register:AddUserForm", +) + def getRegisterSchema(): schema = getSchema( @@ -46,535 +29,3 @@ def getRegisterSchema(): form_name="On Registration", ) return schema - - -class BaseRegistrationForm(AutoExtensibleForm, form.Form): - """Form to be used as base for Register and Add User forms.""" - - label = "" - description = "" - formErrorsMessage = _("There were errors.") - ignoreContext = True - enableCSRFProtection = True - - # this attribute indicates if user was successfully registered - _finishedRegister = False - - @property - def schema(self): - return getRegisterSchema() - - def _get_security_settings(self): - """Return security settings from the registry.""" - registry = getUtility(IRegistry) - return registry.forInterface(ISecuritySchema, prefix="plone") - - def render(self): - if self._finishedRegister: - return self.context.unrestrictedTraverse("registered")() - - return super().render() - - def updateFields(self): - """Fields are dynamic in this form, to be able to handle - different join styles. - """ - settings = self._get_security_settings() - use_email_as_login = settings.use_email_as_login - - # Finally, let autoform process the schema and any FormExtenders do - # their thing - super().updateFields() - - if use_email_as_login: - self.fields["email"].field.description = _( - "help_email_creation_for_login", - default="Enter an email " - "address. This will be your login name. We respect your " - "privacy and will not give the address away to any third " - "parties or expose it anywhere.", - ) - del self.fields["username"] - else: - self.fields["email"].field.description = _( - "help_email_creation", - default="Enter an email address. This is necessary in case " - "the password is lost. We respect your privacy and " - "will not give the address away to any third parties " - "or expose it anywhere.", - ) - - # Change the password description based on PAS Plugin The user needs a - # list of instructions on what kind of password is required. We'll - # reuse password errors as instructions e.g. "Must contain a letter and - # a number". Assume PASPlugin errors are already translated - if self.fields.get("password", None): - registration = getToolByName(self.context, "portal_registration") - err_str = registration.testPasswordValidity("") - if err_str: - msg = _( - "help_password_creation_with_errors", - default="Enter your new password. ${errors}", - mapping=dict(errors=err_str), - ) - self.fields["password"].field.description = msg - - def updateActions(self): - super().updateActions() - self.actions["register"].addClass("btn-primary") - - def generate_user_id(self, data): - """Generate a user id from data. - - Delegates to the standalone function in plone.app.users.utils. - See :func:`plone.app.users.utils.generate_user_id` for details. - """ - return _generate_user_id(self.context, data) - - def generate_login_name(self, data): - """Generate a login name from data. - - Delegates to the standalone function in plone.app.users.utils. - See :func:`plone.app.users.utils.generate_login_name` for details. - """ - return _generate_login_name(self.context, data) - - # Actions validators - def validate_registration(self, action, data): - """Specific business logic for this join form. Note: all this logic - was taken directly from the old validate_registration.py script in - Products/CMFPlone/skins/plone_login/join_form_validate.vpy - """ - - # CSRF protection - CheckAuthenticator(self.request) - - registration = getToolByName(self.context, "portal_registration") - - error_keys = [error.field.getName() for error in action.form.widgets.errors] - - form_field_names = [f for f in self.fields] - - portal = getUtility(ISiteRoot) - - # passwords should match - if "password" in form_field_names: - assert "password_ctl" in form_field_names - # Skip this check if password fields already have an error - if not ("password" in error_keys or "password_ctl" in error_keys): - password = data.get("password") - password_ctl = data.get("password_ctl") - if password != password_ctl: - err_str = _("Passwords do not match.") - notifyWidgetActionExecutionError(action, "password", err_str) - notifyWidgetActionExecutionError(action, "password_ctl", err_str) - - # Password field checked against RegistrationTool - if "password" in form_field_names: - # Skip this check if password fields already have an error - if "password" not in error_keys: - password = data.get("password") - if password: - # Use PAS to test validity - err_str = registration.testPasswordValidity(password) - if err_str: - notifyWidgetActionExecutionError(action, "password", err_str) - - settings = self._get_security_settings() - if settings.use_email_as_login: - username_field = "email" - else: - username_field = "username" - - # The term 'username' is not clear. It may be the user id or - # the login name. So here we try to be explicit. - - # Generate a nice user id and store that in the data. - user_id = self.generate_user_id(data) - # Generate a nice login name and store that in the data. - login_name = self.generate_login_name(data) - - # Do several checks to see if the user id and the login name - # are valid. - # - # Skip these checks if username was already in error list. - # - # Note that if we cannot generate a unique user id, it is not - # necessarily the fault of the username field, but it - # certainly is the most likely cause in a standard Plone - # setup. - - # check if username is valid - # Skip this check if username was already in error list - if username_field not in error_keys: - # user id may not be the same as the portal id. - if user_id == portal.getId(): - err_str = _( - "This username is reserved. Please choose a different name." - ) - notifyWidgetActionExecutionError(action, username_field, err_str) - - # Check if user id is allowed by the member id pattern. - if username_field not in error_keys: - if not registration.isMemberIdAllowed(user_id): - err_str = _( - "The login name you selected is already in use " - "or is not valid. Please choose another." - ) - notifyWidgetActionExecutionError(action, username_field, err_str) - - if username_field not in error_keys: - # Check the uniqueness of the login name, not only when - # use_email_as_login is true, but always. - pas = getToolByName(self, "acl_users") - results = pas.searchUsers(name=login_name, exact_match=True) - if results: - err_str = _( - "The login name you selected is already in use " - "or is not valid. Please choose another." - ) - notifyWidgetActionExecutionError(action, username_field, err_str) - - if "password" in form_field_names and "password" not in error_keys: - # Admin can either set a password or mail the user (or both). - if not (data["password"] or data["mail_me"]): - err_str = _( - "msg_no_password_no_mail_me", - default="You must set a password or choose to send an email.", - ) - - # set error on password field - notifyWidgetActionExecutionError(action, "password", err_str) - notifyWidgetActionExecutionError(action, "mail_me", err_str) - - @button.buttonAndHandler(_("label_register", default="Register"), name="register") - def action_join(self, action): - data, errors = self.extractData() - - # extra password validation - self.validate_registration(action, data) - - if action.form.widgets.errors: - self.status = self.formErrorsMessage - return - - self.handle_join_success(data) - - # XXX Return somewhere else, depending on what - # handle_join_success returns? - came_from = self.request.form.get("came_from") - if came_from: - utool = getToolByName(self.context, "portal_url") - if utool.isURLInPortal(came_from): - self.request.response.redirect(came_from) - return "" - - def handle_join_success(self, data): - # portal should be acquisition wrapped, this is needed for the schema - # adapter below - portal = getToolByName(self.context, "portal_url").getPortalObject() - registration = getToolByName(self.context, "portal_registration") - - # user_id and login_name should be in the data, but let's be safe. - user_id = data.get("user_id", data.get("username")) - login_name = data.get("login_name", data.get("username")) - - # Set the username for good measure, as some code may expect - # it to exist and contain the user id. - data["username"] = user_id - - # The login name may already be in the form, but not - # necessarily, for example when using email as login. This is - # at least needed for logging in immediately when password - # reset is bypassed. We need the login name here, not the - # user id. - self.request.form["form.username"] = login_name - - password = data.get("password") or registration.generatePassword() - - try: - registration.addMember(user_id, password, REQUEST=self.request) - except (AttributeError, ValueError) as err: - logging.exception(err) - IStatusMessage(self.request).addStatusMessage(err, type="error") - self._finishedRegister = False - return - - if user_id != login_name: - # The user id differs from the login name. Set the login - # name correctly. - pas = getToolByName(self.context, "acl_users") - pas.updateLoginName(user_id, login_name) - - # set member properties - self.applyProperties(user_id, data) - - settings = self._get_security_settings() - self._finishedRegister = True - if data.get("mail_me") or ( - not settings.enable_user_pwd_choice and not data.get("password") - ): - # We want to validate the email address (users cannot - # select their own passwords on the register form) or the - # admin has explicitly requested to send an email on the - # 'add new user' form. - try: - # When all goes well, this call actually returns the - # rendered mail_password_response template. As a side - # effect, this removes any status messages added to - # the request so far, as they are already shown in - # this template. - response = registration.registeredNotify(user_id) - return response - except ConflictError: - # Let Zope handle this exception. - raise - except Exception as err: - logging.exception(err) - ctrlOverview = getMultiAdapter( - (portal, self.request), name="overview-controlpanel" - ) - mail_settings_correct = not ctrlOverview.mailhost_warning() - if mail_settings_correct: - # The email settings are correct, so the most - # likely cause of an error is a wrong email - # address. We remove the account: - # Remove the account: - self.context.acl_users.userFolderDelUsers( - [user_id], REQUEST=self.request - ) - self._finishedRegister = False - IStatusMessage(self.request).addStatusMessage( - _( - "status_fatal_password_mail", - default="Failed to create your account: we were " - "unable to send instructions for setting a password " - "to your email address: ${address}", - mapping={"address": data.get("email", "")}, - ), - type="error", - ) - else: - # This should only happen when an admin registers - # a user. The admin should have seen a warning - # already, but we warn again for clarity. - IStatusMessage(self.request).addStatusMessage( - _( - "status_nonfatal_password_mail", - default="This account has been created, but we " - "were unable to send instructions for setting a " - "password to this email address: ${address}", - mapping={"address": data.get("email", "")}, - ), - type="warning", - ) - - def applyProperties(self, userid, data): - portal = getToolByName(self.context, "portal_url").getPortalObject() - mt = getToolByName(self.context, "portal_membership") - member = mt.getMemberById(userid) - - # cache adapters - adapters = {} - - # Set any fields that are simply properties for the new user, rather - # than fields to help create the new user - register_fields = getFieldNames(IRegisterSchema) + getFieldNames(IAddUserSchema) - for k, value in data.items(): - # skip fields not available in the schema - if k in ["login_name", "user_id"]: - continue - - # skip fields that are handled exclusively on user registration and - # are not part of personal information form - if k in register_fields: - continue - - # get schema adapter - schema = self.fields[k].field.interface - if schema in adapters: - adapter = adapters[schema] - else: - adapters[schema] = adapter = getAdapter(portal, schema) - adapter.context = member - adapter.schema = schema - - # finally set value - setattr(adapter, k, value) - - -class RegistrationForm(BaseRegistrationForm): - """Dynamically get fields from user data, through admin config settings.""" - - label = _("heading_registration_form", default="Registration form") - description = "" - template = ViewPageTemplateFile("register_form.pt") - - @property - def showForm(self): - """The form should not be displayed to the user if the system is - incapable of sending emails and email validation is switched on - (users are not allowed to select their own passwords). - """ - portal = getUtility(ISiteRoot) - ctrlOverview = getMultiAdapter( - (portal, self.request), name="overview-controlpanel" - ) - - settings = self._get_security_settings() - # hide form if mailhost_warning == True and - # enable_user_pwd_choice == False - return not ( - ctrlOverview.mailhost_warning() and not settings.enable_user_pwd_choice - ) - - def updateFields(self): - if not self.showForm: - # We do not want to spend time calculating fields that - # will never get displayed. - return - - super().updateFields() - defaultFields = field.Fields(self.fields) - - # Can the user actually set his/her own password? - settings = self._get_security_settings() - if not settings.enable_user_pwd_choice: - # No? Remove the password fields. - defaultFields = defaultFields.omit("password", "password_ctl") - else: - # The portal is not interested in validating emails, and - # the user is not interested in getting an email with a - # link to set his password if he can set this password in - # the current form already. - defaultFields = defaultFields.omit("mail_me") - - self.fields = defaultFields - - def updateWidgets(self): - if not self.showForm: - # We do not want to spend time calculating widgets that - # will never get displayed. - return - - super().updateWidgets() - settings = self._get_security_settings() - if not settings.enable_user_pwd_choice: - # Show a message indicating that a password reset link - # will be mailed to the user. - widget = self.widgets["mail_me"] - widget.mode = DISPLAY_MODE - widget.value = ["selected"] - widget.label = _( - "label_password_change_mail", - default="A URL will be generated and e-mailed to you; " - "follow the link to reach a page where you can " - "change your password and complete the registration " - "process.", - ) - widget.terms = None - widget.updateTerms() - - -class AddUserForm(BaseRegistrationForm): - label = _("heading_add_user_form", default="Add New User") - description = "" - template = ViewPageTemplateFile("newuser_form.pt") - - def updateFields(self): - super().updateFields() - defaultFields = field.Fields(self.fields) - - # The mail_me field needs special handling depending on the - # enable_user_pwd_choice setting and on the correctness of the mail - # settings. - portal = getUtility(ISiteRoot) - ctrlOverview = getMultiAdapter( - (portal, self.request), name="overview-controlpanel" - ) - mail_settings_correct = not ctrlOverview.mailhost_warning() - if mail_settings_correct: - # Make the password fields optional: either specify a - # password or mail the user (or both). The validation - # will check that at least one of the options is chosen. - defaultFields["password"].field.required = False - defaultFields["password_ctl"].field.required = False - settings = self._get_security_settings() - defaultFields["mail_me"].field.default = not settings.enable_user_pwd_choice - - # Append the manager-focused fields - registry = getUtility(IRegistry) - user_group_settings = registry.forInterface( - IUserGroupsSettingsSchema, prefix="plone" - ) - many_groups = user_group_settings.many_groups - if not many_groups: - allFields = defaultFields + field.Fields(IAddUserSchema) - allFields["groups"].widgetFactory = CheckBoxFieldWidget - else: - allFields = defaultFields - self.fields = allFields - - def updateWidgets(self): - super().updateWidgets() - - # set display mode for mail_me field if no mailhost is configured - portal = getUtility(ISiteRoot) - ctrlOverview = getMultiAdapter( - (portal, self.request), name="overview-controlpanel" - ) - mail_settings_correct = not ctrlOverview.mailhost_warning() - if not mail_settings_correct: - widget = self.widgets["mail_me"] - widget.mode = DISPLAY_MODE - widget.value = ["selected"] - widget.label = _( - "label_cant_mail_password_reset", - default="Normally we would offer to send the user an email " - "with instructions to set a password on completion " - "of this form. But this site does not have a valid " - "email setup. You can fix this in the Mail settings.", - ) - widget.terms = None - widget.updateTerms() - - @button.buttonAndHandler(_("label_register", default="Register"), name="register") - def action_join(self, action): - data, errors = self.extractData() - - # extra password validation - self.validate_registration(action, data) - - if action.form.widgets.errors: - self.status = self.formErrorsMessage - return - - self.handle_join_success(data) - - if not self._finishedRegister: - return - - portal_groups = getToolByName(self.context, "portal_groups") - user_id = data["user_id"] - is_zope_manager = getSecurityManager().checkPermission( - ManagePortal, - self.context, - ) - try: - # Add user to the selected group(s) - if data.get("groups", None) is not None: - for groupname in data["groups"]: - group = portal_groups.getGroupById(groupname) - if "Manager" in group.getRoles() and not is_zope_manager: - raise Forbidden - portal_groups.addPrincipalToGroup(user_id, groupname, self.request) - except (AttributeError, ValueError) as err: - IStatusMessage(self.request).addStatusMessage(err, type="error") - return - - IStatusMessage(self.request).addStatusMessage(_("User added."), type="info") - self.request.response.redirect( - self.context.absolute_url() - + "/@@usergroup-userprefs?searchstring=" - + user_id - ) diff --git a/src/plone/app/users/browser/register_form.pt b/src/plone/app/users/browser/register_form.pt deleted file mode 100644 index 384761c..0000000 --- a/src/plone/app/users/browser/register_form.pt +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - diff --git a/src/plone/app/users/browser/registered.pt b/src/plone/app/users/browser/registered.pt deleted file mode 100644 index f2b63f7..0000000 --- a/src/plone/app/users/browser/registered.pt +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - -
- Cookie authentication has been disabled. -
- -
- -

- Welcome! -

- -
- You have been registered. -
- -
-

- You will receive an e-mail shortly containing a URL that will allow you to - set your password. When you receive this e-mail, please follow the link to - complete your registration process. NOTE: The password reset URL will expire - on - . If your - password reset request expires, you may use the "Forgot your password" link - to generate another password reset request. -

- - -

- Click the button to log in immediately. -

- - - - -
- - - - - - - - - - - -
-
-
- -
- -
- - diff --git a/src/plone/app/users/browser/registered.py b/src/plone/app/users/browser/registered.py index bfc5c8b..a53c597 100644 --- a/src/plone/app/users/browser/registered.py +++ b/src/plone/app/users/browser/registered.py @@ -1,14 +1,8 @@ -from Products.CMFCore.utils import getToolByName -from Products.Five import BrowserView -from zope.component import getMultiAdapter +import zope.deferredimport -import datetime +zope.deferredimport.initialize() - -class RegisteredView(BrowserView): - def expire_date(self): - ppr = getToolByName(self.context, "portal_password_reset") - expire_length = datetime.timedelta(days=ppr.getExpirationTimeout()) - expiration_date = datetime.datetime.now() + expire_length - ploneview = getMultiAdapter((self.context, self.request), name="plone") - return ploneview.toLocalizedTime(expiration_date, long_format=1) +zope.deferredimport.deprecated( + "Please use from plone.app.layout.users.registered import RegisteredView instead.", + RegisteredView="plone.app.layout.users.registered:RegisteredView", +) From 6c7028353b415f2021f79537a41c252bc9975488 Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Fri, 29 May 2026 15:33:17 +0200 Subject: [PATCH 08/12] move memberregistration.pt to plone.app.layout --- src/plone/app/users/__init__.py | 1 + .../app/users/browser/memberregistration.pt | 57 ------------------- 2 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 src/plone/app/users/browser/memberregistration.pt diff --git a/src/plone/app/users/__init__.py b/src/plone/app/users/__init__.py index e55c8b7..53b36f1 100644 --- a/src/plone/app/users/__init__.py +++ b/src/plone/app/users/__init__.py @@ -1,5 +1,6 @@ jbot_deprecations = { "plone.app.users.browser.account-panel.pt": "plone.app.layout.users.account-panel.pt", + "plone.app.users.browser.memberregistration.pt": "plone.app.layout.users.memberregistration.pt", "plone.app.users.browser.newuser_form.pt": "plone.app.layout.users.newuser_form.pt", "plone.app.users.browser.registered.pt": "plone.app.layout.users.registered.pt", "plone.app.users.browser.register_form.pt": "plone.app.layout.users.register_form.pt", diff --git a/src/plone/app/users/browser/memberregistration.pt b/src/plone/app/users/browser/memberregistration.pt deleted file mode 100644 index a2d21de..0000000 --- a/src/plone/app/users/browser/memberregistration.pt +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - -
-

Users and Groups

- -
- Portal status message -
- - -
-
- -
- -
- - From 19995dd912910373602948d224a050ec74e1df1c Mon Sep 17 00:00:00 2001 From: Thomas Lotze Date: Fri, 29 May 2026 16:01:02 +0200 Subject: [PATCH 09/12] move password panel to plone.app.layout --- setup.py | 2 - src/plone/app/users/browser/configure.zcml | 13 -- src/plone/app/users/browser/passwordpanel.py | 156 ++----------------- 3 files changed, 16 insertions(+), 155 deletions(-) diff --git a/setup.py b/setup.py index 964213f..ace9433 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,6 @@ "Pillow", "Products.GenericSetup", "Products.PlonePAS >= 5.0.1", - "Products.statusmessages", "Zope", "plone.autoform >= 1.2", "plone.app.event", @@ -73,7 +72,6 @@ "plone.uuid", "z3c.form", "zope.annotation", - "zope.cachedescriptors", "zope.component", "zope.event", "zope.interface", diff --git a/src/plone/app/users/browser/configure.zcml b/src/plone/app/users/browser/configure.zcml index 4704aab..8a6f79c 100644 --- a/src/plone/app/users/browser/configure.zcml +++ b/src/plone/app/users/browser/configure.zcml @@ -11,13 +11,6 @@ file="permissions.zcml" /> - - - - Date: Fri, 29 May 2026 16:18:18 +0200 Subject: [PATCH 10/12] move personal preferences UI to plone.app.layout --- src/plone/app/users/__init__.py | 1 + .../app/users/browser/account-configlet.pt | 61 ------------------ src/plone/app/users/browser/configure.zcml | 38 ----------- .../app/users/browser/personalpreferences.py | 61 ++++-------------- src/plone/app/users/browser/userdatapanel.py | 64 ++++--------------- .../app/users/tests/test_user_data_panel.py | 25 -------- 6 files changed, 27 insertions(+), 223 deletions(-) delete mode 100644 src/plone/app/users/browser/account-configlet.pt delete mode 100644 src/plone/app/users/tests/test_user_data_panel.py diff --git a/src/plone/app/users/__init__.py b/src/plone/app/users/__init__.py index 53b36f1..0403d8e 100644 --- a/src/plone/app/users/__init__.py +++ b/src/plone/app/users/__init__.py @@ -1,4 +1,5 @@ jbot_deprecations = { + "plone.app.users.browser.account-configlet.pt": "plone.app.layout.users.account-configlet.pt", "plone.app.users.browser.account-panel.pt": "plone.app.layout.users.account-panel.pt", "plone.app.users.browser.memberregistration.pt": "plone.app.layout.users.memberregistration.pt", "plone.app.users.browser.newuser_form.pt": "plone.app.layout.users.newuser_form.pt", diff --git a/src/plone/app/users/browser/account-configlet.pt b/src/plone/app/users/browser/account-configlet.pt deleted file mode 100644 index dcd0585..0000000 --- a/src/plone/app/users/browser/account-configlet.pt +++ /dev/null @@ -1,61 +0,0 @@ - - -
-
-

User Details

- -
- Portal status message -
-
- - -
- - diff --git a/src/plone/app/users/browser/configure.zcml b/src/plone/app/users/browser/configure.zcml index 8a6f79c..a4c6f51 100644 --- a/src/plone/app/users/browser/configure.zcml +++ b/src/plone/app/users/browser/configure.zcml @@ -6,38 +6,6 @@ > - - - - - - - - - - - alert("userid")'}) - form = UserDataPanel(portal, request) - description = translate(form.description, context=request) - self.assertTrue("