diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index bc87d14..864a5e8 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -118,6 +118,20 @@ def _href(collection, href): href.lstrip("/")) +def _date_to_datetime(date_): + """Transform a date to a UTC datetime. + + If date_ is a datetime without timezone, return as UTC datetime. If date_ + is already a datetime with timezone, return as is. + + """ + if not isinstance(date_, datetime): + date_ = datetime.combine(date_, datetime.min.time()) + if not date_.tzinfo: + date_ = date_.replace(tzinfo=timezone.utc) + return date_ + + def _comp_match(item, filter_, scope="collection"): """Check whether the ``item`` matches the comp ``filter_``. @@ -220,126 +234,156 @@ def _time_range_match(vobject_item, filter_, child_name): if child_name == "VEVENT": # TODO: check if there's a timezone dtstart = child.dtstart.value - if isinstance(dtstart, datetime) and not dtstart.tzinfo: - dtstart = dtstart.replace(tzinfo=timezone.utc) - if not isinstance(dtstart, datetime): - dtstart_is_datetime = False - # TODO: changing dates to datetimes may be wrong because of tz - dtstart = datetime.combine(dtstart, datetime.min.time()).replace( - tzinfo=timezone.utc) + if child.rruleset: + dtstarts = child.getrruleset(addRDate=True) else: - dtstart_is_datetime = True + dtstarts = (dtstart,) + dtend = getattr(child, "dtend", None) - duration = getattr(child, "duration", None) if dtend is not None: - # Line 1 dtend = dtend.value - if not isinstance(dtend, datetime): - dtend = datetime.combine(dtend, datetime.min.time()).replace( - tzinfo=timezone.utc) - if isinstance(dtend, datetime) and not dtend.tzinfo: - dtend = dtend.replace(tzinfo=timezone.utc) + original_duration = (dtend - dtstart).total_seconds() + dtend = _date_to_datetime(dtend) + + duration = getattr(child, "duration", None) + if duration is not None: + original_duration = duration = duration.value + + for dtstart in dtstarts: + dtstart_is_datetime = isinstance(dtstart, datetime) + dtstart = _date_to_datetime(dtstart) + + if dtstart > end: + break + + if dtend is not None: + # Line 1 + dtend = dtstart + timedelta(seconds=original_duration) + if start < dtend and end > dtstart: + return True + elif duration is not None: + if original_duration is None: + original_duration = duration.seconds + if duration.seconds > 0: + # Line 2 + if start < dtstart + duration and end > dtstart: + return True + elif start <= dtstart and end > dtstart: + # Line 3 + return True + elif dtstart_is_datetime: + # Line 4 + if start <= dtstart and end > dtstart: + return True + elif start < dtstart + timedelta(days=1) and end > dtstart: + # Line 5 + return True - return start < dtend and end > dtstart - elif duration is not None: - duration = duration.value - if duration.seconds > 0: - # Line 2 - return start < dtstart + duration and end > dtstart - else: - # Line 3 - return start <= dtstart and end > dtstart - elif dtstart_is_datetime: - # Line 4 - return start <= dtstart and end > dtstart - else: - # Line 5 - return start < dtstart + timedelta(days=1) and end > dtstart elif child_name == "VTODO": - # TODO: implement this dtstart = getattr(child, "dtstart", None) duration = getattr(child, "duration", None) due = getattr(child, "due", None) completed = getattr(child, "completed", None) created = getattr(child, "created", None) - if dtstart is not None and duration is not None: - # Line 1 - dtstart = dtstart.value - if not isinstance(dtstart, datetime): - dtstart = (datetime.combine(dtstart, datetime.min.time()) - .replace(tzinfo=timezone.utc)) + if dtstart is not None: + dtstart = _date_to_datetime(dtstart.value) + if duration is not None: duration = duration.value - return (start <= dtstart + duration and - (end > dtstart or end >= dtstart + duration)) - elif dtstart is not None and due is not None: - # Line 2 - dtstart = dtstart.value - if not isinstance(dtstart, datetime): - dtstart = (datetime.combine(dtstart, datetime.min.time()) - .replace(tzinfo=timezone.utc)) - due = due.value - if not isinstance(due, datetime): - due = datetime.combine(due, datetime.min.time()).replace( - tzinfo=timezone.utc) - return ((start < due or start <= dtstart) and - (end > dtstart or end >= due)) - elif dtstart is not None: - # Line 3 - dtstart = dtstart.value - if not isinstance(dtstart, datetime): - dtstart = (datetime.combine(dtstart, datetime.min.time()) - .replace(tzinfo=timezone.utc)) - return start <= dtstart and end > dtstart - elif due is not None: - # Line 4 - due = due.value - if not isinstance(due, datetime): - due = datetime.combine(due, datetime.min.time()).replace( - tzinfo=timezone.utc) - return start < due and end >= due - elif completed is not None and created is not None: - # Line 5 - completed = completed.value - created = created.value - return ((start <= created or start <= completed) and - (end >= created or end >= completed)) - elif completed is not None: - # Line 6 - completed = completed.value - return start <= completed and end >= completed + if due is not None: + due = _date_to_datetime(due.value) + if dtstart is not None: + original_duration = (due - dtstart).total_seconds() + if completed is not None: + completed = _date_to_datetime(completed.value) + if created is not None: + created = _date_to_datetime(created.value) + original_duration = (completed - created).total_seconds() elif created is not None: - # Line 7 - created = created.value - return end > created + created = _date_to_datetime(created.value) + + if child.rruleset: + reference_dates = child.getrruleset(addRDate=True) else: - return True + if dtstart is not None: + reference_dates = (dtstart,) + elif due is not None: + reference_dates = (due,) + elif completed is not None: + reference_dates = (completed,) + elif created is not None: + reference_dates = (created,) + else: + # Line 8 + return True + + for reference_date in reference_dates: + reference_date = _date_to_datetime(reference_date) + if reference_date > end: + break + + if dtstart is not None and duration is not None: + # Line 1 + if start <= reference_date + duration and ( + end > reference_date or + end >= reference_date + duration): + return True + elif dtstart is not None and due is not None: + # Line 2 + due = reference_date + timedelta(seconds=original_duration) + if (start < due or start <= reference_date) and ( + end > reference_date or end >= due): + return True + elif dtstart is not None: + if start <= reference_date and end > reference_date: + return True + elif due is not None: + # Line 4 + if start < reference_date and end >= reference_date: + return True + elif completed is not None and created is not None: + # Line 5 + completed = reference_date + timedelta( + seconds=original_duration) + if (start <= reference_date or start <= completed) and ( + end >= reference_date or end >= completed): + return True + elif completed is not None: + # Line 6 + if start <= reference_date and end >= reference_date: + return True + elif created is not None: + # Line 7 + if end > reference_date: + return True elif child_name == "VJOURNAL": dtstart = getattr(child, "dtstart", None) if dtstart is not None: dtstart = dtstart.value - if not isinstance(dtstart, datetime): - dtstart_is_datetime = False - # TODO: changing dates to datetimes may be wrong because of tz - dtstart = (datetime.combine(dtstart, datetime.min.time()) - .replace(tzinfo=timezone.utc)) + if child.rruleset: + dtstarts = child.getrruleset(addRDate=True) else: - dtstart_is_datetime = True + dtstarts = (dtstart,) - if dtstart_is_datetime: - # Line 1 - return start <= dtstart and end > dtstart - else: - # Line 2 - return start < dtstart + timedelta(days=1) and end > dtstart - else: - # Line 3 - return False + for dtstart in dtstarts: + dtstart_is_datetime = isinstance(dtstart, datetime) + dtstart = _date_to_datetime(dtstart) - return True + if dtstart > end: + break + + if dtstart_is_datetime: + # Line 1 + if start <= dtstart and end > dtstart: + return True + elif start < dtstart + timedelta(days=1) and end > dtstart: + # Line 2 + return True + + return False def _text_match(vobject_item, filter_, child_name, attrib_name=None): diff --git a/tests/static/event2.ics b/tests/static/event2.ics index 6f934fe..2844312 100644 --- a/tests/static/event2.ics +++ b/tests/static/event2.ics @@ -27,5 +27,6 @@ UID:event2 SUMMARY:Event2 DTSTART;TZID=Europe/Paris:20130902T180000 DTEND;TZID=Europe/Paris:20130902T190000 +RRULE:FREQ=WEEKLY END:VEVENT END:VCALENDAR diff --git a/tests/static/journal2.ics b/tests/static/journal2.ics index 336499b..27cb7cc 100644 --- a/tests/static/journal2.ics +++ b/tests/static/journal2.ics @@ -25,7 +25,8 @@ UID:journal2 DTSTAMP:19950817T000000 DTSTART;TZID=Europe/Paris:20000101T100000 SUMMARY:happy new year -DESCRIPTION: Happy new year 2001 ! +DESCRIPTION: Happy new year ! +RRULE:FREQ=YEARLY END:VJOURNAL END:VCALENDAR diff --git a/tests/static/todo.ics b/tests/static/todo.ics deleted file mode 100644 index 457f763..0000000 --- a/tests/static/todo.ics +++ /dev/null @@ -1,11 +0,0 @@ -BEGIN:VCALENDAR -PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN -VERSION:2.0 -BEGIN:VTODO -CREATED:20130903T091105Z -LAST-MODIFIED:20130903T091108Z -DTSTAMP:20130903T091108Z -UID:todo -SUMMARY:Todo -END:VTODO -END:VCALENDAR diff --git a/tests/static/todo1.ics b/tests/static/todo1.ics index aa323cb..0ffdede 100644 --- a/tests/static/todo1.ics +++ b/tests/static/todo1.ics @@ -22,5 +22,7 @@ END:VTIMEZONE BEGIN:VTODO DTSTART;TZID=Europe/Paris:20130901T220000 DURATION:PT1H +SUMMARY:Todo +UID:todo END:VTODO END:VCALENDAR diff --git a/tests/static/todo2.ics b/tests/static/todo2.ics index e4b3bad..0642ef6 100644 --- a/tests/static/todo2.ics +++ b/tests/static/todo2.ics @@ -22,5 +22,6 @@ END:VTIMEZONE BEGIN:VTODO DTSTART;TZID=Europe/Paris:20130901T180000 DUE;TZID=Europe/Paris:20130903T180000 +RRULE:FREQ=MONTHLY END:VTODO END:VCALENDAR diff --git a/tests/test_base.py b/tests/test_base.py index ffbc5b4..b55f582 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -69,8 +69,8 @@ class BaseRequests: """Add a todo.""" self.request( "PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR") - todo = get_file_content("todo.ics") - path = "/calendar.ics/todo.ics" + todo = get_file_content("todo1.ics") + path = "/calendar.ics/todo1.ics" status, headers, answer = self.request("PUT", path, todo) assert status == 201 status, headers, answer = self.request("GET", path) @@ -341,7 +341,7 @@ class BaseRequests: answer = self._test_filter([""" - + """], items=5) assert "href>/calendar.ics/event1.ics/calendar.ics/event4.ics/calendar.ics/event5.ics + + + + """], "event", items=2) + assert "href>/calendar.ics/event1.ics/calendar.ics/event2.ics + + + + """], "event", items=2) + assert "href>/calendar.ics/event1.ics/calendar.ics/event2.ics + + + + """], "event", items=2) + assert "href>/calendar.ics/event1.ics/calendar.ics/event2.ics + + + + """], "event", items=2) + assert "href>/calendar.ics/event1.ics/calendar.ics/event2.ics"""], "todo", items=8) assert "href>/calendar.ics/todo7.ics + + + + """], "todo", items=2) + assert "href>/calendar.ics/todo1.ics/calendar.ics/todo2.ics + + + + """], "todo", items=2) + assert "href>/calendar.ics/todo1.ics/calendar.ics/todo2.ics + + + + """], "todo", items=2) + assert "href>/calendar.ics/todo1.ics/calendar.ics/todo2.ics + + + + """], "todo", items=2) + assert "href>/calendar.ics/todo1.ics/calendar.ics/todo2.ics """], "journal", items=3) assert "href>/calendar.ics/journal1.ics/calendar.ics/journal2.ics/calendar.ics/journal2.ics/calendar.ics/journal3.ics @@ -479,6 +549,33 @@ class BaseRequests: assert "href>/calendar.ics/journal2.ics/calendar.ics/journal3.ics + + + + """], "journal", items=2) + assert "href>/calendar.ics/journal1.ics/calendar.ics/journal2.ics + + + + """], "journal", items=2) + assert "href>/calendar.ics/journal1.ics/calendar.ics/journal2.ics + + + + """], "journal", items=2) + assert "href>/calendar.ics/journal1.ics/calendar.ics/journal2.ics