From bc29368009dfc0719adab723cfddd9675a3c79ea Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Sun, 2 Feb 2025 06:15:48 +0100 Subject: [PATCH] feat: add a @userschema/registration endpoint (#1874) * feat: add a @registration-userschema endpoint * Update docs/source/endpoints/userschema.md Co-authored-by: Steve Piercy * Update docs/source/endpoints/userschema.md Co-authored-by: Steve Piercy * Update docs/source/endpoints/userschema.md Co-authored-by: Steve Piercy * Update news/1873.feature Co-authored-by: Steve Piercy * change changlog * change changlog * Update news/1873.feature Co-authored-by: Steve Piercy * Update src/plone/restapi/services/userschema/configure.zcml Co-authored-by: Steve Piercy * Update src/plone/restapi/tests/test_documentation.py Co-authored-by: Steve Piercy * Update src/plone/restapi/tests/test_services_userschema.py Co-authored-by: Steve Piercy * rename test files * rename * Remove misleading note about Plone 5 * Clarify profile schema vs registration schema --------- Co-authored-by: Steve Piercy Co-authored-by: David Glick --- docs/source/endpoints/userschema.md | 31 ++++++-- news/1873.feature | 1 + src/plone/restapi/services/userschema/user.py | 31 +++++++- .../http-examples/userschema_registration.req | 3 + .../userschema_registration.resp | 69 +++++++++++++++++ src/plone/restapi/tests/test_documentation.py | 5 ++ .../restapi/tests/test_services_userschema.py | 74 ++++++++++++++++--- 7 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 news/1873.feature create mode 100644 src/plone/restapi/tests/http-examples/userschema_registration.req create mode 100644 src/plone/restapi/tests/http-examples/userschema_registration.resp diff --git a/docs/source/endpoints/userschema.md b/docs/source/endpoints/userschema.md index a59f582c92..a1e86121cd 100644 --- a/docs/source/endpoints/userschema.md +++ b/docs/source/endpoints/userschema.md @@ -9,26 +9,22 @@ myst: # User schema -```{note} - This is only available on Plone 5. -``` - Users in Plone have a set of properties defined by a default set of fields such as `fullname`, `email`, `portrait`, and so on. These properties define the site user's profile and the user itself via the Plone UI, or the site managers can add them in a variety of ways including PAS plugins. These fields are dynamic and customizable by integrators so they do not adhere to a fixed schema interface. -This dynamic schema is exposed by this endpoint in order to build the user's profile form. +This dynamic schema is exposed by this endpoint in order to build the user's profile form and the registration form. -## Getting the user schema +## Get the schema for the user profile -To get the current user schema, make a request to the `/@userschema` endpoint. +To get the current schema for the user profile, make a request to the `/@userschema` endpoint. ```{eval-rst} .. http:example:: curl httpie python-requests :request: ../../../src/plone/restapi/tests/http-examples/userschema.req ``` -The server will respond with the user schema. +The server will respond with the user profile schema. ```{literalinclude} ../../../src/plone/restapi/tests/http-examples/userschema.resp :language: http @@ -37,3 +33,22 @@ The server will respond with the user schema. The user schema uses the same serialization as the type's JSON schema. See {ref}`types-schema` for detailed documentation about the available field types. + +## Get the registration form + +In Plone you can configure each of the fields of the user schema to be available in only one of either the user profile form or registration form, or in both of them. + +To get the user schema available for the user registration form, make a request to the `@userschema/registration` endpoint. + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/userschema_registration.req +``` + +The server will respond with the user schema for registration. + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/userschema_registration.resp + :language: http +``` + +The user schema uses the same serialization as the type's JSON schema. diff --git a/news/1873.feature b/news/1873.feature new file mode 100644 index 0000000000..72a44ba38e --- /dev/null +++ b/news/1873.feature @@ -0,0 +1 @@ +Add a `@userschema/registration` endpoint to get the fields for the registration form. @erral diff --git a/src/plone/restapi/services/userschema/user.py b/src/plone/restapi/services/userschema/user.py index 17e201bcca..89688de421 100644 --- a/src/plone/restapi/services/userschema/user.py +++ b/src/plone/restapi/services/userschema/user.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from plone.app.users.browser.register import getRegisterSchema from plone.app.users.browser.userdatapanel import getUserDataSchema from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service @@ -6,11 +7,23 @@ from plone.restapi.types.utils import get_fieldsets from plone.restapi.types.utils import get_jsonschema_properties from plone.restapi.types.utils import iter_fields +from zope.interface import implementer +from zope.publisher.interfaces import IPublishTraverse +@implementer(IPublishTraverse) class UserSchemaGet(Service): - def reply(self): - user_schema = getUserDataSchema() + def __init__(self, context, request): + super().__init__(context, request) + self.params = [] + + def publishTraverse(self, request, name): + # Consume any path segments after /@userschema as parameters + self.params.append(name) + return self + + def build_userschema_as_jsonschema(self, user_schema): + """function to build a jsonschema from user schema information""" fieldsets = get_fieldsets(self.context, self.request, user_schema) # Build JSON schema properties @@ -33,3 +46,17 @@ def reply(self): "required": required, "fieldsets": get_fieldset_infos(fieldsets), } + + def reply(self): + if len(self.params) == 0: + return self.build_userschema_as_jsonschema(getUserDataSchema()) + elif len(self.params) == 1 and self.params[0] == "registration": + return self.build_userschema_as_jsonschema(getRegisterSchema()) + + self.request.response.setStatus(400) + return dict( + error=dict( + type="Invalid parameters", + message="Parameters supplied are not valid.", + ) + ) diff --git a/src/plone/restapi/tests/http-examples/userschema_registration.req b/src/plone/restapi/tests/http-examples/userschema_registration.req new file mode 100644 index 0000000000..2a6eeaa0d8 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/userschema_registration.req @@ -0,0 +1,3 @@ +GET /plone/@userschema/registration HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/src/plone/restapi/tests/http-examples/userschema_registration.resp b/src/plone/restapi/tests/http-examples/userschema_registration.resp new file mode 100644 index 0000000000..17970c2f7a --- /dev/null +++ b/src/plone/restapi/tests/http-examples/userschema_registration.resp @@ -0,0 +1,69 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "fieldsets": [ + { + "behavior": "plone", + "fields": [ + "fullname", + "email", + "username", + "password", + "password_ctl", + "mail_me" + ], + "id": "default", + "title": "Default" + } + ], + "properties": { + "email": { + "description": "We will use this address if you need to recover your password", + "factory": "Email", + "title": "Email", + "type": "string", + "widget": "email" + }, + "fullname": { + "description": "Enter full name, e.g. John Smith.", + "factory": "Text line (String)", + "title": "Full Name", + "type": "string" + }, + "mail_me": { + "default": false, + "description": "", + "factory": "Yes/No", + "title": "Send a confirmation mail with a link to set the password", + "type": "boolean" + }, + "password": { + "description": "Enter your new password.", + "factory": "Password", + "title": "Password", + "type": "string", + "widget": "password" + }, + "password_ctl": { + "description": "Re-enter the password. Make sure the passwords are identical.", + "factory": "Password", + "title": "Confirm password", + "type": "string", + "widget": "password" + }, + "username": { + "description": "Enter a user name, usually something like 'jsmith'. No spaces or special characters. Usernames and passwords are case sensitive, make sure the caps lock key is not enabled. This is the name used to log in.", + "factory": "Text line (String)", + "title": "User Name", + "type": "string" + } + }, + "required": [ + "email", + "username", + "password", + "password_ctl" + ], + "type": "object" +} diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 5f2c92d97d..c11c41e0cf 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -2516,6 +2516,11 @@ def test_documentation_schema_user(self): save_request_and_response_for_docs("userschema", response) + def test_documentation_schema_user_registration(self): + response = self.api_session.get("/@userschema/registration") + + save_request_and_response_for_docs("userschema_registration", response) + class TestRules(TestDocumentationBase): layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING diff --git a/src/plone/restapi/tests/test_services_userschema.py b/src/plone/restapi/tests/test_services_userschema.py index 5a09bb1e48..04257810c0 100644 --- a/src/plone/restapi/tests/test_services_userschema.py +++ b/src/plone/restapi/tests/test_services_userschema.py @@ -12,15 +12,6 @@ import unittest -try: - from Products.CMFPlone.factory import _IMREALLYPLONE5 # noqa -except ImportError: - PLONE5 = False -else: - PLONE5 = True - - -@unittest.skipIf(not PLONE5, "Just Plone 5 currently.") class TestUserSchemaEndpoint(unittest.TestCase): layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING @@ -63,8 +54,39 @@ def test_userschema_get(self): self.assertTrue("object", response["type"]) + def test_userschema_registration_get(self): + response = self.api_session.get("/@userschema/registration") + + self.assertEqual(200, response.status_code) + response = response.json() + + self.assertIn("fullname", response["fieldsets"][0]["fields"]) + self.assertIn("email", response["fieldsets"][0]["fields"]) + self.assertIn("password", response["fieldsets"][0]["fields"]) + self.assertIn("password_ctl", response["fieldsets"][0]["fields"]) + self.assertIn("username", response["fieldsets"][0]["fields"]) + self.assertIn("mail_me", response["fieldsets"][0]["fields"]) + + self.assertIn("fullname", response["properties"]) + self.assertIn("email", response["properties"]) + self.assertIn("password", response["properties"]) + self.assertIn("password_ctl", response["properties"]) + self.assertIn("username", response["properties"]) + self.assertIn("mail_me", response["properties"]) + + self.assertIn("email", response["required"]) + self.assertIn("username", response["required"]) + self.assertIn("password", response["required"]) + self.assertIn("password_ctl", response["required"]) + + self.assertTrue("object", response["type"]) + + def test_userschema_with_invalid_params(self): + response = self.api_session.get("/@userschema/something-invalid") + + self.assertEqual(400, response.status_code) + -@unittest.skipIf(not PLONE5, "Just Plone 5 currently.") class TestCustomUserSchema(unittest.TestCase): """test userschema endpoint with a custom defined schema. we have taken the same example as in plone.app.users, thatç @@ -133,7 +155,7 @@ def setUp(self): False Age - + False Department @@ -159,7 +181,7 @@ def setUp(self): False Pi - + False Vegetarian @@ -196,3 +218,31 @@ def test_userschema_get(self): self.assertIn("skills", response["fieldsets"][0]["fields"]) self.assertIn("pi", response["fieldsets"][0]["fields"]) self.assertIn("vegetarian", response["fieldsets"][0]["fields"]) + + def test_userschema_for_registration_get(self): + response = self.api_session.get("/@userschema/registration") + + self.assertEqual(200, response.status_code) + response = response.json() + # Default fields + self.assertIn("fullname", response["fieldsets"][0]["fields"]) + self.assertIn("email", response["fieldsets"][0]["fields"]) + self.assertIn("username", response["fieldsets"][0]["fields"]) + self.assertIn("password", response["fieldsets"][0]["fields"]) + self.assertIn("password_ctl", response["fieldsets"][0]["fields"]) + self.assertIn("mail_me", response["fieldsets"][0]["fields"]) + + # added fields + self.assertIn("department", response["fieldsets"][0]["fields"]) + self.assertIn("vegetarian", response["fieldsets"][0]["fields"]) + + # fields not shown in the regisration form + self.assertNotIn("home_page", response["fieldsets"][0]["fields"]) + self.assertNotIn("description", response["fieldsets"][0]["fields"]) + self.assertNotIn("location", response["fieldsets"][0]["fields"]) + self.assertNotIn("portrait", response["fieldsets"][0]["fields"]) + self.assertNotIn("birthdate", response["fieldsets"][0]["fields"]) + self.assertNotIn("another_date", response["fieldsets"][0]["fields"]) + self.assertNotIn("age", response["fieldsets"][0]["fields"]) + self.assertNotIn("skills", response["fieldsets"][0]["fields"]) + self.assertNotIn("pi", response["fieldsets"][0]["fields"])