Initialize FishIQ development foundation

This commit is contained in:
reaper
2026-08-28 10:56:14 -05:00
commit 3eb459a307
81 changed files with 1642 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
+1
View File
@@ -0,0 +1 @@
+33
View File
@@ -0,0 +1,33 @@
import os
from pathlib import Path
import dj_database_url
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "unsafe-development-key")
DEBUG = os.getenv("DJANGO_DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "api"]
INSTALLED_APPS = [
"django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles",
"corsheaders", "rest_framework", "fishing",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [{"BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True,
"OPTIONS": {"context_processors": ["django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages"]}}]
WSGI_APPLICATION = "config.wsgi.application"
DATABASES = {"default": dj_database_url.config(default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}")}
AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]
+5
View File
@@ -0,0 +1,5 @@
from django.contrib import admin
from django.urls import include, path
urlpatterns = [path("admin/", admin.site.urls), path("api/", include("fishing.urls"))]
+5
View File
@@ -0,0 +1,5 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()
+1
View File
@@ -0,0 +1 @@
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class FishingConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "fishing"
@@ -0,0 +1,14 @@
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(name="Species", fields=[("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("common_name", models.CharField(max_length=120, unique=True)), ("scientific_name", models.CharField(blank=True, max_length=160))]),
migrations.CreateModel(name="WaterBody", fields=[("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("name", models.CharField(max_length=180)), ("region", models.CharField(max_length=120)), ("latitude", models.DecimalField(decimal_places=6, max_digits=9)), ("longitude", models.DecimalField(decimal_places=6, max_digits=9)), ("species", models.ManyToManyField(blank=True, related_name="water_bodies", to="fishing.species"))], options={"constraints": [models.UniqueConstraint(fields=("name", "region"), name="unique_water_body_region")]}),
migrations.CreateModel(name="Trip", fields=[("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("starts_at", models.DateTimeField()), ("ends_at", models.DateTimeField()), ("status", models.CharField(choices=[("planned", "Planned"), ("active", "Active"), ("complete", "Complete")], default="planned", max_length=12)), ("notes", models.TextField(blank=True)), ("created_at", models.DateTimeField(auto_now_add=True)), ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="trips", to=settings.AUTH_USER_MODEL)), ("target_species", models.ManyToManyField(related_name="trips", to="fishing.species")), ("water_body", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name="trips", to="fishing.waterbody"))]),
migrations.CreateModel(name="Recommendation", fields=[("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("title", models.CharField(max_length=180)), ("rationale", models.TextField()), ("tackle_setup", models.JSONField(default=dict)), ("confidence", models.DecimalField(decimal_places=3, max_digits=4)), ("generated_at", models.DateTimeField(auto_now_add=True)), ("trip", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="recommendations", to="fishing.trip"))]),
]
+1
View File
@@ -0,0 +1 @@
+40
View File
@@ -0,0 +1,40 @@
from django.conf import settings
from django.db import models
class Species(models.Model):
common_name = models.CharField(max_length=120, unique=True)
scientific_name = models.CharField(max_length=160, blank=True)
def __str__(self): return self.common_name
class WaterBody(models.Model):
name = models.CharField(max_length=180)
region = models.CharField(max_length=120)
latitude = models.DecimalField(max_digits=9, decimal_places=6)
longitude = models.DecimalField(max_digits=9, decimal_places=6)
species = models.ManyToManyField(Species, related_name="water_bodies", blank=True)
class Meta:
constraints = [models.UniqueConstraint(fields=["name", "region"], name="unique_water_body_region")]
def __str__(self): return f"{self.name}, {self.region}"
class Trip(models.Model):
STATUS_CHOICES = [("planned", "Planned"), ("active", "Active"), ("complete", "Complete")]
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="trips")
water_body = models.ForeignKey(WaterBody, on_delete=models.PROTECT, related_name="trips")
target_species = models.ManyToManyField(Species, related_name="trips")
starts_at = models.DateTimeField()
ends_at = models.DateTimeField()
status = models.CharField(max_length=12, choices=STATUS_CHOICES, default="planned")
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Recommendation(models.Model):
trip = models.ForeignKey(Trip, on_delete=models.CASCADE, related_name="recommendations")
title = models.CharField(max_length=180)
rationale = models.TextField()
tackle_setup = models.JSONField(default=dict)
confidence = models.DecimalField(max_digits=4, decimal_places=3)
generated_at = models.DateTimeField(auto_now_add=True)
+16
View File
@@ -0,0 +1,16 @@
from rest_framework import serializers
from .models import Recommendation, Species, Trip, WaterBody
class SpeciesSerializer(serializers.ModelSerializer):
class Meta: model = Species; fields = "__all__"
class WaterBodySerializer(serializers.ModelSerializer):
species = SpeciesSerializer(many=True, read_only=True)
class Meta: model = WaterBody; fields = "__all__"
class TripSerializer(serializers.ModelSerializer):
class Meta: model = Trip; fields = "__all__"; read_only_fields = ("owner",)
class RecommendationSerializer(serializers.ModelSerializer):
class Meta: model = Recommendation; fields = "__all__"
+11
View File
@@ -0,0 +1,11 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import RecommendationViewSet, SpeciesViewSet, TripViewSet, WaterBodyViewSet, health
router = DefaultRouter()
router.register("species", SpeciesViewSet)
router.register("water-bodies", WaterBodyViewSet)
router.register("trips", TripViewSet, basename="trip")
router.register("recommendations", RecommendationViewSet, basename="recommendation")
urlpatterns = [path("health/", health), path("", include(router.urls))]
+30
View File
@@ -0,0 +1,30 @@
from django.http import JsonResponse
from rest_framework import permissions, viewsets
from .models import Recommendation, Species, Trip, WaterBody
from .serializers import RecommendationSerializer, SpeciesSerializer, TripSerializer, WaterBodySerializer
def health(_request):
return JsonResponse({"status": "ok", "service": "fishiq-api"})
class ReadOnlyCatalogViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [permissions.AllowAny]
class SpeciesViewSet(ReadOnlyCatalogViewSet):
queryset = Species.objects.all().order_by("common_name")
serializer_class = SpeciesSerializer
class WaterBodyViewSet(ReadOnlyCatalogViewSet):
queryset = WaterBody.objects.prefetch_related("species").all().order_by("name")
serializer_class = WaterBodySerializer
class TripViewSet(viewsets.ModelViewSet):
serializer_class = TripSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self): return Trip.objects.filter(owner=self.request.user).select_related("water_body")
def perform_create(self, serializer): serializer.save(owner=self.request.user)
class RecommendationViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = RecommendationSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self): return Recommendation.objects.filter(trip__owner=self.request.user)
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
+7
View File
@@ -0,0 +1,7 @@
Django>=5.2,<5.3
djangorestframework>=3.16,<3.17
django-cors-headers>=4.7,<5
dj-database-url>=2.3,<3
psycopg[binary]>=3.2,<4
redis>=6,<7