-
Notifications
You must be signed in to change notification settings - Fork 213
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
DanielleBadobre
wants to merge
15
commits into
hngprojects:dev
Choose a base branch
from
DanielleBadobre:dev
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+640
−2
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
e3a37e9
feat: implemented feature request api
DanielleBadobre d4fbb15
Merge branch 'dev' of https://github.com/DanielleBadobre/hng_boilerpl…
DanielleBadobre e282084
Fix(test): feature request tests with correct priority type and mock …
DanielleBadobre bcbac2f
fix(tests): Resolve test failures and improve test coverage for featu…
DanielleBadobre 7985237
Merge branch 'dev' into dev
DanielleBadobre 3338679
chore: remove sqlalchemy test url from alembic.ini
DanielleBadobre 6379110
chore: Delete tests/v1/feature_request/screenshots directory
DanielleBadobre 3478ac9
Merge branch 'dev' into dev
DanielleBadobre e3e944a
Merge branch 'hngprojects:dev' into dev
DanielleBadobre bb9a8f7
refactor: remove status from FeatureRequestCreate schema
DanielleBadobre 9a5c9a6
feat: restrict feature request status updates to admin users only
DanielleBadobre 0dfa0ea
feat: Set feature request status to "Pending" and remove duplicate sc…
DanielleBadobre ae32b53
test: update feature request tests for status restriction
DanielleBadobre e1426b2
Merge branch 'hngprojects:dev' into dev
DanielleBadobre e5e92f9
Merge branch 'dev' into dev
DanielleBadobre File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ?