Initial updates with FE
This commit is contained in:
@@ -20,6 +20,6 @@ RUN uv sync --no-dev
|
|||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8003
|
||||||
|
|
||||||
ENTRYPOINT ["sh", "/app/scripts/entrypoint.sh"]
|
ENTRYPOINT ["sh", "/app/scripts/entrypoint.sh"]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Skeleton Django backend for WaterTrek, using `uv` for package management and Doc
|
|||||||
|
|
||||||
3. Visit the health endpoint:
|
3. Visit the health endpoint:
|
||||||
|
|
||||||
- `http://localhost:8000/booking/health/`
|
- `http://localhost:8003/booking/health/`
|
||||||
|
|
||||||
## Run locally with uv
|
## Run locally with uv
|
||||||
|
|
||||||
@@ -42,5 +42,5 @@ uv run python manage.py runserver
|
|||||||
The `web` container runs Gunicorn:
|
The `web` container runs Gunicorn:
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import VendorProfile
|
from .models import CustomerProfile, VendorProfile
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@@ -74,8 +75,46 @@ class VendorRegistrationSerializer(serializers.Serializer):
|
|||||||
return user
|
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):
|
class UserMeSerializer(serializers.ModelSerializer):
|
||||||
vendor_profile = VendorProfileSerializer(read_only=True)
|
vendor_profile = VendorProfileSerializer(read_only=True)
|
||||||
|
customer_profile = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@@ -88,4 +127,12 @@ class UserMeSerializer(serializers.ModelSerializer):
|
|||||||
"is_vendor",
|
"is_vendor",
|
||||||
"is_customer",
|
"is_customer",
|
||||||
"vendor_profile",
|
"vendor_profile",
|
||||||
|
"customer_profile",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_customer_profile(self, obj):
|
||||||
|
try:
|
||||||
|
profile = obj.customer_profile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return None
|
||||||
|
return CustomerProfileSerializer(profile).data
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
||||||
|
|
||||||
from .views import MeView, VendorProfileMeView, VendorRegistrationView
|
from .views import (
|
||||||
|
CustomerRegistrationView,
|
||||||
|
MeView,
|
||||||
|
PasswordResetRequestView,
|
||||||
|
VendorProfileMeView,
|
||||||
|
VendorRegistrationView,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("register/vendor/", VendorRegistrationView.as_view(), name="register_vendor"),
|
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/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
path("me/", MeView.as_view(), name="me"),
|
path("me/", MeView.as_view(), name="me"),
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from .models import VendorProfile
|
from .models import VendorProfile
|
||||||
from .serializers import UserMeSerializer, VendorProfileSerializer, VendorRegistrationSerializer
|
from .serializers import (
|
||||||
|
CustomerRegistrationSerializer,
|
||||||
|
UserMeSerializer,
|
||||||
|
VendorProfileSerializer,
|
||||||
|
VendorRegistrationSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VendorRegistrationView(generics.CreateAPIView):
|
class VendorRegistrationView(generics.CreateAPIView):
|
||||||
@@ -18,6 +23,31 @@ class VendorRegistrationView(generics.CreateAPIView):
|
|||||||
return Response(UserMeSerializer(user).data, status=201)
|
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):
|
class MeView(APIView):
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ services:
|
|||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
container_name: watertrek-web
|
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:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8003:8003"
|
||||||
environment:
|
environment:
|
||||||
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-change-me}
|
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-change-me}
|
||||||
DJANGO_DEBUG: 1
|
DJANGO_DEBUG: 1
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ class EquipmentApiTests(APITestCase):
|
|||||||
is_active=True,
|
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):
|
def test_public_equipment_list_detail_and_filter(self):
|
||||||
list_res = self.client.get("/api/v1/equipment/items/")
|
list_res = self.client.get("/api/v1/equipment/items/")
|
||||||
self.assertEqual(list_res.status_code, status.HTTP_200_OK)
|
self.assertEqual(list_res.status_code, status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from .views import PublicEquipmentDetailView, PublicEquipmentListView, VendorEquipmentViewSet, VendorStorefrontView
|
from .views import (
|
||||||
|
PublicEquipmentCategoryListView,
|
||||||
|
PublicEquipmentDetailView,
|
||||||
|
PublicEquipmentListView,
|
||||||
|
VendorEquipmentViewSet,
|
||||||
|
VendorStorefrontView,
|
||||||
|
)
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register("vendor/items", VendorEquipmentViewSet, basename="vendor-equipment-item")
|
router.register("vendor/items", VendorEquipmentViewSet, basename="vendor-equipment-item")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include(router.urls)),
|
path("", include(router.urls)),
|
||||||
|
path("categories/", PublicEquipmentCategoryListView.as_view(), name="equipment_public_categories"),
|
||||||
path("items/", PublicEquipmentListView.as_view(), name="equipment_public_list"),
|
path("items/", PublicEquipmentListView.as_view(), name="equipment_public_list"),
|
||||||
path("items/<str:public_id>/", PublicEquipmentDetailView.as_view(), name="equipment_public_detail"),
|
path("items/<str:public_id>/", PublicEquipmentDetailView.as_view(), name="equipment_public_detail"),
|
||||||
path("storefront/<slug:slug>/", VendorStorefrontView.as_view(), name="vendor_storefront"),
|
path("storefront/<slug:slug>/", VendorStorefrontView.as_view(), name="vendor_storefront"),
|
||||||
|
|||||||
@@ -8,8 +8,16 @@ from rest_framework.views import APIView
|
|||||||
from booking.models import Booking
|
from booking.models import Booking
|
||||||
from marketing.mixins import EquipmentListingClickTrackingMixin
|
from marketing.mixins import EquipmentListingClickTrackingMixin
|
||||||
|
|
||||||
from .models import EquipmentItem
|
from .models import EquipmentCategory, EquipmentItem
|
||||||
from .serializers import EquipmentItemSerializer
|
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):
|
class PublicEquipmentListView(generics.ListAPIView):
|
||||||
|
|||||||
Reference in New Issue
Block a user