initial release
This commit is contained in:
parent
e156de22c3
commit
b3ac11a85e
37 changed files with 3649 additions and 669 deletions
149
backend/app/services/page_service.py
Normal file
149
backend/app/services/page_service.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import sqlite3
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from app.database import get_connection
|
||||
from app.models import Page, PageCreateRequest, PageUpdateRequest
|
||||
|
||||
|
||||
def _row_to_page(row: sqlite3.Row) -> Page:
|
||||
return Page(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
slug=row["slug"],
|
||||
sort_order=row["sort_order"],
|
||||
is_system=bool(row["is_system"]),
|
||||
is_hidden=bool(row["is_hidden"]),
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
||||
|
||||
def list_pages(db_path: Path) -> list[Page]:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM pages ORDER BY is_system ASC, sort_order ASC"
|
||||
).fetchall()
|
||||
return [_row_to_page(row) for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_page(db_path: Path, page_id: str) -> Page | None:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM pages WHERE id = ?", (page_id,)).fetchone()
|
||||
return _row_to_page(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_page_by_slug(db_path: Path, slug: str) -> Page | None:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM pages WHERE slug = ?", (slug,)).fetchone()
|
||||
return _row_to_page(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_page(db_path: Path, request: PageCreateRequest) -> Page:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT COALESCE(MAX(sort_order) + 1, 0) AS next_order FROM pages WHERE is_system = 0"
|
||||
).fetchone()
|
||||
sort_order: int = row["next_order"] if row else 0
|
||||
|
||||
page_id = str(uuid4())
|
||||
now = datetime.now(UTC).isoformat()
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO pages"
|
||||
" (id, name, slug, sort_order, is_system, is_hidden, created_at, updated_at)"
|
||||
" VALUES (?, ?, ?, ?, 0, 0, ?, ?)",
|
||||
(page_id, request.name, request.slug, sort_order, now, now),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return Page(
|
||||
id=page_id,
|
||||
name=request.name,
|
||||
slug=request.slug,
|
||||
sort_order=sort_order,
|
||||
is_system=False,
|
||||
is_hidden=False,
|
||||
created_at=datetime.fromisoformat(now),
|
||||
updated_at=datetime.fromisoformat(now),
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_page(db_path: Path, page_id: str, request: PageUpdateRequest) -> Page | None:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
existing = conn.execute("SELECT * FROM pages WHERE id = ?", (page_id,)).fetchone()
|
||||
if existing is None:
|
||||
return None
|
||||
|
||||
now = datetime.now(UTC).isoformat()
|
||||
name = request.name if request.name is not None else existing["name"]
|
||||
slug = request.slug if request.slug is not None else existing["slug"]
|
||||
is_hidden = (
|
||||
int(request.is_hidden) if request.is_hidden is not None else existing["is_hidden"]
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
"UPDATE pages SET name = ?, slug = ?, is_hidden = ?, updated_at = ? WHERE id = ?",
|
||||
(name, slug, is_hidden, now, page_id),
|
||||
)
|
||||
conn.commit()
|
||||
return get_page(db_path, page_id)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_page(db_path: Path, page_id: str) -> tuple[bool, str | None]:
|
||||
"""Cascade-deletes the page and all its segments."""
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
existing = conn.execute("SELECT * FROM pages WHERE id = ?", (page_id,)).fetchone()
|
||||
if existing is None:
|
||||
return False, "not_found"
|
||||
if existing["is_system"]:
|
||||
return False, "system_page"
|
||||
|
||||
conn.execute("DELETE FROM segments WHERE page_id = ?", (page_id,))
|
||||
conn.execute("DELETE FROM pages WHERE id = ?", (page_id,))
|
||||
conn.commit()
|
||||
return True, None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def reorder_pages(db_path: Path, page_ids: list[str]) -> list[Page]:
|
||||
"""Reorders only non-system pages."""
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
existing_ids = {
|
||||
row["id"]
|
||||
for row in conn.execute("SELECT id FROM pages WHERE is_system = 0").fetchall()
|
||||
}
|
||||
requested_ids = set(page_ids)
|
||||
missing = requested_ids - existing_ids
|
||||
if missing:
|
||||
raise ValueError(f"Page IDs not found: {missing}")
|
||||
|
||||
now = datetime.now(UTC).isoformat()
|
||||
for index, pid in enumerate(page_ids):
|
||||
conn.execute(
|
||||
"UPDATE pages SET sort_order = ?, updated_at = ? WHERE id = ?",
|
||||
(index, now, pid),
|
||||
)
|
||||
conn.commit()
|
||||
return list_pages(db_path)
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
@ -16,6 +16,7 @@ def _row_to_segment(row: sqlite3.Row) -> Segment:
|
|||
title=row["title"],
|
||||
content=row["content"],
|
||||
metadata=json.loads(row["metadata"]),
|
||||
page_id=dict(row).get("page_id"),
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
|
@ -25,7 +26,9 @@ def create_segment(db_path: Path, request: SegmentCreateRequest) -> Segment:
|
|||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT COALESCE(MAX(sort_order) + 1, 0) AS next_order FROM segments"
|
||||
"SELECT COALESCE(MAX(sort_order) + 1, 0) AS next_order"
|
||||
" FROM segments WHERE page_id = ?",
|
||||
(request.page_id,),
|
||||
).fetchone()
|
||||
sort_order: int = row["next_order"] if row else 0
|
||||
|
||||
|
|
@ -33,9 +36,9 @@ def create_segment(db_path: Path, request: SegmentCreateRequest) -> Segment:
|
|||
now = datetime.now(UTC).isoformat()
|
||||
metadata_json = json.dumps(request.metadata)
|
||||
|
||||
cols = "id, type, sort_order, title, content, metadata, created_at, updated_at"
|
||||
cols = "id, type, sort_order, title, content, metadata, page_id, created_at, updated_at"
|
||||
conn.execute(
|
||||
f"INSERT INTO segments ({cols}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
f"INSERT INTO segments ({cols}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
str(segment_id),
|
||||
request.type.value,
|
||||
|
|
@ -43,6 +46,7 @@ def create_segment(db_path: Path, request: SegmentCreateRequest) -> Segment:
|
|||
request.title,
|
||||
request.content,
|
||||
metadata_json,
|
||||
request.page_id,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
|
|
@ -56,6 +60,7 @@ def create_segment(db_path: Path, request: SegmentCreateRequest) -> Segment:
|
|||
title=request.title,
|
||||
content=request.content,
|
||||
metadata=request.metadata,
|
||||
page_id=request.page_id,
|
||||
created_at=datetime.fromisoformat(now),
|
||||
updated_at=datetime.fromisoformat(now),
|
||||
)
|
||||
|
|
@ -63,10 +68,16 @@ def create_segment(db_path: Path, request: SegmentCreateRequest) -> Segment:
|
|||
conn.close()
|
||||
|
||||
|
||||
def list_segments(db_path: Path) -> list[Segment]:
|
||||
def list_segments(db_path: Path, page_id: str | None = None) -> list[Segment]:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
rows = conn.execute("SELECT * FROM segments ORDER BY sort_order ASC").fetchall()
|
||||
if page_id is not None:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM segments WHERE page_id = ? ORDER BY sort_order ASC",
|
||||
(page_id,),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute("SELECT * FROM segments ORDER BY sort_order ASC").fetchall()
|
||||
return [_row_to_segment(row) for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
@ -140,6 +151,13 @@ def reorder_segments(db_path: Path, segment_ids: list[UUID]) -> list[Segment]:
|
|||
)
|
||||
conn.commit()
|
||||
|
||||
return list_segments(db_path)
|
||||
# Return segments scoped to the same page as the first segment in the list
|
||||
if segment_ids:
|
||||
row = conn.execute(
|
||||
"SELECT page_id FROM segments WHERE id = ?", (str(segment_ids[0]),)
|
||||
).fetchone()
|
||||
page_id = row["page_id"] if row else None
|
||||
return list_segments(db_path, page_id=page_id)
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
|
|||
91
backend/app/services/team_service.py
Normal file
91
backend/app/services/team_service.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import sqlite3
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from app.database import get_connection
|
||||
from app.models import TeamMember, TeamMemberCreateRequest
|
||||
|
||||
|
||||
def _row_to_member(row: sqlite3.Row) -> TeamMember:
|
||||
return TeamMember(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
student_number=row["student_number"],
|
||||
sort_order=row["sort_order"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
||||
|
||||
def list_members(db_path: Path) -> list[TeamMember]:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
rows = conn.execute("SELECT * FROM team_members ORDER BY sort_order ASC").fetchall()
|
||||
return [_row_to_member(row) for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def add_member(db_path: Path, request: TeamMemberCreateRequest) -> TeamMember:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT COALESCE(MAX(sort_order) + 1, 0) AS next_order FROM team_members"
|
||||
).fetchone()
|
||||
sort_order: int = row["next_order"] if row else 0
|
||||
member_id = str(uuid4())
|
||||
now = datetime.now(UTC).isoformat()
|
||||
conn.execute(
|
||||
"INSERT INTO team_members"
|
||||
" (id, name, student_number, sort_order, created_at, updated_at)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(member_id, request.name, request.student_number, sort_order, now, now),
|
||||
)
|
||||
conn.commit()
|
||||
return TeamMember(
|
||||
id=member_id,
|
||||
name=request.name,
|
||||
student_number=request.student_number,
|
||||
sort_order=sort_order,
|
||||
created_at=datetime.fromisoformat(now),
|
||||
updated_at=datetime.fromisoformat(now),
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_member(db_path: Path, member_id: str) -> bool:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM team_members WHERE id = ?", (member_id,)
|
||||
).fetchone()
|
||||
if existing is None:
|
||||
return False
|
||||
conn.execute("DELETE FROM team_members WHERE id = ?", (member_id,))
|
||||
conn.commit()
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def reorder_members(db_path: Path, member_ids: list[str]) -> list[TeamMember]:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
existing_ids = {
|
||||
row["id"] for row in conn.execute("SELECT id FROM team_members").fetchall()
|
||||
}
|
||||
missing = set(member_ids) - existing_ids
|
||||
if missing:
|
||||
raise ValueError(f"Member IDs not found: {missing}")
|
||||
now = datetime.now(UTC).isoformat()
|
||||
for index, mid in enumerate(member_ids):
|
||||
conn.execute(
|
||||
"UPDATE team_members SET sort_order = ?, updated_at = ? WHERE id = ?",
|
||||
(index, now, mid),
|
||||
)
|
||||
conn.commit()
|
||||
return list_members(db_path)
|
||||
finally:
|
||||
conn.close()
|
||||
Loading…
Add table
Add a link
Reference in a new issue