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

Added create and fetch aliases using csv. #1812

Closed
wants to merge 3 commits into from
Closed
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
38 changes: 34 additions & 4 deletions docs/source/endpoints/aliases.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ Response:
:language: http
```

## Adding URL aliases in bulk
## Adding URL aliases in bulk via JSON

You can add multiple URL aliases for multiple pages by sending a `POST` request to the `/@aliases` endpoint on site `root`. **datetime** parameter is optional:
You can add multiple URL aliases for multiple pages by sending a `POST` request to the `/@aliases` endpoint on site `root` using JSON payload. **datetime** parameter is optional:
Faakhir30 marked this conversation as resolved.
Show resolved Hide resolved

```{eval-rst}
.. http:example:: curl httpie python-requests
Expand All @@ -85,10 +85,25 @@ Response:
:language: http
```

## Adding URL aliases in bulk via CSV

## Listing all available aliases
You can add multiple URL aliases for multiple pages by sending a `POST` request to the `/@aliases` endpoint on site `root` using CSV file. **datetime** parameter is optional:
Faakhir30 marked this conversation as resolved.
Show resolved Hide resolved

To list all aliases, send a `GET` request to the `/@aliases` endpoint on site `root`:
```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../../src/plone/restapi/tests/http-examples/aliases_root_add_csv_format.req
```

Response:

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


## Listing all available aliases via JSON

To list all aliases in JSON format, send a `GET` request to the `/@aliases` endpoint on site `root`:

```{eval-rst}
.. http:example:: curl httpie python-requests
Expand All @@ -101,6 +116,21 @@ Response:
:language: http
```

## Listing all available aliases via CSV

To download all aliases as a csv file, send a `GET` request to the `/@aliases` endpoint on site `root`:
Faakhir30 marked this conversation as resolved.
Show resolved Hide resolved

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

Response:

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

## Filter aliases

To search for specific aliases, send a `GET` request to the `/@aliases` endpoint on site `root` with a `q` parameter:
Expand Down
1 change: 1 addition & 0 deletions news/1812.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added create and fetch aliases in CSV format. @Faakhir30
39 changes: 35 additions & 4 deletions src/plone/restapi/services/aliases/add.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
from DateTime import DateTime
from DateTime.interfaces import DateTimeError
from plone.app.redirector.interfaces import IRedirectionStorage
from plone.restapi import _
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from Products.CMFPlone.controlpanel.browser.redirects import absolutize_path
from Products.CMFPlone.controlpanel.browser.redirects import RedirectsControlPanel
from Products.statusmessages.interfaces import IStatusMessage
from zExceptions import BadRequest
from zope.component import getMultiAdapter
from zope.component.hooks import getSite
from zope.component import getUtility
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse

import plone.protect.interfaces
import logging

logger = logging.getLogger("Plone")


@implementer(IPublishTraverse)
Expand Down Expand Up @@ -83,15 +90,35 @@ def edit_for_navigation_root(self, alias):
class AliasesRootPost(Service):
"""Creates new aliases via controlpanel"""

def reply(self):
data = json_body(self.request)
def _reply_csv(self):
form = self.request.form
if not form.get("file"):
raise BadRequest("No file uploaded")
controlpanel = RedirectsControlPanel(self.context, self.request)
storage = getUtility(IRedirectionStorage)
aliases = data.get("items", [])
status = IStatusMessage(self.request)
portal = getSite()
file = form["file"]
controlpanel.upload(file, portal, storage, status)
file.close()

if err := status.show():
if err[0].type == "error":
raise BadRequest(err[0].message)
elif err[0].type == "info":
logger.info(err[0].message)
return self.reply_no_content()

def reply(self):
# Disable CSRF protection
if "IDisableCSRFProtection" in dir(plone.protect.interfaces):
alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)
if "multipart/form-data" in self.request.getHeader("Content-Type"):
return self._reply_csv()

storage = getUtility(IRedirectionStorage)
data = json_body(self.request)
aliases = data.get("items", [])
for alias in aliases:
redirection = alias.get("path")
target = alias.get("redirect-to")
Expand All @@ -113,7 +140,11 @@ def reply(self):

date = alias.get("datetime", None)
if date:
date = DateTime(date)
try:
date = DateTime(date)
except DateTimeError:
logger.warning("Failed to parse as DateTime: %s", date)
date = None

storage.add(abs_redirection, abs_target, now=date, manual=True)

Expand Down
9 changes: 9 additions & 0 deletions src/plone/restapi/services/aliases/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
name="@aliases"
/>

<plone:service
method="GET"
accept="text/csv"
factory=".get.AliasesGet"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="zope2.View"
name="@aliases"
/>

<plone:service
method="GET"
accept="application/json,application/schema+json"
Expand Down
56 changes: 48 additions & 8 deletions src/plone/restapi/services/aliases/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from zope.component.hooks import getSite
from zope.interface import implementer
from zope.interface import Interface
import json


@implementer(IExpandableElement)
Expand All @@ -26,7 +27,8 @@ def reply_item(self):
redirects = storage.redirects(context_path)
aliases = [deroot_path(alias) for alias in redirects]
self.request.response.setStatus(201)
return [{"path": alias} for alias in aliases]
self.request.response.setHeader("Content-Type", "application/json")
return [{"path": alias} for alias in aliases], len(aliases)

def reply_root(self):
"""
Expand All @@ -48,31 +50,69 @@ def reply_root(self):

newbatch = RedirectsControlPanel(self.context, self.request).redirects()
items_total = len([item for item in newbatch])
self.request.response.setHeader("Content-Type", "application/json")

return redirects, items_total

def reply_root_csv(self):
batch = RedirectsControlPanel(self.context, self.request).redirects()
redirects = [entry for entry in batch]

for redirect in redirects:
del redirect["redirect"]
redirect["datetime"] = datetimelike_to_iso(redirect["datetime"])
self.request.response.setStatus(201)

self.request.form["b_start"] = "0"
self.request.form["b_size"] = "1000000"
self.request.__annotations__.pop("plone.memoize")

filestream = RedirectsControlPanel(self.context, self.request).download()
content = filestream.read()
self.request.response.setHeader("Content-Type", "text/csv")
self.request.response.setHeader(
"Content-Disposition", "attachment; filename=redirects.csv"
)
self.request.response.setHeader("Content-Length", str(len(content)))
return content

def __call__(self, expand=False):
result = {"aliases": {"@id": f"{self.context.absolute_url()}/@aliases"}}
if not expand:
return result

if IPloneSiteRoot.providedBy(self.context):
items, items_total = self.reply_root()
result["aliases"]["items"] = items
result["aliases"]["items_total"] = items_total
if self.request.getHeader("Accept") == "text/csv":
result["aliases"]["items"] = self.reply_root_csv()
return result
else:
items, items_total = self.reply_root()
else:
result["aliases"]["items"] = self.reply_item()
result["aliases"]["items_total"] = len(result["aliases"]["items"])

items, items_total = self.reply_item()
result["aliases"]["items"] = items
result["aliases"]["items_total"] = items_total
return result


_no_content_marker = object()


class AliasesGet(Service):
"""Get aliases"""

def reply(self):
aliases = Aliases(self.context, self.request)
return aliases(expand=True)["aliases"]

def render(self):
self.check_permission()
content = self.reply()
if self.request.getHeader("Accept") == "text/csv":
return content["items"]
if content is not _no_content_marker:
return json.dumps(
content, indent=2, sort_keys=True, separators=(", ", ": ")
)


def deroot_path(path):
"""Remove the portal root from alias"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
POST /plone/@aliases HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="test_file.csv"
Content-Type: text/csv

old path,new path,datetime,manual
/old-page,/front-page,2022/01/01 00:00:00 GMT+0,True

------WebKitFormBoundary7MA4YWxkTrZu0gW--
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
HTTP/1.1 204 No Content

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GET /plone/@aliases HTTP/1.1
Accept: text/csv
Authorization: Basic YWRtaW46c2VjcmV0
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
HTTP/1.1 201 Created
Content-Type: text/csv; charset=utf-8

old path,new path,datetime,manual
/fizzbuzz,/front-page,2022/05/05 00:00:00 GMT+0,True
/old-page,/front-page,2022/05/05 00:00:00 GMT+0,True
53 changes: 53 additions & 0 deletions src/plone/restapi/tests/test_documentation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from base64 import b64encode
from datetime import datetime
from datetime import timezone
import io
from pkg_resources import resource_filename
from plone import api
from plone.app.discussion.interfaces import ICommentAddedEvent
Expand Down Expand Up @@ -2097,6 +2098,58 @@ def test_aliases_root_get(self):
response = self.api_session.get(url + query)
save_request_and_response_for_docs("aliases_root_get", response)

def test_aliases_root_get_csv_format(self):
url = f"{self.portal.absolute_url()}/@aliases"
query = ""

payload = {
"items": [
{
"path": "/old-page",
"redirect-to": "/front-page",
"datetime": "2022-05-05",
},
{
"path": "/fizzbuzz",
"redirect-to": "/front-page",
"datetime": "2022-05-05",
},
]
}
response = self.api_session.post(url, json=payload)
self.api_session.headers.update({"Content-Type": "application/json"})
self.api_session.headers.update({"Accept": "text/csv"})
response = self.api_session.get(url + query)
save_request_and_response_for_docs("aliases_root_get_csv_format", response)

def test_aliases_root_add_csv_format(self):
url = f"{self.portal.absolute_url()}/@aliases"

content = b"old path,new path,datetime,manual\n/old-page,/front-page,2022/01/01 00:00:00 GMT+0,True\n"
csv_file = io.BytesIO(content)
csv_file.name = "test_file.csv"

# Setting a fixed boundary intentionally to make the producing .req and .resp files deterministic
boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"

# Manually construct the multipart body
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="file"; filename="{csv_file.name}"\r\n'
"Content-Type: text/csv\r\n\r\n"
f"{content.decode()}\r\n"
f"--{boundary}--\r\n"
)

headers = {
"Accept": "application/json",
"Authorization": "Basic YWRtaW46c2VjcmV0",
"Content-Type": f"multipart/form-data; boundary={boundary}",
}

response = self.api_session.post(url, headers=headers, data=body)
save_request_and_response_for_docs("aliases_root_add_csv_format", response)

def test_aliases_root_filter(self):
# Get aliases
url = f"{self.portal.absolute_url()}/@aliases"
Expand Down
Loading
Loading