Skip to content
Open
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
37 changes: 37 additions & 0 deletions .agents/rules/django-code-gen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
trigger: always_on
---

# Django & DRF Version Constraints

## Target Framework Versions
* **Django**: `4.2.16` (LTS)
* **Django REST Framework (DRF)**: `3.15.2` (or latest compatible `3.15.x`)
* **Python**: `3.10` / `3.11` / `3.12`

## Guidelines for Code Generation
1. **Django 4.2 Compatibility**:
- Use `django.urls.path` and `re_path` instead of legacy `url()`.
- Prefer `async` view functions and `os.path` / `pathlib.Path` standards compatible with Django 4.2+.
- Use modern field options and class-based views (CBVs) or `@api_view` decorators conforming to Django 4.2 API standards.
- Do NOT use features introduced in Django 5.0+ (such as `GeneratedField` or database-computed defaults).

2. **Django REST Framework 3.15 Compatibility**:
- Write serializers using `serializers.ModelSerializer` or `serializers.Serializer`.
- **Ninja-Ready DRF Endpoints**:
- **View Structure**: Prefer `@api_view` function views or explicit `APIView` methods over `ModelViewSet` magic. This aligns directly with Django Ninja's route functions (`@api.get`, `@api.post`).
- **Strict Type Annotations**: Require full Python type hints on all view arguments (e.g., `def list_items(request: Request, item_id: int) -> Response:`) and function return types.
- **Decoupled Business Logic**: Move query filters, mutations, and domain logic into standalone service/selector functions rather than putting logic inside DRF Serializer `create()`/`update()` methods or ViewSet hooks.
- **Symmetric Schemas**: Separate input payload serializers from output serializers (e.g., `ItemCreateSerializer` vs `ItemSchemaSerializer`) to mirror Django Ninja's Pydantic input schemas and `response=` models.
- Standardize responses using `rest_framework.response.Response` and `rest_framework.status`.

## Authentication & User Model Guidelines

1. **Ninja-Ready Auth & Permissions**:
- Use standard HTTP Bearer token headers (`Authorization: Bearer <token>`) rather than session cookies or custom headers.
- Isolate permission logic into dedicated `BasePermission` classes or helper properties on the User model (e.g., `user.is_admin_user`) rather than inline `if` statements inside views.
- Avoid overriding `request.user` or attaching custom attributes directly to `request`; rely on standard user model properties.

2. **Custom User Model**:
- Always reference the custom user model using `settings.AUTH_USER_MODEL` for ForeignKeys/OneToOneFields.
- Use `django.contrib.auth.get_user_model()` in application code and serializers.
11 changes: 11 additions & 0 deletions .agents/rules/python-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
trigger: always_on
---

# Python Execution Constraints

- Python commands, tests, migrations, and scripts in this repository must be executed inside the Docker `web` container.
- When running terminal execution (e.g., executing scripts, running tests, or managing Django):
- Use `docker compose exec web python manage.py ...` for running commands against active services.
- Or use `docker compose run --rm web python ...` if services are not already running.
- Ensure all Django management commands and test suites run through the Docker environment.
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.gitignore
.idea
venv
__pycache__
*.pyc
*.pyo
*.pyd
.DS_Store
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
POSTGRES_DB=postgis_34_sample
POSTGRES_USER=padam_user
POSTGRES_PASSWORD=padam_p@ssw0rd
POSTGRES_HOST=db
POSTGRES_PORT=5432
SECRET_KEY=django-insecure-change-me-in-production
DEBUG=True
ALLOWED_HOSTS=*
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ ENV/
# pipenv: https://github.com/kennethreitz/pipenv
/Pipfile

# dotenv environment variable files
.env
.env.*
!.env.example

# Database
/db.sqlite3

# Editors stuff
.idea
.idea/
.vscode
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/padam-django-tech-test.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Install system dependencies for GeoDjango and psycopg2
RUN apt-get update && apt-get install -y --no-install-recommends \
binutils \
gdal-bin \
libgdal-dev \
libpq-dev \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

COPY . /app/

EXPOSE 8000

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
28 changes: 24 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
run: ## Run the test server.
python manage.py runserver_plus
.PHONY: build up down logs migrate makemigrations test shell

install: ## Install the python requirements.
pip install -r requirements.txt
build: ## Build docker images
docker compose build

up: ## Start containers
docker compose up -d

down: ## Stop containers
docker compose down

logs: ## Tail container logs
docker compose logs -f web

migrate: ## Run database migrations
docker compose exec web python manage.py migrate

makemigrations: ## Create database migrations
docker compose exec web python manage.py makemigrations

test: ## Run test suite inside container
docker compose exec web python manage.py test

shell: ## Open Django shell inside web container
docker compose exec web python manage.py shell
98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,53 @@ more time-consuming than others...
- You can reduce the scope of the project if you're short of time. A draft response is already a good thing.
- Be ready to present the subject, justify your choices and talk about how you would have done the parts you left out.


# Deliverable - Tahina

I chose PostGIS as the database backend to accurately model the domain requirements for Padam Mobility's route calculations.

I focused on 1 of the 2 trip constraints:

> The same bus cannot be assigned to several routes at the same time, with overlapping start and end times.

Here is how I designed the application's database models:

```mermaid
erDiagram
Place ||--o| BusStop : locates
User ||--o{ Bus : drives
User ||--o{ BusShift : works
Bus ||--o{ BusShift : works
BusShift ||--|{ BusStop : includes

User {
int id PK
boolean is_driver
time shift_start_time "e.g., 08:00:00"
time shift_end_time "e.g., 17:00:00"
string shift_days "e.g., Mon-Fri"
}
Bus {
int id PK
string licence_plate "e.g., AA-001-AA"
}
Place {
int id PK
Point location PK
}
BusStop {
int id PK
Point place_id FK
string name
datetime departure_time "optional"
}
BusShift {
int id PK
datetime departure_time
datetime arrival_time
float duration
BusStop[] stops
}
```



Expand Down Expand Up @@ -193,3 +239,53 @@ en temps que d'autres ...
- Privilégier la qualité et les bonnes pratiques.
- Vous pouvez réduire le périmètre du projet si vous manquez de temps. Une ébauche de réponse est déjà une bonne chose.
- Soyez prêt à présenter le sujet, à justifier vos choix et à parler de comment vous auriez fait les parties que vous avez laisser de côté.

# Livrable - Tahina


J'ai choisi PostGIS comme backend pour une bonne modélisation du besoin autour des calculs d'itinéraires de Padam Mobility.

Je me suis concentré sur 1/2 des contraintes d'un trajet :

> Un même bus ne peut être assigné, en même temps, à plusieurs trajets dont les heures de début et fin se
chevaucheraient.

Voilà comment j'ai vu les modèles de base de donées de l'application :

```mermaid
erDiagram
Place ||--o| BusStop : locates
User ||--o{ Bus : drives
User ||--o{ BusShift : works
Bus ||--o{ BusShift : works
BusShift ||--|{ BusStop : includes

User {
int id PK
boolean is_driver
time shift_start_time "e.g., 08:00:00"
time shift_end_time "e.g., 17:00:00"
string shift_days "e.g., Lun-Ven"
}
Bus {
int id PK
string licence_plate "e.g., AA-001-AA"
}
Place {
int id PK
Point location PK
}
BusStop {
int id PK
Point place_id FK
string name
datetime departure_time "optional"
}
BusShift {
int id PK
datetime departure_time
datetime arrival_time
float duration
BusStop[] stops
}
```
34 changes: 34 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
services:
db:
image: postgis/postgis:15-3.4-alpine
container_name: padam_postgis
restart: always
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5

web:
build: .
container_name: padam_web
restart: always
command: python manage.py runserver 0.0.0.0:8000
env_file:
- .env
volumes:
- .:/app
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy

volumes:
postgres_data:
27 changes: 27 additions & 0 deletions padam_django/apps/fleet/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
from django import forms
from django.contrib import admin

from . import models


class BusStopInlineFormSet(forms.BaseInlineFormSet):
def clean(self):
super().clean()
active_stops = 0
for form in self.forms:
if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
active_stops += 1
if active_stops < 2:
raise forms.ValidationError("A bus shift must have at least 2 stops.")


class BusStopInline(admin.TabularInline):
model = models.BusStop
formset = BusStopInlineFormSet
extra = 2


@admin.register(models.Bus)
class BusAdmin(admin.ModelAdmin):
pass
Expand All @@ -11,3 +29,12 @@ class BusAdmin(admin.ModelAdmin):
@admin.register(models.Driver)
class DriverAdmin(admin.ModelAdmin):
pass


@admin.register(models.BusShift)
class BusShiftAdmin(admin.ModelAdmin):
list_display = ('__str__', 'bus', 'driver', 'departure_time', 'arrival_time', 'duration')
readonly_fields = ('departure_time', 'arrival_time', 'duration')
inlines = [BusStopInline]


19 changes: 19 additions & 0 deletions padam_django/apps/fleet/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,22 @@ class BusFactory(factory.django.DjangoModelFactory):

class Meta:
model = models.Bus


class BusShiftFactory(factory.django.DjangoModelFactory):
bus = factory.SubFactory(BusFactory)
driver = factory.SubFactory(DriverFactory)

class Meta:
model = models.BusShift


class BusStopFactory(factory.django.DjangoModelFactory):
place = factory.SubFactory('padam_django.apps.geography.factories.PlaceFactory')
bus_shift = factory.SubFactory(BusShiftFactory)
name = factory.LazyFunction(fake.street_name)
departure_time = factory.LazyFunction(fake.date_time)

class Meta:
model = models.BusStop

Loading