diff --git a/radicale/app/report.py b/radicale/app/report.py index 44e41d1e..8cb1e9b2 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -233,10 +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) + + main_filters.append(filter_copy) # Retrieve everything required for finishing the request. retrieved_items = list(retrieve_items( @@ -306,6 +313,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: @@ -322,9 +334,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 @@ -340,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 bedc9d12..2b10110d 100644 --- a/radicale/tests/test_expand.py +++ b/radicale/tests/test_expand.py @@ -347,3 +347,123 @@ 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")) + + start = "20060101T000000Z" + end = "20060104T000000Z" + + request = f""" + + + + + + + + + + + + + + + """ + 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"] + 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 + 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"] + assert type(response) is dict + 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