From bb8af62f2d85efeb92618091483da9baab3bb37e Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Fri, 10 Apr 2026 21:41:26 -0500 Subject: [PATCH] Initial updates with FE --- Dockerfile | 2 +- README.md | 4 ++-- accounts/serializers.py | 49 ++++++++++++++++++++++++++++++++++++++++- accounts/urls.py | 10 ++++++++- accounts/views.py | 32 ++++++++++++++++++++++++++- docker-compose.yml | 4 ++-- equipment/tests.py | 8 +++++++ equipment/urls.py | 9 +++++++- equipment/views.py | 12 ++++++++-- 9 files changed, 119 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4105ff9..cb09110 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,6 @@ RUN uv sync --no-dev COPY . /app -EXPOSE 8000 +EXPOSE 8003 ENTRYPOINT ["sh", "/app/scripts/entrypoint.sh"] diff --git a/README.md b/README.md index 6fcc2b4..0ed24c3 100644 --- a/README.md +++ b/README.md @@ -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 ``` \ No newline at end of file diff --git a/accounts/serializers.py b/accounts/serializers.py index 95e8a39..6e005b1 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -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 diff --git a/accounts/urls.py b/accounts/urls.py index e4a1691..c988e42 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -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"), diff --git a/accounts/views.py b/accounts/views.py index 3d2c513..f450cb7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -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,) diff --git a/docker-compose.yml b/docker-compose.yml index 757a75d..0a54ea2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/equipment/tests.py b/equipment/tests.py index 9d52c74..bf687af 100644 --- a/equipment/tests.py +++ b/equipment/tests.py @@ -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) diff --git a/equipment/urls.py b/equipment/urls.py index 8458f3c..e390e9a 100644 --- a/equipment/urls.py +++ b/equipment/urls.py @@ -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//", PublicEquipmentDetailView.as_view(), name="equipment_public_detail"), path("storefront//", VendorStorefrontView.as_view(), name="vendor_storefront"), diff --git a/equipment/views.py b/equipment/views.py index 3fca213..7f1ed3a 100644 --- a/equipment/views.py +++ b/equipment/views.py @@ -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):