Skip to content

Commit

Permalink
feat: add a @userschema/registration endpoint (#1874)
Browse files Browse the repository at this point in the history
* feat: add a @registration-userschema endpoint

* Update docs/source/endpoints/userschema.md

Co-authored-by: Steve Piercy <[email protected]>

* Update docs/source/endpoints/userschema.md

Co-authored-by: Steve Piercy <[email protected]>

* Update docs/source/endpoints/userschema.md

Co-authored-by: Steve Piercy <[email protected]>

* Update news/1873.feature

Co-authored-by: Steve Piercy <[email protected]>

* change changlog

* change changlog

* Update news/1873.feature

Co-authored-by: Steve Piercy <[email protected]>

* Update src/plone/restapi/services/userschema/configure.zcml

Co-authored-by: Steve Piercy <[email protected]>

* Update src/plone/restapi/tests/test_documentation.py

Co-authored-by: Steve Piercy <[email protected]>

* Update src/plone/restapi/tests/test_services_userschema.py

Co-authored-by: Steve Piercy <[email protected]>

* rename test files

* rename

* Remove misleading note about Plone 5

* Clarify profile schema vs registration schema

---------

Co-authored-by: Steve Piercy <[email protected]>
Co-authored-by: David Glick <[email protected]>
  • Loading branch information
3 people authored Feb 2, 2025
1 parent 722fe2c commit bc29368
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 22 deletions.
31 changes: 23 additions & 8 deletions docs/source/endpoints/userschema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
1 change: 1 addition & 0 deletions news/1873.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a `@userschema/registration` endpoint to get the fields for the registration form. @erral
31 changes: 29 additions & 2 deletions src/plone/restapi/services/userschema/user.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
# -*- 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
from plone.restapi.types.utils import get_fieldset_infos
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
Expand All @@ -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.",
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GET /plone/@userschema/registration HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
69 changes: 69 additions & 0 deletions src/plone/restapi/tests/http-examples/userschema_registration.resp
Original file line number Diff line number Diff line change
@@ -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"
}
5 changes: 5 additions & 0 deletions src/plone/restapi/tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 62 additions & 12 deletions src/plone/restapi/tests/test_services_userschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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ç
Expand Down Expand Up @@ -133,7 +155,7 @@ def setUp(self):
<required>False</required>
<title>Age</title>
</field>
<field name="department" type="zope.schema.Choice" users:forms="In User Profile">
<field name="department" type="zope.schema.Choice" users:forms="In User Profile|On Registration">
<description/>
<required>False</required>
<title>Department</title>
Expand All @@ -159,7 +181,7 @@ def setUp(self):
<required>False</required>
<title>Pi</title>
</field>
<field name="vegetarian" type="zope.schema.Bool" users:forms="In User Profile">
<field name="vegetarian" type="zope.schema.Bool" users:forms="In User Profile|On Registration">
<description/>
<required>False</required>
<title>Vegetarian</title>
Expand Down Expand Up @@ -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"])

0 comments on commit bc29368

Please sign in to comment.