From 1520dfcfe3f7791e066a590862bda730f991b324 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 31 May 2026 16:34:06 +0200 Subject: [PATCH] sharing/api: add support for removing per share Action dict entries --- radicale/sharing/__init__.py | 29 ++++++++- radicale/tests/test_sharing.py | 109 +++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index bee138b0..4e850d26 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: @@ -943,7 +944,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 +1254,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",