From 64b3216c4ef03ef7658bb7ec666ed97b379e44d3 Mon Sep 17 00:00:00 2001 From: Habeeb Ajayi Date: Sat, 1 Mar 2025 22:55:45 +0100 Subject: [PATCH 1/2] feat: Add user report feature for code of conduct violations - Implemented a new Report model for the database to store user reports. - Created POST /reports endpoint for users to report violations. - Created GET /reports endpoint to allow admin users to view all reports (restricted access). - Added necessary validations and error handling for the report submission process. --- api/v1/models/__init__.py | 1 + api/v1/models/report.py | 18 +++++++++ api/v1/routes/__init__.py | 2 + api/v1/routes/report.py | 51 +++++++++++++++++++++++++ api/v1/schemas/report.py | 17 +++++++++ api/v1/services/.invite.py.swp | Bin 0 -> 16384 bytes api/v1/services/report.py | 66 +++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+) create mode 100644 api/v1/models/report.py create mode 100644 api/v1/routes/report.py create mode 100644 api/v1/schemas/report.py create mode 100644 api/v1/services/.invite.py.swp create mode 100644 api/v1/services/report.py diff --git a/api/v1/models/__init__.py b/api/v1/models/__init__.py index 1468114a2..a7d9cb9ec 100644 --- a/api/v1/models/__init__.py +++ b/api/v1/models/__init__.py @@ -31,3 +31,4 @@ from api.v1.models.reset_password_token import ResetPasswordToken from api.v1.models.faq_inquiries import FAQInquiries from api.v1.models.totp_device import TOTPDevice +from api.v1.models.report import Report \ No newline at end of file diff --git a/api/v1/models/report.py b/api/v1/models/report.py new file mode 100644 index 000000000..0c069bd83 --- /dev/null +++ b/api/v1/models/report.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, String, Text, ForeignKey, Enum as SqlAlchemyEnum +from api.v1.models.base_model import BaseTableModel +from enum import Enum + + +class ReportStatusEnum(Enum): + resolved = "resolved" + pending = "pending" + + +class Report(BaseTableModel): + + __tablename__ = "reports" + + reported_by = Column(String, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + reported_user = Column(String, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + reason = Column(String, nullable=False) + status = Column(SqlAlchemyEnum(ReportStatusEnum), default=ReportStatusEnum.pending) diff --git a/api/v1/routes/__init__.py b/api/v1/routes/__init__.py index e82a87a39..6b8a5f4c9 100644 --- a/api/v1/routes/__init__.py +++ b/api/v1/routes/__init__.py @@ -46,6 +46,7 @@ from api.v1.routes.settings import settings from api.v1.routes.terms_and_conditions import terms_and_conditions from api.v1.routes.stripe import subscription_ +from api.v1.routes.report import report api_version_one = APIRouter(prefix="/api/v1") @@ -96,3 +97,4 @@ api_version_one.include_router(terms_and_conditions) api_version_one.include_router(product_comment) api_version_one.include_router(subscription_) +api_version_one.include_router(report) diff --git a/api/v1/routes/report.py b/api/v1/routes/report.py new file mode 100644 index 000000000..a956dc5fa --- /dev/null +++ b/api/v1/routes/report.py @@ -0,0 +1,51 @@ +from api.v1.models.report import Report +from api.v1.schemas.report import ReportCreateSchema, ReportResponseSchema +from fastapi import Depends, APIRouter, HTTPException, status, Request +from sqlalchemy.orm import Session +from api.db.database import get_db +from api.utils.logger import logger +from api.utils.success_response import success_response +from api.v1.models.user import User +from api.v1.services.user import user_service +from api.v1.services.report import report_service +from fastapi.encoders import jsonable_encoder + + + +report = APIRouter(prefix="/reports", tags=["Reports"]) + +@report.post("", response_model=success_response, status_code=status.HTTP_201_CREATED) +def create_report(report_request: ReportCreateSchema, reported_by: User = Depends(user_service.get_current_user), db: Session = Depends(get_db)): + + reported_user = user_service.get_user_by_id(db, report_request.reported_user) + + + if (reported_by.id == reported_user.id): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User cannot report itself") + + reason = report_request.reason + + if (len(reason) == 0): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="reason user been reported cannot be empty") + + report = report_service.create(db, report_request, reported_by.id) + + return success_response( + status_code=status.HTTP_201_CREATED, + message="report successfully created", + data=jsonable_encoder(report) + ) + +@report.get("", response_model=success_response, status_code=status.HTTP_200_OK) +def get_all_report(admin_user: User = Depends(user_service.get_current_super_admin), db: Session = Depends(get_db)): + + reports = report_service.fetch_all(db) + + if not reports: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No report Found") + + return success_response( + status_code=status.HTTP_200_OK, + message="Report retrieced successfully", + data=jsonable_encoder(reports) + ) \ No newline at end of file diff --git a/api/v1/schemas/report.py b/api/v1/schemas/report.py new file mode 100644 index 000000000..d13a9da6b --- /dev/null +++ b/api/v1/schemas/report.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel +from datetime import datetime + + + +class ReportCreateSchema(BaseModel): + reported_user: str + reason: str + + +class ReportResponseSchema(BaseModel): + reported_by: str + reported_user: str + reason: str + status: str + created_at: datetime + updated_at: datetime diff --git a/api/v1/services/.invite.py.swp b/api/v1/services/.invite.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..6a6e13bfcddf44cb98c590a3f7bdced80b2d0f79 GIT binary patch literal 16384 zcmeHN+p8o;8L!<{*JKkl$wOYt*+6FSY|lB#5^%_%%QY1|}^+7?Z z;Wtfpef8D%{l2QNF4Oy~*S4#maHQ+4p+q((*4sac~2l&%nguDuT8~7IRJa7|u3iu>&2XGoV1^n$?g0Ch!g5Q@{YYANa#rLVf|f z4Ez{)4!8+?9QYXUF!1`j2zd?o5g>r;zy;tg;5Y9i4x8w!8b}=8c$@QlN|PDyo?X<04_hi>s?E zG+|YgX2Z_rjWXpdq3ZJrrHp22{&c6;QYn}aP!q((bedM(cA@?ag898kd!QN=kX&1i~e=Rsudvk9%gu_`GYa>;7IZ z&>foH#=-#=6<;xq4d_lirg{T4P(Cb2Xc|u$K7>l-j3C1$FV3nN&v6rFkvpU6n5bg? zbG*`ely5uEnD5BOWd;ajS_mInT?^N@H!tmOZk$wa7E9MrM>2VWfO1L2FjTo>pkuF| z+VUNYiwK&ITIO}8(2oznP~Z+ysf@j5ys{>)CHXjacTCs#l5w}C4-|A5l zEWB>lQJopG>dvN^EI@@7u?&HTttQcSiEK|^))ojXWk>n}l}!_tRZ%yI=3=ST z$5A@1jA(_07a0>lycjDw97-o!A(0B?b!#Q6M9$E|T4*_}x2rE$#$u$@;Bdk?vyonE zPaxu^6SEsEo>h=0Qnk72;4-s35yTY8d&$$h>e|9KP3@a~9=ckUab}p?dRC`$Nl!h}yE@tFM6Oc12<)g{S2=mbB)kFLt~1~AY^Lib#al&6o;KoBXpG~NOs9a8doiKrnOe4Sg`|RgOw~#1V)Hjchz+* z)uwuiB^}qAZ7FiW)JW{q%fwm%zLS5}6kwB4zciDpDKO6~c8Vo>2un;2H1?cm_NJ|8)k`#@QZ2=tJD*M5aDwQ^z^-e5k~k zO>?$0E9Dlmi}S#dbA;3qQ74|EJo401Cu>q-MntyLh?(=lrPJ|7LW&zH9M~{eRQqI4 z2uC=ctKZ>f>}))|MAhzxZ)Vmk#gjkwPzMALDj9_7t&EB~_*g{&VHMfj;QM zv;1QLeh=}mAuaZw!pTI?)V{>WCwT#TF3)mxgh8tU R|ME&+q;G}?`4@>G{{%u2wp;)J literal 0 HcmV?d00001 diff --git a/api/v1/services/report.py b/api/v1/services/report.py new file mode 100644 index 000000000..e0941d83c --- /dev/null +++ b/api/v1/services/report.py @@ -0,0 +1,66 @@ +from typing import Any, Optional, List +from sqlalchemy.orm import Session +from api.core.base.services import Service +from api.v1.models.report import Report +from api.v1.schemas.report import ReportCreateSchema, ReportResponseSchema +from api.utils.db_validators import check_model_existence +from sqlalchemy import distinct +from fastapi import HTTPException + + +class ReportService(Service): + """Report Services""" + + def create(self, db: Session, schema: ReportCreateSchema, user_id: str): + '''Create a new Region''' + new_report = Report(**schema.model_dump(), reported_by=user_id) + db.add(new_report) + db.commit() + db.refresh(new_report) + + return new_report + + + def fetch_all(self, db: Session, **query_params: Optional[Any]): + '''Fetch all Region with option to search using query parameters''' + + query = db.query(Report) + + # Enable filter by query parameter + if query_params: + for column, value in query_params.items(): + if hasattr(Report, column) and value: + query = query.filter(getattr(Report, column).ilike(f'%{value}%')) + + return query.all() + + + def fetch(self, db: Session, report_id: str): + '''Fetches a Region by id''' + + report = check_model_existence(db, Report, report_id) + return report + + + def update(self, db: Session, report_id: str): + '''Updates a Region''' + + region = self.fetch(db=db, report_id=report_id) + + # Update the fields with the provided schema data + + db.commit() + db.refresh(region) + return region + + + def delete(self, db: Session, report_id: str): + '''Deletes a region service''' + + report = self.fetch(db=db, report_id=report_id) + db.delete(report) + db.commit() + + + +report_service = ReportService() From 0ea1f46f01683ea721464059a74765ab6031d342 Mon Sep 17 00:00:00 2001 From: Habeeb Ajayi Date: Sun, 2 Mar 2025 09:19:30 +0100 Subject: [PATCH 2/2] remove ./api/v1/service/.invite.py.swp file --- api/v1/services/.invite.py.swp | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 api/v1/services/.invite.py.swp diff --git a/api/v1/services/.invite.py.swp b/api/v1/services/.invite.py.swp deleted file mode 100644 index 6a6e13bfcddf44cb98c590a3f7bdced80b2d0f79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHN+p8o;8L!<{*JKkl$wOYt*+6FSY|lB#5^%_%%QY1|}^+7?Z z;Wtfpef8D%{l2QNF4Oy~*S4#maHQ+4p+q((*4sac~2l&%nguDuT8~7IRJa7|u3iu>&2XGoV1^n$?g0Ch!g5Q@{YYANa#rLVf|f z4Ez{)4!8+?9QYXUF!1`j2zd?o5g>r;zy;tg;5Y9i4x8w!8b}=8c$@QlN|PDyo?X<04_hi>s?E zG+|YgX2Z_rjWXpdq3ZJrrHp22{&c6;QYn}aP!q((bedM(cA@?ag898kd!QN=kX&1i~e=Rsudvk9%gu_`GYa>;7IZ z&>foH#=-#=6<;xq4d_lirg{T4P(Cb2Xc|u$K7>l-j3C1$FV3nN&v6rFkvpU6n5bg? zbG*`ely5uEnD5BOWd;ajS_mInT?^N@H!tmOZk$wa7E9MrM>2VWfO1L2FjTo>pkuF| z+VUNYiwK&ITIO}8(2oznP~Z+ysf@j5ys{>)CHXjacTCs#l5w}C4-|A5l zEWB>lQJopG>dvN^EI@@7u?&HTttQcSiEK|^))ojXWk>n}l}!_tRZ%yI=3=ST z$5A@1jA(_07a0>lycjDw97-o!A(0B?b!#Q6M9$E|T4*_}x2rE$#$u$@;Bdk?vyonE zPaxu^6SEsEo>h=0Qnk72;4-s35yTY8d&$$h>e|9KP3@a~9=ckUab}p?dRC`$Nl!h}yE@tFM6Oc12<)g{S2=mbB)kFLt~1~AY^Lib#al&6o;KoBXpG~NOs9a8doiKrnOe4Sg`|RgOw~#1V)Hjchz+* z)uwuiB^}qAZ7FiW)JW{q%fwm%zLS5}6kwB4zciDpDKO6~c8Vo>2un;2H1?cm_NJ|8)k`#@QZ2=tJD*M5aDwQ^z^-e5k~k zO>?$0E9Dlmi}S#dbA;3qQ74|EJo401Cu>q-MntyLh?(=lrPJ|7LW&zH9M~{eRQqI4 z2uC=ctKZ>f>}))|MAhzxZ)Vmk#gjkwPzMALDj9_7t&EB~_*g{&VHMfj;QM zv;1QLeh=}mAuaZw!pTI?)V{>WCwT#TF3)mxgh8tU R|ME&+q;G}?`4@>G{{%u2wp;)J