Initialize FishIQ development foundation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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"))]),
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__"
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user