Skip to content

Commit d7c653e

Browse files
nishika26sourabhlodhasourabhlodhaavirajsingh7Ishankoradia
authored
Organization/project : Crud, Endpoint and Test Cases (#63)
* trial * pushing all * models file * renaming * Rename Project.py to project.py * Rename oganization.py to organization.py * Update README.md (#44) * changes (#45) Co-authored-by: sourabhlodha <[email protected]> * Readme update (#47) rename project and stack --------- Co-authored-by: sourabhlodha <[email protected]> * fix create_user endpoint (#62) * standard api response and http exception handling (#67) * standardization and edits * small edits * small edits * small edits * fixed project post * trial * pushing all * models file * renaming * Rename Project.py to project.py * Rename oganization.py to organization.py * standardization and edits * small edits * small edits * small edits * fixed project post * remove these files since they were somehow pushed into this branch * re-push the docker file * re-push utils file * re-push the file * fixing test cases --------- Co-authored-by: Sourabh Lodha <[email protected]> Co-authored-by: sourabhlodha <[email protected]> Co-authored-by: Aviraj Gour <[email protected]> Co-authored-by: Ishankoradia <[email protected]>
1 parent 765c2ac commit d7c653e

File tree

14 files changed

+548
-3
lines changed

14 files changed

+548
-3
lines changed

backend/app/api/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from fastapi import APIRouter
22

3-
from app.api.routes import items, login, private, users, utils
3+
4+
from app.api.routes import items, login, private, users, utils,project,organization
5+
46
from app.core.config import settings
57

68
api_router = APIRouter()
79
api_router.include_router(login.router)
810
api_router.include_router(users.router)
911
api_router.include_router(utils.router)
1012
api_router.include_router(items.router)
13+
api_router.include_router(organization.router)
14+
api_router.include_router(project.router)
1115

1216

1317
if settings.ENVIRONMENT == "local":
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from typing import Any, List
2+
3+
from fastapi import APIRouter, Depends, HTTPException
4+
from sqlalchemy import func
5+
from sqlmodel import Session, select
6+
7+
from app.models import Organization, OrganizationCreate, OrganizationUpdate, OrganizationPublic
8+
from app.api.deps import (
9+
CurrentUser,
10+
SessionDep,
11+
get_current_active_superuser,
12+
)
13+
from app.crud.organization import create_organization, get_organization_by_id
14+
from app.utils import APIResponse
15+
16+
router = APIRouter(prefix="/organizations", tags=["organizations"])
17+
18+
19+
# Retrieve organizations
20+
@router.get("/", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[List[OrganizationPublic]])
21+
def read_organizations(session: SessionDep, skip: int = 0, limit: int = 100):
22+
count_statement = select(func.count()).select_from(Organization)
23+
count = session.exec(count_statement).one()
24+
25+
statement = select(Organization).offset(skip).limit(limit)
26+
organizations = session.exec(statement).all()
27+
28+
return APIResponse.success_response(organizations)
29+
30+
# Create a new organization
31+
@router.post("/", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[OrganizationPublic])
32+
def create_new_organization(*, session: SessionDep, org_in: OrganizationCreate):
33+
new_org = create_organization(session=session, org_create=org_in)
34+
return APIResponse.success_response(new_org)
35+
36+
37+
@router.get("/{org_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[OrganizationPublic])
38+
def read_organization(*, session: SessionDep, org_id: int):
39+
"""
40+
Retrieve an organization by ID.
41+
"""
42+
org = get_organization_by_id(session=session, org_id=org_id)
43+
if org is None:
44+
raise HTTPException(status_code=404, detail="Organization not found")
45+
return APIResponse.success_response(org)
46+
47+
48+
# Update an organization
49+
@router.patch("/{org_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[OrganizationPublic])
50+
def update_organization(*, session: SessionDep, org_id: int, org_in: OrganizationUpdate):
51+
org = get_organization_by_id(session=session, org_id=org_id)
52+
if org is None:
53+
raise HTTPException(status_code=404, detail="Organization not found")
54+
55+
org_data = org_in.model_dump(exclude_unset=True)
56+
org = org.model_copy(update=org_data)
57+
58+
59+
session.add(org)
60+
session.commit()
61+
session.flush()
62+
63+
return APIResponse.success_response(org)
64+
65+
66+
# Delete an organization
67+
@router.delete("/{org_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[None])
68+
def delete_organization(session: SessionDep, org_id: int):
69+
org = get_organization_by_id(session=session, org_id=org_id)
70+
if org is None:
71+
raise HTTPException(status_code=404, detail="Organization not found")
72+
73+
session.delete(org)
74+
session.commit()
75+
76+
return APIResponse.success_response(None)

backend/app/api/routes/project.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from typing import Any, List
2+
3+
from fastapi import APIRouter, Depends, HTTPException
4+
from sqlalchemy import func
5+
from sqlmodel import Session, select
6+
7+
from app.models import Project, ProjectCreate, ProjectUpdate, ProjectPublic
8+
from app.api.deps import (
9+
CurrentUser,
10+
SessionDep,
11+
get_current_active_superuser,
12+
)
13+
from app.crud.project import create_project, get_project_by_id, get_projects_by_organization
14+
from app.utils import APIResponse
15+
16+
router = APIRouter(prefix="/projects", tags=["projects"])
17+
18+
19+
# Retrieve projects
20+
@router.get("/",dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[List[ProjectPublic]])
21+
def read_projects(session: SessionDep, skip: int = 0, limit: int = 100):
22+
count_statement = select(func.count()).select_from(Project)
23+
count = session.exec(count_statement).one()
24+
25+
statement = select(Project).offset(skip).limit(limit)
26+
projects = session.exec(statement).all()
27+
28+
return APIResponse.success_response(projects)
29+
30+
31+
# Create a new project
32+
@router.post("/", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[ProjectPublic])
33+
def create_new_project(*, session: SessionDep, project_in: ProjectCreate):
34+
project = create_project(session=session, project_create=project_in)
35+
return APIResponse.success_response(project)
36+
37+
@router.get("/{project_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[ProjectPublic])
38+
def read_project(*, session: SessionDep, project_id: int) :
39+
"""
40+
Retrieve a project by ID.
41+
"""
42+
project = get_project_by_id(session=session, project_id=project_id)
43+
if project is None:
44+
raise HTTPException(status_code=404, detail="Project not found")
45+
return APIResponse.success_response(project)
46+
47+
48+
# Update a project
49+
@router.patch("/{project_id}",dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[ProjectPublic])
50+
def update_project(*, session: SessionDep, project_id: int, project_in: ProjectUpdate):
51+
project = get_project_by_id(session=session, project_id=project_id)
52+
if project is None:
53+
raise HTTPException(status_code=404, detail="Project not found")
54+
55+
project_data = project_in.model_dump(exclude_unset=True)
56+
project = project.model_copy(update=project_data)
57+
58+
session.add(project)
59+
session.commit()
60+
session.flush()
61+
return APIResponse.success_response(project)
62+
63+
64+
# Delete a project
65+
@router.delete("/{project_id}",dependencies=[Depends(get_current_active_superuser)])
66+
def delete_project(session: SessionDep, project_id: int):
67+
project = get_project_by_id(session=session, project_id=project_id)
68+
if project is None:
69+
raise HTTPException(status_code=404, detail="Project not found")
70+
71+
session.delete(project)
72+
session.commit()
73+
74+
return APIResponse.success_response(None)

backend/app/crud/organization.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Any, Optional
2+
3+
from sqlmodel import Session, select
4+
5+
from app.models import Organization, OrganizationCreate
6+
7+
def create_organization(*, session: Session, org_create: OrganizationCreate) -> Organization:
8+
db_org = Organization.model_validate(org_create)
9+
session.add(db_org)
10+
session.commit()
11+
session.refresh(db_org)
12+
return db_org
13+
14+
15+
def get_organization_by_id(*, session: Session, org_id: int) -> Optional[Organization]:
16+
statement = select(Organization).where(Organization.id == org_id)
17+
return session.exec(statement).first()
18+
19+
def get_organization_by_name(*, session: Session, name: str) -> Optional[Organization]:
20+
statement = select(Organization).where(Organization.name == name)
21+
return session.exec(statement).first()

backend/app/crud/project.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import List, Optional
2+
3+
from sqlmodel import Session, select
4+
5+
from app.models import Project, ProjectCreate
6+
7+
8+
def create_project(*, session: Session, project_create: ProjectCreate) -> Project:
9+
db_project = Project.model_validate(project_create)
10+
session.add(db_project)
11+
session.commit()
12+
session.refresh(db_project)
13+
return db_project
14+
15+
def get_project_by_id(*, session: Session, project_id: int) -> Optional[Project]:
16+
statement = select(Project).where(Project.id == project_id)
17+
return session.exec(statement).first()
18+
19+
def get_projects_by_organization(*, session: Session, org_id: int) -> List[Project]:
20+
statement = select(Project).where(Project.organization_id == org_id)
21+
return session.exec(statement).all()

backend/app/models/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,18 @@
1111
UserUpdateMe,
1212
NewPassword,
1313
UpdatePassword,
14+
)
15+
from .organization import (
16+
Organization,
17+
OrganizationCreate,
18+
OrganizationPublic,
19+
OrganizationsPublic,
20+
OrganizationUpdate,
21+
)
22+
from .project import (
23+
Project,
24+
ProjectCreate,
25+
ProjectPublic,
26+
ProjectsPublic,
27+
ProjectUpdate,
1428
)

backend/app/models/organization.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from sqlmodel import Field, Relationship, SQLModel
2+
3+
4+
# Shared properties for an Organization
5+
class OrganizationBase(SQLModel):
6+
name: str = Field(unique=True, index=True, max_length=255)
7+
is_active: bool = True
8+
9+
10+
# Properties to receive via API on creation
11+
class OrganizationCreate(OrganizationBase):
12+
pass
13+
14+
15+
# Properties to receive via API on update, all are optional
16+
class OrganizationUpdate(SQLModel):
17+
name: str | None = Field(default=None, max_length=255)
18+
is_active: bool | None = Field(default=None)
19+
20+
21+
# Database model for Organization
22+
class Organization(OrganizationBase, table=True):
23+
id: int = Field(default=None, primary_key=True)
24+
25+
26+
# Properties to return via API
27+
class OrganizationPublic(OrganizationBase):
28+
id: int
29+
30+
31+
class OrganizationsPublic(SQLModel):
32+
data: list[OrganizationPublic]
33+
count: int

backend/app/models/project.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from sqlmodel import Field, Relationship, SQLModel
2+
3+
4+
# Shared properties for a Project
5+
class ProjectBase(SQLModel):
6+
name: str = Field(index=True, max_length=255)
7+
description: str | None = Field(default=None, max_length=500)
8+
is_active: bool = True
9+
10+
11+
# Properties to receive via API on creation
12+
class ProjectCreate(ProjectBase):
13+
organization_id: int
14+
15+
16+
# Properties to receive via API on update, all are optional
17+
class ProjectUpdate(SQLModel):
18+
name: str | None = Field(default=None, max_length=255)
19+
description: str | None = Field(default=None, max_length=500)
20+
is_active: bool | None = Field(default=None)
21+
22+
23+
# Database model for Project
24+
class Project(ProjectBase, table=True):
25+
id: int = Field(default=None, primary_key=True)
26+
organization_id: int = Field(foreign_key="organization.id")
27+
28+
29+
# Properties to return via API
30+
class ProjectPublic(ProjectBase):
31+
id: int
32+
organization_id: int
33+
34+
35+
class ProjectsPublic(SQLModel):
36+
data: list[ProjectPublic]
37+
count: int
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import pytest
2+
from fastapi.testclient import TestClient
3+
from sqlmodel import Session, select
4+
5+
from app import crud
6+
from app.core.config import settings
7+
from app.core.security import verify_password
8+
from app.models import User, UserCreate
9+
from app.tests.utils.utils import random_email, random_lower_string
10+
from app.models import Organization, OrganizationCreate, OrganizationUpdate
11+
from app.api.deps import get_db
12+
from app.main import app
13+
from app.crud.organization import create_organization, get_organization_by_id
14+
15+
client = TestClient(app)
16+
17+
@pytest.fixture
18+
def test_organization(db: Session, superuser_token_headers: dict[str, str]):
19+
unique_name = f"TestOrg-{random_lower_string()}"
20+
org_data = OrganizationCreate(name=unique_name, is_active=True)
21+
organization = create_organization(session=db, org_create=org_data)
22+
db.commit()
23+
return organization
24+
25+
# Test retrieving organizations
26+
def test_read_organizations(db: Session, superuser_token_headers: dict[str, str]):
27+
response = client.get(f"{settings.API_V1_STR}/organizations/", headers=superuser_token_headers)
28+
assert response.status_code == 200
29+
response_data = response.json()
30+
assert "data" in response_data
31+
assert isinstance(response_data["data"], list)
32+
33+
# Test creating an organization
34+
def test_create_organization(db: Session, superuser_token_headers: dict[str, str]):
35+
unique_name = f"Org-{random_lower_string()}"
36+
org_data = {"name": unique_name, "is_active": True}
37+
response = client.post(
38+
f"{settings.API_V1_STR}/organizations/", json=org_data, headers=superuser_token_headers
39+
)
40+
41+
assert 200 <= response.status_code < 300
42+
created_org = response.json()
43+
assert "data" in created_org # Make sure there's a 'data' field
44+
created_org_data = created_org["data"]
45+
org = get_organization_by_id(session=db, org_id=created_org_data["id"])
46+
assert org is not None # The organization should be found in the DB
47+
assert org.name == created_org_data["name"]
48+
assert org.is_active == created_org_data["is_active"]
49+
50+
51+
def test_update_organization(db: Session, test_organization: Organization, superuser_token_headers: dict[str, str]):
52+
unique_name = f"UpdatedOrg-{random_lower_string()}" # Ensure a unique name
53+
update_data = {"name": unique_name, "is_active": False}
54+
55+
response = client.patch(
56+
f"{settings.API_V1_STR}/organizations/{test_organization.id}",
57+
json=update_data,
58+
headers=superuser_token_headers,
59+
)
60+
61+
assert response.status_code == 200
62+
updated_org = response.json()["data"]
63+
assert "name" in updated_org
64+
assert updated_org["name"] == update_data["name"]
65+
assert "is_active" in updated_org
66+
assert updated_org["is_active"] == update_data["is_active"]
67+
68+
69+
# Test deleting an organization
70+
def test_delete_organization(db: Session, test_organization: Organization, superuser_token_headers: dict[str, str]):
71+
response = client.delete(
72+
f"{settings.API_V1_STR}/organizations/{test_organization.id}", headers=superuser_token_headers
73+
)
74+
assert response.status_code == 200
75+
response = client.get(f"{settings.API_V1_STR}/organizations/{test_organization.id}", headers=superuser_token_headers)
76+
assert response.status_code == 404
77+

0 commit comments

Comments
 (0)