-
Notifications
You must be signed in to change notification settings - Fork 7
Organization/project : Crud, Endpoint and Test Cases #63
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
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
fc9d8a5
trial
nishika26 899ebdd
pushing all
nishika26 40c1236
models file
nishika26 74e76dd
renaming
nishika26 abd3a9b
Rename Project.py to project.py
nishika26 9bd1c21
Rename oganization.py to organization.py
nishika26 42528cb
Update README.md (#44)
sourabhlodha d96f486
changes (#45)
sourabhlodha 06782dc
Readme update (#47)
sourabhlodha daeeaf9
fix create_user endpoint (#62)
avirajsingh7 f95f5f2
standard api response and http exception handling (#67)
avirajsingh7 d74252a
standardization and edits
nishika26 df7086e
small edits
nishika26 c411c57
small edits
nishika26 680603a
small edits
nishika26 659f93c
fixed project post
nishika26 f80c3b3
trial
nishika26 f54e388
pushing all
nishika26 d57e7b2
models file
nishika26 8c8db09
renaming
nishika26 a93dfa8
Rename Project.py to project.py
nishika26 02ee436
Rename oganization.py to organization.py
nishika26 2bd25de
standardization and edits
nishika26 ef0ab03
small edits
nishika26 fedba96
small edits
nishika26 9b7502a
small edits
nishika26 0e8903d
fixed project post
nishika26 534b893
Merge remote-tracking branch 'origin/feature/org_project' into featur…
Ishankoradia 88ad308
remove these files since they were somehow pushed into this branch
Ishankoradia 4c58d15
re-push the docker file
Ishankoradia bd7170d
re-push utils file
Ishankoradia e2c0b14
re-push the file
Ishankoradia 62260ed
Merge branch 'staging' into feature/org_project
nishika26 ff360a4
fixing test cases
nishika26 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 hidden or 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 hidden or 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,76 @@ | ||
| from typing import Any, List | ||
|
|
||
| from fastapi import APIRouter, Depends, HTTPException | ||
| from sqlalchemy import func | ||
| from sqlmodel import Session, select | ||
|
|
||
| from app.models import Organization, OrganizationCreate, OrganizationUpdate, OrganizationPublic | ||
| from app.api.deps import ( | ||
| CurrentUser, | ||
| SessionDep, | ||
| get_current_active_superuser, | ||
| ) | ||
| from app.crud.organization import create_organization, get_organization_by_id | ||
| from app.utils import APIResponse | ||
|
|
||
| router = APIRouter(prefix="/organizations", tags=["organizations"]) | ||
|
|
||
|
|
||
| # Retrieve organizations | ||
| @router.get("/", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[List[OrganizationPublic]]) | ||
| def read_organizations(session: SessionDep, skip: int = 0, limit: int = 100): | ||
| count_statement = select(func.count()).select_from(Organization) | ||
| count = session.exec(count_statement).one() | ||
|
|
||
| statement = select(Organization).offset(skip).limit(limit) | ||
| organizations = session.exec(statement).all() | ||
|
|
||
| return APIResponse.success_response(organizations) | ||
|
|
||
| # Create a new organization | ||
| @router.post("/", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[OrganizationPublic]) | ||
| def create_new_organization(*, session: SessionDep, org_in: OrganizationCreate): | ||
| new_org = create_organization(session=session, org_create=org_in) | ||
| return APIResponse.success_response(new_org) | ||
|
|
||
|
|
||
| @router.get("/{org_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[OrganizationPublic]) | ||
| def read_organization(*, session: SessionDep, org_id: int): | ||
| """ | ||
| Retrieve an organization by ID. | ||
| """ | ||
| org = get_organization_by_id(session=session, org_id=org_id) | ||
| if org is None: | ||
| raise HTTPException(status_code=404, detail="Organization not found") | ||
| return APIResponse.success_response(org) | ||
|
|
||
|
|
||
| # Update an organization | ||
| @router.patch("/{org_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[OrganizationPublic]) | ||
| def update_organization(*, session: SessionDep, org_id: int, org_in: OrganizationUpdate): | ||
| org = get_organization_by_id(session=session, org_id=org_id) | ||
| if org is None: | ||
| raise HTTPException(status_code=404, detail="Organization not found") | ||
|
|
||
| org_data = org_in.model_dump(exclude_unset=True) | ||
| org = org.model_copy(update=org_data) | ||
|
|
||
|
|
||
| session.add(org) | ||
| session.commit() | ||
| session.flush() | ||
|
|
||
| return APIResponse.success_response(org) | ||
|
|
||
|
|
||
| # Delete an organization | ||
| @router.delete("/{org_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[None]) | ||
| def delete_organization(session: SessionDep, org_id: int): | ||
| org = get_organization_by_id(session=session, org_id=org_id) | ||
| if org is None: | ||
| raise HTTPException(status_code=404, detail="Organization not found") | ||
|
|
||
| session.delete(org) | ||
| session.commit() | ||
|
|
||
| return APIResponse.success_response(None) |
This file contains hidden or 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,74 @@ | ||
| from typing import Any, List | ||
|
|
||
| from fastapi import APIRouter, Depends, HTTPException | ||
| from sqlalchemy import func | ||
| from sqlmodel import Session, select | ||
|
|
||
| from app.models import Project, ProjectCreate, ProjectUpdate, ProjectPublic | ||
| from app.api.deps import ( | ||
| CurrentUser, | ||
| SessionDep, | ||
| get_current_active_superuser, | ||
| ) | ||
| from app.crud.project import create_project, get_project_by_id, get_projects_by_organization | ||
| from app.utils import APIResponse | ||
|
|
||
| router = APIRouter(prefix="/projects", tags=["projects"]) | ||
|
|
||
|
|
||
| # Retrieve projects | ||
| @router.get("/",dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[List[ProjectPublic]]) | ||
| def read_projects(session: SessionDep, skip: int = 0, limit: int = 100): | ||
| count_statement = select(func.count()).select_from(Project) | ||
| count = session.exec(count_statement).one() | ||
|
|
||
| statement = select(Project).offset(skip).limit(limit) | ||
| projects = session.exec(statement).all() | ||
|
|
||
| return APIResponse.success_response(projects) | ||
|
|
||
|
|
||
| # Create a new project | ||
| @router.post("/", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[ProjectPublic]) | ||
| def create_new_project(*, session: SessionDep, project_in: ProjectCreate): | ||
| project = create_project(session=session, project_create=project_in) | ||
| return APIResponse.success_response(project) | ||
|
|
||
| @router.get("/{project_id}", dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[ProjectPublic]) | ||
| def read_project(*, session: SessionDep, project_id: int) : | ||
| """ | ||
| Retrieve a project by ID. | ||
| """ | ||
| project = get_project_by_id(session=session, project_id=project_id) | ||
| if project is None: | ||
| raise HTTPException(status_code=404, detail="Project not found") | ||
| return APIResponse.success_response(project) | ||
|
|
||
|
|
||
| # Update a project | ||
| @router.patch("/{project_id}",dependencies=[Depends(get_current_active_superuser)], response_model=APIResponse[ProjectPublic]) | ||
| def update_project(*, session: SessionDep, project_id: int, project_in: ProjectUpdate): | ||
| project = get_project_by_id(session=session, project_id=project_id) | ||
| if project is None: | ||
| raise HTTPException(status_code=404, detail="Project not found") | ||
|
|
||
| project_data = project_in.model_dump(exclude_unset=True) | ||
| project = project.model_copy(update=project_data) | ||
|
|
||
| session.add(project) | ||
| session.commit() | ||
| session.flush() | ||
| return APIResponse.success_response(project) | ||
|
|
||
|
|
||
| # Delete a project | ||
| @router.delete("/{project_id}",dependencies=[Depends(get_current_active_superuser)]) | ||
| def delete_project(session: SessionDep, project_id: int): | ||
| project = get_project_by_id(session=session, project_id=project_id) | ||
| if project is None: | ||
| raise HTTPException(status_code=404, detail="Project not found") | ||
|
|
||
| session.delete(project) | ||
| session.commit() | ||
|
|
||
| return APIResponse.success_response(None) |
This file contains hidden or 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,21 @@ | ||
| from typing import Any, Optional | ||
|
|
||
| from sqlmodel import Session, select | ||
|
|
||
| from app.models import Organization, OrganizationCreate | ||
|
|
||
| def create_organization(*, session: Session, org_create: OrganizationCreate) -> Organization: | ||
| db_org = Organization.model_validate(org_create) | ||
| session.add(db_org) | ||
| session.commit() | ||
| session.refresh(db_org) | ||
| return db_org | ||
|
|
||
|
|
||
| def get_organization_by_id(*, session: Session, org_id: int) -> Optional[Organization]: | ||
| statement = select(Organization).where(Organization.id == org_id) | ||
| return session.exec(statement).first() | ||
|
|
||
| def get_organization_by_name(*, session: Session, name: str) -> Optional[Organization]: | ||
| statement = select(Organization).where(Organization.name == name) | ||
| return session.exec(statement).first() |
This file contains hidden or 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,21 @@ | ||
| from typing import List, Optional | ||
|
|
||
| from sqlmodel import Session, select | ||
|
|
||
| from app.models import Project, ProjectCreate | ||
|
|
||
|
|
||
| def create_project(*, session: Session, project_create: ProjectCreate) -> Project: | ||
| db_project = Project.model_validate(project_create) | ||
| session.add(db_project) | ||
| session.commit() | ||
| session.refresh(db_project) | ||
| return db_project | ||
|
|
||
| def get_project_by_id(*, session: Session, project_id: int) -> Optional[Project]: | ||
| statement = select(Project).where(Project.id == project_id) | ||
| return session.exec(statement).first() | ||
|
|
||
| def get_projects_by_organization(*, session: Session, org_id: int) -> List[Project]: | ||
| statement = select(Project).where(Project.organization_id == org_id) | ||
| return session.exec(statement).all() |
This file contains hidden or 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 hidden or 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,33 @@ | ||
| from sqlmodel import Field, Relationship, SQLModel | ||
|
|
||
|
|
||
| # Shared properties for an Organization | ||
| class OrganizationBase(SQLModel): | ||
| name: str = Field(unique=True, index=True, max_length=255) | ||
| is_active: bool = True | ||
|
|
||
|
|
||
| # Properties to receive via API on creation | ||
| class OrganizationCreate(OrganizationBase): | ||
| pass | ||
|
|
||
|
|
||
| # Properties to receive via API on update, all are optional | ||
| class OrganizationUpdate(SQLModel): | ||
| name: str | None = Field(default=None, max_length=255) | ||
| is_active: bool | None = Field(default=None) | ||
|
|
||
|
|
||
| # Database model for Organization | ||
| class Organization(OrganizationBase, table=True): | ||
| id: int = Field(default=None, primary_key=True) | ||
|
|
||
|
|
||
| # Properties to return via API | ||
| class OrganizationPublic(OrganizationBase): | ||
| id: int | ||
|
|
||
|
|
||
| class OrganizationsPublic(SQLModel): | ||
| data: list[OrganizationPublic] | ||
| count: int |
This file contains hidden or 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,37 @@ | ||
| from sqlmodel import Field, Relationship, SQLModel | ||
|
|
||
|
|
||
| # Shared properties for a Project | ||
| class ProjectBase(SQLModel): | ||
| name: str = Field(index=True, max_length=255) | ||
| description: str | None = Field(default=None, max_length=500) | ||
| is_active: bool = True | ||
|
|
||
|
|
||
| # Properties to receive via API on creation | ||
| class ProjectCreate(ProjectBase): | ||
| organization_id: int | ||
|
|
||
|
|
||
| # Properties to receive via API on update, all are optional | ||
| class ProjectUpdate(SQLModel): | ||
| name: str | None = Field(default=None, max_length=255) | ||
| description: str | None = Field(default=None, max_length=500) | ||
| is_active: bool | None = Field(default=None) | ||
|
|
||
|
|
||
| # Database model for Project | ||
| class Project(ProjectBase, table=True): | ||
| id: int = Field(default=None, primary_key=True) | ||
| organization_id: int = Field(foreign_key="organization.id") | ||
|
|
||
|
|
||
| # Properties to return via API | ||
| class ProjectPublic(ProjectBase): | ||
| id: int | ||
| organization_id: int | ||
|
|
||
|
|
||
| class ProjectsPublic(SQLModel): | ||
| data: list[ProjectPublic] | ||
| count: int |
This file contains hidden or 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,77 @@ | ||
| import pytest | ||
| from fastapi.testclient import TestClient | ||
| from sqlmodel import Session, select | ||
|
|
||
| from app import crud | ||
| from app.core.config import settings | ||
| from app.core.security import verify_password | ||
| from app.models import User, UserCreate | ||
| from app.tests.utils.utils import random_email, random_lower_string | ||
| from app.models import Organization, OrganizationCreate, OrganizationUpdate | ||
| from app.api.deps import get_db | ||
| from app.main import app | ||
| from app.crud.organization import create_organization, get_organization_by_id | ||
|
|
||
| client = TestClient(app) | ||
|
|
||
| @pytest.fixture | ||
| def test_organization(db: Session, superuser_token_headers: dict[str, str]): | ||
| unique_name = f"TestOrg-{random_lower_string()}" | ||
| org_data = OrganizationCreate(name=unique_name, is_active=True) | ||
| organization = create_organization(session=db, org_create=org_data) | ||
| db.commit() | ||
| return organization | ||
|
|
||
| # Test retrieving organizations | ||
| def test_read_organizations(db: Session, superuser_token_headers: dict[str, str]): | ||
| response = client.get(f"{settings.API_V1_STR}/organizations/", headers=superuser_token_headers) | ||
| assert response.status_code == 200 | ||
| response_data = response.json() | ||
| assert "data" in response_data | ||
| assert isinstance(response_data["data"], list) | ||
|
|
||
| # Test creating an organization | ||
| def test_create_organization(db: Session, superuser_token_headers: dict[str, str]): | ||
| unique_name = f"Org-{random_lower_string()}" | ||
| org_data = {"name": unique_name, "is_active": True} | ||
| response = client.post( | ||
| f"{settings.API_V1_STR}/organizations/", json=org_data, headers=superuser_token_headers | ||
| ) | ||
|
|
||
| assert 200 <= response.status_code < 300 | ||
| created_org = response.json() | ||
| assert "data" in created_org # Make sure there's a 'data' field | ||
| created_org_data = created_org["data"] | ||
| org = get_organization_by_id(session=db, org_id=created_org_data["id"]) | ||
| assert org is not None # The organization should be found in the DB | ||
| assert org.name == created_org_data["name"] | ||
| assert org.is_active == created_org_data["is_active"] | ||
|
|
||
|
|
||
| def test_update_organization(db: Session, test_organization: Organization, superuser_token_headers: dict[str, str]): | ||
| unique_name = f"UpdatedOrg-{random_lower_string()}" # Ensure a unique name | ||
| update_data = {"name": unique_name, "is_active": False} | ||
|
|
||
| response = client.patch( | ||
| f"{settings.API_V1_STR}/organizations/{test_organization.id}", | ||
| json=update_data, | ||
| headers=superuser_token_headers, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| updated_org = response.json()["data"] | ||
| assert "name" in updated_org | ||
| assert updated_org["name"] == update_data["name"] | ||
| assert "is_active" in updated_org | ||
| assert updated_org["is_active"] == update_data["is_active"] | ||
|
|
||
|
|
||
| # Test deleting an organization | ||
| def test_delete_organization(db: Session, test_organization: Organization, superuser_token_headers: dict[str, str]): | ||
| response = client.delete( | ||
| f"{settings.API_V1_STR}/organizations/{test_organization.id}", headers=superuser_token_headers | ||
| ) | ||
| assert response.status_code == 200 | ||
| response = client.get(f"{settings.API_V1_STR}/organizations/{test_organization.id}", headers=superuser_token_headers) | ||
| assert response.status_code == 404 | ||
|
|
||
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.
Uh oh!
There was an error while loading. Please reload this page.