From b9c9452e73402f76a32a55e32a62f707778a8666 Mon Sep 17 00:00:00 2001 From: georgegale228 Date: Wed, 19 Aug 2026 18:25:05 +0000 Subject: [PATCH] =?UTF-8?q?feedback=202026=20implement=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/settings.py | 2 +- app/database/migrations.py | 24 +++++++++ app/main.py | 2 + app/routers/api.py | 2 + app/routers/feedback_public.py | 27 ++++++++++ app/routers/feedbacks.py | 94 ++++++++++++++++++++++++++++++++++ app/schemas/models.py | 45 ++++++++++++++++ app/utils/feedback.py | 70 +++++++++++++++++++++++++ 8 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 app/routers/feedback_public.py create mode 100644 app/routers/feedbacks.py create mode 100644 app/utils/feedback.py diff --git a/app/core/settings.py b/app/core/settings.py index 5ff0b46..0a8eede 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -33,7 +33,7 @@ smtp_password=config("SMTP_PASSWORD", default="password"), # Admin SMTP settings for internal notification service admin_smtp_server=config("ADMIN_SMTP_SERVER", default=None), - admin_smtp_port=config("ADMIN_SMTP_PORT", default=None, cast=int), + admin_smtp_port=config("ADMIN_SMTP_PORT", default=None), admin_smtp_user=config("ADMIN_SMTP_USER", default=None), admin_smtp_password=config("ADMIN_SMTP_PASSWORD", default=None), paydunya_public_key=config("PAYDUNYA_PUBLIC_KEY", default=None), diff --git a/app/database/migrations.py b/app/database/migrations.py index 57d0882..0b2f6e6 100644 --- a/app/database/migrations.py +++ b/app/database/migrations.py @@ -521,6 +521,24 @@ REFERENCES events(id) ON DELETE CASCADE );""", + """ + CREATE TABLE IF NOT EXISTS feedbacks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + sex VARCHAR(64), + age VARCHAR(32), + profession VARCHAR(255), + country VARCHAR(120), + python_level VARCHAR(120), + heard TEXT, + rating INTEGER CHECK (rating >= 1 AND rating <= 5), + overall TEXT, + favorite TEXT, + improvements TEXT, + comments TEXT, + is_resolved BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + );""", ] @@ -539,6 +557,12 @@ ALTER_TABLE_QUERIES = [ "ALTER TABLE sponsors_partners ADD COLUMN IF NOT EXISTS package_tier package_tier_enum;", + "ALTER TABLE feedbacks DROP COLUMN IF EXISTS event_code;", + "ALTER TABLE feedbacks DROP COLUMN IF EXISTS name;", + "ALTER TABLE feedbacks DROP COLUMN IF EXISTS email;", + "ALTER TABLE feedbacks DROP COLUMN IF EXISTS subject;", + "ALTER TABLE feedbacks DROP COLUMN IF EXISTS message;", + "ALTER TABLE feedbacks ADD COLUMN IF NOT EXISTS days JSONB DEFAULT '[]'::jsonb;", ] diff --git a/app/main.py b/app/main.py index ba658d8..58c4766 100644 --- a/app/main.py +++ b/app/main.py @@ -8,6 +8,7 @@ from app.routers.api import api_routers from app.routers.notifications import api_router as notifications_router from app.webhooks.payments_callback import api_router as payments_callback_router +from app.routers.feedback_public import api_router as feedback_public_router from app.core.settings import logger, settings from pathlib import Path from datetime import datetime, timezone @@ -150,3 +151,4 @@ async def favicon(): app.include_router(api_routers) app.include_router(payments_callback_router) app.include_router(notifications_router) +app.include_router(feedback_public_router) diff --git a/app/routers/api.py b/app/routers/api.py index a925471..a6fbfed 100644 --- a/app/routers/api.py +++ b/app/routers/api.py @@ -16,6 +16,7 @@ from app.routers.vauchers import api_router as vauchers_router from app.routers.teams import api_router as teams_router from app.routers.access_grant import api_router as access_grant_router +from app.routers.feedbacks import api_router as feedbacks_router from fastapi import APIRouter from app.core.security import verify_api_key @@ -40,3 +41,4 @@ api_routers.include_router(vauchers_router) api_routers.include_router(teams_router) api_routers.include_router(access_grant_router) +api_routers.include_router(feedbacks_router) diff --git a/app/routers/feedback_public.py b/app/routers/feedback_public.py new file mode 100644 index 0000000..a0f053a --- /dev/null +++ b/app/routers/feedback_public.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, BackgroundTasks, Request, status, HTTPException + +from app.utils.feedback import add_feedback + +from app.schemas.models import ( + FeedbackBase, + MessageResponse, +) +from app.core.settings import logger + + +api_router = APIRouter(prefix="/api/feedback", tags=["feedback"]) + + +@api_router.post("/", response_model=MessageResponse, status_code=status.HTTP_201_CREATED) +async def submit_public_feedback(request: Request, background_tasks: BackgroundTasks, payload: FeedbackBase): + """Public endpoint to submit feedback without API key.""" + try: + background_tasks.add_task( + add_feedback, request.app.state.db_pool, payload.model_dump(mode="json")) + return {"message": "Feedback received successfully"} + except Exception as e: + logger.error(f"Error adding public feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error adding feedback") diff --git a/app/routers/feedbacks.py b/app/routers/feedbacks.py new file mode 100644 index 0000000..8205c72 --- /dev/null +++ b/app/routers/feedbacks.py @@ -0,0 +1,94 @@ +from fastapi import APIRouter, BackgroundTasks, Depends, Request, status, HTTPException + +from app.utils.feedback import ( + add_feedback, get_feedback_by_id, get_all_feedbacks, update_feedback, delete_feedback) + +from app.schemas.models import ( + FeedbackSummary, + MessageResponse, + FeedbackUpdate, + FeedbackBase, + +) +from app.database.connection import get_db_connection +from app.core.settings import logger + + +api_router = APIRouter(prefix="/feedbacks", tags=["feedbacks"]) + + +@api_router.get("/", response_model=list[FeedbackSummary]) +async def _get_all_feedbacks(db=Depends(get_db_connection)): + try: + feedbacks = await get_all_feedbacks(db) + if not feedbacks: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail="No feedbacks found") + return feedbacks + except Exception as e: + logger.error(f"Error retrieving feedbacks: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error retrieving feedbacks") + + +@api_router.get("/{feedback_id}", response_model=FeedbackSummary) +async def _get_feedback_by_id(feedback_id: str, db=Depends(get_db_connection)): + try: + feedback = await get_feedback_by_id(db, feedback_id) + if not feedback: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=f"Feedback with id {feedback_id} not found") + return feedback + except Exception as e: + logger.error( + f"Error retrieving feedback with id {feedback_id}: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error retrieving feedback") + + +@api_router.post("/send", response_model=MessageResponse, status_code=status.HTTP_201_CREATED) +async def add_feedback_message(request: Request, payload: FeedbackBase, background_tasks: BackgroundTasks): + """Add a new feedback.""" + try: + background_tasks.add_task( + add_feedback, request.app.state.db_pool, payload.model_dump(mode="json")) + return {"message": "Feedback received successfully"} + except Exception as e: + logger.error(f"Error adding feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error adding feedback") + + +@api_router.put("/{feedback_id}", response_model=MessageResponse) +async def _update_feedback(feedback_id: str, payload: FeedbackUpdate, background_tasks: BackgroundTasks, db=Depends(get_db_connection)): + try: + data_to_update = {k: v for k, + v in payload.model_dump(mode="json").items() if v is not None} + + result = await update_feedback(db, feedback_id, data_to_update, background_tasks) + return result + except Exception as e: + logger.error(f"Error updating feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error updating feedback") + + +@api_router.delete("/{feedback_id}", response_model=MessageResponse) +async def _delete_feedback(feedback_id: str, background_tasks: BackgroundTasks, db=Depends(get_db_connection)): + try: + result = await delete_feedback(db, feedback_id, background_tasks) + return result + except Exception as e: + logger.error(f"Error deleting feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error deleting feedback") diff --git a/app/schemas/models.py b/app/schemas/models.py index 08208f0..d097fed 100644 --- a/app/schemas/models.py +++ b/app/schemas/models.py @@ -122,6 +122,51 @@ class ContactMessageUpdate(BaseModel): default_factory=lambda: datetime.now(timezone.utc)) +class FeedbackBase(BaseModel): + sex: str | None = None + age: str | None = None + profession: str | None = None + country: str | None = None + python_level: str | None = None + heard: str | None = None + rating: int | None = Field(default=None, ge=1, le=5) + overall: str | None = None + favorite: str | None = None + improvements: str | None = None + comments: str | None = None + days: List[str] = Field(default_factory=list) + + +class FeedbackSummary(FeedbackBase): + id: UUID + is_resolved: bool = False + created_at: datetime + updated_at: datetime + + +class FeedbacksList(BaseModel): + feedbacks: list[FeedbackSummary] = Field( + default_factory=list) + + +class FeedbackUpdate(BaseModel): + sex: str | None = None + age: str | None = None + profession: str | None = None + country: str | None = None + python_level: str | None = None + heard: str | None = None + rating: int | None = Field(default=None, ge=1, le=5) + overall: str | None = None + favorite: str | None = None + improvements: str | None = None + comments: str | None = None + days: List[str] | None = None + is_resolved: bool | None = None + updated_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc)) + + class APIKeyResponse(BaseModel): api_key: str diff --git a/app/utils/feedback.py b/app/utils/feedback.py new file mode 100644 index 0000000..841ec66 --- /dev/null +++ b/app/utils/feedback.py @@ -0,0 +1,70 @@ +from fastapi import HTTPException, BackgroundTasks +from app.core.settings import logger + +from app.database.orm import select, insert, update, delete + + +async def add_feedback(db, payload: dict): + try: + await insert(db, "feedbacks", payload) + return {"message": "Feedback received successfully"} + except Exception as e: + logger.error(f"Error adding feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error adding feedback") + + +async def delete_feedback(db, feedback_id: str, background_tasks: BackgroundTasks): + try: + background_tasks.add_task( + delete, db, "feedbacks", filter={"id": feedback_id}) + return {"message": "Feedback deleted successfully"} + except Exception as e: + logger.error(f"Error deleting feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException(status_code=500, detail="Error deleting feedback") + + +async def get_feedback_by_id(db, feedback_id: str): + try: + feedback = await select(db, "feedbacks", filter={"id": feedback_id}) + if not feedback: + logger.error(f"Feedback with id {feedback_id} not found") + return feedback[0] + except Exception as e: + logger.error(f"Error retrieving feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException(status_code=500, detail="Error retrieving feedback") + + +async def get_all_feedbacks(db): + try: + feedbacks = await select(db, "feedbacks") + return feedbacks + except Exception as e: + logger.error(f"Error retrieving all feedbacks: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=500, detail="Error retrieving all feedbacks") + + +async def update_feedback(db, feedback_id: str, payload: dict, background_tasks: BackgroundTasks): + try: + existing = await select(db, "feedbacks", filter={"id": feedback_id}) + if not existing: + logger.error(f"Feedback with id {feedback_id} not found") + raise HTTPException( + status_code=404, detail=f"Feedback with id {feedback_id} not found") + background_tasks.add_task( + update, db, "feedbacks", payload, filter={"id": feedback_id}) + return {"message": "Feedback updated successfully"} + except Exception as e: + logger.error(f"Error updating feedback: {str(e)}") + if isinstance(e, HTTPException): + raise e + raise HTTPException(status_code=500, detail="Error updating feedback")