Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
24 changes: 24 additions & 0 deletions app/database/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);""",

]

Expand All @@ -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;",
]


Expand Down
2 changes: 2 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions app/routers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
27 changes: 27 additions & 0 deletions app/routers/feedback_public.py
Original file line number Diff line number Diff line change
@@ -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")
94 changes: 94 additions & 0 deletions app/routers/feedbacks.py
Original file line number Diff line number Diff line change
@@ -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")
45 changes: 45 additions & 0 deletions app/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
70 changes: 70 additions & 0 deletions app/utils/feedback.py
Original file line number Diff line number Diff line change
@@ -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")