Tier 3: appointments CRUD + time tracking with topbar timer
This commit is contained in:
@@ -27,6 +27,9 @@ TRASH_ENTITIES = [
|
||||
{"table": "meetings", "label": "Meetings", "name_col": "title", "url": "/meetings/{id}"},
|
||||
{"table": "decisions", "label": "Decisions", "name_col": "title", "url": "/decisions/{id}"},
|
||||
{"table": "files", "label": "Files", "name_col": "original_filename", "url": "/files"},
|
||||
{"table": "weblinks", "label": "Weblinks", "name_col": "label", "url": "/weblinks"},
|
||||
{"table": "weblink_folders", "label": "Weblink Folders", "name_col": "name", "url": "/weblinks"},
|
||||
{"table": "appointments", "label": "Appointments", "name_col": "title", "url": "/appointments/{id}"},
|
||||
{"table": "capture", "label": "Capture", "name_col": "raw_text", "url": "/capture"},
|
||||
{"table": "daily_focus", "label": "Focus Items", "name_col": "id", "url": "/focus"},
|
||||
]
|
||||
|
||||
302
routers/appointments.py
Normal file
302
routers/appointments.py
Normal file
@@ -0,0 +1,302 @@
|
||||
"""Appointments CRUD: scheduling with contacts, recurrence, all-day support."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form, Query
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from core.database import get_db
|
||||
from core.base_repository import BaseRepository
|
||||
from core.sidebar import get_sidebar_data
|
||||
|
||||
router = APIRouter(prefix="/appointments", tags=["appointments"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_appointments(
|
||||
request: Request,
|
||||
status: Optional[str] = None,
|
||||
timeframe: Optional[str] = "upcoming",
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
repo = BaseRepository("appointments", db)
|
||||
|
||||
# Build filter and sort based on timeframe
|
||||
if timeframe == "past":
|
||||
result = await db.execute(text("""
|
||||
SELECT * FROM appointments
|
||||
WHERE is_deleted = false AND start_at < now()
|
||||
ORDER BY start_at DESC
|
||||
LIMIT 100
|
||||
"""))
|
||||
elif timeframe == "all":
|
||||
result = await db.execute(text("""
|
||||
SELECT * FROM appointments
|
||||
WHERE is_deleted = false
|
||||
ORDER BY start_at DESC
|
||||
LIMIT 200
|
||||
"""))
|
||||
else:
|
||||
# upcoming (default)
|
||||
result = await db.execute(text("""
|
||||
SELECT * FROM appointments
|
||||
WHERE is_deleted = false AND start_at >= CURRENT_DATE
|
||||
ORDER BY start_at ASC
|
||||
LIMIT 100
|
||||
"""))
|
||||
|
||||
appointments = [dict(r._mapping) for r in result]
|
||||
|
||||
# Get contact counts per appointment
|
||||
if appointments:
|
||||
ids = [str(a["id"]) for a in appointments]
|
||||
placeholders = ", ".join(f"'{i}'" for i in ids)
|
||||
contact_result = await db.execute(text(f"""
|
||||
SELECT appointment_id, count(*) as cnt
|
||||
FROM contact_appointments
|
||||
WHERE appointment_id IN ({placeholders})
|
||||
GROUP BY appointment_id
|
||||
"""))
|
||||
contact_counts = {str(r._mapping["appointment_id"]): r._mapping["cnt"] for r in contact_result}
|
||||
for a in appointments:
|
||||
a["contact_count"] = contact_counts.get(str(a["id"]), 0)
|
||||
|
||||
count = len(appointments)
|
||||
|
||||
return templates.TemplateResponse("appointments.html", {
|
||||
"request": request,
|
||||
"sidebar": sidebar,
|
||||
"appointments": appointments,
|
||||
"count": count,
|
||||
"timeframe": timeframe or "upcoming",
|
||||
"page_title": "Appointments",
|
||||
"active_nav": "appointments",
|
||||
})
|
||||
|
||||
|
||||
@router.get("/new")
|
||||
async def new_appointment(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
|
||||
# Load contacts for attendee selection
|
||||
result = await db.execute(text("""
|
||||
SELECT id, first_name, last_name, company
|
||||
FROM contacts WHERE is_deleted = false
|
||||
ORDER BY first_name, last_name
|
||||
"""))
|
||||
contacts = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("appointment_form.html", {
|
||||
"request": request,
|
||||
"sidebar": sidebar,
|
||||
"appointment": None,
|
||||
"contacts": contacts,
|
||||
"selected_contacts": [],
|
||||
"page_title": "New Appointment",
|
||||
"active_nav": "appointments",
|
||||
})
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_appointment(
|
||||
request: Request,
|
||||
title: str = Form(...),
|
||||
description: Optional[str] = Form(None),
|
||||
location: Optional[str] = Form(None),
|
||||
start_date: str = Form(...),
|
||||
start_time: Optional[str] = Form(None),
|
||||
end_date: Optional[str] = Form(None),
|
||||
end_time: Optional[str] = Form(None),
|
||||
all_day: Optional[str] = Form(None),
|
||||
recurrence: Optional[str] = Form(None),
|
||||
tags: Optional[str] = Form(None),
|
||||
contact_ids: Optional[list[str]] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
is_all_day = all_day == "on"
|
||||
|
||||
# Build start_at
|
||||
if is_all_day:
|
||||
start_at = f"{start_date}T00:00:00"
|
||||
else:
|
||||
start_at = f"{start_date}T{start_time or '09:00'}:00"
|
||||
|
||||
# Build end_at
|
||||
end_at = None
|
||||
if end_date:
|
||||
if is_all_day:
|
||||
end_at = f"{end_date}T23:59:59"
|
||||
else:
|
||||
end_at = f"{end_date}T{end_time or '10:00'}:00"
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"description": description or None,
|
||||
"location": location or None,
|
||||
"start_at": start_at,
|
||||
"end_at": end_at,
|
||||
"all_day": is_all_day,
|
||||
"recurrence": recurrence or None,
|
||||
"tags": [t.strip() for t in tags.split(",") if t.strip()] if tags else None,
|
||||
}
|
||||
|
||||
repo = BaseRepository("appointments", db)
|
||||
appointment = await repo.create(data)
|
||||
|
||||
# Add contact associations
|
||||
if contact_ids:
|
||||
for cid in contact_ids:
|
||||
if cid:
|
||||
await db.execute(text("""
|
||||
INSERT INTO contact_appointments (contact_id, appointment_id)
|
||||
VALUES (:cid, :aid)
|
||||
ON CONFLICT DO NOTHING
|
||||
"""), {"cid": cid, "aid": str(appointment["id"])})
|
||||
|
||||
return RedirectResponse(url=f"/appointments/{appointment['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/{appointment_id}")
|
||||
async def appointment_detail(
|
||||
request: Request,
|
||||
appointment_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
repo = BaseRepository("appointments", db)
|
||||
appointment = await repo.get(appointment_id)
|
||||
|
||||
if not appointment:
|
||||
return RedirectResponse(url="/appointments", status_code=303)
|
||||
|
||||
# Get associated contacts
|
||||
result = await db.execute(text("""
|
||||
SELECT c.id, c.first_name, c.last_name, c.company, c.email, ca.role
|
||||
FROM contact_appointments ca
|
||||
JOIN contacts c ON ca.contact_id = c.id
|
||||
WHERE ca.appointment_id = :aid AND c.is_deleted = false
|
||||
ORDER BY c.first_name, c.last_name
|
||||
"""), {"aid": appointment_id})
|
||||
contacts = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("appointment_detail.html", {
|
||||
"request": request,
|
||||
"sidebar": sidebar,
|
||||
"appointment": appointment,
|
||||
"contacts": contacts,
|
||||
"page_title": appointment["title"],
|
||||
"active_nav": "appointments",
|
||||
})
|
||||
|
||||
|
||||
@router.get("/{appointment_id}/edit")
|
||||
async def edit_appointment_form(
|
||||
request: Request,
|
||||
appointment_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
repo = BaseRepository("appointments", db)
|
||||
appointment = await repo.get(appointment_id)
|
||||
|
||||
if not appointment:
|
||||
return RedirectResponse(url="/appointments", status_code=303)
|
||||
|
||||
# All contacts
|
||||
result = await db.execute(text("""
|
||||
SELECT id, first_name, last_name, company
|
||||
FROM contacts WHERE is_deleted = false
|
||||
ORDER BY first_name, last_name
|
||||
"""))
|
||||
contacts = [dict(r._mapping) for r in result]
|
||||
|
||||
# Currently linked contacts
|
||||
result = await db.execute(text("""
|
||||
SELECT contact_id FROM contact_appointments WHERE appointment_id = :aid
|
||||
"""), {"aid": appointment_id})
|
||||
selected_contacts = [str(r._mapping["contact_id"]) for r in result]
|
||||
|
||||
return templates.TemplateResponse("appointment_form.html", {
|
||||
"request": request,
|
||||
"sidebar": sidebar,
|
||||
"appointment": appointment,
|
||||
"contacts": contacts,
|
||||
"selected_contacts": selected_contacts,
|
||||
"page_title": f"Edit: {appointment['title']}",
|
||||
"active_nav": "appointments",
|
||||
})
|
||||
|
||||
|
||||
@router.post("/{appointment_id}/edit")
|
||||
async def update_appointment(
|
||||
request: Request,
|
||||
appointment_id: str,
|
||||
title: str = Form(...),
|
||||
description: Optional[str] = Form(None),
|
||||
location: Optional[str] = Form(None),
|
||||
start_date: str = Form(...),
|
||||
start_time: Optional[str] = Form(None),
|
||||
end_date: Optional[str] = Form(None),
|
||||
end_time: Optional[str] = Form(None),
|
||||
all_day: Optional[str] = Form(None),
|
||||
recurrence: Optional[str] = Form(None),
|
||||
tags: Optional[str] = Form(None),
|
||||
contact_ids: Optional[list[str]] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
is_all_day = all_day == "on"
|
||||
|
||||
if is_all_day:
|
||||
start_at = f"{start_date}T00:00:00"
|
||||
else:
|
||||
start_at = f"{start_date}T{start_time or '09:00'}:00"
|
||||
|
||||
end_at = None
|
||||
if end_date:
|
||||
if is_all_day:
|
||||
end_at = f"{end_date}T23:59:59"
|
||||
else:
|
||||
end_at = f"{end_date}T{end_time or '10:00'}:00"
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"description": description or None,
|
||||
"location": location or None,
|
||||
"start_at": start_at,
|
||||
"end_at": end_at,
|
||||
"all_day": is_all_day,
|
||||
"recurrence": recurrence or None,
|
||||
"tags": [t.strip() for t in tags.split(",") if t.strip()] if tags else None,
|
||||
}
|
||||
|
||||
repo = BaseRepository("appointments", db)
|
||||
await repo.update(appointment_id, data)
|
||||
|
||||
# Rebuild contact associations
|
||||
await db.execute(text("DELETE FROM contact_appointments WHERE appointment_id = :aid"), {"aid": appointment_id})
|
||||
if contact_ids:
|
||||
for cid in contact_ids:
|
||||
if cid:
|
||||
await db.execute(text("""
|
||||
INSERT INTO contact_appointments (contact_id, appointment_id)
|
||||
VALUES (:cid, :aid)
|
||||
ON CONFLICT DO NOTHING
|
||||
"""), {"cid": cid, "aid": appointment_id})
|
||||
|
||||
return RedirectResponse(url=f"/appointments/{appointment_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{appointment_id}/delete")
|
||||
async def delete_appointment(
|
||||
request: Request,
|
||||
appointment_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("appointments", db)
|
||||
await repo.soft_delete(appointment_id)
|
||||
return RedirectResponse(url="/appointments", status_code=303)
|
||||
@@ -137,6 +137,34 @@ SEARCH_ENTITIES = [
|
||||
"url": "/decisions/{id}",
|
||||
"icon": "decision",
|
||||
},
|
||||
{
|
||||
"type": "weblinks",
|
||||
"label": "Weblinks",
|
||||
"query": """
|
||||
SELECT w.id, w.label as name, NULL as status,
|
||||
NULL as domain_name, NULL as project_name,
|
||||
ts_rank(w.search_vector, websearch_to_tsquery('english', :q)) as rank
|
||||
FROM weblinks w
|
||||
WHERE w.is_deleted = false AND w.search_vector @@ websearch_to_tsquery('english', :q)
|
||||
ORDER BY rank DESC LIMIT :lim
|
||||
""",
|
||||
"url": "/weblinks",
|
||||
"icon": "weblink",
|
||||
},
|
||||
{
|
||||
"type": "appointments",
|
||||
"label": "Appointments",
|
||||
"query": """
|
||||
SELECT a.id, a.title as name, NULL as status,
|
||||
a.location as domain_name, NULL as project_name,
|
||||
ts_rank(a.search_vector, websearch_to_tsquery('english', :q)) as rank
|
||||
FROM appointments a
|
||||
WHERE a.is_deleted = false AND a.search_vector @@ websearch_to_tsquery('english', :q)
|
||||
ORDER BY rank DESC LIMIT :lim
|
||||
""",
|
||||
"url": "/appointments/{id}",
|
||||
"icon": "appointment",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
233
routers/weblinks.py
Normal file
233
routers/weblinks.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""Weblinks: organized bookmark directory with recursive folders."""
|
||||
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from typing import Optional
|
||||
|
||||
from core.database import get_db
|
||||
from core.base_repository import BaseRepository
|
||||
from core.sidebar import get_sidebar_data
|
||||
|
||||
router = APIRouter(prefix="/weblinks", tags=["weblinks"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_weblinks(
|
||||
request: Request,
|
||||
folder_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
|
||||
# Get all folders for tree nav
|
||||
result = await db.execute(text("""
|
||||
SELECT wf.*, (SELECT count(*) FROM folder_weblinks fw WHERE fw.folder_id = wf.id) as link_count
|
||||
FROM weblink_folders wf
|
||||
WHERE wf.is_deleted = false
|
||||
ORDER BY wf.sort_order, wf.name
|
||||
"""))
|
||||
all_folders = [dict(r._mapping) for r in result]
|
||||
|
||||
# Top-level folders and child folders
|
||||
top_folders = [f for f in all_folders if f.get("parent_id") is None]
|
||||
child_folder_map = {}
|
||||
for f in all_folders:
|
||||
pid = f.get("parent_id")
|
||||
if pid:
|
||||
child_folder_map.setdefault(str(pid), []).append(f)
|
||||
|
||||
# Current folder info
|
||||
current_folder = None
|
||||
if folder_id:
|
||||
for f in all_folders:
|
||||
if str(f["id"]) == folder_id:
|
||||
current_folder = f
|
||||
break
|
||||
|
||||
# Get weblinks (filtered by folder or all unfiled)
|
||||
if folder_id:
|
||||
result = await db.execute(text("""
|
||||
SELECT w.* FROM weblinks w
|
||||
JOIN folder_weblinks fw ON fw.weblink_id = w.id
|
||||
WHERE fw.folder_id = :fid AND w.is_deleted = false
|
||||
ORDER BY fw.sort_order, w.label
|
||||
"""), {"fid": folder_id})
|
||||
else:
|
||||
# Show all weblinks
|
||||
result = await db.execute(text("""
|
||||
SELECT w.* FROM weblinks w
|
||||
WHERE w.is_deleted = false
|
||||
ORDER BY w.sort_order, w.label
|
||||
"""))
|
||||
items = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("weblinks.html", {
|
||||
"request": request, "sidebar": sidebar, "items": items,
|
||||
"top_folders": top_folders, "child_folder_map": child_folder_map,
|
||||
"current_folder": current_folder,
|
||||
"current_folder_id": folder_id or "",
|
||||
"page_title": "Weblinks", "active_nav": "weblinks",
|
||||
})
|
||||
|
||||
|
||||
@router.get("/create")
|
||||
async def create_form(
|
||||
request: Request,
|
||||
folder_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
result = await db.execute(text(
|
||||
"SELECT id, name FROM weblink_folders WHERE is_deleted = false ORDER BY name"
|
||||
))
|
||||
folders = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("weblink_form.html", {
|
||||
"request": request, "sidebar": sidebar,
|
||||
"folders": folders,
|
||||
"page_title": "New Weblink", "active_nav": "weblinks",
|
||||
"item": None,
|
||||
"prefill_folder_id": folder_id or "",
|
||||
})
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_weblink(
|
||||
request: Request,
|
||||
label: str = Form(...),
|
||||
url: str = Form(...),
|
||||
description: Optional[str] = Form(None),
|
||||
folder_id: Optional[str] = Form(None),
|
||||
tags: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("weblinks", db)
|
||||
data = {"label": label, "url": url, "description": description}
|
||||
if tags and tags.strip():
|
||||
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
|
||||
weblink = await repo.create(data)
|
||||
|
||||
# Add to folder if specified
|
||||
if folder_id and folder_id.strip():
|
||||
await db.execute(text("""
|
||||
INSERT INTO folder_weblinks (folder_id, weblink_id)
|
||||
VALUES (:fid, :wid) ON CONFLICT DO NOTHING
|
||||
"""), {"fid": folder_id, "wid": weblink["id"]})
|
||||
|
||||
redirect_url = f"/weblinks?folder_id={folder_id}" if folder_id and folder_id.strip() else "/weblinks"
|
||||
return RedirectResponse(url=redirect_url, status_code=303)
|
||||
|
||||
|
||||
@router.get("/{weblink_id}/edit")
|
||||
async def edit_form(weblink_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("weblinks", db)
|
||||
sidebar = await get_sidebar_data(db)
|
||||
item = await repo.get(weblink_id)
|
||||
if not item:
|
||||
return RedirectResponse(url="/weblinks", status_code=303)
|
||||
|
||||
result = await db.execute(text(
|
||||
"SELECT id, name FROM weblink_folders WHERE is_deleted = false ORDER BY name"
|
||||
))
|
||||
folders = [dict(r._mapping) for r in result]
|
||||
|
||||
# Current folder assignment
|
||||
result = await db.execute(text(
|
||||
"SELECT folder_id FROM folder_weblinks WHERE weblink_id = :wid LIMIT 1"
|
||||
), {"wid": weblink_id})
|
||||
row = result.first()
|
||||
current_folder_id = str(row[0]) if row else ""
|
||||
|
||||
return templates.TemplateResponse("weblink_form.html", {
|
||||
"request": request, "sidebar": sidebar,
|
||||
"folders": folders,
|
||||
"page_title": "Edit Weblink", "active_nav": "weblinks",
|
||||
"item": item,
|
||||
"prefill_folder_id": current_folder_id,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/{weblink_id}/edit")
|
||||
async def update_weblink(
|
||||
weblink_id: str,
|
||||
label: str = Form(...),
|
||||
url: str = Form(...),
|
||||
description: Optional[str] = Form(None),
|
||||
folder_id: Optional[str] = Form(None),
|
||||
tags: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("weblinks", db)
|
||||
data = {
|
||||
"label": label, "url": url,
|
||||
"description": description if description and description.strip() else None,
|
||||
}
|
||||
if tags and tags.strip():
|
||||
data["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
else:
|
||||
data["tags"] = None
|
||||
|
||||
await repo.update(weblink_id, data)
|
||||
|
||||
# Update folder assignment
|
||||
await db.execute(text("DELETE FROM folder_weblinks WHERE weblink_id = :wid"), {"wid": weblink_id})
|
||||
if folder_id and folder_id.strip():
|
||||
await db.execute(text("""
|
||||
INSERT INTO folder_weblinks (folder_id, weblink_id)
|
||||
VALUES (:fid, :wid) ON CONFLICT DO NOTHING
|
||||
"""), {"fid": folder_id, "wid": weblink_id})
|
||||
|
||||
return RedirectResponse(url="/weblinks", status_code=303)
|
||||
|
||||
|
||||
@router.post("/{weblink_id}/delete")
|
||||
async def delete_weblink(weblink_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("weblinks", db)
|
||||
await repo.soft_delete(weblink_id)
|
||||
referer = request.headers.get("referer", "/weblinks")
|
||||
return RedirectResponse(url=referer, status_code=303)
|
||||
|
||||
|
||||
# ---- Folders ----
|
||||
|
||||
@router.get("/folders/create")
|
||||
async def create_folder_form(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
sidebar = await get_sidebar_data(db)
|
||||
result = await db.execute(text(
|
||||
"SELECT id, name FROM weblink_folders WHERE is_deleted = false ORDER BY name"
|
||||
))
|
||||
parent_folders = [dict(r._mapping) for r in result]
|
||||
|
||||
return templates.TemplateResponse("weblink_folder_form.html", {
|
||||
"request": request, "sidebar": sidebar,
|
||||
"parent_folders": parent_folders,
|
||||
"page_title": "New Folder", "active_nav": "weblinks",
|
||||
"item": None,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/folders/create")
|
||||
async def create_folder(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
parent_id: Optional[str] = Form(None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
repo = BaseRepository("weblink_folders", db)
|
||||
data = {"name": name}
|
||||
if parent_id and parent_id.strip():
|
||||
data["parent_id"] = parent_id
|
||||
await repo.create(data)
|
||||
return RedirectResponse(url="/weblinks", status_code=303)
|
||||
|
||||
|
||||
@router.post("/folders/{folder_id}/delete")
|
||||
async def delete_folder(folder_id: str, request: Request, db: AsyncSession = Depends(get_db)):
|
||||
repo = BaseRepository("weblink_folders", db)
|
||||
await repo.soft_delete(folder_id)
|
||||
return RedirectResponse(url="/weblinks", status_code=303)
|
||||
Reference in New Issue
Block a user