Compare commits
2 Commits
189aa18be8
...
0edd41e46a
| Author | SHA1 | Date | |
|---|---|---|---|
| 0edd41e46a | |||
| e7f469f477 |
@@ -343,11 +343,13 @@ def report(subpath):
|
||||
return _xml_response(multistatus)
|
||||
|
||||
if tag == _qn('c', 'calendar-query'):
|
||||
# Parse optional time-range
|
||||
# Parse optional time-range. start ohne end = "ab jetzt offen";
|
||||
# end ohne start = "bis X"; beide None = kein Filter.
|
||||
start, end = _extract_time_range(root)
|
||||
q = CalendarEvent.query.filter_by(calendar_id=cal.id)
|
||||
if start is not None:
|
||||
if end is not None:
|
||||
q = q.filter(CalendarEvent.dtstart < end)
|
||||
if start is not None:
|
||||
q = q.filter(
|
||||
(CalendarEvent.dtend >= start) | (CalendarEvent.dtstart >= start)
|
||||
| (CalendarEvent.recurrence_rule.isnot(None))
|
||||
@@ -368,17 +370,22 @@ def _extract_time_range(root: ET.Element):
|
||||
if not s:
|
||||
return None
|
||||
s = s.replace('Z', '+00:00')
|
||||
dt = None
|
||||
try:
|
||||
return datetime.fromisoformat(s)
|
||||
dt = datetime.fromisoformat(s)
|
||||
except ValueError:
|
||||
# Compact ICS form: 20260412T120000Z
|
||||
try:
|
||||
return datetime.strptime(s, '%Y%m%dT%H%M%S%z')
|
||||
dt = datetime.strptime(s, '%Y%m%dT%H%M%S%z')
|
||||
except ValueError:
|
||||
try:
|
||||
return datetime.strptime(s[:15], '%Y%m%dT%H%M%S').replace(tzinfo=timezone.utc)
|
||||
dt = datetime.strptime(s[:15], '%Y%m%dT%H%M%S').replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
return None
|
||||
# Unsere DB-Spalten sind tz-naive (lokal UTC) - Vergleich ginge
|
||||
# sonst mit TypeError. Also tz-Info abstreifen.
|
||||
if dt.tzinfo is not None:
|
||||
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return dt
|
||||
return parse(tr.get('start')), parse(tr.get('end'))
|
||||
|
||||
|
||||
@@ -386,7 +393,7 @@ def _extract_time_range(root: ET.Element):
|
||||
# GET single event
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dav_bp.route('/<username>/<cal_part>/<filename>', methods=['GET'])
|
||||
@dav_bp.route('/<username>/<cal_part>/<filename>', methods=['GET', 'HEAD'])
|
||||
@basic_auth
|
||||
def get_event(username, cal_part, filename):
|
||||
user: User = request.dav_user
|
||||
@@ -500,6 +507,53 @@ def delete_calendar(username, cal_part):
|
||||
return Response('', 204)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROPPATCH (Clients setzen gerne Anzeigefarbe/-name). Wir persistieren
|
||||
# den Kalenderfarbe (calendar-color) + Displayname; andere Properties
|
||||
# bestaetigen wir als "angewendet" damit DAVx5/Apple zufrieden sind.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dav_bp.route('/<username>/<cal_part>/', methods=['PROPPATCH'])
|
||||
@dav_bp.route('/<username>/<cal_part>', methods=['PROPPATCH'])
|
||||
@basic_auth
|
||||
def proppatch_calendar(username, cal_part):
|
||||
user: User = request.dav_user
|
||||
if username != user.username:
|
||||
return Response('', 403)
|
||||
cal_id = _parse_calendar_path(cal_part)
|
||||
cal = _calendar_for(user, cal_id) if cal_id else None
|
||||
if not cal:
|
||||
return Response('Not found', 404)
|
||||
try:
|
||||
root = ET.fromstring(request.data or b'<x/>')
|
||||
except ET.ParseError:
|
||||
return Response('Malformed XML', 400)
|
||||
|
||||
for el in root.iter():
|
||||
tag = el.tag
|
||||
if tag == _qn('ic', 'calendar-color') and el.text:
|
||||
cal.color = el.text.strip()[:7]
|
||||
elif tag == _qn('d', 'displayname') and el.text:
|
||||
cal.name = el.text.strip()[:255]
|
||||
db.session.commit()
|
||||
|
||||
# Respond with 207 marking everything as applied so the client is happy.
|
||||
multistatus = ET.Element(_qn('d', 'multistatus'))
|
||||
href = _href_calendar(user.username, cal.id)
|
||||
resp = ET.SubElement(multistatus, _qn('d', 'response'))
|
||||
ET.SubElement(resp, _qn('d', 'href')).text = href
|
||||
propstat = ET.SubElement(resp, _qn('d', 'propstat'))
|
||||
prop = ET.SubElement(propstat, _qn('d', 'prop'))
|
||||
# Echo back everything the client asked to set
|
||||
for set_block in root.findall(_qn('d', 'set')):
|
||||
inner_prop = set_block.find(_qn('d', 'prop'))
|
||||
if inner_prop is not None:
|
||||
for child in inner_prop:
|
||||
ET.SubElement(prop, child.tag)
|
||||
ET.SubElement(propstat, _qn('d', 'status')).text = 'HTTP/1.1 200 OK'
|
||||
return _xml_response(multistatus)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MKCALENDAR (create a new calendar collection via the DAV URL)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user