Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a @userschema/registration endpoint #1874

Merged
merged 16 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"])