caldav: add rrule/until convertion to date (#88393) #499

Merged
yweber merged 2 commits from wip/88393-fix-rrule-until into main 2024-03-20 18:29:27 +01:00
3 changed files with 120 additions and 18 deletions

View File

@ -55,7 +55,7 @@ EVENT_SCHEMA_PART = {
'CATEGORY': {'type': 'string'},
'TRANSP': {
'type': 'boolean',
'description': 'Transparent if true else opaque (RFC 5545 3.8.2.7)',
'description': _('Transparent if true else opaque (RFC 5545 3.8.2.7)'),
},
'RRULE': {
'description': _('Recurrence rule (RFC 5545 3.8.5.3)'),
@ -84,6 +84,10 @@ EVENT_SCHEMA_PART = {
'type': 'integer',
'minimum': 1,
},
'UNTIL': {
'type': 'string',
'description': _('Date or date and time indicating the end of recurrence'),
yweber marked this conversation as resolved Outdated

s/reccurence/recurrence (aussi il manque un appel à gettext, non ? mais c'est le cas aussi pour d'autres chaînes plus haut)

s/reccurence/recurrence (aussi il manque un appel à gettext, non ? mais c'est le cas aussi pour d'autres chaînes plus haut)
},
},
},
},
@ -201,6 +205,7 @@ class CalDAV(BaseResource):
vevent = vevent[0]
# vevent.update(post_data) do not convert values as expected
for k, v in post_data.items():
vevent.pop(k)
vevent.add(k, v)
try:
# do not use ical.save(no_create=True) : no_create fails on some calDAV
@ -300,15 +305,33 @@ class CalDAV(BaseResource):
if 'CATEGORY' in data:
data['CATEGORIES'] = [data.pop('CATEGORY')]
for dt_field in ('DTSTART', 'DTEND'):
value = data[dt_field]
if 'RRULE' in data and 'UNTIL' in data['RRULE']:
try:
data[dt_field] = parse_date(value) or parse_datetime(value)
data['RRULE']['UNTIL'] = self._parse_date_or_datetime(data['RRULE']['UNTIL'])
except ValueError:
data[dt_field] = None
if not data[dt_field]:
raise APIError(
_('Unable to convert field %s=%r: not a valid date nor date-time') % (dt_field, value),
_('Unable to convert field %s=%r: not a valid date nor date-time')
% ('RRULE/UNTIL', data['RRULE']['UNTIL']),
http_status=400,
)
for dt_field in ('DTSTART', 'DTEND'):
if dt_field not in data:
continue
try:
data[dt_field] = self._parse_date_or_datetime(data[dt_field])
except ValueError:
raise APIError(
_('Unable to convert field %s=%r: not a valid date nor date-time')
% (dt_field, data[dt_field]),
http_status=400,
)
yweber marked this conversation as resolved Outdated

Ça peut plus explicitement s'appeler parse_date_or_datetime

Ça peut plus explicitement s'appeler parse_date_or_datetime
def _parse_date_or_datetime(self, value):
try:
ret = parse_date(value) or parse_datetime(value)
except ValueError:
ret = None
if not ret:
raise ValueError('Invalid value')
return ret

View File

@ -138,7 +138,9 @@ setup(
scripts=['manage.py'],
include_package_data=True,
install_requires=[
'caldav',
'caldav == 0.11.0',
'icalendar == 4.0.3',
'recurring-ical-events == 2.0.1',
'django >= 3.2, <3.3',
'django-model-utils<4.3',
'requests',

View File

@ -41,6 +41,42 @@ PROPFIND_REPLY = '''<?xml version="1.0" encoding="utf-8"?>
</D:multistatus>
'''
FAKE_ICS = '''BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//EGroupware//NONSGML EGroupware Calendar 17.1.003//FR
BEGIN:VTIMEZONE
TZID:Europe/Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;VALUE=DATE:20240227
DTEND;VALUE=DATE:20240228
X-MICROSOFT-CDO-ALLDAYEVENT:TRUE
SUMMARY:Yann Weber off
RRULE:FREQ=WEEKLY;BYDAY=TU
TRANSP:TRANSPARENT
CATEGORIES:Journées off
UID:01234567-abcd
STATUS:CONFIRMED
CREATED:20240320T095450Z
LAST-MODIFIED:20240320T095450Z
DTSTAMP:20240320T095513Z
END:VEVENT
END:VCALENDAR
'''
SOME_EVENTS = [
{'DTSTART': '2020-02-20T20:02', 'DTEND': '2020-02-22T22:43', 'SUMMARY': 'Test'},
@ -308,18 +344,24 @@ def test_caldav_event_create_ok_net_err(app, caldav_conn, event):
@pytest.mark.parametrize(
'dtstart,dtend',
'dtstart,dtend,until',
[
('2020-13-13', '2021-01-01'),
('2020-01-01T01:02:03', '2020-01-01T02:64:02'),
('23-01-12', '2023-01-13'),
('13:30', '2023-01-01T14:00'),
('2020-13-13', '2021-01-01', '2021-12-01'),
('2020-01-01T01:02:03', '2020-01-01T02:64:02', '2021-12-01'),
('23-01-12', '2023-01-13', '2021-12-01'),
('13:30', '2023-01-01T14:00', '2021-12-01'),
('2023-10-02', '2023-10-03', '2024-13-12'),
],
)
def test_caldav_event_create_bad_dates(app, caldav_conn, dtstart, dtend):
def test_caldav_event_create_bad_dates(app, caldav_conn, dtstart, dtend, until):
url = tests.utils.generic_endpoint_url('caldav', 'event/create')
username = 'toto'
event = {'DTSTART': dtstart, 'DTEND': dtend, 'SUMMARY': 'hello'}
event = {
'DTSTART': dtstart,
'DTEND': dtend,
'SUMMARY': 'hello',
'RRULE': {'FREQ': 'WEEKLY', 'BYDAY': 'FR', 'UNTIL': until},
}
qs_params = {'username': username}
resp = app_post(app, url, qs_params, event, status=400)
@ -331,6 +373,9 @@ def test_caldav_event_create_bad_dates(app, caldav_conn, dtstart, dtend):
@pytest.mark.parametrize('transp', [True, False])
@responses.activate
def test_caldav_event_update_ok(app, transp, caldav_conn):
"""This test mock de caldav lib in order to check if the
icalendar lib raises error on event modifications
"""
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
username = 'toto'
evt_id = '01234567-abcd'
@ -342,7 +387,7 @@ def test_caldav_event_update_ok(app, transp, caldav_conn):
'DTEND': '2020-03-30',
'SUMMARY': 'foobar',
'TRANSP': transp,
'RRULE': {'FREQ': 'MONTHLY', 'BYDAY': ['FR']},
'RRULE': {'FREQ': 'MONTHLY', 'BYDAY': ['FR'], 'UNTIL': '2020-10-01'},
}
qs_params = {'username': username, 'event_id': evt_id}
@ -365,13 +410,45 @@ def test_caldav_event_update_ok(app, transp, caldav_conn):
elif k == 'RRULE':
assert isinstance(evt[k], icalendar.vRecur)
for rk, rv in event['RRULE'].items():
assert evt[k][rk] == rv
if rk == 'UNTIL':
assert evt[k][rk] == datetime.date.fromisoformat(rv)
else:
assert evt[k][rk] == rv
elif k in ('DTSTART', 'DTEND'):
assert evt.decoded(k) == datetime.date.fromisoformat(v)
else:
assert str(evt[k]) == v
@responses.activate
def test_caldav_event_update_ok_nomock(app, caldav_conn):
"""This test let the caldav lib think it is able to retrieve
an event in order to modify it.
This can trigger errors from icalendar lib when caldav
attempt to save the event
"""
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
username = 'toto'
evt_id = '01234567-abcd'
evt_url = DAV_URL + get_event_path(caldav_conn, username, evt_id)
event = {
'DTSTART': '2020-02-20',
'DTEND': '2020-03-30',
'SUMMARY': 'foobar',
'TRANSP': True,
'RRULE': {'FREQ': 'MONTHLY', 'BYDAY': ['FR'], 'UNTIL': '2020-10-01'},
}
qs_params = {'username': username, 'event_id': evt_id}
responses.add(responses.GET, evt_url, body=FAKE_ICS)
responses.add(responses.PUT, evt_url, status=201)
resp = app_post(app, url, qs_params, event)
assert resp.json['err'] == 0
assert resp.json['data']['event_id'] == evt_id
@responses.activate
def test_caldav_event_update_notfound(app, caldav_conn):
url = tests.utils.generic_endpoint_url('caldav', 'event/update')