FastAPI + PostgreSQL 16. KYC, issue sistemi, permission/group yönetimi, session yönetimi, API client auth (kışla kapısı), officials/persons CRUD. Migration 0001–0013 dahil.
275 lines
9.2 KiB
Python
275 lines
9.2 KiB
Python
from datetime import datetime, timezone, timedelta
|
||
from psycopg import AsyncConnection
|
||
|
||
EDIT_GRACE_PERIOD = timedelta(minutes=15)
|
||
|
||
|
||
async def list_categories(conn: AsyncConnection, parent_id: int | None = None) -> list[dict]:
|
||
rows = await (await conn.execute(
|
||
"""SELECT id, parent_id, name, slug, icon
|
||
FROM issue_categories
|
||
WHERE (parent_id = %s OR (%s IS NULL AND parent_id IS NULL))
|
||
ORDER BY name""",
|
||
(parent_id, parent_id)
|
||
)).fetchall()
|
||
return [{"id": r[0], "parent_id": r[1], "name": r[2], "slug": r[3], "icon": r[4]} for r in rows]
|
||
|
||
|
||
async def get_category(conn: AsyncConnection, category_id: int) -> dict | None:
|
||
row = await (await conn.execute(
|
||
"SELECT id, parent_id, name, slug, icon FROM issue_categories WHERE id = %s",
|
||
(category_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
return None
|
||
return {"id": row[0], "parent_id": row[1], "name": row[2], "slug": row[3], "icon": row[4]}
|
||
|
||
|
||
async def create_issue(
|
||
conn: AsyncConnection,
|
||
reporter_id: int,
|
||
title: str,
|
||
body: str,
|
||
category_id: int,
|
||
location_id: int,
|
||
) -> dict:
|
||
cat = await get_category(conn, category_id)
|
||
if not cat:
|
||
raise ValueError("Kategori bulunamadı")
|
||
|
||
loc = await (await conn.execute(
|
||
"SELECT id FROM locations WHERE id = %s", (location_id,)
|
||
)).fetchone()
|
||
if not loc:
|
||
raise ValueError("Konum bulunamadı")
|
||
|
||
row = await (await conn.execute(
|
||
"""INSERT INTO issues (title, body, category_id, location_id, reporter_id)
|
||
VALUES (%s, %s, %s, %s, %s)
|
||
RETURNING id, status, created_at""",
|
||
(title, body, category_id, location_id, reporter_id)
|
||
)).fetchone()
|
||
await conn.commit()
|
||
return {"id": row[0], "status": row[1], "created_at": row[2]}
|
||
|
||
|
||
async def get_issue(conn: AsyncConnection, issue_id: int) -> dict | None:
|
||
row = await (await conn.execute(
|
||
"""SELECT i.id, i.title, i.body, i.status, i.created_at, i.updated_at,
|
||
i.reporter_id, i.category_id, i.location_id,
|
||
ic.name AS category_name, ic.slug AS category_slug,
|
||
l.name AS location_name,
|
||
(SELECT COUNT(*) FROM issue_votes v WHERE v.issue_id = i.id AND v.vote = 'resolved') AS votes_resolved,
|
||
(SELECT COUNT(*) FROM issue_votes v WHERE v.issue_id = i.id AND v.vote = 'ongoing') AS votes_ongoing,
|
||
(SELECT COUNT(*) FROM issue_comments c WHERE c.issue_id = i.id AND NOT c.is_deleted) AS comment_count
|
||
FROM issues i
|
||
JOIN issue_categories ic ON ic.id = i.category_id
|
||
JOIN locations l ON l.id = i.location_id
|
||
WHERE i.id = %s""",
|
||
(issue_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
return None
|
||
return {
|
||
"id": row[0], "title": row[1], "body": row[2], "status": row[3],
|
||
"created_at": row[4], "updated_at": row[5],
|
||
"reporter_id": row[6], "category_id": row[7], "location_id": row[8],
|
||
"category_name": row[9], "category_slug": row[10],
|
||
"location_name": row[11],
|
||
"votes": {"resolved": row[12], "ongoing": row[13]},
|
||
"comment_count": row[14],
|
||
}
|
||
|
||
|
||
async def list_issues(
|
||
conn: AsyncConnection,
|
||
location_id: int | None = None,
|
||
category_id: int | None = None,
|
||
status: str | None = None,
|
||
reporter_id: int | None = None,
|
||
limit: int = 20,
|
||
offset: int = 0,
|
||
) -> list[dict]:
|
||
filters = []
|
||
params: list = []
|
||
if location_id:
|
||
filters.append("i.location_id = %s"); params.append(location_id)
|
||
if category_id:
|
||
filters.append("i.category_id = %s"); params.append(category_id)
|
||
if status:
|
||
filters.append("i.status = %s"); params.append(status)
|
||
if reporter_id:
|
||
filters.append("i.reporter_id = %s"); params.append(reporter_id)
|
||
|
||
where = ("WHERE " + " AND ".join(filters)) if filters else ""
|
||
params += [limit, offset]
|
||
|
||
rows = await (await conn.execute(
|
||
f"""SELECT i.id, i.title, i.status, i.created_at,
|
||
ic.name AS category_name, l.name AS location_name,
|
||
(SELECT COUNT(*) FROM issue_votes v WHERE v.issue_id = i.id) AS vote_count
|
||
FROM issues i
|
||
JOIN issue_categories ic ON ic.id = i.category_id
|
||
JOIN locations l ON l.id = i.location_id
|
||
{where}
|
||
ORDER BY i.created_at DESC
|
||
LIMIT %s OFFSET %s""",
|
||
params
|
||
)).fetchall()
|
||
return [
|
||
{"id": r[0], "title": r[1], "status": r[2], "created_at": r[3],
|
||
"category_name": r[4], "location_name": r[5], "vote_count": r[6]}
|
||
for r in rows
|
||
]
|
||
|
||
|
||
async def update_issue(
|
||
conn: AsyncConnection,
|
||
issue_id: int,
|
||
user_id: int,
|
||
is_admin: bool,
|
||
title: str | None = None,
|
||
body: str | None = None,
|
||
) -> dict:
|
||
row = await (await conn.execute(
|
||
"SELECT reporter_id, created_at, status FROM issues WHERE id = %s", (issue_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
raise ValueError("Sorun bulunamadı")
|
||
|
||
reporter_id, created_at, status = row
|
||
|
||
if not is_admin:
|
||
if reporter_id != user_id:
|
||
raise PermissionError("Bu sorunu düzenleme yetkiniz yok")
|
||
age = datetime.now(timezone.utc) - created_at
|
||
if age > EDIT_GRACE_PERIOD:
|
||
raise PermissionError("Düzenleme süresi doldu (15 dakika)")
|
||
|
||
updates = []
|
||
params = []
|
||
if title is not None:
|
||
updates.append("title = %s"); params.append(title)
|
||
if body is not None:
|
||
updates.append("body = %s"); params.append(body)
|
||
if not updates:
|
||
raise ValueError("Güncellenecek alan yok")
|
||
|
||
updates.append("updated_at = NOW()")
|
||
params.append(issue_id)
|
||
await conn.execute(
|
||
f"UPDATE issues SET {', '.join(updates)} WHERE id = %s", params
|
||
)
|
||
await conn.commit()
|
||
return {"ok": True}
|
||
|
||
|
||
async def update_status(
|
||
conn: AsyncConnection,
|
||
issue_id: int,
|
||
new_status: str,
|
||
) -> dict:
|
||
row = await (await conn.execute(
|
||
"SELECT id FROM issues WHERE id = %s", (issue_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
raise ValueError("Sorun bulunamadı")
|
||
await conn.execute(
|
||
"UPDATE issues SET status = %s, updated_at = NOW() WHERE id = %s",
|
||
(new_status, issue_id)
|
||
)
|
||
await conn.commit()
|
||
return {"ok": True}
|
||
|
||
|
||
async def delete_issue(conn: AsyncConnection, issue_id: int) -> dict:
|
||
row = await (await conn.execute(
|
||
"SELECT id FROM issues WHERE id = %s", (issue_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
raise ValueError("Sorun bulunamadı")
|
||
await conn.execute("DELETE FROM issues WHERE id = %s", (issue_id,))
|
||
await conn.commit()
|
||
return {"ok": True}
|
||
|
||
|
||
async def cast_vote(conn: AsyncConnection, issue_id: int, user_id: int, vote: str) -> dict:
|
||
row = await (await conn.execute(
|
||
"SELECT id FROM issues WHERE id = %s", (issue_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
raise ValueError("Sorun bulunamadı")
|
||
await conn.execute(
|
||
"""INSERT INTO issue_votes (issue_id, user_id, vote)
|
||
VALUES (%s, %s, %s)
|
||
ON CONFLICT (issue_id, user_id) DO UPDATE SET vote = EXCLUDED.vote""",
|
||
(issue_id, user_id, vote)
|
||
)
|
||
await conn.commit()
|
||
return {"ok": True}
|
||
|
||
|
||
async def list_comments(conn: AsyncConnection, issue_id: int) -> list[dict]:
|
||
rows = await (await conn.execute(
|
||
"""SELECT c.id, c.parent_id, c.user_id, c.body, c.is_deleted, c.created_at, c.updated_at
|
||
FROM issue_comments c
|
||
WHERE c.issue_id = %s
|
||
ORDER BY c.created_at ASC""",
|
||
(issue_id,)
|
||
)).fetchall()
|
||
return [
|
||
{
|
||
"id": r[0], "parent_id": r[1], "user_id": r[2],
|
||
"body": "[silindi]" if r[4] else r[3],
|
||
"is_deleted": r[4], "created_at": r[5], "updated_at": r[6],
|
||
}
|
||
for r in rows
|
||
]
|
||
|
||
|
||
async def add_comment(
|
||
conn: AsyncConnection,
|
||
issue_id: int,
|
||
user_id: int,
|
||
body: str,
|
||
parent_id: int | None = None,
|
||
) -> dict:
|
||
row = await (await conn.execute(
|
||
"SELECT id FROM issues WHERE id = %s", (issue_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
raise ValueError("Sorun bulunamadı")
|
||
|
||
if parent_id:
|
||
parent = await (await conn.execute(
|
||
"SELECT id FROM issue_comments WHERE id = %s AND issue_id = %s AND NOT is_deleted",
|
||
(parent_id, issue_id)
|
||
)).fetchone()
|
||
if not parent:
|
||
raise ValueError("Yanıt verilen yorum bulunamadı")
|
||
|
||
result = await (await conn.execute(
|
||
"""INSERT INTO issue_comments (issue_id, user_id, body, parent_id)
|
||
VALUES (%s, %s, %s, %s)
|
||
RETURNING id, created_at""",
|
||
(issue_id, user_id, body, parent_id)
|
||
)).fetchone()
|
||
await conn.commit()
|
||
return {"id": result[0], "created_at": result[1]}
|
||
|
||
|
||
async def delete_comment(conn: AsyncConnection, comment_id: int, user_id: int, is_admin: bool) -> dict:
|
||
row = await (await conn.execute(
|
||
"SELECT user_id FROM issue_comments WHERE id = %s AND NOT is_deleted",
|
||
(comment_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
raise ValueError("Yorum bulunamadı")
|
||
if not is_admin and row[0] != user_id:
|
||
raise PermissionError("Bu yorumu silme yetkiniz yok")
|
||
await conn.execute(
|
||
"UPDATE issue_comments SET is_deleted = TRUE, updated_at = NOW() WHERE id = %s",
|
||
(comment_id,)
|
||
)
|
||
await conn.commit()
|
||
return {"ok": True}
|