Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ RUN git config --system init.defaultBranch main
RUN git config --system core.autocrlf input
RUN git config --system color.ui auto

# Ensure vscode user owns the workspace directory
RUN chown -R $USERNAME:$USERNAME /workspace

# Configure zsh
USER $USERNAME
ENV HOME=/home/$USERNAME
Expand All @@ -71,10 +74,14 @@ RUN curl https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.
# Set Locale for Functional Autocompletion in zsh
RUN sudo locale-gen en_US.UTF-8

# Install python dev dependencies
COPY pyproject.toml /workspace/pyproject.toml
RUN pip install --upgrade pip
RUN pip install -e ".[dev]"

# Install Backend Dependencies
COPY backend/requirements.txt /workspace/backend/requirements.txt
WORKDIR /workspace/backend
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

# Expose application ports
Expand Down
14 changes: 6 additions & 8 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"gruntfuggly.todo-tree",
"ms-azuretools.vscode-docker",
"ms-python.isort"
"ms-azuretools.vscode-docker"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
Expand Down Expand Up @@ -75,7 +74,10 @@
"python.defaultInterpreterPath": "/usr/local/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.tabSize": 4
"editor.tabSize": 4,
"editor.codeActionsOnSave": {
"source.organizeImports": "never" // Let Ruff handle import sorting
}
},
"python.analysis.extraPaths": ["backend"],
"python.testing.pytestEnabled": true,
Expand All @@ -87,11 +89,7 @@
"reportImportCycles": "error"
},
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"ruff.enable": true,
"ruff.organizeImports": true,
"ruff.fixAll": true,
"ruff.lineLength": 100
"python.analysis.autoImportCompletions": true
}
}
}
Expand Down
14 changes: 13 additions & 1 deletion .devcontainer/post_create.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
#!/bin/bash
set -e

cd ../frontend
# Expected to run from .devcontainer/

echo "================== Installing pre-commit hooks ================="
cd ..
pre-commit install
pre-commit install-hooks

echo ""
echo "=============== Installing frontend dependencies ==============="
cd ./frontend
npm i --verbose

echo ""
echo "==================== Setting up the database ==================="
cd ../backend
python -m script.create_db
python -m script.create_test_db
Expand Down
10 changes: 6 additions & 4 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

title: ""
labels: ""
assignees: ""
---

### Motivation

Why are we doing this? What problem does it solve?

### Deliverables

- Specific thing to implement/fix
- Another deliverable
- etc.

### Important Notes

- Dependencies (e.g., Depends on #XX)
- Implementation constraints or preferences
- Any other context or gotchas
28 changes: 28 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
repos:
# General purpose
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

# Python
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
hooks:
- id: ruff-check # Linting
args: [--fix] # Auto-fix issues
- id: ruff-format # Formatting

# JavaScript/TypeScript and other Prettier compatible files
- repo: local
hooks:
- id: prettier
name: prettier
# Runs prettier on staged, removing the `frontend/` prefix to work with relative filepaths
entry: bash -c 'cd frontend && npx prettier --write "${@/#frontend\//}"' --
language: system
files: ^frontend/.*\.(js|jsx|ts|tsx|json|css|md)$
pass_filenames: true
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,17 @@ Or you can do the actions manually. Then,

## Running The App

*If you haven't run in a day or more, run `python -m script.reset_dev` from the `/backend` directory to ensure all mock data is updated to be centered around today's date*
_If you haven't run in a day or more, run `python -m script.reset_dev` from the `/backend` directory to ensure all mock data is updated to be centered around today's date_

### VSCode Debugger (Recommended)

Navigate to the "Debug and Run" tab on the VSCode side bar.

At the top of the side bar, next to the green play button, select the desired module to run

- **Backend**: Starts the FastAPI backend on http://localhost:8000
- **Purge & Frontend**: Starts the Next.js frontend on http://localhost:3000
- *The "Purge" part of this is referring to the task that kills any `next dev` processes in order to address a devcontainer issue. Note that this prevents you from running multiple of these debug sessions concurrently. If mulitple are needed, refer to the manual instructions below*
- _The "Purge" part of this is referring to the task that kills any `next dev` processes in order to address a devcontainer issue. Note that this prevents you from running multiple of these debug sessions concurrently. If mulitple are needed, refer to the manual instructions below_
- **Full Stack**: Starts both of the above in separate terminals

Then simply press the green play button
Expand Down
2 changes: 1 addition & 1 deletion backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ POSTGRES_PASSWORD=admin
POSTGRES_HOST=db
POSTGRES_PORT=5432
HOST=localhost
GOOGLE_MAPS_API_KEY=DA_KEEYYY #replace with your actual API key
GOOGLE_MAPS_API_KEY=DA_KEEYYY #replace with your actual API key
2 changes: 1 addition & 1 deletion backend/script/create_db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sqlalchemy import create_engine, text
from src.core.config import env
from src.core.database import server_url
from sqlalchemy import create_engine, text

engine = create_engine(server_url(sync=True), isolation_level="AUTOCOMMIT")

Expand Down
6 changes: 2 additions & 4 deletions backend/script/create_test_db.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from src.core.database import server_url
from sqlalchemy import create_engine, text
from src.core.database import server_url

engine = create_engine(server_url(sync=True), isolation_level="AUTOCOMMIT")

with engine.connect() as connection:
# Check if test database already exists
result = connection.execute(
text("SELECT 1 FROM pg_database WHERE datname = 'ocsl_test'")
)
result = connection.execute(text("SELECT 1 FROM pg_database WHERE datname = 'ocsl_test'"))
if result.fetchone():
print("Test database 'ocsl_test' already exists")
else:
Expand Down
2 changes: 1 addition & 1 deletion backend/script/delete_db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sqlalchemy import create_engine, text
from src.core.config import env
from src.core.database import server_url
from sqlalchemy import create_engine, text

engine = create_engine(server_url(sync=True), isolation_level="AUTOCOMMIT")

Expand Down
11 changes: 6 additions & 5 deletions backend/script/reset_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import asyncio
import json
import re
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path

import src.modules # Ensure all modules are imported so their entities are registered # noqa: F401
Expand All @@ -29,7 +29,7 @@ def parse_date(date_str: str | None) -> datetime | None:
if not date_str or date_str == "null":
return None

now = datetime.now(timezone.utc)
now = datetime.now(UTC)

if date_str.startswith("NOW"):
match = re.match(r"NOW([+-])(\d+)([hdwmy])", date_str)
Expand All @@ -55,7 +55,7 @@ def parse_date(date_str: str | None) -> datetime | None:
# Parse ISO format string and ensure it's timezone-aware
dt = datetime.fromisoformat(date_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt = dt.replace(tzinfo=UTC)
return dt


Expand All @@ -80,7 +80,6 @@ async def reset_dev():
async with AsyncSessionLocal() as session:
with open(
str(Path(__file__).parent.parent.parent / "frontend" / "shared" / "mock_data.json"),
"r",
) as f:
data = json.load(f)

Expand Down Expand Up @@ -144,7 +143,9 @@ async def reset_dev():

for party_data in data["parties"]:
party_datetime = parse_date(party_data["party_datetime"])
assert party_datetime is not None, f"party_datetime required for party {party_data['id']}"
assert party_datetime is not None, (
f"party_datetime required for party {party_data['id']}"
)

party = PartyEntity(
party_datetime=party_datetime,
Expand Down
4 changes: 2 additions & 2 deletions backend/src/core/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class HTTPBearer401(HTTPBearer):
async def __call__(self, request: Request):
try:
return await super().__call__(request)
except Exception:
raise CredentialsException()
except Exception as e:
raise CredentialsException() from e


bearer_scheme = HTTPBearer401()
Expand Down
2 changes: 1 addition & 1 deletion backend/src/core/database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import AsyncGenerator
from collections.abc import AsyncGenerator

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
Expand Down
6 changes: 1 addition & 5 deletions backend/src/core/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from typing import Generic, TypeVar

from pydantic import BaseModel

T = TypeVar("T")


class PaginatedResponse(BaseModel, Generic[T]):
class PaginatedResponse[T](BaseModel):
items: list[T]
total_records: int
page_size: int
Expand Down
8 changes: 4 additions & 4 deletions backend/src/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
from .student.student_entity import StudentEntity

__all__ = [
"StudentEntity",
"AccountEntity",
"PoliceEntity",
"PartyEntity",
"LocationEntity",
"ComplaintEntity",
"LocationEntity",
"PartyEntity",
"PoliceEntity",
"StudentEntity",
]
8 changes: 4 additions & 4 deletions backend/src/modules/account/account_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ async def create_account(self, data: AccountData) -> AccountDto:
try:
self.session.add(new_account)
await self.session.commit()
except IntegrityError:
except IntegrityError as e:
# handle race condition where another session inserted the same email
raise AccountConflictException(data.email)
raise AccountConflictException(data.email) from e
await self.session.refresh(new_account)
return new_account.to_dto()

Expand All @@ -117,8 +117,8 @@ async def update_account(self, account_id: int, data: AccountData) -> AccountDto
try:
self.session.add(account_entity)
await self.session.commit()
except IntegrityError:
raise AccountConflictException(data.email)
except IntegrityError as e:
raise AccountConflictException(data.email) from e
await self.session.refresh(account_entity)
return account_entity.to_dto()

Expand Down
4 changes: 2 additions & 2 deletions backend/src/modules/complaint/complaint_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async def create_complaint(self, location_id: int, data: ComplaintData) -> Compl
except IntegrityError as e:
# Foreign key constraint violation indicates location doesn't exist
if "locations" in str(e).lower() or "foreign key" in str(e).lower():
raise LocationNotFoundException(location_id)
raise LocationNotFoundException(location_id) from e
raise
await self.session.refresh(new_complaint)
return new_complaint.to_dto()
Expand All @@ -78,7 +78,7 @@ async def update_complaint(
except IntegrityError as e:
# Foreign key constraint violation indicates location doesn't exist
if "locations" in str(e).lower() or "foreign key" in str(e).lower():
raise LocationNotFoundException(location_id)
raise LocationNotFoundException(location_id) from e
raise
await self.session.refresh(complaint_entity)
return complaint_entity.to_dto()
Expand Down
4 changes: 2 additions & 2 deletions backend/src/modules/location/location_entity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Self

from sqlalchemy import DECIMAL, DateTime, Index, Integer, String
Expand Down Expand Up @@ -61,7 +61,7 @@ def to_dto(self) -> LocationDto:

hold_exp = self.hold_expiration
if hold_exp is not None and hold_exp.tzinfo is None:
hold_exp = hold_exp.replace(tzinfo=timezone.utc)
hold_exp = hold_exp.replace(tzinfo=UTC)

return LocationDto(
id=self.id,
Expand Down
12 changes: 6 additions & 6 deletions backend/src/modules/location/location_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ async def autocomplete_address(
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
except Exception:
) from e
except Exception as e:
# Log error in production
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to fetch address suggestions. Please try again later.",
)
) from e


@location_router.get(
Expand All @@ -78,12 +78,12 @@ async def get_place_details(
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
except Exception:
) from e
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to fetch place details. Please try again later.",
)
) from e


@location_router.get("/", response_model=PaginatedLocationResponse)
Expand Down
Loading