Merge pull request #2167 from pbiering/rrdate-period
proper PERIOD handling for RDATE depending on vobject version
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## 3.7.6.dev
|
## 3.7.6.dev
|
||||||
* Extension: item verification on commandline
|
* Extension: item verification on commandline
|
||||||
|
* Improvement: catch lack of support of PERIOD in vobject <= 0.9.9
|
||||||
|
|
||||||
## 3.7.5
|
## 3.7.5
|
||||||
* Add: [sharing] conversion_bday_summary_template (customize summary)
|
* Add: [sharing] conversion_bday_summary_template (customize summary)
|
||||||
|
|||||||
@@ -200,6 +200,27 @@ def check_and_sanitize_items(
|
|||||||
logger.trace("ITEM/check_and_sanitize_item: remove existing tzinfo (dtstart has none): '%s' -> '%s'", date, dates.value[i])
|
logger.trace("ITEM/check_and_sanitize_item: remove existing tzinfo (dtstart has none): '%s' -> '%s'", date, dates.value[i])
|
||||||
if all(type(d) is type(ref_date) for d in dates.value):
|
if all(type(d) is type(ref_date) for d in dates.value):
|
||||||
continue
|
continue
|
||||||
|
if dates.params.get("VALUE") == ["PERIOD"]:
|
||||||
|
if not utils.vobject_supports_period():
|
||||||
|
raise ValueError("PERIOD not supported by used vobject=%s in object %r" % (utils.package_version("vobject"), component_uid))
|
||||||
|
# period = period-explicit / period-start
|
||||||
|
# period-explicit = date-time "/" date-time
|
||||||
|
# period-start = date-time "/" dur-value
|
||||||
|
for i, date in enumerate(dates.value):
|
||||||
|
if not isinstance(date, tuple):
|
||||||
|
raise ValueError("invalid PERIOD (not a tuple) in object %r" % component_uid)
|
||||||
|
if len(date) != 2:
|
||||||
|
raise ValueError("invalid PERIOD (not 2 elements) in object %r" % component_uid)
|
||||||
|
if type(date[0]) is datetime.datetime and type(date[1]) is datetime.datetime:
|
||||||
|
if (date[0] > date[1]):
|
||||||
|
raise ValueError("invalid PERIOD (end before start) in object %r" % component_uid)
|
||||||
|
# skip explicit tzinfo check for now, no buggy client known
|
||||||
|
logger.trace("ITEM/check_and_sanitize_item: PERIOD/start-stop found: '%s'", date)
|
||||||
|
elif type(date[0]) is datetime.datetime and type(date[1]) is datetime.timedelta:
|
||||||
|
logger.trace("ITEM/check_and_sanitize_item: PERIOD/start-duration found: '%s'", date)
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid PERIOD (element types not matching) in object %r" % component_uid)
|
||||||
|
continue
|
||||||
for i, date in enumerate(dates.value):
|
for i, date in enumerate(dates.value):
|
||||||
dates.value[i] = ref_date.replace(
|
dates.value[i] = ref_date.replace(
|
||||||
date.year, date.month, date.day)
|
date.year, date.month, date.day)
|
||||||
@@ -568,8 +589,8 @@ class Item:
|
|||||||
try:
|
try:
|
||||||
self._text = self.vobject_item.serialize()
|
self._text = self.vobject_item.serialize()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError("Failed to serialize item %r from %r: %s" %
|
raise RuntimeError("Failed to serialize item %r with UID %r from %r: %s" %
|
||||||
(self.href, self._collection_path,
|
(self.href, self.uid, self._collection_path,
|
||||||
e)) from e
|
e)) from e
|
||||||
return self._text
|
return self._text
|
||||||
|
|
||||||
|
|||||||
13
radicale/tests/static/event_rdate_period_start_duration.ics
Normal file
13
radicale/tests/static/event_rdate_period_start_duration.ics
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:test
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-rdate-period-start-duration
|
||||||
|
DESCRIPTION:event with rdate period start duration
|
||||||
|
DTSTART:20000101T000000Z
|
||||||
|
DURATION:PT1H
|
||||||
|
DTSTAMP:20000101T000000Z
|
||||||
|
RRULE:FREQ=WEEKLY
|
||||||
|
RDATE;VALUE=PERIOD:20000102T000000Z/PT2H
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:test
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-rdate-period-start-duration-multi
|
||||||
|
DESCRIPTION:event with rdate period multiple start duration
|
||||||
|
DTSTART:20000101T000000Z
|
||||||
|
DURATION:PT1H
|
||||||
|
DTSTAMP:20000101T000000Z
|
||||||
|
RRULE:FREQ=WEEKLY
|
||||||
|
RDATE;VALUE=PERIOD:20000102T000000Z/PT2H,20010102T000000Z/PT2H
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
13
radicale/tests/static/event_rdate_period_start_stop.ics
Normal file
13
radicale/tests/static/event_rdate_period_start_stop.ics
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:test
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-rdate-period-time-start-stop
|
||||||
|
DESCRIPTION:event with rdate period start and stop
|
||||||
|
DTSTART:20000101T000000Z
|
||||||
|
DURATION:PT1H
|
||||||
|
DTSTAMP:20000101T000000Z
|
||||||
|
RRULE:FREQ=WEEKLY
|
||||||
|
RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z/
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:test
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-rdate-period-time-start-stop-multi
|
||||||
|
DESCRIPTION:event with rdate period multiple start and stop
|
||||||
|
DTSTART:20000101T000000Z
|
||||||
|
DURATION:PT1H
|
||||||
|
DTSTAMP:20000101T000000Z
|
||||||
|
RRULE:FREQ=WEEKLY
|
||||||
|
RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z,20010102T000000Z/20010402T000000Z
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
@@ -151,6 +151,48 @@ permissions: RrWw""")
|
|||||||
assert "Event" in answer
|
assert "Event" in answer
|
||||||
assert "UID:event" in answer
|
assert "UID:event" in answer
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
|
||||||
|
def test_add_event_with_rdate_period_start_duration_single(self) -> None:
|
||||||
|
"""Add an event with RDATE/PERIOD with start+duration."""
|
||||||
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
event = get_file_content("event_rdate_period_start_duration.ics")
|
||||||
|
path = "/calendar.ics/event_rdate_period_start_duration.ics"
|
||||||
|
self.put(path, event)
|
||||||
|
_, headers, answer = self.request("GET", path, check=200)
|
||||||
|
assert "RDATE;VALUE=PERIOD:20000102T000000Z/PT2H" in answer
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
|
||||||
|
def test_add_event_with_rdate_period_start_duration_multi(self) -> None:
|
||||||
|
"""Add an event with RDATE/PERIOD with multiple start+duration."""
|
||||||
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
event = get_file_content("event_rdate_period_start_duration_multi.ics")
|
||||||
|
path = "/calendar.ics/event_rdate_period_start_duration_multi.ics"
|
||||||
|
self.put(path, event)
|
||||||
|
_, headers, answer = self.request("GET", path, check=200)
|
||||||
|
assert "RDATE;VALUE=PERIOD:20000102T000000Z/PT2H,20010102T000000Z/PT2H" in answer
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
|
||||||
|
def test_add_event_with_rdate_period_start_stop_single(self) -> None:
|
||||||
|
"""Add an event with RDATE/PERIOD with start+stop."""
|
||||||
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
event = get_file_content("event_rdate_period_start_stop.ics")
|
||||||
|
path = "/calendar.ics/event_rdate_period_start_stop.ics"
|
||||||
|
self.put(path, event)
|
||||||
|
_, headers, answer = self.request("GET", path, check=200)
|
||||||
|
assert "RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z" in answer
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD")
|
||||||
|
def test_add_event_with_rdate_period_start_stop_multi(self) -> None:
|
||||||
|
"""Add an event with RDATE/PERIOD with multiple start+stop."""
|
||||||
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
event = get_file_content("event_rdate_period_start_stop_multi.ics")
|
||||||
|
path = "/calendar.ics/event_rdate_period_start_stop_multi.ics"
|
||||||
|
self.put(path, event)
|
||||||
|
_, headers, answer = self.request("GET", path, check=200)
|
||||||
|
# wrapped answer
|
||||||
|
assert "RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z,20010102T000000Z/20010" in answer
|
||||||
|
assert " 402T000000Z" in answer
|
||||||
|
|
||||||
def test_add_event_with_desc_ok(self) -> None:
|
def test_add_event_with_desc_ok(self) -> None:
|
||||||
"""Add an event."""
|
"""Add an event."""
|
||||||
self.mkcalendar("/calendar.ics/")
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from importlib import import_module, metadata
|
|||||||
from string import ascii_letters, digits, punctuation
|
from string import ascii_letters, digits, punctuation
|
||||||
from typing import Callable, Sequence, Tuple, Type, TypeVar, Union
|
from typing import Callable, Sequence, Tuple, Type, TypeVar, Union
|
||||||
|
|
||||||
|
import vobject
|
||||||
|
|
||||||
from radicale import config
|
from radicale import config
|
||||||
from radicale.log import logger
|
from radicale.log import logger
|
||||||
|
|
||||||
@@ -107,6 +109,43 @@ def vobject_supports_vcard4() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# global cache
|
||||||
|
vobject_supports_period_cache: Union[bool, None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def vobject_supports_period() -> bool:
|
||||||
|
"""Check if vobject supports period (requires version > 0.9.9)."""
|
||||||
|
global vobject_supports_period_cache
|
||||||
|
|
||||||
|
if vobject_supports_period_cache is not None:
|
||||||
|
return vobject_supports_period_cache
|
||||||
|
|
||||||
|
content_test = """
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:test
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-rdate-period-time-start-stop
|
||||||
|
DTSTART:20000101T000000Z
|
||||||
|
DURATION:PT1H
|
||||||
|
DESCRIPTION:event with rdate period start and stop
|
||||||
|
DTSTAMP:20000101T000000Z
|
||||||
|
RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z
|
||||||
|
RRULE:FREQ=WEEKLY
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
"""
|
||||||
|
obj = vobject.readOne(content_test)
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj.serialize()
|
||||||
|
except Exception:
|
||||||
|
vobject_supports_period_cache = False
|
||||||
|
else:
|
||||||
|
vobject_supports_period_cache = True
|
||||||
|
return vobject_supports_period_cache
|
||||||
|
|
||||||
|
|
||||||
def packages_version():
|
def packages_version():
|
||||||
versions = []
|
versions = []
|
||||||
versions.append("python=%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2]))
|
versions.append("python=%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2]))
|
||||||
@@ -373,11 +412,11 @@ def textwrap_str(content: str, limit: int = DEFAULT_LIMIT_CONTENT) -> str:
|
|||||||
|
|
||||||
def dataToHex(data, count):
|
def dataToHex(data, count):
|
||||||
result = ''
|
result = ''
|
||||||
for item in range(count):
|
for i in range(count):
|
||||||
if ((item > 0) and ((item % 8) == 0)):
|
if ((i > 0) and ((i % 8) == 0)):
|
||||||
result += ' '
|
result += ' '
|
||||||
if (item < len(data)):
|
if (i < len(data)):
|
||||||
result += '%02x' % data[item] + ' '
|
result += '%02x' % data[i] + ' '
|
||||||
else:
|
else:
|
||||||
result += ' '
|
result += ' '
|
||||||
return result
|
return result
|
||||||
@@ -385,9 +424,9 @@ def dataToHex(data, count):
|
|||||||
|
|
||||||
def dataToAscii(data, count):
|
def dataToAscii(data, count):
|
||||||
result = ''
|
result = ''
|
||||||
for item in range(count):
|
for i in range(count):
|
||||||
if (item < len(data)):
|
if (i < len(data)):
|
||||||
char = chr(data[item])
|
char = chr(data[i])
|
||||||
if char in ascii_letters or \
|
if char in ascii_letters or \
|
||||||
char in digits or \
|
char in digits or \
|
||||||
char in punctuation or \
|
char in punctuation or \
|
||||||
@@ -400,9 +439,9 @@ def dataToAscii(data, count):
|
|||||||
|
|
||||||
def dataToSpecial(data, count):
|
def dataToSpecial(data, count):
|
||||||
result = ''
|
result = ''
|
||||||
for item in range(count):
|
for i in range(count):
|
||||||
if (item < len(data)):
|
if (i < len(data)):
|
||||||
char = chr(data[item])
|
char = chr(data[i])
|
||||||
if char == '\r':
|
if char == '\r':
|
||||||
result += 'C'
|
result += 'C'
|
||||||
elif char == '\n':
|
elif char == '\n':
|
||||||
|
|||||||
Reference in New Issue
Block a user