Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create feature request api endpoint #1204

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
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 alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url =
sqlalchemy.url =


[post_write_hooks]
Expand Down
2 changes: 1 addition & 1 deletion api/v1/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
from api.v1.models.wishlist import Wishlist
from api.v1.models.totp_device import TOTPDevice
from api.v1.models.bookmark import Bookmark

from api.v1.models.feature_request import FeatureRequest
26 changes: 26 additions & 0 deletions api/v1/models/feature_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Feature Request data model
"""

from sqlalchemy import Column, String, text, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from api.v1.models.base_model import BaseTableModel


class FeatureRequest(BaseTableModel):
__tablename__ = "feature_requests"

title = Column(String, nullable=False)
description = Column(String, nullable=False)
priority = Column(String, server_default=text("'Low'")) # Low, Medium, High
status = Column(String, server_default=text("'Pending'")) # Pending, Approved, Rejected
is_deleted = Column(Boolean, server_default=text("false"))

# Foreign Keys
user_id = Column(String, ForeignKey("users.id"), nullable=False)

# Relationships
user = relationship("User", back_populates="feature_requests")

def __str__(self):
return self.title
4 changes: 4 additions & 0 deletions api/v1/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class User(BaseTableModel):
"Bookmark", back_populates="user", cascade="delete"
)

feature_requests = relationship(
"FeatureRequest", back_populates="user", cascade="all, delete-orphan"
)

def to_dict(self):
obj_dict = super().to_dict()
obj_dict.pop("password")
Expand Down
2 changes: 2 additions & 0 deletions api/v1/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from api.v1.routes.terms_and_conditions import terms_and_conditions
from api.v1.routes.stripe import subscription_
from api.v1.routes.wishlist import wishlist
from api.v1.routes.feature_request import feature_request

api_version_one = APIRouter(prefix="/api/v1")

Expand Down Expand Up @@ -98,3 +99,4 @@
api_version_one.include_router(product_comment)
api_version_one.include_router(subscription_)
api_version_one.include_router(wishlist)
api_version_one.include_router(feature_request)
134 changes: 134 additions & 0 deletions api/v1/routes/feature_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from api.db.database import get_db
from api.v1.schemas.feature_request import (
FeatureRequestCreate,
FeatureRequestResponse,
FeatureRequestUpdate
)
from api.v1.services.feature_request import FeatureRequestService
from api.v1.services.user import user_service
from api.v1.models.user import User

feature_request = APIRouter(prefix="/feature-request", tags=["Feature Requests"])


@feature_request.post("/", response_model=FeatureRequestResponse, status_code=status.HTTP_201_CREATED)
def create_feature_request(
feature_request_data: FeatureRequestCreate,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user)
):
"""
Create a new feature request
"""
return FeatureRequestService.create_feature_request(db, feature_request_data, current_user.id)


@feature_request.get("/", response_model=List[FeatureRequestResponse])
def get_feature_requests(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user)
):
"""
Get all feature requests
"""
# If user is a superadmin, return all feature requests
if current_user.is_superadmin:
return FeatureRequestService.get_feature_requests(db, skip, limit)
# Otherwise, return only the user's feature requests
return FeatureRequestService.get_user_feature_requests(db, current_user.id, skip, limit)


@feature_request.get("/{feature_request_id}", response_model=FeatureRequestResponse)
def get_feature_request(
feature_request_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user)
):
"""
Get a feature request by ID
"""
feature_request = FeatureRequestService.get_feature_request_by_id(db, feature_request_id)
if not feature_request:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Feature request not found"
)

# Check if the user is allowed to access this feature request
if not current_user.is_superadmin and feature_request.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this feature request"
)

return feature_request


@feature_request.put("/{feature_request_id}", response_model=FeatureRequestResponse)
def update_feature_request(
feature_request_id: str,
feature_request_update: FeatureRequestUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user)
):
"""
Update a feature request
"""
existing_feature_request = FeatureRequestService.get_feature_request_by_id(db, feature_request_id)
if not existing_feature_request:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Feature request not found"
)

# Check if the user is allowed to update this feature request
if not current_user.is_superadmin and existing_feature_request.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this feature request"
)

# Prevent non-superadmins from updating the status field
update_data = feature_request_update.dict(exclude_unset=True)
if not current_user.is_superadmin and "status" in update_data:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can update the status field"
)
Comment on lines +98 to +102
Copy link
Contributor

@samuelogboye samuelogboye Mar 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Youve already made a check to verify admin access in line 89
these lines of code will never be reached.

You can remove it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok sir, but actually the two checks serve different purposes.

  • The first check (line 89) verifies if the user has permission to update the feature request at all. It checks if the user is either a superadmin OR the owner of the feature request.

  • The second check (lines 98-102) only applies when a user is trying to update the "status" field. Even if a user passes the first check (because they're the owner of the request), they'll still hit this second check if they're trying to update the status field.

should i still remove the lines ?


updated_feature_request = FeatureRequestService.update_feature_request(
db, feature_request_id, feature_request_update
)
return updated_feature_request


@feature_request.delete("/{feature_request_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_feature_request(
feature_request_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user)
):
"""
Delete a feature request
"""
existing_feature_request = FeatureRequestService.get_feature_request_by_id(db, feature_request_id)
if not existing_feature_request:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Feature request not found"
)

# Check if the user is allowed to delete this feature request
if not current_user.is_superadmin and existing_feature_request.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this feature request"
)

FeatureRequestService.delete_feature_request(db, feature_request_id)
return None
35 changes: 35 additions & 0 deletions api/v1/schemas/feature_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field


class FeatureRequestBase(BaseModel):
title: str = Field(..., description="Title of the feature request")
description: str = Field(..., description="Detailed description of the requested feature")
priority: str = Field(default="Low", description="Priority level (Low, Medium, High)")

class FeatureRequestCreate(FeatureRequestBase):
pass


class FeatureRequestUpdate(BaseModel):
title: Optional[str] = Field(None, description="Title of the feature request")
description: Optional[str] = Field(None, description="Detailed description of the requested feature")
priority: Optional[str] = Field(None, description="Priority level (Low, Medium, High)")
status: Optional[str] = Field(None, description="Status (Pending, Approved, Rejected) - can only be modified by admins")


class FeatureRequestInDB(FeatureRequestBase):
id: str
created_at: datetime
updated_at: datetime
user_id: str
status: str = "Pending" # Always included in DB model
is_deleted: bool = False

class Config:
orm_mode = True


class FeatureRequestResponse(FeatureRequestInDB):
pass
54 changes: 54 additions & 0 deletions api/v1/services/feature_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from api.v1.models.feature_request import FeatureRequest
from api.v1.schemas.feature_request import FeatureRequestCreate, FeatureRequestUpdate, FeatureRequestBase, FeatureRequestInDB, FeatureRequestResponse


class FeatureRequestService:
@staticmethod
def create_feature_request(db: Session, feature_request_data: FeatureRequestCreate, user_id: str):
# Convert to dict and explicitly set status to "Pending"
feature_request_dict = feature_request_data.dict()
feature_request_dict["status"] = "Pending"

db_feature_request = FeatureRequest(**feature_request_data.dict(), user_id=user_id)
db.add(db_feature_request)
db.commit()
db.refresh(db_feature_request)
return db_feature_request

@staticmethod
def get_feature_requests(db: Session, skip: int = 0, limit: int = 100):
return db.query(FeatureRequest).offset(skip).limit(limit).all()

@staticmethod
def get_user_feature_requests(db: Session, user_id: str, skip: int = 0, limit: int = 100):
return db.query(FeatureRequest).filter(FeatureRequest.user_id == user_id).offset(skip).limit(limit).all()

@staticmethod
def get_feature_request_by_id(db: Session, feature_request_id: str):
return db.query(FeatureRequest).filter(FeatureRequest.id == feature_request_id).first()

@staticmethod
def update_feature_request(db: Session, feature_request_id: str, feature_request_update: FeatureRequestUpdate):
db_feature_request = db.query(FeatureRequest).filter(FeatureRequest.id == feature_request_id).first()
if db_feature_request:
for key, value in feature_request_update.dict(exclude_unset=True).items():
setattr(db_feature_request, key, value)
db.commit()
db.refresh(db_feature_request)
return db_feature_request

@staticmethod
def delete_feature_request(db: Session, feature_request_id: str):
db_feature_request = db.query(FeatureRequest).filter(FeatureRequest.id == feature_request_id).first()
if db_feature_request:
db.delete(db_feature_request)
db.commit()
return db_feature_request


class Config:
orm_mode = True
Binary file added test.db
Binary file not shown.
Loading