diff --git a/.env b/.env
index 98c8196862..4cc0da44b8 100644
--- a/.env
+++ b/.env
@@ -34,4 +34,4 @@ SENTRY_DSN=
# Configure these with your own Docker registry images
DOCKER_IMAGE_BACKEND=backend
-DOCKER_IMAGE_FRONTEND=frontend
+DOCKER_IMAGE_FRONTEND=frontend
\ No newline at end of file
diff --git a/README.md b/README.md
index afe124f3fb..1c7e31fc09 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-## Technology Stack and Features
+## Technology Stack and Features getit
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API.
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM).
diff --git a/backend/app/api/main.py b/backend/app/api/main.py
index 09e0663fc3..8f20c24969 100644
--- a/backend/app/api/main.py
+++ b/backend/app/api/main.py
@@ -1,9 +1,17 @@
+# This script sets up the main API router for the FastAPI application, organizing and including various sub-routers to handle specific sections of the API. Each sub-router, defined in separate route modules (items, login, users, utils, and geospatial), corresponds to different functional areas of the application, such as user management, item handling, utility functions, and geospatial data handling. By consolidating these routers under the main api_router, this script provides a centralized routing structure, enabling modular API section management and streamlined route access for the application’s endpoints.
+
+# Import APIRouter from FastAPI and the other route modules
from fastapi import APIRouter
-from app.api.routes import items, login, users, utils
+# Import the other route modules
+from app.api.routes import items, login, users, utils, geospatial
+# Initialize the main API router
api_router = APIRouter()
+
+# Include routers for different API sections
api_router.include_router(login.router, tags=["login"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
api_router.include_router(items.router, prefix="/items", tags=["items"])
+api_router.include_router(geospatial.router, prefix="/geospatial", tags=["geospatial"])
\ No newline at end of file
diff --git a/backend/app/api/routes/geospatial.py b/backend/app/api/routes/geospatial.py
new file mode 100644
index 0000000000..59e8086c9d
--- /dev/null
+++ b/backend/app/api/routes/geospatial.py
@@ -0,0 +1,79 @@
+from fastapi import APIRouter, Depends, File, UploadFile, HTTPException
+from sqlalchemy.orm import Session
+from typing import List
+from uuid import UUID
+import os # Import os to handle file path and directory creation
+
+from app import crud
+from app.models import GeoFile
+from app.api import deps
+from app.models import User
+
+router = APIRouter()
+
+# Dependency to get the current authenticated user
+# This uses the get_current_user function from deps.py
+# that decodes the JWT token, fetches the user from the database,
+# and ensures the user is active.
+@router.post("/upload", response_model=GeoFile)
+def upload_geofile(
+ file: UploadFile = File(...), # Accepts an uploaded file
+ description: str = None, # Optional description for the file
+ db: Session = Depends(deps.get_db), # Database session dependency
+ current_user: User = Depends(deps.get_current_user), # Get the current authenticated user
+):
+ """
+ Endpoint to upload a geospatial file. The file is saved to a designated directory,
+ and a record is created in the database associating the file with the current user.
+ """
+ # Directory where files will be stored
+ upload_dir = "uploads"
+ os.makedirs(upload_dir, exist_ok=True) # Create directory if it doesn't exist
+
+ # Construct file path
+ file_location = os.path.join(upload_dir, file.filename)
+
+ # Write the file to the storage path
+ with open(file_location, "wb+") as file_object:
+ file_object.write(file.file.read())
+
+ # Create geofile record in the database with user ownership
+ geofile = crud.create_geofile(
+ db=db,
+ owner_id=current_user.id,
+ file_name=file.filename,
+ file_path=file_location,
+ description=description,
+ )
+ return geofile # Return the created geofile record
+
+
+# Dependency to get a list of geofiles uploaded by the current user
+@router.get("/", response_model=List[GeoFile])
+def list_geofiles(
+ db: Session = Depends(deps.get_db), # Database session dependency
+ current_user: User = Depends(deps.get_current_user), # Get the current authenticated user
+):
+ """
+ Retrieve a list of geofiles uploaded by the current user.
+ """
+ return crud.get_user_geofiles(db=db, user_id=current_user.id)
+
+
+# Dependency to retrieve details of a specific geofile by its ID.
+# Access to geofile is restricted to the file's owner.
+@router.get("/{geofile_id}", response_model=GeoFile)
+def get_geofile(
+ geofile_id: UUID, # Unique identifier for the geofile
+ db: Session = Depends(deps.get_db), # Database session dependency
+ current_user: User = Depends(deps.get_current_user), # Get the current authenticated user
+):
+ """
+ Retrieve details of a specific geofile by its ID. Access is restricted to the file's owner.
+ """
+ geofile = crud.get_geofile(db=db, geofile_id=geofile_id)
+
+ # Check if geofile exists and if the user is the owner
+ if not geofile or geofile.owner_id != current_user.id:
+ raise HTTPException(status_code=404, detail="Geofile not found")
+ return geofile # Return geofile details if access is allowed
\ No newline at end of file
diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py
index 67196c2366..e118d16d9a 100644
--- a/backend/app/api/routes/items.py
+++ b/backend/app/api/routes/items.py
@@ -1,13 +1,11 @@
import uuid
from typing import Any
-
from fastapi import APIRouter, HTTPException
from sqlmodel import func, select
+from app.api.deps import CurrentUser, SessionDep # Custom dependencies for user and session
+from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message # Importing models
-from app.api.deps import CurrentUser, SessionDep
-from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message
-
-router = APIRouter()
+router = APIRouter() # Create an APIRouter instance for routing
@router.get("/", response_model=ItemsPublic)
@@ -15,15 +13,19 @@ def read_items(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any:
"""
- Retrieve items.
+ Retrieve items for the logged-in user. If the user is a superuser, they can see all items.
+ Otherwise, they can only see their own items.
"""
-
+ # Check if the current user is a superuser
if current_user.is_superuser:
+ # Count all items in the database
count_statement = select(func.count()).select_from(Item)
count = session.exec(count_statement).one()
+ # Retrieve items with pagination
statement = select(Item).offset(skip).limit(limit)
items = session.exec(statement).all()
else:
+ # For non-superusers, only retrieve items owned by the logged-in user
count_statement = (
select(func.count())
.select_from(Item)
@@ -38,19 +40,19 @@ def read_items(
)
items = session.exec(statement).all()
- return ItemsPublic(data=items, count=count)
+ return ItemsPublic(data=items, count=count) # Return items and total count
@router.get("/{id}", response_model=ItemPublic)
def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
"""
- Get item by ID.
+ Get a single item by ID. Users can only access their own items unless they are a superuser.
"""
- item = session.get(Item, id)
+ item = session.get(Item, id) # Retrieve item by ID
if not item:
- raise HTTPException(status_code=404, detail="Item not found")
+ raise HTTPException(status_code=404, detail="Item not found") # Item not found
if not current_user.is_superuser and (item.owner_id != current_user.id):
- raise HTTPException(status_code=400, detail="Not enough permissions")
+ raise HTTPException(status_code=400, detail="Not enough permissions") # Permission check
return item
@@ -59,13 +61,14 @@ def create_item(
*, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate
) -> Any:
"""
- Create new item.
+ Create a new item. The item will be associated with the logged-in user.
"""
+ # Create a new Item object, validating and assigning the owner (current user)
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
- session.add(item)
- session.commit()
- session.refresh(item)
- return item
+ session.add(item) # Add the new item to the session
+ session.commit() # Commit the transaction to the database
+ session.refresh(item) # Refresh the item object to get the updated values
+ return item # Return the newly created item
@router.put("/{id}", response_model=ItemPublic)
@@ -77,19 +80,19 @@ def update_item(
item_in: ItemUpdate,
) -> Any:
"""
- Update an item.
+ Update an existing item. Users can only update their own items unless they are superusers.
"""
- item = session.get(Item, id)
+ item = session.get(Item, id) # Retrieve item by ID
if not item:
- raise HTTPException(status_code=404, detail="Item not found")
+ raise HTTPException(status_code=404, detail="Item not found") # Item not found
if not current_user.is_superuser and (item.owner_id != current_user.id):
- raise HTTPException(status_code=400, detail="Not enough permissions")
- update_dict = item_in.model_dump(exclude_unset=True)
- item.sqlmodel_update(update_dict)
- session.add(item)
- session.commit()
- session.refresh(item)
- return item
+ raise HTTPException(status_code=400, detail="Not enough permissions") # Permission check
+ update_dict = item_in.model_dump(exclude_unset=True) # Prepare update data
+ item.sqlmodel_update(update_dict) # Apply updates to the item
+ session.add(item) # Add updated item to session
+ session.commit() # Commit the transaction
+ session.refresh(item) # Refresh the item object to get the updated values
+ return item # Return the updated item
@router.delete("/{id}")
@@ -97,13 +100,13 @@ def delete_item(
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
) -> Message:
"""
- Delete an item.
+ Delete an item. Users can only delete their own items unless they are superusers.
"""
- item = session.get(Item, id)
+ item = session.get(Item, id) # Retrieve item by ID
if not item:
- raise HTTPException(status_code=404, detail="Item not found")
+ raise HTTPException(status_code=404, detail="Item not found") # Item not found
if not current_user.is_superuser and (item.owner_id != current_user.id):
- raise HTTPException(status_code=400, detail="Not enough permissions")
- session.delete(item)
- session.commit()
- return Message(message="Item deleted successfully")
+ raise HTTPException(status_code=400, detail="Not enough permissions") # Permission check
+ session.delete(item) # Delete the item from the session
+ session.commit() # Commit the transaction
+ return Message(message="Item deleted successfully") # Return a success message
diff --git a/backend/app/api/routes/login.py b/backend/app/api/routes/login.py
index fe7e94d5c1..8273ef8f8d 100644
--- a/backend/app/api/routes/login.py
+++ b/backend/app/api/routes/login.py
@@ -5,6 +5,7 @@
from fastapi.responses import HTMLResponse
from fastapi.security import OAuth2PasswordRequestForm
+# Import necessary modules and dependencies
from app import crud
from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser
from app.core import security
@@ -18,6 +19,7 @@
verify_password_reset_token,
)
+# Initialize the APIRouter for defining login and password-related endpoints
router = APIRouter()
@@ -26,7 +28,8 @@ def login_access_token(
session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> Token:
"""
- OAuth2 compatible token login, get an access token for future requests
+ OAuth2 compatible token login, get an access token for future requests.
+ Validates user credentials and returns a JWT access token if successful.
"""
user = crud.authenticate(
session=session, email=form_data.username, password=form_data.password
@@ -46,7 +49,8 @@ def login_access_token(
@router.post("/login/test-token", response_model=UserPublic)
def test_token(current_user: CurrentUser) -> Any:
"""
- Test access token
+ Test access token.
+ Validates the current access token and returns user details if valid.
"""
return current_user
@@ -54,7 +58,8 @@ def test_token(current_user: CurrentUser) -> Any:
@router.post("/password-recovery/{email}")
def recover_password(email: str, session: SessionDep) -> Message:
"""
- Password Recovery
+ Password recovery.
+ Generates and sends a password reset token to the user's email if they exist in the system.
"""
user = crud.get_user_by_email(session=session, email=email)
@@ -78,7 +83,8 @@ def recover_password(email: str, session: SessionDep) -> Message:
@router.post("/reset-password/")
def reset_password(session: SessionDep, body: NewPassword) -> Message:
"""
- Reset password
+ Reset password.
+ Verifies the provided token, checks if the user exists and is active, then updates their password.
"""
email = verify_password_reset_token(token=body.token)
if not email:
@@ -105,7 +111,8 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
)
def recover_password_html_content(email: str, session: SessionDep) -> Any:
"""
- HTML Content for Password Recovery
+ HTML content for password recovery.
+ Generates HTML content of the password recovery email for preview, accessible only to superusers.
"""
user = crud.get_user_by_email(session=session, email=email)
@@ -121,4 +128,4 @@ def recover_password_html_content(email: str, session: SessionDep) -> Any:
return HTMLResponse(
content=email_data.html_content, headers={"subject:": email_data.subject}
- )
+ )
\ No newline at end of file
diff --git a/backend/app/crud.py b/backend/app/crud.py
index 905bf48724..f0f419850d 100644
--- a/backend/app/crud.py
+++ b/backend/app/crud.py
@@ -1,3 +1,6 @@
+# This script is a set of utility functions that manage user and item creation, updates, and authentication within a FastAPI application using SQLModel.
+# Each function interacts with the database through a Session object, performing standard CRUD operations and handling security concerns like password hashing and verification.
+# Database interaction functions (CRUD operations) for data. CRUD stands for Create, Read, Update, and Delete
import uuid
from typing import Any
@@ -6,6 +9,7 @@
from app.core.security import get_password_hash, verify_password
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
+# create_user: Creates a new user in the database, hashing the user's password before saving it.
def create_user(*, session: Session, user_create: UserCreate) -> User:
db_obj = User.model_validate(
@@ -16,6 +20,7 @@ def create_user(*, session: Session, user_create: UserCreate) -> User:
session.refresh(db_obj)
return db_obj
+# update_user: Updates an existing user's information in the database, including re-hashing the password if it’s updated.
def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any:
user_data = user_in.model_dump(exclude_unset=True)
@@ -30,12 +35,14 @@ def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any:
session.refresh(db_user)
return db_user
+# get_user_by_email: Retrieves a user from the database based on their email address.
def get_user_by_email(*, session: Session, email: str) -> User | None:
statement = select(User).where(User.email == email)
session_user = session.exec(statement).first()
return session_user
+# authenticate: Authenticates a user by verifying their email and password, returning the user if the credentials match.
def authenticate(*, session: Session, email: str, password: str) -> User | None:
db_user = get_user_by_email(session=session, email=email)
@@ -45,6 +52,7 @@ def authenticate(*, session: Session, email: str, password: str) -> User | None:
return None
return db_user
+# create_item: Creates a new item in the database, associating it with the owner (a specific user).
def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item:
db_item = Item.model_validate(item_in, update={"owner_id": owner_id})
@@ -52,3 +60,25 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -
session.commit()
session.refresh(db_item)
return db_item
+
+from app.models import GeoFile # Import the GeoFile model
+
+# create_geofile: Adds a new GeoFile record to the database.
+
+def create_geofile(session: Session, file_name: str, file_path: str, description: str = None) -> GeoFile:
+ geofile = GeoFile(file_name=file_name, file_path=file_path, description=description)
+ session.add(geofile) # Add the new GeoFile instance to the session.
+ session.commit() # Commit the transaction to save the GeoFile in the database.
+ session.refresh(geofile) # Refresh the instance to load any updated database state.
+ return geofile # Return the created GeoFile object.
+
+# get_geofile: Retrieves a GeoFile record by its unique ID.
+
+def get_geofile(session: Session, geofile_id: uuid.UUID) -> GeoFile | None:
+ return session.get(GeoFile, geofile_id) # Get the GeoFile by its ID, or None if not found.
+
+# list_geofiles: Retrieves all GeoFile records from the database.
+
+def list_geofiles(session: Session) -> list[GeoFile]:
+ statement = select(GeoFile) # Prepare a query to select all GeoFile records.
+ return session.exec(statement).all() # Execute the query and return all results as a list.
\ No newline at end of file
diff --git a/backend/app/main.py b/backend/app/main.py
index 4c252a1722..cf3ba40a57 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,3 +1,5 @@
+#This file initializes and configures the main FastAPI application. It sets up error tracking with Sentry, custom route ID generation, and CORS middleware for secure frontend-backend communication. The primary API router, which includes all route modules, is also attached here, allowing the app to serve endpoints under a unified API prefix. This centralized setup keeps core configurations and middleware management in one place, while routing and business logic remain modular in separate files.
+
import sentry_sdk
from fastapi import FastAPI
from fastapi.routing import APIRoute
@@ -11,16 +13,18 @@ def custom_generate_unique_id(route: APIRoute) -> str:
return f"{route.tags[0]}-{route.name}"
+# Initialize Sentry if DSN is set and environment is not local
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True)
+# Initialize FastAPI app
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
generate_unique_id_function=custom_generate_unique_id,
)
-# Set all CORS enabled origins
+# Set up CORS middleware for frontend-backend communication
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
@@ -32,4 +36,5 @@ def custom_generate_unique_id(route: APIRoute) -> str:
allow_headers=["*"],
)
-app.include_router(api_router, prefix=settings.API_V1_STR)
+# Include main API router
+app.include_router(api_router, prefix=settings.API_V1_STR)
\ No newline at end of file
diff --git a/backend/app/models.py b/backend/app/models.py
index 90ef5559e3..839e3a00e3 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -1,10 +1,28 @@
import uuid
-
+from datetime import datetime
from pydantic import EmailStr
from sqlmodel import Field, Relationship, SQLModel
-
-# Shared properties
+# Define a new database model called GeoFile, which will create a table in the database
+class GeoFile(SQLModel, table=True):
+ # Unique identifier for each record, generated using UUIDs
+ id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
+
+ # The name of the file as it was uploaded
+ file_name: str = Field(max_length=255)
+
+ # The date and time when the file was uploaded, set to the current time by default
+ upload_date: datetime = Field(default_factory=datetime.utcnow)
+
+ # Path where the file is stored on the server
+ file_path: str = Field(max_length=255)
+
+ # Optional field for a description of the file, with a maximum length of 500 characters
+ description: str | None = Field(default=None, max_length=500)
+
+# UserBase: A base class that defines shared properties for the User model.
+# It includes fields like email, full_name, is_active, and is_superuser.
+# This class is used as a base for other models related to users.
class UserBase(SQLModel):
email: EmailStr = Field(unique=True, index=True, max_length=255)
is_active: bool = True
@@ -12,67 +30,83 @@ class UserBase(SQLModel):
full_name: str | None = Field(default=None, max_length=255)
-# Properties to receive via API on creation
+# UserCreate: Inherits from UserBase and adds a password field.
+# This model is used when creating a new user via the API.
class UserCreate(UserBase):
password: str = Field(min_length=8, max_length=40)
+# UserRegister: Similar to UserCreate but used specifically for user registration.
+# Includes fields for email, password, and optional full_name.
class UserRegister(SQLModel):
email: EmailStr = Field(max_length=255)
password: str = Field(min_length=8, max_length=40)
full_name: str | None = Field(default=None, max_length=255)
-# Properties to receive via API on update, all are optional
+# UserUpdate: Used for updating existing user details.
+# It allows partial updates by making all fields optional.
class UserUpdate(UserBase):
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore
password: str | None = Field(default=None, min_length=8, max_length=40)
+# UserUpdateMe: A model for updating the current user's details.
+# It allows users to update their own email and full name.
class UserUpdateMe(SQLModel):
full_name: str | None = Field(default=None, max_length=255)
email: EmailStr | None = Field(default=None, max_length=255)
+# UpdatePassword: A model for updating a user's password.
+# It includes fields for the current and new passwords.
class UpdatePassword(SQLModel):
current_password: str = Field(min_length=8, max_length=40)
new_password: str = Field(min_length=8, max_length=40)
-# Database model, database table inferred from class name
+# User: Represents the User table in the database.
+# Inherits from UserBase and adds fields for id, hashed_password, and relationships.
class User(UserBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
hashed_password: str
items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True)
-# Properties to return via API, id is always required
+# UserPublic: A model for returning public user data via the API.
+# It excludes sensitive information like the hashed password.
class UserPublic(UserBase):
id: uuid.UUID
+# UsersPublic: A model for returning a list of public user data.
+# It includes the user data and a count of how many users are returned.
class UsersPublic(SQLModel):
data: list[UserPublic]
count: int
-# Shared properties
+# ItemBase: A base class that defines shared properties for the Item model.
+# It includes fields like title and description, which are used in other item-related models.
class ItemBase(SQLModel):
title: str = Field(min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=255)
-# Properties to receive on item creation
+# ItemCreate: Inherits from ItemBase and is used when creating a new item.
+# It allows the creation of an item with the basic fields defined in ItemBase.
class ItemCreate(ItemBase):
pass
-# Properties to receive on item update
+# ItemUpdate: Used for updating an existing item.
+# It allows partial updates by making all fields optional.
class ItemUpdate(ItemBase):
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
-# Database model, database table inferred from class name
+# Item: Represents the Item table in the database.
+# Inherits from ItemBase and adds fields for id, owner_id, and relationships.
class Item(ItemBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
title: str = Field(max_length=255)
@@ -82,33 +116,41 @@ class Item(ItemBase, table=True):
owner: User | None = Relationship(back_populates="items")
-# Properties to return via API, id is always required
+# ItemPublic: A model for returning public item data via the API.
+# It includes the item's id and owner_id but excludes sensitive information.
class ItemPublic(ItemBase):
id: uuid.UUID
owner_id: uuid.UUID
+# ItemsPublic: A model for returning a list of public item data.
+# It includes the item data and a count of how many items are returned.
class ItemsPublic(SQLModel):
data: list[ItemPublic]
count: int
-# Generic message
+# Message: A generic model used to return simple messages via the API.
+# Useful for operations that need to return a success or error message.
class Message(SQLModel):
message: str
-# JSON payload containing access token
+# Token: A model used to return an access token as a JSON payload.
+# Includes the access token and its type (typically "bearer").
class Token(SQLModel):
access_token: str
token_type: str = "bearer"
-# Contents of JWT token
+# TokenPayload: Represents the contents of a JWT token.
+# It is used internally to decode and validate tokens.
class TokenPayload(SQLModel):
sub: str | None = None
+# NewPassword: A model for handling password reset requests.
+# Includes the reset token and the new password.
class NewPassword(SQLModel):
token: str
- new_password: str = Field(min_length=8, max_length=40)
+ new_password: str = Field(min_length=8, max_length=40)
\ No newline at end of file
diff --git a/backend/poetry.lock b/backend/poetry.lock
index f56ce480f0..fa3a1c257b 100644
--- a/backend/poetry.lock
+++ b/backend/poetry.lock
@@ -461,6 +461,24 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8)"]
+[[package]]
+name = "geoalchemy2"
+version = "0.15.2"
+description = "Using SQLAlchemy with Spatial Databases"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "GeoAlchemy2-0.15.2-py3-none-any.whl", hash = "sha256:546455dc39f5bcdfc5b871e57d3f7546c8a6f798eb364c474200f488ace6fd32"},
+ {file = "geoalchemy2-0.15.2.tar.gz", hash = "sha256:3af0272db927373e74ee3b064cdc9464ba08defdb945c51745db1b841482f5dc"},
+]
+
+[package.dependencies]
+packaging = "*"
+SQLAlchemy = ">=1.4"
+
+[package.extras]
+shapely = ["Shapely (>=1.7)"]
+
[[package]]
name = "greenlet"
version = "3.0.3"
@@ -2081,4 +2099,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "7ec220bee66b5bc207f9a8b2f4ca9100da0213bb9d0a407b51cac3dc8201e97c"
+content-hash = "1f03ecea4f81784139fa92435abac0cebcb6c54f0b995618ad09f93f53d1cc45"
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 671a864645..be02845c83 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -26,6 +26,7 @@ bcrypt = "4.0.1"
pydantic-settings = "^2.2.1"
sentry-sdk = {extras = ["fastapi"], version = "^1.40.6"}
pyjwt = "^2.8.0"
+geoalchemy2 = "^0.15.2"
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
diff --git a/docker-compose.yml b/docker-compose.yml
index d614942cbd..1506f6f0df 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
db:
- image: postgres:12
+ image: postgis/postgis:12-3.1
restart: always
volumes:
- app-db-data:/var/lib/postgresql/data/pgdata
diff --git a/frontend/package.json b/frontend/package.json
index b9b2e3b51a..dbd0332064 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,9 +23,12 @@
"framer-motion": "10.16.16",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.13",
"react-hook-form": "7.49.3",
- "react-icons": "5.0.1"
+ "react-icons": "5.0.1",
+ "react-leaflet": "^4.2.1",
+ "leaflet": "^1.9.4"
},
"devDependencies": {
"@biomejs/biome": "1.6.1",
@@ -39,6 +42,7 @@
"@vitejs/plugin-react-swc": "^3.5.0",
"dotenv": "^16.4.5",
"typescript": "^5.2.2",
- "vite": "^5.0.13"
+ "vite": "^5.0.13",
+ "@types/leaflet": "^1.9.3"
}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/Geospatial/LeafletMap.tsx b/frontend/src/components/Geospatial/LeafletMap.tsx
new file mode 100644
index 0000000000..2c03982768
--- /dev/null
+++ b/frontend/src/components/Geospatial/LeafletMap.tsx
@@ -0,0 +1,116 @@
+import React, { useEffect, useRef } from "react";
+import {
+ MapContainer,
+ GeoJSON,
+ TileLayer,
+ LayersControl,
+ Marker,
+ Popup,
+ ScaleControl,
+ ZoomControl,
+} from "react-leaflet";
+import L, { Map, LatLngTuple } from "leaflet"; // Import Leaflet for map and layers handling
+import "./leaflet.css"; // Import CSS for the map styling
+import markerIconPng from "./icons/placeholder.png"; // Import a custom marker icon for the map
+
+// Define initial map center coordinates with explicit type LatLngTuple
+const center: LatLngTuple = [51.505, -0.09];
+
+// Define a custom marker icon for the map
+const icon = new L.Icon({
+ iconUrl: markerIconPng, // Path to the icon image
+ iconSize: [25, 41], // Size of the icon
+ iconAnchor: [12, 41], // Position of the icon anchor point (bottom of the icon)
+ popupAnchor: [1, -34], // Popup anchor relative to the icon
+ shadowSize: [41, 41], // Size of the shadow for the marker icon
+});
+
+interface LeafletMapComponentProps {
+ geojson: any; // GeoJSON data passed to the component to display on the map
+}
+
+// LeafletMapComponent renders a Leaflet map with custom layers and geoJSON data
+const LeafletMapComponent: React.FC = ({ geojson }) => {
+ const mapRef = useRef