memleketmeselesi/mm_api/services/issue.py
Mukan Erkin 2498e75594 init: memleketmeselesi platform — API + migrations
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.
2026-04-27 23:06:59 +03:00

275 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}