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 30, 2025
1 parent 259d969 commit b749334
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 8 deletions.
27 changes: 23 additions & 4 deletions docs/source/endpoints/userschema.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
myst:
html_meta:
"description": "Given a user in the site, one can get its properties and available fields."
"property=og:description": "Given a user in the site, one can get its properties and available fields."
"property=og:title": "User schema"
"keywords": "Plone, plone.restapi, REST, API, Users, profile"
'description': 'Given a user in the site, one can get its properties and available fields.'
'property=og:description': 'Given a user in the site, one can get its properties and available fields.'
'property=og:title': 'User schema'
'keywords': 'Plone, plone.restapi, REST, API, Users, profile'
---

# User schema
Expand Down 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.feat
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]
8 changes: 8 additions & 0 deletions src/plone/restapi/services/userschema/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@
name="@userschema"
/>

<plone:service
method="GET"
factory=".user.RegistrationUserSchema"
for="Products.CMFCore.interfaces.ISiteRoot"
permission="zope2.View"
name="@registration-userschema"
/>

</configure>
19 changes: 17 additions & 2 deletions src/plone/restapi/services/userschema/user.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,9 +9,14 @@
from plone.restapi.types.utils import iter_fields


class UserSchemaGet(Service):
class UserSchemaBuilder(Service):

def reply(self):
user_schema = getUserDataSchema()
raise NotImplementedError("The class must implement the reply method")


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 +39,12 @@ def reply(self):
"required": required,
"fieldsets": get_fieldset_infos(fieldsets),
}

class UserSchemaGet(UserSchemaBuilder):
def reply(self):
return self.build_userschema_as_jsonschema(getUserDataSchema())


class RegistrationUserSchema(UserSchemaBuilder):
def reply(self):
return self.build_userschema_as_jsonschema(getRegisterSchema())
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GET /plone/@registration-userschema 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("/@registration-userschema")

save_request_and_response_for_docs("registration_userschema", response)


class TestRules(TestDocumentationBase):
layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
Expand Down
59 changes: 57 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,33 @@ def test_userschema_get(self):

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

def test_registration_userschema_get(self):
response = self.api_session.get("/@registration-userschema")

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"])


@unittest.skipIf(not PLONE5, "Just Plone 5 currently.")
class TestCustomUserSchema(unittest.TestCase):
Expand Down Expand Up @@ -133,7 +160,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 +186,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 +223,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("/@registration-userschema")

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 b749334

Please sign in to comment.