Skip to content

Commit

Permalink
feat: add a @registration-userschema endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
erral committed Jan 31, 2025
1 parent 259d969 commit e08829e
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 4 deletions.
19 changes: 19 additions & 0 deletions docs/source/endpoints/userschema.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,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.

## Getting the registration form

In Plone we can configure each of the fields of the user schema to be available only in the user edit form, in the registration form or in both of them.

To get the user schema available for the user registration form, make a request to the `/@registration-userschema` endpoint.

```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../../src/plone/restapi/tests/http-examples/registration_userschema.req
```

The server will respond with the user schema.

```{literalinclude} ../../../src/plone/restapi/tests/http-examples/registration_userschema.resp
:language: http
```

The user schema uses the same serialization as the type's JSON schema.
2 changes: 2 additions & 0 deletions news/1873.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a @registration-userschema endpoint to get the fields for the registration form
[erral]
1 change: 1 addition & 0 deletions src/plone/restapi/services/userschema/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
name="@userschema"
/>


</configure>
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/registration_userschema.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_registration_schema_user(self):
response = self.api_session.get("/@userschema/registration")

save_request_and_response_for_docs("registration_userschema", response)


class TestRules(TestDocumentationBase):
layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
Expand Down
64 changes: 62 additions & 2 deletions src/plone/restapi/tests/test_services_userschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,38 @@ def test_userschema_get(self):

self.assertTrue("object", response["type"])

def test_registration_userschema_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):
Expand Down Expand Up @@ -133,7 +165,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 +191,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 +228,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 e08829e

Please sign in to comment.