fix: Runde-2-Fixes – Monatsauswahl, CalDAV-Update, Lösch-Dialog, EXDATE
- Monatsansicht: selectedDate von currentDate getrennt, Klick verschiebt View nicht mehr - Selected-Day Styling: weißer Text auf Primary-Hintergrund statt nur Textfarbe - Kontextmenü: --bg-surface statt fehlendem --bg-card - CalDAV Update/Delete: parent Calendar-Objekt übergeben (behebt NoneType-Fehler) - HA-Kalender im Kalender-Selektor ergänzt - Browser-confirm() durch styled Modal-Dialog ersetzt mit Serie/Einzeln-Option - EXDATE-Support: einzelne Vorkommen wiederkehrender Termine löschen (lokal + CalDAV) - Fehlende i18n-Keys für Lösch-Dialog ergänzt (DE + EN)
This commit is contained in:
@@ -4,7 +4,7 @@ from datetime import date, datetime, timedelta, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import caldav
|
||||
from icalendar import Calendar, Event, vRecur
|
||||
from icalendar import Calendar, Event, vDatetime, vRecur
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -213,10 +213,15 @@ def create_event(
|
||||
|
||||
|
||||
def update_event(
|
||||
url: str, username: str, password: str, event_url: str, data: Dict
|
||||
url: str, username: str, password: str, event_url: str, data: Dict,
|
||||
calendar_url: str = None,
|
||||
):
|
||||
client = _client(url, username, password)
|
||||
resource = caldav.Event(client=client, url=event_url)
|
||||
if calendar_url:
|
||||
cal_obj = client.calendar(url=calendar_url)
|
||||
resource = caldav.Event(client=client, url=event_url, parent=cal_obj)
|
||||
else:
|
||||
resource = caldav.Event(client=client, url=event_url)
|
||||
resource.load()
|
||||
raw = resource.data
|
||||
|
||||
@@ -258,15 +263,31 @@ def update_event(
|
||||
elif "RRULE" in component:
|
||||
del component["RRULE"]
|
||||
|
||||
if "exdate" in data and data["exdate"]:
|
||||
# Parse YYYYMMDD string into a proper EXDATE
|
||||
exdate_str = data["exdate"]
|
||||
# Determine if event uses dates or datetimes
|
||||
dtstart_prop = component.get("DTSTART")
|
||||
if dtstart_prop and isinstance(dtstart_prop.dt, date) and not isinstance(dtstart_prop.dt, datetime):
|
||||
exdate_val = date(int(exdate_str[:4]), int(exdate_str[4:6]), int(exdate_str[6:8]))
|
||||
else:
|
||||
exdate_val = datetime(int(exdate_str[:4]), int(exdate_str[4:6]), int(exdate_str[6:8]), tzinfo=timezone.utc)
|
||||
component.add("exdate", [exdate_val])
|
||||
|
||||
new_cal.add_component(component)
|
||||
|
||||
resource.data = new_cal.to_ical().decode("utf-8")
|
||||
resource.save()
|
||||
|
||||
|
||||
def delete_event(url: str, username: str, password: str, event_url: str):
|
||||
def delete_event(url: str, username: str, password: str, event_url: str,
|
||||
calendar_url: str = None):
|
||||
client = _client(url, username, password)
|
||||
resource = caldav.Event(client=client, url=event_url)
|
||||
if calendar_url:
|
||||
cal_obj = client.calendar(url=calendar_url)
|
||||
resource = caldav.Event(client=client, url=event_url, parent=cal_obj)
|
||||
else:
|
||||
resource = caldav.Event(client=client, url=event_url)
|
||||
resource.delete()
|
||||
|
||||
|
||||
|
||||
@@ -90,6 +90,13 @@ def _migrate():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
conn.execute(text("ALTER TABLE local_events ADD COLUMN exdate TEXT"))
|
||||
conn.commit()
|
||||
logging.info("Migration: added exdate to local_events")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_migrate()
|
||||
|
||||
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
||||
|
||||
@@ -113,6 +113,7 @@ class LocalEvent(Base):
|
||||
description = Column(Text, nullable=True)
|
||||
color = Column(String(7), nullable=True)
|
||||
rrule = Column(Text, nullable=True)
|
||||
exdate = Column(Text, nullable=True) # Comma-separated YYYYMMDD dates to exclude
|
||||
|
||||
calendar = relationship("LocalCalendar", back_populates="events")
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ class EventUpdate(BaseModel):
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
rrule: Optional[str] = None
|
||||
exdate: Optional[str] = None
|
||||
|
||||
|
||||
def _account_dict(a: models.CalDAVAccount) -> dict:
|
||||
@@ -84,6 +85,13 @@ def _account_dict(a: models.CalDAVAccount) -> dict:
|
||||
def _expand_recurring_local(ev, local_cal, range_start, range_end):
|
||||
"""Expand a recurring LocalEvent into individual occurrences within the date range."""
|
||||
results = []
|
||||
# Parse excluded dates
|
||||
excluded = set()
|
||||
if ev.exdate:
|
||||
for d in ev.exdate.split(","):
|
||||
d = d.strip()
|
||||
if d:
|
||||
excluded.add(d)
|
||||
try:
|
||||
ev_start_str = ev.start.replace("Z", "+00:00")
|
||||
ev_end_str = ev.end.replace("Z", "+00:00")
|
||||
@@ -98,6 +106,9 @@ def _expand_recurring_local(ev, local_cal, range_start, range_end):
|
||||
occurrences = rule.between(r_start - timedelta(days=1), r_end + timedelta(days=1), inc=True)
|
||||
for occ in occurrences:
|
||||
occ_start = occ.date()
|
||||
occ_key = occ_start.strftime("%Y%m%d")
|
||||
if occ_key in excluded:
|
||||
continue
|
||||
occ_end = occ_start + duration
|
||||
results.append({
|
||||
"id": ev.uid,
|
||||
@@ -132,6 +143,9 @@ def _expand_recurring_local(ev, local_cal, range_start, range_end):
|
||||
r_end = r_end.replace(tzinfo=dt_timezone.utc)
|
||||
occurrences = rule.between(r_start - timedelta(days=1), r_end + timedelta(days=1), inc=True)
|
||||
for occ in occurrences:
|
||||
occ_key = occ.strftime("%Y%m%d")
|
||||
if occ_key in excluded:
|
||||
continue
|
||||
occ_end = occ + duration
|
||||
results.append({
|
||||
"id": ev.uid,
|
||||
@@ -548,6 +562,7 @@ def update_event(
|
||||
.all()
|
||||
)
|
||||
account = None
|
||||
cal_url = None
|
||||
if calendar_id is not None:
|
||||
cal = (
|
||||
db.query(models.Calendar)
|
||||
@@ -557,8 +572,15 @@ def update_event(
|
||||
)
|
||||
if cal:
|
||||
account = next((a for a in accounts if a.id == cal.account_id), None)
|
||||
cal_url = cal.cal_id
|
||||
if not account:
|
||||
account = _find_account_for_event_url(event_url, accounts)
|
||||
# Try to find the calendar URL for the account
|
||||
if account and not cal_url:
|
||||
for c in account.calendars:
|
||||
if event_url.startswith(c.cal_id) or event_url.startswith(_normalize_url(c.cal_id)):
|
||||
cal_url = c.cal_id
|
||||
break
|
||||
if not account:
|
||||
raise HTTPException(404, "Event not found or not authorized")
|
||||
try:
|
||||
@@ -568,6 +590,7 @@ def update_event(
|
||||
account.password,
|
||||
event_url,
|
||||
data.model_dump(exclude_none=True) if data else {},
|
||||
calendar_url=cal_url,
|
||||
)
|
||||
return {"ok": True}
|
||||
except Exception as exc:
|
||||
@@ -588,6 +611,7 @@ def delete_event(
|
||||
.all()
|
||||
)
|
||||
account = None
|
||||
cal_url = None
|
||||
if calendar_id is not None:
|
||||
cal = (
|
||||
db.query(models.Calendar)
|
||||
@@ -597,13 +621,20 @@ def delete_event(
|
||||
)
|
||||
if cal:
|
||||
account = next((a for a in accounts if a.id == cal.account_id), None)
|
||||
cal_url = cal.cal_id
|
||||
if not account:
|
||||
account = _find_account_for_event_url(event_url, accounts)
|
||||
if account and not cal_url:
|
||||
for c in account.calendars:
|
||||
if event_url.startswith(c.cal_id) or event_url.startswith(_normalize_url(c.cal_id)):
|
||||
cal_url = c.cal_id
|
||||
break
|
||||
if not account:
|
||||
raise HTTPException(404, "Event not found or not authorized")
|
||||
try:
|
||||
caldav_client.delete_event(
|
||||
account.url, account.username, account.password, event_url
|
||||
account.url, account.username, account.password, event_url,
|
||||
calendar_url=cal_url,
|
||||
)
|
||||
return {"ok": True}
|
||||
except Exception as exc:
|
||||
|
||||
@@ -44,6 +44,7 @@ class EventUpdate(BaseModel):
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
rrule: Optional[str] = None
|
||||
exdate: Optional[str] = None
|
||||
|
||||
|
||||
def _cal_dict(cal: models.LocalCalendar) -> dict:
|
||||
@@ -67,6 +68,7 @@ def _event_dict(ev: models.LocalEvent, cal: models.LocalCalendar) -> dict:
|
||||
"description": ev.description or "",
|
||||
"color": ev.color,
|
||||
"rrule": ev.rrule,
|
||||
"exdate": ev.exdate,
|
||||
"calendar_id": f"local-{cal.id}",
|
||||
"calendar_name": cal.name,
|
||||
"calendarColor": cal.color,
|
||||
@@ -225,6 +227,12 @@ def update_event(
|
||||
ev.color = data.color
|
||||
if data.rrule is not None:
|
||||
ev.rrule = data.rrule if data.rrule else None
|
||||
if data.exdate is not None:
|
||||
existing = ev.exdate or ""
|
||||
dates = [d for d in existing.split(",") if d]
|
||||
if data.exdate not in dates:
|
||||
dates.append(data.exdate)
|
||||
ev.exdate = ",".join(dates)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user