diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml
index 8c338a3c..8828c7d7 100644
--- a/.github/workflows/build_and_deploy.yml
+++ b/.github/workflows/build_and_deploy.yml
@@ -99,7 +99,7 @@ jobs:
# Cleanup db upgrade containers
docker compose --file contrib/pg-upgrade/docker-compose.yml down
-
+
docker compose --file docker-compose.vm.yml --env-file .env up \
--detach --remove-orphans --force-recreate --pull=always
env:
diff --git a/README.md b/README.md
index b7c08723..d4fa28f0 100644
--- a/README.md
+++ b/README.md
@@ -33,12 +33,11 @@
-
**Drone TM** is an integrated digital public good solution designed to harness
the power of the crowd to generate high-resolution aerial maps of any location.
-This innovative platform provides drone pilots, particularly in developing
-countries, with job opportunities while contributing to the creation of
+This innovative platform provides drone pilots, particularly in developing
+countries, with job opportunities while contributing to the creation of
high-resolution datasets crucial for disaster response and community resilience.
## Problem Statement
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index c1b5a76b..96025dd9 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -1,2 +1 @@
# Installation Guide
-
diff --git a/docs/about/about.md b/docs/about/about.md
index 0a2b3a34..50e08209 100644
--- a/docs/about/about.md
+++ b/docs/about/about.md
@@ -1,2 +1 @@
# 📖 History
-
diff --git a/docs/about/team.md b/docs/about/team.md
index ef98dc76..a3a0ea77 100644
--- a/docs/about/team.md
+++ b/docs/about/team.md
@@ -1 +1 @@
-# The Drone TM Team
\ No newline at end of file
+# The Drone TM Team
diff --git a/mkdocs.yml b/mkdocs.yml
index c1a61734..b35d6689 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -72,4 +72,4 @@ nav:
- Contribution Guidelines: CONTRIBUTING.md
- Code of Conduct: https://docs.hotosm.org/code-of-conduct
- FAQ: about/faq.md
- - The Team: about/team.md
\ No newline at end of file
+ - The Team: about/team.md
diff --git a/src/backend/app/db/database.py b/src/backend/app/db/database.py
index fa1c2d35..fa7cce69 100644
--- a/src/backend/app/db/database.py
+++ b/src/backend/app/db/database.py
@@ -1,10 +1,7 @@
"""Config for the DTM database connection."""
+
from databases import Database
from app.config import settings
-from sqlalchemy import create_engine
-from sqlalchemy.orm import declarative_base, sessionmaker
-
-Base = declarative_base()
class DatabaseConnection:
@@ -12,16 +9,9 @@ class DatabaseConnection:
def __init__(self):
self.database = Database(
- settings.DTM_DB_URL.unicode_string(), min_size=5, max_size=20
- )
- # self.database = Database(settings.DTM_DB_URL.unicode_string())
- self.engine = create_engine(
settings.DTM_DB_URL.unicode_string(),
- pool_size=20,
- max_overflow=-1,
- )
- self.SessionLocal = sessionmaker(
- autocommit=False, autoflush=False, bind=self.engine
+ min_size=5,
+ max_size=20,
)
async def connect(self):
@@ -32,27 +22,11 @@ async def disconnect(self):
"""Disconnect from the database."""
await self.database.disconnect()
- def create_db_session(self):
- """Create a new SQLAlchemy DB session."""
- db = self.SessionLocal()
- try:
- return db
- finally:
- db.close()
-
-
-db_connection = DatabaseConnection() # Create a single instance
-
-def get_db():
- """Yield a new database session."""
- return db_connection.create_db_session()
+db_connection = DatabaseConnection()
-async def encode_db():
+async def get_db():
"""Get the encode database connection"""
- try:
- await db_connection.connect()
- yield db_connection.database
- finally:
- await db_connection.disconnect()
+ await db_connection.connect()
+ yield db_connection.database
diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py
index f07d8d6f..1b2f2d0f 100644
--- a/src/backend/app/db/db_models.py
+++ b/src/backend/app/db/db_models.py
@@ -14,9 +14,9 @@
ARRAY,
LargeBinary,
)
+from sqlalchemy.orm import declarative_base
from sqlalchemy.dialects.postgresql import UUID
-from app.db.database import Base
from geoalchemy2 import Geometry, WKBElement
from app.models.enums import (
TaskStatus,
@@ -33,6 +33,9 @@
from app.utils import timestamp
+Base = declarative_base()
+
+
class DbUser(Base):
__tablename__ = "users"
diff --git a/src/backend/app/email_templates/mapping_approved_or_rejected.html b/src/backend/app/email_templates/mapping_approved_or_rejected.html
index c7bf1b35..22e38e8d 100644
--- a/src/backend/app/email_templates/mapping_approved_or_rejected.html
+++ b/src/backend/app/email_templates/mapping_approved_or_rejected.html
@@ -115,9 +115,7 @@
Dear {{ drone_operator_name }},
-
- {{ email_body }}
-
+
{{ email_body }}
Please find below the details of the {{ task_status }} task:
{{ task_status|capitalize }} Task Details
@@ -127,7 +125,9 @@
{{ task_status|capitalize }} Task Details
Description: {{ description }}
{% if task_status == 'approved' %}
-
Start Mapping
+
Start Mapping
{% endif %}
diff --git a/src/backend/app/email_templates/mapping_requests.html b/src/backend/app/email_templates/mapping_requests.html
index 6c9de9b9..d50d4706 100644
--- a/src/backend/app/email_templates/mapping_requests.html
+++ b/src/backend/app/email_templates/mapping_requests.html
@@ -125,11 +125,10 @@
Mapping Task Details
-
Drone Operator: {{drone_operator_name}}
-
Task ID: {{task_id}}
+
Drone Operator: {{drone_operator_name}}
+
Task ID: {{task_id}}
Project:{{project_name}}
-
- Description: {{description}}
+
Description: {{description}}
Start Mapping FastAPI:
return _app
+@asynccontextmanager
+async def lifespan(
+ app: FastAPI,
+):
+ """FastAPI startup/shutdown event."""
+ log.debug("Starting up FastAPI server.")
+ await db_connection.connect()
+
+ yield
+
+ # Shutdown events
+ log.debug("Shutting down FastAPI server.")
+ await db_connection.disconnect()
+
+
api = get_application()
diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py
index aeca6709..c59e9e43 100644
--- a/src/backend/app/projects/project_routes.py
+++ b/src/backend/app/projects/project_routes.py
@@ -6,7 +6,6 @@
import geojson
from datetime import timedelta
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form
-from sqlalchemy.orm import Session
from loguru import logger as log
from app.projects import project_schemas, project_crud
from app.db import database
@@ -15,7 +14,6 @@
from app.s3 import s3_client
from app.config import settings
from databases import Database
-from app.db import db_models
from shapely.geometry import shape, mapping
from shapely.ops import unary_union
@@ -26,9 +24,9 @@
@router.delete("/{project_id}", tags=["Projects"])
-def delete_project_by_id(
+async def delete_project_by_id(
project_id: uuid.UUID,
- db: Session = Depends(database.get_db),
+ db: Database = Depends(database.get_db),
user: AuthUser = Depends(login_required),
):
"""
@@ -36,7 +34,7 @@ def delete_project_by_id(
Args:
project_id (int): The ID of the project to delete.
- db (Session): The database session dependency.
+ db (Database): The database session dependency.
Returns:
dict: A confirmation message.
@@ -44,34 +42,31 @@ def delete_project_by_id(
Raises:
HTTPException: If the project is not found.
"""
- # Query for the project
- project = (
- db.query(db_models.DbProject)
- .filter(db_models.DbProject.id == project_id)
- .first()
- )
- if not project:
- raise HTTPException(status_code=404, detail="Project not found.")
+ delete_query = """
+ WITH deleted_project AS (
+ DELETE FROM projects
+ WHERE id = :project_id
+ RETURNING id
+ ), deleted_tasks AS (
+ DELETE FROM tasks
+ WHERE project_id = :project_id
+ RETURNING project_id
+ )
+ SELECT id FROM deleted_project
+ """
- # Query and delete associated tasks
- tasks = (
- db.query(db_models.DbTask)
- .filter(db_models.DbTask.project_id == project_id)
- .all()
- )
- for task in tasks:
- db.delete(task)
+ result = await db.fetch_one(query=delete_query, values={"project_id": project_id})
+
+ if not result:
+ raise HTTPException(status_code=404)
- # Delete the project
- db.delete(project)
- db.commit()
return {"message": f"Project ID: {project_id} is deleted successfully."}
@router.post("/create_project", tags=["Projects"])
async def create_project(
project_info: project_schemas.ProjectIn,
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
user_data: AuthUser = Depends(login_required),
):
"""Create a project in database."""
@@ -90,7 +85,7 @@ async def create_project(
async def upload_project_task_boundaries(
project_id: uuid.UUID,
task_geojson: UploadFile = File(...),
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
user: AuthUser = Depends(login_required),
):
"""Set project task boundaries using split GeoJSON from frontend.
@@ -209,7 +204,7 @@ async def generate_presigned_url(
async def read_projects(
skip: int = 0,
limit: int = 100,
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
user_data: AuthUser = Depends(login_required),
):
"Return all projects"
@@ -222,7 +217,7 @@ async def read_projects(
)
async def read_project(
project_id: uuid.UUID,
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
user_data: AuthUser = Depends(login_required),
):
"""Get a specific project and all associated tasks by ID."""
diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py
index 0520f428..bc673b04 100644
--- a/src/backend/app/tasks/task_routes.py
+++ b/src/backend/app/tasks/task_routes.py
@@ -20,9 +20,7 @@
@router.get("/states/{project_id}")
-async def task_states(
- project_id: uuid.UUID, db: Database = Depends(database.encode_db)
-):
+async def task_states(project_id: uuid.UUID, db: Database = Depends(database.get_db)):
"""Get all tasks states for a project."""
return await task_crud.all_tasks_states(db, project_id)
@@ -35,7 +33,7 @@ async def new_event(
task_id: uuid.UUID,
detail: task_schemas.NewEvent,
user_data: AuthUser = Depends(login_required),
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
):
user_id = user_data.id
@@ -205,7 +203,7 @@ async def new_event(
async def get_pending_tasks(
project_id: uuid.UUID,
user_data: AuthUser = Depends(login_required),
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
):
"""Get a list of pending tasks for a specific project and user."""
user_id = user_data.id
diff --git a/src/backend/app/users/oauth_routes.py b/src/backend/app/users/oauth_routes.py
index 942d92fd..071d1c21 100644
--- a/src/backend/app/users/oauth_routes.py
+++ b/src/backend/app/users/oauth_routes.py
@@ -62,7 +62,7 @@ async def update_token(user_data: AuthUser = Depends(login_required)):
@router.get("/my-info/")
async def my_data(
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
user_data: AuthUser = Depends(login_required),
):
"""Read access token and get user details from Google"""
diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py
index 5f424be9..2d174765 100644
--- a/src/backend/app/users/user_deps.py
+++ b/src/backend/app/users/user_deps.py
@@ -1,62 +1,46 @@
-import jwt
-from typing import Annotated
-from fastapi import Depends, HTTPException, Request, status, Header
-from fastapi.security import OAuth2PasswordBearer
-from jwt.exceptions import InvalidTokenError
-from pydantic import ValidationError
-from sqlalchemy.orm import Session
+from fastapi import HTTPException, Request, Header
from app.config import settings
-from app.db import database
-from app.users import user_crud, user_schemas
-from app.db.db_models import DbUser
+from app.users import user_crud
from app.users.auth import Auth
from app.users.user_schemas import AuthUser
from loguru import logger as log
-reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{settings.API_PREFIX}/users/login")
+# TODO do we need this code anymore?
+# reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{settings.API_PREFIX}/users/login")
+# # SessionDep = Annotated[
+# # Database,
+# # Depends(database.get_db),
+# # ]
# SessionDep = Annotated[
-# Database,
-# Depends(database.encode_db),
+# Session,
+# Depends(database.get_sqlalchemy_db),
# ]
-SessionDep = Annotated[
- Session,
- Depends(database.get_db),
-]
-TokenDep = Annotated[str, Depends(reusable_oauth2)]
-
-
-def get_current_user(session: SessionDep, token: TokenDep):
- try:
- payload = jwt.decode(
- token, settings.SECRET_KEY, algorithms=[user_crud.ALGORITHM]
- )
- token_data = user_schemas.TokenPayload(**payload)
-
- except (InvalidTokenError, ValidationError):
- raise HTTPException(
- status_code=status.HTTP_403_FORBIDDEN,
- detail="Could not validate credentials",
- )
-
- user = session.get(DbUser, token_data.sub)
-
- if not user:
- raise HTTPException(status_code=404, detail="User not found")
- if not user.is_active:
- raise HTTPException(status_code=400, detail="Inactive user")
- return user
-
-
-CurrentUser = Annotated[DbUser, Depends(get_current_user)]
-
-
-def get_current_active_superuser(current_user: CurrentUser):
- if not current_user.is_superuser:
- raise HTTPException(
- status_code=403, detail="The user doesn't have enough privileges"
- )
- return current_user
+# TokenDep = Annotated[str, Depends(reusable_oauth2)]
+# def get_current_user(session: SessionDep, token: TokenDep):
+# try:
+# payload = jwt.decode(
+# token, settings.SECRET_KEY, algorithms=[user_crud.ALGORITHM]
+# )
+# token_data = user_schemas.TokenPayload(**payload)
+# except (InvalidTokenError, ValidationError):
+# raise HTTPException(
+# status_code=status.HTTP_403_FORBIDDEN,
+# detail="Could not validate credentials",
+# )
+# user = session.get(DbUser, token_data.sub)
+# if not user:
+# raise HTTPException(status_code=404, detail="User not found")
+# if not user.is_active:
+# raise HTTPException(status_code=400, detail="Inactive user")
+# return user
+# CurrentUser = Annotated[DbUser, Depends(get_current_user)]
+# def get_current_active_superuser(current_user: CurrentUser):
+# if not current_user.is_superuser:
+# raise HTTPException(
+# status_code=403, detail="The user doesn't have enough privileges"
+# )
+# return current_user
async def init_google_auth():
@@ -81,6 +65,13 @@ async def login_required(
request: Request, access_token: str = Header(None)
) -> AuthUser:
"""Dependency to inject into endpoints requiring login."""
+ if settings.DEBUG:
+ return AuthUser(
+ id="0",
+ email="admin@hotosm.org",
+ name="admin",
+ img_url="",
+ )
if not access_token:
raise HTTPException(status_code=401, detail="No access token provided")
diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py
index b441b3c2..ec431209 100644
--- a/src/backend/app/users/user_routes.py
+++ b/src/backend/app/users/user_routes.py
@@ -24,7 +24,7 @@
@router.post("/login/")
async def login_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
) -> Token:
"""
OAuth2 compatible token login, get an access token for future requests
@@ -53,7 +53,7 @@ async def login_access_token(
async def update_user_profile(
user_id: str,
profile_update: ProfileUpdate,
- db: Database = Depends(database.encode_db),
+ db: Database = Depends(database.get_db),
user_data: AuthUser = Depends(login_required),
):
"""