Skip to content

Commit

Permalink
Merge pull request #1444 from plone/users-endpoint-search-fullname
Browse files Browse the repository at this point in the history
@users: Support search for fullname, email, id with `?search=`
Do you release, @tisto ?
  • Loading branch information
ksuess authored Jun 27, 2022
2 parents 353e2e2 + bd48bbd commit ab4f921
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 19 deletions.
17 changes: 16 additions & 1 deletion docs/source/users.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Filtering by `id`:
:request: ../../src/plone/restapi/tests/http-examples/users_filtered_by_username.req
```

The server will respond with a list of the filtered users in the portal where the `username` starts with the `query` parameter's value:
The server will respond with a list of the filtered users in the portal where the `username` contains the `query` parameter's value:

```{literalinclude} ../../src/plone/restapi/tests/http-examples/users_filtered_by_username.resp
:language: http
Expand All @@ -88,6 +88,21 @@ The server will respond with a list of users where the users are member of one o
The endpoint also takes a `limit` parameter.
Its default is a maximum of 25 users at a time for performance reasons.

### Search users

Search by `id`, `fullname` and `email`:

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

The server will respond with a list of users where the `fullname`, `email` or `id` contains the `query` parameter's value:

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


## Create User

Expand Down
1 change: 1 addition & 0 deletions news/1443.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@users: Support search for fullname, email, id with ?search= [ksuess]
77 changes: 63 additions & 14 deletions src/plone/restapi/services/users/get.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from AccessControl import getSecurityManager
from itertools import chain
from plone.app.workflow.browser.sharing import merge_search_results
from plone.restapi.interfaces import ISerializeToJson, ISerializeToJsonSummary
from plone.restapi.services import Service
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.utils import normalizeString
from urllib.parse import parse_qs
from zExceptions import BadRequest
from zope.component import getMultiAdapter
from zope.component import queryMultiAdapter
from zope.component.hooks import getSite
from zope.interface import implementer
Expand All @@ -23,6 +26,7 @@ def __init__(self, context, request):
self.portal_membership = getToolByName(portal, "portal_membership")
self.acl_users = getToolByName(portal, "acl_users")
self.query = parse_qs(self.request["QUERY_STRING"])
self.search_term = self.query.get("search", [""])[0]

def publishTraverse(self, request, name):
# Consume any path segments after /@users as parameters
Expand All @@ -46,21 +50,67 @@ def _sort_users(users):
)
return users

def _principal_search_results(
self, search_for_principal, get_principal_by_id, principal_type, id_key
):

hunter = getMultiAdapter((self.context, self.request), name="pas_search")

principals = []
for principal_info in search_for_principal(hunter, self.search_term):
principal_id = principal_info[id_key]
principals.append(get_principal_by_id(principal_id))

return principals

def _get_users(self):
results = {user["userid"] for user in self.acl_users.searchUsers()}
users = [self.portal_membership.getMemberById(userid) for userid in results]
return self._sort_users(users)

def _get_filtered_users(self, query, groups_filter, limit):
results = self.acl_users.searchUsers(id=query, max_results=limit)
users = [
self.portal_membership.getMemberById(user["userid"]) for user in results
]
def _user_search_results(self):
def search_for_principal(hunter, search_term):
return merge_search_results(
chain(
*(
hunter.searchUsers(**{field: search_term})
for field in ["name", "fullname", "email"]
)
),
"userid",
)

def get_principal_by_id(user_id):
mtool = getToolByName(self.context, "portal_membership")
return mtool.getMemberById(user_id)

return self._principal_search_results(
search_for_principal, get_principal_by_id, "user", "userid"
)

def _get_filtered_users(self, query, groups_filter, search_term, limit):
"""Filter or search users by id, fullname, email and/or groups.
Args:
query (str): filter by query
groups_filter (list of str): list of groups
search_term (str): search by id, fullname, email
limit (integer): limit result
Returns:
list: list of users sorted by fullname
"""
if search_term:
users = self._user_search_results()
else:
results = self.acl_users.searchUsers(id=query, max_results=limit)
users = [
self.portal_membership.getMemberById(user["userid"]) for user in results
]
if groups_filter:
users = [
user for user in users if set(user.getGroups()) & set(groups_filter)
]

return self._sort_users(users)

def has_permission_to_query(self):
Expand All @@ -80,14 +130,13 @@ def has_permission_to_access_user_info(self):
def reply(self):
if len(self.query) > 0 and len(self.params) == 0:
query = self.query.get("query", "")
groups_filter = self.query.get(
"groups-filter:list", self.query.get("groups-filter%3Alist", [])
)
limit = self.query.get("limit", DEFAULT_SEARCH_RESULTS_LIMIT)
if query or groups_filter:
# Someone is searching users, check if they are authorized
groups_filter = self.query.get("groups-filter:list", [])
limit = self.query.get("limit", [DEFAULT_SEARCH_RESULTS_LIMIT])[0]
if query or groups_filter or self.search_term or limit:
if self.has_permission_to_query():
users = self._get_filtered_users(query, groups_filter, limit)
users = self._get_filtered_users(
query, groups_filter, self.search_term, limit
)
result = []
for user in users:
serializer = queryMultiAdapter(
Expand All @@ -99,7 +148,7 @@ def reply(self):
self.request.response.setStatus(401)
return
else:
raise BadRequest("Query string supplied is not valid")
raise BadRequest("Parameters supplied are not valid")

if len(self.params) == 0:
# Someone is asking for all users, check if they are authorized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Content-Type: application/json
"newVersion": "0006",
"required": false
},
"version": "8.22.1.dev0"
"version": "8.23.1.dev0"
},
{
"@id": "http://localhost:55001/plone/@addons/plone.session",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
GET /plone/@users?query=noa HTTP/1.1
GET /plone/@users?query=oam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Content-Type: application/json
"email": "[email protected]",
"fullname": "Noam Avram Chomsky",
"groups": {
"@id": "http://localhost:55001/plone/@users?query=noa",
"@id": "http://localhost:55001/plone/@users?query=oam",
"items": [
{
"id": "AuthenticatedUsers",
Expand Down
3 changes: 3 additions & 0 deletions src/plone/restapi/tests/http-examples/users_searched.req
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GET /plone/@users?search=avram HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
34 changes: 34 additions & 0 deletions src/plone/restapi/tests/http-examples/users_searched.resp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
HTTP/1.1 200 OK
Content-Type: application/json

[
{
"@id": "http://localhost:55001/plone/@users/noam",
"description": "Professor of Linguistics",
"email": "[email protected]",
"fullname": "Noam Avram Chomsky",
"groups": {
"@id": "http://localhost:55001/plone/@users?search=avram",
"items": [
{
"id": "AuthenticatedUsers",
"title": "AuthenticatedUsers"
},
{
"id": "Reviewers",
"title": "Reviewers"
}
],
"items_total": 2
},
"home_page": "web.mit.edu/chomsky",
"id": "noam",
"location": "Cambridge, MA",
"portrait": null,
"roles": [
"Member",
"Reviewer"
],
"username": "noam"
}
]
18 changes: 17 additions & 1 deletion src/plone/restapi/tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ def test_documentation_users_filtered_get(self):
api.group.add_user(groupname="Reviewers", username="noam")
transaction.commit()
# filter by username
response = self.api_session.get("@users", params={"query": "noa"})
response = self.api_session.get("@users", params={"query": "oam"})
save_request_and_response_for_docs("users_filtered_by_username", response)
# filter by groups
response = self.api_session.get(
Expand All @@ -945,6 +945,22 @@ def test_documentation_users_filtered_get(self):
)
save_request_and_response_for_docs("users_filtered_by_groups", response)

def test_documentation_users_searched_get(self):
properties = {
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"description": "Professor of Linguistics",
"location": "Cambridge, MA",
}
api.user.create(
email="[email protected]", username="noam", properties=properties
)
api.group.add_user(groupname="Reviewers", username="noam")
transaction.commit()
# search by fullname
response = self.api_session.get("@users", params={"search": "avram"})
save_request_and_response_for_docs("users_searched", response)

def test_documentation_users_created(self):
response = self.api_session.post(
"/@users",
Expand Down

0 comments on commit ab4f921

Please sign in to comment.