diff --git a/SHARING.md b/SHARING.md index 6fd52791..1bf7df0b 100644 --- a/SHARING.md +++ b/SHARING.md @@ -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 @@ -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": {} + } + ] +} +``` diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index bee138b0..340f97e5 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -128,6 +128,7 @@ SHARING_BDAY_AGE_MAX_DEFAULT: int = 99 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: @@ -555,7 +556,6 @@ class BaseSharing: # 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 = {} @@ -943,7 +943,7 @@ class BaseSharing: 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: @@ -1253,6 +1253,32 @@ class BaseSharing: 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": diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 3d08ab67..521a3858 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -5199,6 +5199,115 @@ permissions: RrWw""") }} _, 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",