Lokale Kalender und iCal-URL-Abonnements
Neue Features: - Lokale Kalender erstellen mit vollem Event-CRUD (in SQLite gespeichert) - iCal-URLs abonnieren mit Auto-Refresh und lokalem Caching - iCal-Events sind editierbar/löschbar (Änderungen als lokale Overrides) - Sidebar zeigt alle 3 Kalendertypen mit Farbe, Umbenennen, Löschen - Dropdown "Kalender hinzufügen" mit 3 Optionen (Lokal, CalDAV, iCal) Backend: models.py (4 neue Tabellen), local_router.py, ical_router.py Frontend: Neue Modals, erweiterte Sidebar, Source-basiertes Event-Routing
This commit is contained in:
@@ -12,7 +12,7 @@ from sqlalchemy import text
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from database import Base, engine
|
||||
from routers import auth_router, caldav_router, profile_router, settings_router, users_router
|
||||
from routers import auth_router, caldav_router, ical_router, local_router, profile_router, settings_router, users_router
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@@ -41,6 +41,8 @@ app.include_router(users_router.router, prefix="/api/users", tags=["users"])
|
||||
app.include_router(caldav_router.router, prefix="/api/caldav", tags=["caldav"])
|
||||
app.include_router(settings_router.router, prefix="/api/settings", tags=["settings"])
|
||||
app.include_router(profile_router.router, prefix="/api/profile", tags=["profile"])
|
||||
app.include_router(local_router.router, prefix="/api/local", tags=["local"])
|
||||
app.include_router(ical_router.router, prefix="/api/ical", tags=["ical"])
|
||||
|
||||
FRONTEND_DIR = Path(__file__).parent.parent / "frontend"
|
||||
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from database import Base
|
||||
|
||||
@@ -21,6 +21,12 @@ class User(Base):
|
||||
settings = relationship(
|
||||
"UserSettings", back_populates="user", uselist=False, cascade="all, delete-orphan"
|
||||
)
|
||||
local_calendars = relationship(
|
||||
"LocalCalendar", back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
ical_subscriptions = relationship(
|
||||
"ICalSubscription", back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class CalDAVAccount(Base):
|
||||
@@ -67,3 +73,68 @@ class UserSettings(Base):
|
||||
dim_past_events = Column(Boolean, default=False)
|
||||
|
||||
user = relationship("User", back_populates="settings")
|
||||
|
||||
|
||||
class LocalCalendar(Base):
|
||||
__tablename__ = "local_calendars"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
color = Column(String(7), default="#34a853")
|
||||
enabled = Column(Boolean, default=True)
|
||||
|
||||
user = relationship("User", back_populates="local_calendars")
|
||||
events = relationship("LocalEvent", back_populates="calendar", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class LocalEvent(Base):
|
||||
__tablename__ = "local_events"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
calendar_id = Column(Integer, ForeignKey("local_calendars.id"), nullable=False)
|
||||
uid = Column(String(255), nullable=False, unique=True)
|
||||
title = Column(String(255), nullable=False)
|
||||
start = Column(String(50), nullable=False)
|
||||
end = Column(String(50), nullable=False)
|
||||
all_day = Column(Boolean, default=False)
|
||||
location = Column(String(500), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
color = Column(String(7), nullable=True)
|
||||
|
||||
calendar = relationship("LocalCalendar", back_populates="events")
|
||||
|
||||
|
||||
class ICalSubscription(Base):
|
||||
__tablename__ = "ical_subscriptions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
url = Column(String(1000), nullable=False)
|
||||
color = Column(String(7), default="#46bdc6")
|
||||
enabled = Column(Boolean, default=True)
|
||||
refresh_minutes = Column(Integer, default=60)
|
||||
last_fetched = Column(DateTime, nullable=True)
|
||||
cached_ics = Column(Text, nullable=True)
|
||||
|
||||
user = relationship("User", back_populates="ical_subscriptions")
|
||||
overrides = relationship("ICalOverride", back_populates="subscription", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ICalOverride(Base):
|
||||
__tablename__ = "ical_overrides"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
subscription_id = Column(Integer, ForeignKey("ical_subscriptions.id"), nullable=False)
|
||||
event_uid = Column(String(500), nullable=False)
|
||||
hidden = Column(Boolean, default=False)
|
||||
title = Column(String(255), nullable=True)
|
||||
start = Column(String(50), nullable=True)
|
||||
end = Column(String(50), nullable=True)
|
||||
all_day = Column(Boolean, nullable=True)
|
||||
location = Column(String(500), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
color = Column(String(7), nullable=True)
|
||||
|
||||
subscription = relationship("ICalSubscription", back_populates="overrides")
|
||||
|
||||
@@ -9,6 +9,7 @@ import caldav_client
|
||||
import models
|
||||
from auth import get_current_user
|
||||
from database import get_db
|
||||
from routers.ical_router import _refresh_if_needed, get_events_for_subscription
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -283,6 +284,58 @@ def get_events(
|
||||
"Error fetching calendar %s: %s", calendar.id, exc
|
||||
)
|
||||
|
||||
# ── Local calendar events ─────────────────────────────
|
||||
local_calendars = (
|
||||
db.query(models.LocalCalendar)
|
||||
.filter(
|
||||
models.LocalCalendar.user_id == current_user.id,
|
||||
models.LocalCalendar.enabled == True,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for local_cal in local_calendars:
|
||||
local_events = (
|
||||
db.query(models.LocalEvent)
|
||||
.filter(
|
||||
models.LocalEvent.calendar_id == local_cal.id,
|
||||
models.LocalEvent.start < end,
|
||||
models.LocalEvent.end > start,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for ev in local_events:
|
||||
all_events.append({
|
||||
"id": ev.uid,
|
||||
"url": f"local://{ev.uid}",
|
||||
"title": ev.title,
|
||||
"start": ev.start,
|
||||
"end": ev.end,
|
||||
"allDay": ev.all_day,
|
||||
"location": ev.location or "",
|
||||
"description": ev.description or "",
|
||||
"color": ev.color,
|
||||
"calendar_id": f"local-{local_cal.id}",
|
||||
"calendar_name": local_cal.name,
|
||||
"calendarColor": local_cal.color,
|
||||
"source": "local",
|
||||
})
|
||||
|
||||
# ── iCal subscription events ──────────────────────────
|
||||
ical_subs = (
|
||||
db.query(models.ICalSubscription)
|
||||
.filter(
|
||||
models.ICalSubscription.user_id == current_user.id,
|
||||
models.ICalSubscription.enabled == True,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for sub in ical_subs:
|
||||
try:
|
||||
_refresh_if_needed(sub, db)
|
||||
all_events.extend(get_events_for_subscription(sub, start_dt, end_dt, db))
|
||||
except Exception as exc:
|
||||
logger.error("Error fetching iCal subscription %s: %s", sub.id, exc)
|
||||
|
||||
return all_events
|
||||
|
||||
|
||||
|
||||
359
backend/routers/ical_router.py
Normal file
359
backend/routers/ical_router.py
Normal file
@@ -0,0 +1,359 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import requests as http_requests
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import models
|
||||
from auth import get_current_user
|
||||
from caldav_client import _parse_ics
|
||||
from database import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class SubscriptionCreate(BaseModel):
|
||||
name: str
|
||||
url: str
|
||||
color: str = "#46bdc6"
|
||||
refresh_minutes: int = 60
|
||||
|
||||
|
||||
class SubscriptionUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
refresh_minutes: Optional[int] = None
|
||||
|
||||
|
||||
def _sub_dict(sub: models.ICalSubscription) -> dict:
|
||||
return {
|
||||
"id": sub.id,
|
||||
"name": sub.name,
|
||||
"url": sub.url,
|
||||
"color": sub.color,
|
||||
"enabled": sub.enabled,
|
||||
"refresh_minutes": sub.refresh_minutes,
|
||||
"last_fetched": sub.last_fetched.isoformat() if sub.last_fetched else None,
|
||||
}
|
||||
|
||||
|
||||
def _fetch_ics(url: str) -> str:
|
||||
"""Download .ics content from a URL."""
|
||||
try:
|
||||
resp = http_requests.get(url, timeout=30, allow_redirects=True)
|
||||
resp.raise_for_status()
|
||||
return resp.text
|
||||
except http_requests.RequestException as e:
|
||||
raise ValueError(f"Fehler beim Abrufen der URL: {e}")
|
||||
|
||||
|
||||
def _refresh_if_needed(sub: models.ICalSubscription, db: Session, force: bool = False):
|
||||
"""Refresh cached ICS data if stale or forced."""
|
||||
now = datetime.now(timezone.utc)
|
||||
if (
|
||||
not force
|
||||
and sub.cached_ics
|
||||
and sub.last_fetched
|
||||
and (now - sub.last_fetched.replace(tzinfo=timezone.utc)).total_seconds()
|
||||
< sub.refresh_minutes * 60
|
||||
):
|
||||
return # Cache still fresh
|
||||
|
||||
try:
|
||||
sub.cached_ics = _fetch_ics(sub.url)
|
||||
sub.last_fetched = now
|
||||
db.commit()
|
||||
except ValueError:
|
||||
logger.warning("Could not refresh iCal subscription %s", sub.id)
|
||||
if not sub.cached_ics:
|
||||
raise
|
||||
|
||||
|
||||
def get_events_for_subscription(
|
||||
sub: models.ICalSubscription, start_dt: datetime, end_dt: datetime, db: Session = None
|
||||
) -> list:
|
||||
"""Parse cached ICS and filter events within the date range, applying local overrides."""
|
||||
if not sub.cached_ics:
|
||||
return []
|
||||
|
||||
# Load overrides for this subscription
|
||||
overrides = {}
|
||||
if db:
|
||||
for ov in (
|
||||
db.query(models.ICalOverride)
|
||||
.filter(models.ICalOverride.subscription_id == sub.id)
|
||||
.all()
|
||||
):
|
||||
overrides[ov.event_uid] = ov
|
||||
|
||||
all_events = _parse_ics(sub.cached_ics, f"ical://{sub.id}")
|
||||
filtered = []
|
||||
for ev in all_events:
|
||||
uid = ev["id"]
|
||||
|
||||
# Skip hidden (deleted) events
|
||||
ov = overrides.get(uid)
|
||||
if ov and ov.hidden:
|
||||
continue
|
||||
|
||||
# Apply overrides
|
||||
if ov:
|
||||
if ov.title is not None:
|
||||
ev["title"] = ov.title
|
||||
if ov.start is not None:
|
||||
ev["start"] = ov.start
|
||||
if ov.end is not None:
|
||||
ev["end"] = ov.end
|
||||
if ov.all_day is not None:
|
||||
ev["allDay"] = ov.all_day
|
||||
if ov.location is not None:
|
||||
ev["location"] = ov.location
|
||||
if ov.description is not None:
|
||||
ev["description"] = ov.description
|
||||
if ov.color is not None:
|
||||
ev["color"] = ov.color
|
||||
|
||||
try:
|
||||
ev_start = ev["start"]
|
||||
ev_end = ev["end"]
|
||||
if ev["allDay"]:
|
||||
if ev_end > start_dt.strftime("%Y-%m-%d") and ev_start < end_dt.strftime("%Y-%m-%d"):
|
||||
filtered.append(ev)
|
||||
else:
|
||||
ev_s = datetime.fromisoformat(ev_start.replace("Z", "+00:00"))
|
||||
ev_e = datetime.fromisoformat(ev_end.replace("Z", "+00:00"))
|
||||
if ev_s.tzinfo is None:
|
||||
ev_s = ev_s.replace(tzinfo=timezone.utc)
|
||||
if ev_e.tzinfo is None:
|
||||
ev_e = ev_e.replace(tzinfo=timezone.utc)
|
||||
if ev_e > start_dt and ev_s < end_dt:
|
||||
filtered.append(ev)
|
||||
except Exception:
|
||||
filtered.append(ev)
|
||||
|
||||
for ev in filtered:
|
||||
ev["calendar_id"] = f"ical-{sub.id}"
|
||||
ev["calendar_name"] = sub.name
|
||||
ev["calendarColor"] = sub.color
|
||||
ev["source"] = "ical"
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
# ── Subscription CRUD ─────────────────────────────────────
|
||||
|
||||
@router.get("/subscriptions")
|
||||
def list_subscriptions(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
subs = (
|
||||
db.query(models.ICalSubscription)
|
||||
.filter(models.ICalSubscription.user_id == current_user.id)
|
||||
.all()
|
||||
)
|
||||
return [_sub_dict(s) for s in subs]
|
||||
|
||||
|
||||
@router.post("/subscriptions")
|
||||
def create_subscription(
|
||||
data: SubscriptionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
# Validate URL by fetching it
|
||||
try:
|
||||
ics_content = _fetch_ics(data.url)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
|
||||
sub = models.ICalSubscription(
|
||||
user_id=current_user.id,
|
||||
name=data.name,
|
||||
url=data.url,
|
||||
color=data.color,
|
||||
refresh_minutes=data.refresh_minutes,
|
||||
cached_ics=ics_content,
|
||||
last_fetched=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(sub)
|
||||
db.commit()
|
||||
db.refresh(sub)
|
||||
return _sub_dict(sub)
|
||||
|
||||
|
||||
@router.put("/subscriptions/{sub_id}")
|
||||
def update_subscription(
|
||||
sub_id: int,
|
||||
data: SubscriptionUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sub = (
|
||||
db.query(models.ICalSubscription)
|
||||
.filter(
|
||||
models.ICalSubscription.id == sub_id,
|
||||
models.ICalSubscription.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sub:
|
||||
raise HTTPException(404, "Subscription not found")
|
||||
if data.name is not None:
|
||||
sub.name = data.name
|
||||
if data.url is not None:
|
||||
sub.url = data.url
|
||||
if data.color is not None:
|
||||
sub.color = data.color
|
||||
if data.enabled is not None:
|
||||
sub.enabled = data.enabled
|
||||
if data.refresh_minutes is not None:
|
||||
sub.refresh_minutes = data.refresh_minutes
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/subscriptions/{sub_id}")
|
||||
def delete_subscription(
|
||||
sub_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sub = (
|
||||
db.query(models.ICalSubscription)
|
||||
.filter(
|
||||
models.ICalSubscription.id == sub_id,
|
||||
models.ICalSubscription.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sub:
|
||||
raise HTTPException(404, "Subscription not found")
|
||||
db.delete(sub)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/subscriptions/{sub_id}/refresh")
|
||||
def refresh_subscription(
|
||||
sub_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sub = (
|
||||
db.query(models.ICalSubscription)
|
||||
.filter(
|
||||
models.ICalSubscription.id == sub_id,
|
||||
models.ICalSubscription.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sub:
|
||||
raise HTTPException(404, "Subscription not found")
|
||||
try:
|
||||
_refresh_if_needed(sub, db, force=True)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
return _sub_dict(sub)
|
||||
|
||||
|
||||
# ── Event overrides (edit/delete iCal events locally) ─────
|
||||
|
||||
class ICalEventUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
start: Optional[str] = None
|
||||
end: Optional[str] = None
|
||||
allDay: Optional[bool] = None
|
||||
location: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
|
||||
|
||||
@router.put("/events/{sub_id}/{event_uid}")
|
||||
def update_ical_event(
|
||||
sub_id: int,
|
||||
event_uid: str,
|
||||
data: ICalEventUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sub = (
|
||||
db.query(models.ICalSubscription)
|
||||
.filter(
|
||||
models.ICalSubscription.id == sub_id,
|
||||
models.ICalSubscription.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sub:
|
||||
raise HTTPException(404, "Subscription not found")
|
||||
|
||||
ov = (
|
||||
db.query(models.ICalOverride)
|
||||
.filter(
|
||||
models.ICalOverride.subscription_id == sub_id,
|
||||
models.ICalOverride.event_uid == event_uid,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not ov:
|
||||
ov = models.ICalOverride(subscription_id=sub_id, event_uid=event_uid)
|
||||
db.add(ov)
|
||||
|
||||
if data.title is not None:
|
||||
ov.title = data.title
|
||||
if data.start is not None:
|
||||
ov.start = data.start
|
||||
if data.end is not None:
|
||||
ov.end = data.end
|
||||
if data.allDay is not None:
|
||||
ov.all_day = data.allDay
|
||||
if data.location is not None:
|
||||
ov.location = data.location
|
||||
if data.description is not None:
|
||||
ov.description = data.description
|
||||
if data.color is not None:
|
||||
ov.color = data.color
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/events/{sub_id}/{event_uid}")
|
||||
def delete_ical_event(
|
||||
sub_id: int,
|
||||
event_uid: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sub = (
|
||||
db.query(models.ICalSubscription)
|
||||
.filter(
|
||||
models.ICalSubscription.id == sub_id,
|
||||
models.ICalSubscription.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not sub:
|
||||
raise HTTPException(404, "Subscription not found")
|
||||
|
||||
ov = (
|
||||
db.query(models.ICalOverride)
|
||||
.filter(
|
||||
models.ICalOverride.subscription_id == sub_id,
|
||||
models.ICalOverride.event_uid == event_uid,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not ov:
|
||||
ov = models.ICalOverride(subscription_id=sub_id, event_uid=event_uid, hidden=True)
|
||||
db.add(ov)
|
||||
else:
|
||||
ov.hidden = True
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
245
backend/routers/local_router.py
Normal file
245
backend/routers/local_router.py
Normal file
@@ -0,0 +1,245 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import models
|
||||
from auth import get_current_user
|
||||
from database import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CalendarCreate(BaseModel):
|
||||
name: str
|
||||
color: str = "#34a853"
|
||||
|
||||
|
||||
class CalendarUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
|
||||
|
||||
class EventCreate(BaseModel):
|
||||
calendar_id: int
|
||||
title: str
|
||||
start: str
|
||||
end: str
|
||||
allDay: bool = False
|
||||
location: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
|
||||
|
||||
class EventUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
start: Optional[str] = None
|
||||
end: Optional[str] = None
|
||||
allDay: Optional[bool] = None
|
||||
location: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
|
||||
|
||||
def _cal_dict(cal: models.LocalCalendar) -> dict:
|
||||
return {
|
||||
"id": cal.id,
|
||||
"name": cal.name,
|
||||
"color": cal.color,
|
||||
"enabled": cal.enabled,
|
||||
}
|
||||
|
||||
|
||||
def _event_dict(ev: models.LocalEvent, cal: models.LocalCalendar) -> dict:
|
||||
return {
|
||||
"id": ev.uid,
|
||||
"url": f"local://{ev.uid}",
|
||||
"title": ev.title,
|
||||
"start": ev.start,
|
||||
"end": ev.end,
|
||||
"allDay": ev.all_day,
|
||||
"location": ev.location or "",
|
||||
"description": ev.description or "",
|
||||
"color": ev.color,
|
||||
"calendar_id": f"local-{cal.id}",
|
||||
"calendar_name": cal.name,
|
||||
"calendarColor": cal.color,
|
||||
"source": "local",
|
||||
}
|
||||
|
||||
|
||||
# ── Calendar CRUD ─────────────────────────────────────────
|
||||
|
||||
@router.get("/calendars")
|
||||
def list_calendars(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
cals = (
|
||||
db.query(models.LocalCalendar)
|
||||
.filter(models.LocalCalendar.user_id == current_user.id)
|
||||
.all()
|
||||
)
|
||||
return [_cal_dict(c) for c in cals]
|
||||
|
||||
|
||||
@router.post("/calendars")
|
||||
def create_calendar(
|
||||
data: CalendarCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
cal = models.LocalCalendar(
|
||||
user_id=current_user.id,
|
||||
name=data.name,
|
||||
color=data.color,
|
||||
)
|
||||
db.add(cal)
|
||||
db.commit()
|
||||
db.refresh(cal)
|
||||
return _cal_dict(cal)
|
||||
|
||||
|
||||
@router.put("/calendars/{calendar_id}")
|
||||
def update_calendar(
|
||||
calendar_id: int,
|
||||
data: CalendarUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
cal = (
|
||||
db.query(models.LocalCalendar)
|
||||
.filter(
|
||||
models.LocalCalendar.id == calendar_id,
|
||||
models.LocalCalendar.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not cal:
|
||||
raise HTTPException(404, "Calendar not found")
|
||||
if data.name is not None:
|
||||
cal.name = data.name
|
||||
if data.color is not None:
|
||||
cal.color = data.color
|
||||
if data.enabled is not None:
|
||||
cal.enabled = data.enabled
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/calendars/{calendar_id}")
|
||||
def delete_calendar(
|
||||
calendar_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
cal = (
|
||||
db.query(models.LocalCalendar)
|
||||
.filter(
|
||||
models.LocalCalendar.id == calendar_id,
|
||||
models.LocalCalendar.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not cal:
|
||||
raise HTTPException(404, "Calendar not found")
|
||||
db.delete(cal)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Event CRUD ────────────────────────────────────────────
|
||||
|
||||
@router.post("/events")
|
||||
def create_event(
|
||||
data: EventCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
cal = (
|
||||
db.query(models.LocalCalendar)
|
||||
.filter(
|
||||
models.LocalCalendar.id == data.calendar_id,
|
||||
models.LocalCalendar.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not cal:
|
||||
raise HTTPException(404, "Calendar not found")
|
||||
|
||||
ev = models.LocalEvent(
|
||||
calendar_id=cal.id,
|
||||
uid=str(uuid.uuid4()),
|
||||
title=data.title,
|
||||
start=data.start,
|
||||
end=data.end,
|
||||
all_day=data.allDay,
|
||||
location=data.location,
|
||||
description=data.description,
|
||||
color=data.color,
|
||||
)
|
||||
db.add(ev)
|
||||
db.commit()
|
||||
db.refresh(ev)
|
||||
return _event_dict(ev, cal)
|
||||
|
||||
|
||||
@router.put("/events/{uid}")
|
||||
def update_event(
|
||||
uid: str,
|
||||
data: EventUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
ev = (
|
||||
db.query(models.LocalEvent)
|
||||
.join(models.LocalCalendar)
|
||||
.filter(
|
||||
models.LocalEvent.uid == uid,
|
||||
models.LocalCalendar.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not ev:
|
||||
raise HTTPException(404, "Event not found")
|
||||
if data.title is not None:
|
||||
ev.title = data.title
|
||||
if data.start is not None:
|
||||
ev.start = data.start
|
||||
if data.end is not None:
|
||||
ev.end = data.end
|
||||
if data.allDay is not None:
|
||||
ev.all_day = data.allDay
|
||||
if data.location is not None:
|
||||
ev.location = data.location
|
||||
if data.description is not None:
|
||||
ev.description = data.description
|
||||
if data.color is not None:
|
||||
ev.color = data.color
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/events/{uid}")
|
||||
def delete_event(
|
||||
uid: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
ev = (
|
||||
db.query(models.LocalEvent)
|
||||
.join(models.LocalCalendar)
|
||||
.filter(
|
||||
models.LocalEvent.uid == uid,
|
||||
models.LocalCalendar.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not ev:
|
||||
raise HTTPException(404, "Event not found")
|
||||
db.delete(ev)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
Reference in New Issue
Block a user