Skip to content

Commit cec91ce

Browse files
authored
Merge branch 'main' into fix_seeaarch_sort_order
2 parents a923efc + ffa36ba commit cec91ce

File tree

17 files changed

+288
-62
lines changed

17 files changed

+288
-62
lines changed

.github/workflows/test-matrix.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
# """
4949
##
5050
- name: Pip cache
51-
uses: actions/cache@v4
51+
uses: actions/cache@v5
5252
with:
5353
path: ~/.cache/pip
5454
key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ local.cfg
4747
/sources/
4848
/venv/
4949
.installed.txt
50+
/.mxdev_cache/
5051

5152
/docs/styles/Microsoft/
5253

docs/source/endpoints/aliases.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ When an object is moved (renamed or cut/pasted into a different location), the r
1515

1616
The API consumer can create, read, and delete aliases.
1717

18-
1918
| Verb | URL | Action |
2019
| -------- | ----------- | -------------------------------------- |
2120
| `POST` | `/@aliases` | Add one or more aliases |
@@ -135,7 +134,20 @@ Response:
135134

136135
## Filter aliases
137136

138-
To search for specific aliases, send a `GET` request to the `/@aliases` endpoint on site `root` with a `q` parameter:
137+
### Parameters
138+
139+
All of the following parameters are optional.
140+
141+
| Name | Type | Description |
142+
| --------- | ------- | ------------------------------------------------------ |
143+
| `query` | string | Full-text search. Can match paths or text fields. |
144+
| `manual` | boolean | Filter by manual or automatically created redirects. |
145+
| `start` | string | Filter redirects created **after** this date. |
146+
| `end` | string | Filter redirects created **before** this date. |
147+
| `b_start` | integer | Batch start index (offset). |
148+
| `b_size` | integer | Batch size (maximum items returned). |
149+
150+
To search for specific aliases, send a `GET` request to the `@aliases` endpoint with one or more of the above named parameters as shown in the following example.
139151

140152
```{eval-rst}
141153
.. http:example:: curl httpie python-requests

news/+0b828c61.tests.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix tests to expect an extra registry record. @mauritsvanrees

news/1791.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`@aliases` service: Add support for filtering aliases for a non-root item. @jnptk

src/plone/restapi/services/aliases/add.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,15 @@ def reply(self):
4242

4343
failed_aliases = []
4444
for alias in aliases:
45+
date = None
4546
if isinstance(alias, dict):
47+
date = alias.get("datetime")
48+
if date:
49+
try:
50+
date = DateTime(date)
51+
except DateTimeError:
52+
logger.warning("Failed to parse as DateTime: %s", date)
53+
4654
alias = alias.get("path")
4755

4856
if alias.startswith("/"):
@@ -61,6 +69,7 @@ def reply(self):
6169
storage.add(
6270
alias,
6371
"/".join(self.context.getPhysicalPath()),
72+
now=date,
6473
manual=True,
6574
)
6675

src/plone/restapi/services/aliases/delete.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from plone.app.redirector.interfaces import IRedirectionStorage
22
from plone.restapi.deserializer import json_body
33
from plone.restapi.services import Service
4-
from plone.restapi.services.aliases.get import deroot_path
4+
from plone.restapi.utils import deroot_path
55
from Products.CMFPlone.controlpanel.browser.redirects import absolutize_path
66
from zope.component import getUtility
77
from zope.interface import alsoProvides

src/plone/restapi/services/aliases/get.py

Lines changed: 94 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1+
from BTrees.OOBTree import OOBTree
2+
from DateTime import DateTime
3+
from DateTime.interfaces import DateTimeError
14
from plone.app.redirector.interfaces import IRedirectionStorage
5+
from plone.restapi.batching import HypermediaBatch
26
from plone.restapi.bbb import IPloneSiteRoot
37
from plone.restapi.interfaces import IExpandableElement
48
from plone.restapi.serializer.converters import datetimelike_to_iso
59
from plone.restapi.services import Service
10+
from plone.restapi.utils import deroot_path
11+
from plone.restapi.utils import is_falsy
12+
from plone.restapi.utils import is_truthy
613
from Products.CMFPlone.controlpanel.browser.redirects import RedirectsControlPanel
14+
from zExceptions import BadRequest
15+
from zExceptions import HTTPNotAcceptable as NotAcceptable
716
from zope.component import adapter
817
from zope.component import getUtility
9-
from zope.component.hooks import getSite
1018
from zope.interface import implementer
1119
from zope.interface import Interface
1220

@@ -22,40 +30,60 @@ def __init__(self, context, request):
2230
self.context = context
2331
self.request = request
2432

25-
def reply_item(self):
33+
def reply(self, query, manual, start, end):
2634
storage = getUtility(IRedirectionStorage)
35+
portal_path = "/".join(self.context.getPhysicalPath()[:2])
2736
context_path = "/".join(self.context.getPhysicalPath())
28-
redirects = storage.redirects(context_path)
29-
aliases = [deroot_path(alias) for alias in redirects]
30-
self.request.response.setStatus(200)
31-
self.request.response.setHeader("Content-Type", "application/json")
32-
return [{"path": alias} for alias in aliases], len(aliases)
33-
34-
def reply_root(self):
35-
"""
36-
redirect-to - target
37-
path - path
38-
redirect - full path with root
39-
"""
40-
batch = RedirectsControlPanel(self.context, self.request).redirects()
41-
redirects = [entry for entry in batch]
42-
43-
for redirect in redirects:
44-
del redirect["redirect"]
45-
redirect["datetime"] = datetimelike_to_iso(redirect["datetime"])
46-
self.request.response.setStatus(200)
4737

48-
self.request.form["b_start"] = "0"
49-
self.request.form["b_size"] = "1000000"
50-
self.request.__annotations__.pop("plone.memoize")
38+
if not IPloneSiteRoot.providedBy(self.context):
39+
tree = OOBTree()
40+
rds = storage.redirects(context_path)
41+
for rd in rds:
42+
rd_full = storage.get_full(rd)
43+
tree[rd] = rd_full
44+
storage = tree
45+
else:
46+
storage = storage._paths
47+
48+
if query and query.startswith("/"):
49+
min_k = f"{portal_path}/{query.strip('/')}"
50+
max_k = min_k[:-1] + chr(ord(min_k[-1]) + 1)
51+
redirects = storage.items(min=min_k, max=max_k, excludemax=True)
52+
elif query:
53+
redirects = [path for path in storage.items() if query in path]
54+
else:
55+
redirects = storage.items()
56+
57+
aliases = []
58+
for path, info in redirects:
59+
if manual != "":
60+
if info[2] != manual:
61+
continue
62+
if start and info[1]:
63+
if info[1] < start:
64+
continue
65+
if end and info[1]:
66+
if info[1] >= end:
67+
continue
68+
69+
redirect = {
70+
"path": deroot_path(path),
71+
"redirect-to": deroot_path(info[0]),
72+
"datetime": datetimelike_to_iso(info[1]),
73+
"manual": info[2],
74+
}
75+
aliases.append(redirect)
76+
77+
batch = HypermediaBatch(self.request, aliases)
5178

52-
newbatch = RedirectsControlPanel(self.context, self.request).redirects()
53-
items_total = len([item for item in newbatch])
79+
self.request.response.setStatus(200)
5480
self.request.response.setHeader("Content-Type", "application/json")
55-
56-
return redirects, items_total
81+
return [i for i in batch], batch.items_total, batch.links
5782

5883
def reply_root_csv(self):
84+
if not IPloneSiteRoot.providedBy(self.context):
85+
raise NotAcceptable("CSV reply is only available from site root.")
86+
5987
batch = RedirectsControlPanel(self.context, self.request).redirects()
6088
redirects = [entry for entry in batch]
6189

@@ -80,19 +108,50 @@ def reply_root_csv(self):
80108
return content
81109

82110
def __call__(self, expand=False):
111+
data = self.request.form
112+
113+
query = data.get("query", data.get("q", None))
114+
manual = data.get("manual", "")
115+
start = data.get("start", None)
116+
end = data.get("end", None)
117+
118+
if query and not isinstance(query, str):
119+
raise BadRequest('Parameter "query" must be a string.')
120+
121+
if manual:
122+
if is_truthy(manual):
123+
manual = True
124+
elif is_falsy(manual):
125+
manual = False
126+
else:
127+
raise BadRequest('Parameter "manual" must be a boolean.')
128+
129+
if start:
130+
try:
131+
start = DateTime(start)
132+
except DateTimeError as e:
133+
raise BadRequest(str(e))
134+
135+
if end:
136+
try:
137+
end = DateTime(end)
138+
except DateTimeError as e:
139+
raise BadRequest(str(e))
140+
83141
result = {"aliases": {"@id": f"{self.context.absolute_url()}/@aliases"}}
84142
if not expand:
85143
return result
86-
if IPloneSiteRoot.providedBy(self.context):
87-
if self.request.getHeader("Accept") == "text/csv":
88-
result["aliases"]["items"] = self.reply_root_csv()
89-
return result
90-
else:
91-
items, items_total = self.reply_root()
144+
if self.request.getHeader("Accept") == "text/csv":
145+
result["aliases"]["items"] = self.reply_root_csv()
146+
return result
92147
else:
93-
items, items_total = self.reply_item()
148+
items, items_total, batching = self.reply(query, manual, start, end)
149+
94150
result["aliases"]["items"] = items
95151
result["aliases"]["items_total"] = items_total
152+
if batching:
153+
result["aliases"]["links"] = batching
154+
96155
return result
97156

98157

@@ -115,12 +174,3 @@ def render(self):
115174
return json.dumps(
116175
content, indent=2, sort_keys=True, separators=(", ", ": ")
117176
)
118-
119-
120-
def deroot_path(path):
121-
"""Remove the portal root from alias"""
122-
portal = getSite()
123-
root_path = "/".join(portal.getPhysicalPath())
124-
if not path.startswith("/"):
125-
path = "/%s" % path
126-
return path.replace(root_path, "", 1)

src/plone/restapi/tests/http-examples/aliases_get.resp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ Content-Type: application/json
55
"@id": "http://localhost:55001/plone/front-page/@aliases",
66
"items": [
77
{
8-
"path": "/simple-alias"
8+
"datetime": "2022-05-05T00:00:00",
9+
"manual": true,
10+
"path": "/simple-alias",
11+
"redirect-to": "/front-page"
912
}
1013
],
1114
"items_total": 1
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
GET /plone/front-page/@aliases?query=/fizzbuzz HTTP/1.1
2+
Accept: application/json
3+
Authorization: Basic YWRtaW46c2VjcmV0

0 commit comments

Comments
 (0)