[V0.2] WORKING Working calendar and AI with full frontend.

This commit is contained in:
c-d-p
2025-04-20 12:12:35 +02:00
parent ee86374da6
commit 6cee996fb3
27 changed files with 996 additions and 488 deletions

View File

@@ -7,6 +7,12 @@ from core.database import get_engine, Base
from modules import router
import logging
# import all models to ensure they are registered before create_all
from modules.calendar.models import CalendarEvent
from modules.auth.models import User
logging.getLogger('passlib').setLevel(logging.ERROR) # fix bc package logging is broken
# Create DB tables (remove in production; use migrations instead)
@@ -29,7 +35,11 @@ app.include_router(router)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:8081"],
allow_origins=[
"http://localhost:8081", # Keep for web testing if needed
"http://192.168.1.9:8081", # Add your mobile device/emulator origin (adjust port if needed)
# Add other origins if necessary, e.g., production frontend URL
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]

View File

@@ -14,18 +14,24 @@ def read_admin():
return {"message": "Admin route"}
@router.get("/cleardb")
def clear_db(db: Annotated[Session, Depends(get_db)]):
def clear_db(db: Annotated[Session, Depends(get_db)], hard: bool):
"""
Clear the database.
'hard' parameter determines if the database should be completely reset.
"""
tables = Base.metadata.tables.keys()
for table in tables:
# delete all tables that isn't the users table
if table != "users":
table = Base.metadata.tables[table]
db.execute(table.delete())
if hard:
Base.metadata.drop_all(bind=db.get_bind())
Base.metadata.create_all(bind=db.get_bind())
return {"message": "Database reset (HARD)"}
else:
tables = Base.metadata.tables.keys()
for table_name in tables:
# delete all tables that isn't the users table
if table_name != "users":
table = Base.metadata.tables[table_name]
db.execute(table.delete())
# delete all non-admin accounts
db.query(User).filter(User.role != UserRole.ADMIN).delete()
db.commit()
return {"message": "Database cleared"}
db.query(User).filter(User.role != UserRole.ADMIN).delete()
db.commit()
return {"message": "Database cleared"}

View File

@@ -2,6 +2,7 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from datetime import datetime
from typing import List, Optional
from modules.auth.dependencies import get_current_user
from core.database import get_db
from core.exceptions import not_found_exception
@@ -19,15 +20,13 @@ def create_event(
):
return create_calendar_event(db, user.id, event)
@router.get("/events", response_model=list[CalendarEventResponse])
@router.get("/events", response_model=List[CalendarEventResponse])
def get_events(
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
start: datetime | None = None,
end: datetime | None = None
start: Optional[datetime] = None,
end: Optional[datetime] = None
):
start = None if start == "" else start
end = None if end == "" else end
return get_calendar_events(db, user.id, start, end)
@router.get("/events/{event_id}", response_model=CalendarEventResponse)
@@ -37,8 +36,6 @@ def get_event_by_id(
db: Session = Depends(get_db)
):
event = get_calendar_event_by_id(db, user.id, event_id)
if not event:
raise not_found_exception()
return event
@router.patch("/events/{event_id}", response_model=CalendarEventResponse)
@@ -50,11 +47,10 @@ def update_event(
):
return update_calendar_event(db, user.id, event_id, event)
@router.delete("/events/{event_id}")
@router.delete("/events/{event_id}", status_code=204)
def delete_event(
event_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
delete_calendar_event(db, user.id, event_id)
return {"message": "Event deleted"}
delete_calendar_event(db, user.id, event_id)

View File

@@ -1,5 +1,5 @@
# modules/calendar/models.py
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON # Add JSON
from sqlalchemy.orm import relationship
from core.database import Base
@@ -12,6 +12,8 @@ class CalendarEvent(Base):
start = Column(DateTime, nullable=False)
end = Column(DateTime)
location = Column(String)
tags = Column(JSON)
color = Column(String) # hex code for color
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # <-- Relationship
# Bi-directional relationship (for eager loading)

View File

@@ -1,31 +1,60 @@
# modules/calendar/schemas.py
from datetime import datetime
from pydantic import BaseModel
from pydantic import BaseModel, field_validator # Add field_validator
from typing import List, Optional # Add List and Optional
class CalendarEventCreate(BaseModel):
# Base schema for common fields, including tags
class CalendarEventBase(BaseModel):
title: str
description: str | None = None
description: Optional[str] = None
start: datetime
end: datetime | None = None
location: str | None = None
end: Optional[datetime] = None
location: Optional[str] = None
color: Optional[str] = None # Assuming color exists
tags: Optional[List[str]] = None # Add optional tags
@field_validator('tags', mode='before')
@classmethod
def tags_validate_null_string(cls, v):
if v == "Null":
return None
return v
# Schema for creating an event (inherits from Base)
class CalendarEventCreate(CalendarEventBase):
pass
# Schema for updating an event (all fields optional)
class CalendarEventUpdate(BaseModel):
title: str | None = None
description: str | None = None
start: datetime | None = None
end: datetime | None = None
location: str | None = None
title: Optional[str] = None
description: Optional[str] = None
start: Optional[datetime] = None
end: Optional[datetime] = None
location: Optional[str] = None
color: Optional[str] = None
tags: Optional[List[str]] = None # Add optional tags for update
class CalendarEventResponse(CalendarEventCreate):
@field_validator('tags', mode='before')
@classmethod
def tags_validate_null_string(cls, v):
if v == "Null":
return None
return v
# Schema for the response (inherits from Base, adds ID and user_id)
class CalendarEventResponse(CalendarEventBase):
id: int
user_id: int
tags: List[str] # Keep as List[str], remove default []
@field_validator('tags', mode='before')
@classmethod
def tags_validate_none_to_list(cls, v):
# If the value from the source object (e.g., ORM model) is None,
# convert it to an empty list before Pydantic validation.
if v is None:
return []
return v
class Config:
from_attributes = True
class CalendarEventUpdate(BaseModel):
title: str | None = None
description: str | None = None
start: datetime | None = None
end: datetime | None = None
location: str | None = None
from_attributes = True # Changed from orm_mode

View File

@@ -1,25 +1,50 @@
# modules/calendar/service.py
from sqlalchemy.orm import Session
from sqlalchemy import or_ # Import or_
from datetime import datetime
from modules.calendar.models import CalendarEvent
from core.exceptions import not_found_exception
from modules.calendar.schemas import CalendarEventCreate, CalendarEventUpdate # Import schemas
def create_calendar_event(db: Session, user_id: int, event_data):
event = CalendarEvent(**event_data.dict(), user_id=user_id)
def create_calendar_event(db: Session, user_id: int, event_data: CalendarEventCreate):
# Ensure tags is None if not provided or empty list, matching model
tags_to_store = event_data.tags if event_data.tags else None
event = CalendarEvent(
**event_data.model_dump(exclude={'tags'}), # Use model_dump and exclude tags initially
tags=tags_to_store, # Set tags separately
user_id=user_id
)
db.add(event)
db.commit()
db.refresh(event)
return event
def get_calendar_events(db: Session, user_id: int, start: datetime, end: datetime):
query = db.query(CalendarEvent).filter(
CalendarEvent.user_id == user_id
)
if start:
query = query.filter(CalendarEvent.start_time >= start)
if end:
query = query.filter(CalendarEvent.end_time <= end)
return query.all()
def get_calendar_events(db: Session, user_id: int, start: datetime | None, end: datetime | None):
query = db.query(CalendarEvent).filter(CalendarEvent.user_id == user_id)
# If start and end dates are provided, filter for events overlapping the range.
# An event overlaps if: event_start < query_end AND (event_end IS NULL OR event_end > query_start)
if start and end:
query = query.filter(
CalendarEvent.start < end, # Event starts before the query window ends
or_(
CalendarEvent.end == None, # Event has no end date (considered single point in time at start)
CalendarEvent.end > start # Event ends after the query window starts
)
)
# If only start is provided, filter events starting on or after start
elif start:
query = query.filter(CalendarEvent.start >= start)
# If only end is provided, filter events ending on or before end (or starting before end if no end date)
elif end:
query = query.filter(
or_(
CalendarEvent.end <= end,
(CalendarEvent.end == None and CalendarEvent.start < end)
)
)
return query.order_by(CalendarEvent.start).all() # Order by start time
def get_calendar_event_by_id(db: Session, user_id: int, event_id: int):
event = db.query(CalendarEvent).filter(
@@ -30,25 +55,23 @@ def get_calendar_event_by_id(db: Session, user_id: int, event_id: int):
raise not_found_exception()
return event
def update_calendar_event(db: Session, user_id: int, event_id: int, event_data):
event = db.query(CalendarEvent).filter(
CalendarEvent.id == event_id,
CalendarEvent.user_id == user_id
).first()
if not event:
raise not_found_exception()
for key, value in event_data.dict().items():
setattr(event, key, value)
def update_calendar_event(db: Session, user_id: int, event_id: int, event_data: CalendarEventUpdate):
event = get_calendar_event_by_id(db, user_id, event_id) # Reuse get_by_id for check
# Use model_dump with exclude_unset=True to only update provided fields
update_data = event_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
# Ensure tags is handled correctly (set to None if empty list provided)
if key == 'tags' and isinstance(value, list) and not value:
setattr(event, key, None)
else:
setattr(event, key, value)
db.commit()
db.refresh(event)
return event
def delete_calendar_event(db: Session, user_id: int, event_id: int):
event = db.query(CalendarEvent).filter(
CalendarEvent.id == event_id,
CalendarEvent.user_id == user_id
).first()
if not event:
raise not_found_exception()
event = get_calendar_event_by_id(db, user_id, event_id) # Reuse get_by_id for check
db.delete(event)
db.commit()