Initial updates with FE

This commit is contained in:
2026-04-10 21:41:26 -05:00
parent 562a8525d0
commit bb8af62f2d
9 changed files with 119 additions and 11 deletions

View File

@@ -20,6 +20,6 @@ RUN uv sync --no-dev
COPY . /app
EXPOSE 8000
EXPOSE 8003
ENTRYPOINT ["sh", "/app/scripts/entrypoint.sh"]

View File

@@ -25,7 +25,7 @@ Skeleton Django backend for WaterTrek, using `uv` for package management and Doc
3. Visit the health endpoint:
- `http://localhost:8000/booking/health/`
- `http://localhost:8003/booking/health/`
## Run locally with uv
@@ -42,5 +42,5 @@ uv run python manage.py runserver
The `web` container runs Gunicorn:
```bash
uv run gunicorn WaterTrek.wsgi:application --bind 0.0.0.0:8000 --workers 3
uv run gunicorn WaterTrek.wsgi:application --bind 0.0.0.0:8003 --workers 3
```

View File

@@ -1,7 +1,8 @@
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from .models import VendorProfile
from .models import CustomerProfile, VendorProfile
User = get_user_model()
@@ -74,8 +75,46 @@ class VendorRegistrationSerializer(serializers.Serializer):
return user
class CustomerProfileSerializer(serializers.ModelSerializer):
class Meta:
model = CustomerProfile
fields = (
"preferred_contact_method",
"emergency_contact_name",
"emergency_contact_phone",
"created_at",
"updated_at",
)
read_only_fields = fields
class CustomerRegistrationSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True, min_length=8)
first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
phone_number = serializers.CharField(required=False, allow_blank=True, max_length=32)
def validate_email(self, value):
if User.objects.filter(email__iexact=value).exists():
raise serializers.ValidationError("A user with this email already exists.")
return value
def create(self, validated_data):
password = validated_data.pop("password")
user = User.objects.create_user(
password=password,
is_vendor=False,
is_customer=True,
**validated_data,
)
CustomerProfile.objects.create(user=user)
return user
class UserMeSerializer(serializers.ModelSerializer):
vendor_profile = VendorProfileSerializer(read_only=True)
customer_profile = serializers.SerializerMethodField()
class Meta:
model = User
@@ -88,4 +127,12 @@ class UserMeSerializer(serializers.ModelSerializer):
"is_vendor",
"is_customer",
"vendor_profile",
"customer_profile",
)
def get_customer_profile(self, obj):
try:
profile = obj.customer_profile
except ObjectDoesNotExist:
return None
return CustomerProfileSerializer(profile).data

View File

@@ -1,10 +1,18 @@
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import MeView, VendorProfileMeView, VendorRegistrationView
from .views import (
CustomerRegistrationView,
MeView,
PasswordResetRequestView,
VendorProfileMeView,
VendorRegistrationView,
)
urlpatterns = [
path("register/vendor/", VendorRegistrationView.as_view(), name="register_vendor"),
path("register/customer/", CustomerRegistrationView.as_view(), name="register_customer"),
path("password/reset/request/", PasswordResetRequestView.as_view(), name="password_reset_request"),
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("me/", MeView.as_view(), name="me"),

View File

@@ -4,7 +4,12 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from .models import VendorProfile
from .serializers import UserMeSerializer, VendorProfileSerializer, VendorRegistrationSerializer
from .serializers import (
CustomerRegistrationSerializer,
UserMeSerializer,
VendorProfileSerializer,
VendorRegistrationSerializer,
)
class VendorRegistrationView(generics.CreateAPIView):
@@ -18,6 +23,31 @@ class VendorRegistrationView(generics.CreateAPIView):
return Response(UserMeSerializer(user).data, status=201)
class CustomerRegistrationView(generics.CreateAPIView):
serializer_class = CustomerRegistrationSerializer
permission_classes = (permissions.AllowAny,)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
return Response(UserMeSerializer(user).data, status=201)
class PasswordResetRequestView(APIView):
"""Accepts email for UX parity; outbound email is not wired yet."""
permission_classes = (permissions.AllowAny,)
def post(self, request):
return Response(
{
"detail": "If an account exists for this email, you will receive password reset instructions once email delivery is enabled."
},
status=200,
)
class MeView(APIView):
permission_classes = (permissions.IsAuthenticated,)

View File

@@ -2,11 +2,11 @@ services:
web:
build: .
container_name: watertrek-web
command: uv run gunicorn WaterTrek.wsgi:application --bind 0.0.0.0:8000 --workers 3
command: uv run gunicorn WaterTrek.wsgi:application --bind 0.0.0.0:8003 --workers 3
volumes:
- .:/app
ports:
- "8000:8000"
- "8003:8003"
environment:
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-change-me}
DJANGO_DEBUG: 1

View File

@@ -38,6 +38,14 @@ class EquipmentApiTests(APITestCase):
is_active=True,
)
def test_public_equipment_categories_list(self):
res = self.client.get("/api/v1/equipment/categories/")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(len(res.data), 1)
self.assertEqual(res.data[0]["id"], self.category.id)
self.assertEqual(res.data[0]["name"], "Boat")
self.assertEqual(res.data[0]["slug"], "boat")
def test_public_equipment_list_detail_and_filter(self):
list_res = self.client.get("/api/v1/equipment/items/")
self.assertEqual(list_res.status_code, status.HTTP_200_OK)

View File

@@ -1,13 +1,20 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import PublicEquipmentDetailView, PublicEquipmentListView, VendorEquipmentViewSet, VendorStorefrontView
from .views import (
PublicEquipmentCategoryListView,
PublicEquipmentDetailView,
PublicEquipmentListView,
VendorEquipmentViewSet,
VendorStorefrontView,
)
router = DefaultRouter()
router.register("vendor/items", VendorEquipmentViewSet, basename="vendor-equipment-item")
urlpatterns = [
path("", include(router.urls)),
path("categories/", PublicEquipmentCategoryListView.as_view(), name="equipment_public_categories"),
path("items/", PublicEquipmentListView.as_view(), name="equipment_public_list"),
path("items/<str:public_id>/", PublicEquipmentDetailView.as_view(), name="equipment_public_detail"),
path("storefront/<slug:slug>/", VendorStorefrontView.as_view(), name="vendor_storefront"),

View File

@@ -8,8 +8,16 @@ from rest_framework.views import APIView
from booking.models import Booking
from marketing.mixins import EquipmentListingClickTrackingMixin
from .models import EquipmentItem
from .serializers import EquipmentItemSerializer
from .models import EquipmentCategory, EquipmentItem
from .serializers import EquipmentCategorySerializer, EquipmentItemSerializer
class PublicEquipmentCategoryListView(generics.ListAPIView):
"""Read-only list of equipment categories for storefronts and vendor create flows."""
serializer_class = EquipmentCategorySerializer
permission_classes = (permissions.AllowAny,)
queryset = EquipmentCategory.objects.all().order_by("name")
class PublicEquipmentListView(generics.ListAPIView):