From 68f8d61bdeefc55d8570c7da1658daeb80426718 Mon Sep 17 00:00:00 2001 From: Georgiy Date: Mon, 14 Jul 2025 18:23:28 +0300 Subject: [PATCH 1/7] Skip items with no events after expansion to prevent empty in responses. Enhance component filtering --- radicale/app/report.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/radicale/app/report.py b/radicale/app/report.py index 44e41d1e..03f17e88 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -238,6 +238,18 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], if expand is None or time_range_element is None: main_filters.append(filter_) + # Extract requested component types from filters + requested_components = set() + has_vcalendar_filter = False + for filter_ in filters: + for comp_filter in filter_.findall(".//" + xmlutils.make_clark("C:comp-filter")): + component_name = comp_filter.get("name") + if component_name: + if component_name == "VCALENDAR": + has_vcalendar_filter = True + else: + requested_components.add(component_name) + # Retrieve everything required for finishing the request. retrieved_items = list(retrieve_items( base_prefix, path, collection, hreferences, main_filters, multistatus)) @@ -263,6 +275,13 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], raise RuntimeError("Failed to filter item %r from %r: %s" % (item.href, collection.path, e)) from e + # Skip items that don't match requested component types, unless VCALENDAR filter allows all components + if requested_components and not has_vcalendar_filter: + if item.component_name not in requested_components: + logger.debug("Skipping component %r (type: %s) as it doesn't match requested components %s", + item.href, item.component_name, requested_components) + continue + found_props = [] not_found_props = [] @@ -306,6 +325,11 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], time_range_start=time_range_start, time_range_end=time_range_end, max_occurrence=max_occurrence, ) + + if n_vev == 0: + logger.debug("No VEVENTs found after expansion for %r, skipping", item.href) + continue + n_vevents += n_vev found_props.append(expanded_element) else: From a76f80a62081d869641a62b08d288f056771af17 Mon Sep 17 00:00:00 2001 From: Georgiy Date: Wed, 16 Jul 2025 22:31:58 +0300 Subject: [PATCH 2/7] Separate time-range filter only for VEVENT component --- radicale/app/report.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index 03f17e88..8f77c97b 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -228,22 +228,25 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], expand = root.find(".//" + xmlutils.make_clark("C:expand")) # if we have expand prop we use "filter (except time range) -> expand -> filter (only time range)" approach - time_range_element = None + vevent_time_range = None main_filters = [] for filter_ in filters: - # extract time-range filter for processing after main filters - # for expand request - time_range_element = filter_.find(".//" + xmlutils.make_clark("C:time-range")) + if expand is not None: + for comp_filter in filter_.findall(".//" + xmlutils.make_clark("C:comp-filter")): + if comp_filter.get("name", "").upper() == "VEVENT": + vevent_time_range = comp_filter.find(".//" + xmlutils.make_clark("C:time-range")) + if vevent_time_range is not None: + comp_filter.remove(vevent_time_range) + break - if expand is None or time_range_element is None: - main_filters.append(filter_) + main_filters.append(filter_) # Extract requested component types from filters requested_components = set() has_vcalendar_filter = False for filter_ in filters: for comp_filter in filter_.findall(".//" + xmlutils.make_clark("C:comp-filter")): - component_name = comp_filter.get("name") + component_name = comp_filter.get("name", "").upper() if component_name: if component_name == "VCALENDAR": has_vcalendar_filter = True @@ -316,8 +319,8 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], time_range_start = None time_range_end = None - if time_range_element is not None: - time_range_start, time_range_end = radicale_filter.parse_time_range(time_range_element) + if vevent_time_range is not None: + time_range_start, time_range_end = radicale_filter.parse_time_range(vevent_time_range) (expanded_element, n_vev) = _expand( element=element, item=copy.copy(item), From 40217a1360162031b68c68d64d570ca92dde5e9d Mon Sep 17 00:00:00 2001 From: Georgiy Date: Wed, 16 Jul 2025 23:26:40 +0300 Subject: [PATCH 3/7] Return empty REPORT response if no items found --- radicale/app/report.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index 8f77c97b..c58e8e78 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -349,9 +349,11 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], assert item.href uri = pathutils.unstrip_path( posixpath.join(collection.path, item.href)) - multistatus.append(xml_item_response( - base_prefix, uri, found_props=found_props, - not_found_props=not_found_props, found_item=True)) + + if found_props or not_found_props: + multistatus.append(xml_item_response( + base_prefix, uri, found_props=found_props, + not_found_props=not_found_props, found_item=True)) return client.MULTI_STATUS, multistatus From 76188c210e1d358938c3c667678cd3b1d55a17ff Mon Sep 17 00:00:00 2001 From: Georgiy Date: Thu, 17 Jul 2025 09:03:55 +0300 Subject: [PATCH 4/7] Revert time_range processing --- radicale/app/report.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index c58e8e78..3af9422a 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -228,18 +228,15 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], expand = root.find(".//" + xmlutils.make_clark("C:expand")) # if we have expand prop we use "filter (except time range) -> expand -> filter (only time range)" approach - vevent_time_range = None + time_range_element = None main_filters = [] for filter_ in filters: - if expand is not None: - for comp_filter in filter_.findall(".//" + xmlutils.make_clark("C:comp-filter")): - if comp_filter.get("name", "").upper() == "VEVENT": - vevent_time_range = comp_filter.find(".//" + xmlutils.make_clark("C:time-range")) - if vevent_time_range is not None: - comp_filter.remove(vevent_time_range) - break + # extract time-range filter for processing after main filters + # for expand request + time_range_element = filter_.find(".//" + xmlutils.make_clark("C:time-range")) - main_filters.append(filter_) + if expand is None or time_range_element is None: + main_filters.append(filter_) # Extract requested component types from filters requested_components = set() @@ -319,8 +316,8 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], time_range_start = None time_range_end = None - if vevent_time_range is not None: - time_range_start, time_range_end = radicale_filter.parse_time_range(vevent_time_range) + if time_range_element is not None: + time_range_start, time_range_end = radicale_filter.parse_time_range(time_range_element) (expanded_element, n_vev) = _expand( element=element, item=copy.copy(item), From e94aa1e7cfa86affaadfad8d64b2cc312f01f435 Mon Sep 17 00:00:00 2001 From: Georgiy Date: Thu, 17 Jul 2025 16:26:21 +0300 Subject: [PATCH 5/7] Tests for comp-filters and not found events --- radicale/tests/test_expand.py | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/radicale/tests/test_expand.py b/radicale/tests/test_expand.py index bedc9d12..faa81078 100644 --- a/radicale/tests/test_expand.py +++ b/radicale/tests/test_expand.py @@ -347,3 +347,114 @@ permissions: RrWw""") check=check) assert len(responses) == 0 assert status == check + + def test_report_vcalendar_all_components(self) -> None: + """Test calendar-query with comp-filter VCALENDAR, returns all components.""" + self.mkcalendar("/test/") + self.put("/test/calendar.ics", get_file_content("event_daily_rrule.ics")) + self.put("/test/todo.ics", get_file_content("todo1.ics")) + + request = """ + + + + + + + + + """ + status, responses = self.report("/test", request) + assert status == 207 + assert len(responses) == 2 + assert "/test/calendar.ics" in responses + assert "/test/todo.ics" in responses + + def test_report_vevent_only(self) -> None: + """Test calendar-query with comp-filter VEVENT, returns only VEVENT.""" + self.mkcalendar("/test/") + self.put("/test/calendar.ics", get_file_content("event_daily_rrule.ics")) + self.put("/test/todo.ics", get_file_content("todo1.ics")) + + request = """ + + + + + + + + + + + """ + status, responses = self.report("/test", request) + assert status == 207 + assert len(responses) == 1 + assert "/test/calendar.ics" in responses + vevent_response = responses["/test/calendar.ics"] + status, element = vevent_response["C:calendar-data"] + assert status == 200 and element.text + assert "BEGIN:VEVENT" in element.text + assert "UID:" in element.text + assert "BEGIN:VTODO" not in element.text + + def test_report_time_range_no_vevent(self) -> None: + """Test calendar-query with time-range filter, no matching VEVENT.""" + self.mkcalendar("/test/") + self.put("/test/calendar.ics/", get_file_content("event_daily_rrule.ics")) + + request = """ + + + + + + + + + + + + + + + """ + status, responses = self.report("/test", request) + assert status == 207 + assert len(responses) == 0 + + def test_report_time_range_one_vevent(self) -> None: + """Test calendar-query with time-range filter, matches one VEVENT.""" + self.mkcalendar("/test/") + self.put("/test/calendar1.ics/", get_file_content("event_daily_rrule.ics")) + self.put("/test/calendar2.ics/", get_file_content("event1.ics")) + + start = "20060101T000000Z" + end = "20060104T000000Z" + + request = f""" + + + + + + + + + + + + + + + """ + status, responses = self.report("/test", request) + assert status == 207 + assert len(responses) == 1 + response = responses["/test/calendar1.ics"] + status, element = response["C:calendar-data"] + assert status == 200 and element.text + assert "BEGIN:VEVENT" in element.text + assert "RECURRENCE-ID:20060103T170000Z" in element.text + assert "DTSTART:20060103T170000Z" in element.text From 3a6c72e93a4bd4ec2b547ea160a5f0b1d2122cea Mon Sep 17 00:00:00 2001 From: Georgiy Date: Thu, 17 Jul 2025 16:34:38 +0300 Subject: [PATCH 6/7] pep fix --- radicale/tests/test_expand.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/radicale/tests/test_expand.py b/radicale/tests/test_expand.py index faa81078..16ca3855 100644 --- a/radicale/tests/test_expand.py +++ b/radicale/tests/test_expand.py @@ -393,6 +393,7 @@ permissions: RrWw""") assert len(responses) == 1 assert "/test/calendar.ics" in responses vevent_response = responses["/test/calendar.ics"] + assert type(vevent_response) is dict status, element = vevent_response["C:calendar-data"] assert status == 200 and element.text assert "BEGIN:VEVENT" in element.text @@ -453,6 +454,7 @@ permissions: RrWw""") assert status == 207 assert len(responses) == 1 response = responses["/test/calendar1.ics"] + assert type(response) is dict status, element = response["C:calendar-data"] assert status == 200 and element.text assert "BEGIN:VEVENT" in element.text From 2c4cd321326bb542c138e3db2a098e293e94f2db Mon Sep 17 00:00:00 2001 From: Georgiy Date: Fri, 18 Jul 2025 16:37:49 +0300 Subject: [PATCH 7/7] Fixed extraction of time-range filter from request when processing expand property --- radicale/app/report.py | 32 +++++++++++--------------------- radicale/tests/test_expand.py | 13 ++++++++++--- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index 3af9422a..8cb1e9b2 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -233,22 +233,17 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], for filter_ in filters: # extract time-range filter for processing after main filters # for expand request - time_range_element = filter_.find(".//" + xmlutils.make_clark("C:time-range")) + filter_copy = copy.deepcopy(filter_) - if expand is None or time_range_element is None: - main_filters.append(filter_) + if expand is not None: + for comp_filter in filter_copy.findall(".//" + xmlutils.make_clark("C:comp-filter")): + if comp_filter.get("name", "").upper() == "VCALENDAR": + continue + time_range_element = comp_filter.find(xmlutils.make_clark("C:time-range")) + if time_range_element is not None: + comp_filter.remove(time_range_element) - # Extract requested component types from filters - requested_components = set() - has_vcalendar_filter = False - for filter_ in filters: - for comp_filter in filter_.findall(".//" + xmlutils.make_clark("C:comp-filter")): - component_name = comp_filter.get("name", "").upper() - if component_name: - if component_name == "VCALENDAR": - has_vcalendar_filter = True - else: - requested_components.add(component_name) + main_filters.append(filter_copy) # Retrieve everything required for finishing the request. retrieved_items = list(retrieve_items( @@ -275,13 +270,6 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], raise RuntimeError("Failed to filter item %r from %r: %s" % (item.href, collection.path, e)) from e - # Skip items that don't match requested component types, unless VCALENDAR filter allows all components - if requested_components and not has_vcalendar_filter: - if item.component_name not in requested_components: - logger.debug("Skipping component %r (type: %s) as it doesn't match requested components %s", - item.href, item.component_name, requested_components) - continue - found_props = [] not_found_props = [] @@ -366,6 +354,8 @@ def _expand( ) -> Tuple[ET.Element, int]: vevent_component: vobject.base.Component = copy.copy(item.vobject_item) logger.info("Expanding event %s", item.href) + logger.debug(f"Expand range: {start} to {end}") + logger.debug(f"Time range: {time_range_start} to {time_range_end}") # Split the vevents included in the component into one that contains the # recurrence information and others that contain a recurrence id to diff --git a/radicale/tests/test_expand.py b/radicale/tests/test_expand.py index 16ca3855..2b10110d 100644 --- a/radicale/tests/test_expand.py +++ b/radicale/tests/test_expand.py @@ -376,14 +376,21 @@ permissions: RrWw""") self.put("/test/calendar.ics", get_file_content("event_daily_rrule.ics")) self.put("/test/todo.ics", get_file_content("todo1.ics")) - request = """ + start = "20060101T000000Z" + end = "20060104T000000Z" + + request = f""" - + + + - + + +