Skip to content
Merged
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
78 changes: 78 additions & 0 deletions SHARING.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,8 @@ Execute delete+create in case `PathOrToken` needs to be changed.
* Output: text/plain|application/json
* Level-2 entries in "Actions" can be deleted using special value `#DEL#` (see API example below related to "bday" conversion)
* Examples:
###### form->text
Expand Down Expand Up @@ -738,3 +740,79 @@ BEGIN:VCALENDAR
...
END:VCALENDAR
```
##### Adjusting templates per share
```bash
## set specific config: conversion_bday_summary_template
curl -u owner:$ownerpw -H "Content-Type: application/json" -d '{ "PathMapped": "/owner/addressbook/", "PathOrToken": "/owner/bday-of-addressbook/", "Actions": { "config": { "conversion_bday_summary_template": "{fn} ({year}" }}}' http://localhost:5232/.sharing/v1/map/update
{"ApiVersion": 1, "Status": "success"}
```
```bash
## list adjusted share
curl -s -H "Content-Type: application/json" -u owner:$ownerpw -d '{ "PathMapped": "/owner/addressbook/", "PathOrToken": "/owner/bday-of-addressbook/"}' http://localhost:5232/.sharing/v1/map/list | jq
{
"ApiVersion": 1,
"Lines": 1,
"Status": "success",
"Content": [
{
"ShareType": "map",
"PathOrToken": "/owner/bday-of-addressbook/",
"PathMapped": "/owner/addressbook/",
"Conversion": "bday",
"Owner": "owner",
"User": "owner",
"Permissions": "r",
"EnabledByOwner": true,
"EnabledByUser": true,
"HiddenByOwner": false,
"HiddenByUser": false,
"TimestampCreated": 1774339430,
"TimestampUpdated": 1780240328,
"Properties": {},
"Actions": {
"config": {
"conversion_bday_summary_template": "{fn} ({year}"
}
}
}
]
}
```
```bash
## delete specific config: conversion_bday_summary_template (using special value "#DEL#")
curl -u owner:$ownerpw -H "Content-Type: application/json" -d '{ "PathMapped": "/owner/addressbook/", "PathOrToken": "/owner/bday-of-addressbook/", "Actions": { "config": { "conversion_bday_summary_template": "#DEL#" }}}' http://localhost:5232/.sharing/v1/map/update
{"ApiVersion": 1, "Status": "success"}
```
```bash
## list share (specific config is deleted)
curl -s -H "Content-Type: application/json" -u owner:$ownerpw -d '{ "PathMapped": "/owner/addressbook/", "PathOrToken": "/owner/bday-of-addressbook/"}' http://localhost:5232/.sharing/v1/map/list | jq
{
"ApiVersion": 1,
"Lines": 1,
"Status": "success",
"Content": [
{
"ShareType": "map",
"PathOrToken": "/owner/bday-of-addressbook/",
"PathMapped": "/owner/addressbook/",
"Conversion": "bday",
"Owner": "owner",
"User": "owner",
"Permissions": "r",
"EnabledByOwner": true,
"EnabledByUser": true,
"HiddenByOwner": false,
"HiddenByUser": false,
"TimestampCreated": 1774339430,
"TimestampUpdated": 1780240558,
"Properties": {},
"Actions": {}
}
]
}
```
30 changes: 28 additions & 2 deletions radicale/sharing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
SHARING_BDAY_SUMMARY_TEMPLATE_DEFAULT: str = "[{n:f} {n:g}|{fn}|{nickname}] ({year}) (BDAY)"
SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT: str = "BDAY={year}-{month}-{day}"
SHARING_BDAY_CATEGORIES_DEFAULT: str = 'Birthday'
SHARING_ACTIONS_DELETE_VALUE: str = '#DEL#'


def check_bday_max_age(data: Any) -> int:
Expand Down Expand Up @@ -555,7 +556,6 @@ def sharing_collection_resolver(self, path: str, user: str) -> Union[dict, None]

# adjust a share
def sharing_collection_update(self, ShareType: str, PathOrToken: str, OwnerOrUser: str, Properties: dict) -> None:
""" returning dict with PathMapped, Owner, Permissions or None if not found"""
logger.info("sharing/collection/update: ShareType=%r PathOrToken=%r OwnerOrUser=%r", ShareType, PathOrToken, OwnerOrUser)
# Filter properies for permitted ones
properties_filtered: dict = {}
Expand Down Expand Up @@ -943,7 +943,7 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
if level1 in ACTIONS_WHITELIST:
for level2 in request_data['Actions'][level1]:
if level2 in ACTIONS_WHITELIST[level1]:
if callable(ACTIONS_WHITELIST[level1][level2]):
if callable(ACTIONS_WHITELIST[level1][level2]) and request_data['Actions'][level1][level2] != SHARING_ACTIONS_DELETE_VALUE:
try:
value = ACTIONS_WHITELIST[level1][level2](request_data['Actions'][level1][level2])
except ValueError:
Expand Down Expand Up @@ -1253,6 +1253,32 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
logger.trace("" + api_info + ": clear property %r", prop)
del Properties[prop]

if 'Actions' in request_data:
if Actions is None:
# clear actions
Actions = {}
elif Actions == {}:
# empty, nothing to do
pass
elif share['Actions'] is not None:
# replace properties
for level1 in share['Actions']:
if level1 not in Actions:
Actions[level1] = {} # initialize level1
for level2 in share['Actions'][level1]:
logger.trace("" + api_info + ": check for existing Actions entry %r->%r", level1, level2)
if level2 not in Actions[level1]:
logger.trace("" + api_info + ": overtake Actions entry %r->%r", level1, level2)
Actions[level1][level2] = share['Actions'][level1][level2]
elif Actions[level1][level2] == SHARING_ACTIONS_DELETE_VALUE:
# unset, do nothing
logger.trace("" + api_info + ": delete Actions entry %r->%r", level1, level2)
del Actions[level1][level2]
if len(Actions[level1]) == 0:
logger.trace("" + api_info + ": delete Actions entry %r", level1)
# unset level1
del Actions[level1]

if Permissions is not None and share['Conversion'] is not None:
Permissions = str(Permissions)
if share['Conversion'] == "bday":
Expand Down
109 changes: 109 additions & 0 deletions radicale/tests/test_sharing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5199,6 +5199,115 @@ def test_sharing_api_map_vcf_bday_template(self) -> None:
}}
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)

self.configure({
"sharing": {"conversion_bday_description_template": sharing.SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT,
"conversion_bday_alarm_trigger_template": "",
}
})

# update template
logging.info("\n*** update map(bday) user/owner:r with valid description template -> 200")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_summary_template": "{fn} ({year})",
"conversion_bday_description_template": "Birthday={year}-{month}-{day}"
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)

logging.info("\n*** GET collection user format: description -> ok")
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
assert "DESCRIPTION:Birthday=1990-01-01" in answer
assert "SUMMARY:Test-FN-C3 (1990)" in answer

# update template
logging.info("\n*** update map(bday) user/owner:r with valid empty description template -> 200")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_description_template": ""
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)

logging.info("\n*** GET collection user format: description -> ok")
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
assert "DESCRIPTION:Birthday=" not in answer
assert "DESCRIPTION:BDAY=" not in answer
assert "SUMMARY:Test-FN-C3 (1990)" in answer

# update template
logging.info("\n*** update map(bday) user/owner:r DEL description template -> 200")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_description_template": sharing.SHARING_ACTIONS_DELETE_VALUE
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)

logging.info("\n*** GET collection user format: description -> ok")
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
assert "DESCRIPTION:Birthday=" not in answer
assert "DESCRIPTION:BDAY=" in answer
assert "SUMMARY:Test-FN-C3 (1990)" in answer

# update template
logging.info("\n*** update map(bday) user/owner:r DEL summary template -> 200")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_summary_template": sharing.SHARING_ACTIONS_DELETE_VALUE
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)

logging.info("\n*** GET collection user format: description -> ok")
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
assert "DESCRIPTION:Birthday=" not in answer
assert "DESCRIPTION:BDAY=" in answer
assert "SUMMARY:Family3Test Given3Test !n:a! (Birthday)" in answer

# update template
logging.info("\n*** update map(bday) user/owner:r with valid age max -> 200")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_age_max": 5,
"conversion_bday_summary_template": "{fn} ({year}/{age})",
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)

logging.info("\n*** GET collection user format: summary with age -> ok")
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
assert "Test-FN-C3 (1990/0)" in answer
assert "Test-FN-C3 (1990/5)" in answer
assert "Test-FN-C3 (1990/6)" not in answer

# update template
logging.info("\n*** update map(bday) user/owner:r with valid age max -> 200")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_age_max": sharing.SHARING_ACTIONS_DELETE_VALUE
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)

logging.info("\n*** GET collection user format: summary with age -> ok")
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
assert "Test-FN-C3 (1990/0)" in answer
assert "Test-FN-C3 (1990/5)" in answer
assert "Test-FN-C3 (1990/6)" in answer

def test_sharing_api_map_vcf_bday_age_template(self) -> None:
"""share-by-map with conversion=bday template tests with age."""
self.configure({"auth": {"type": "htpasswd",
Expand Down
Loading