inital commit
This commit is contained in:
204
marketing/views.py
Normal file
204
marketing/views.py
Normal file
@@ -0,0 +1,204 @@
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from rest_framework import generics, permissions, status
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from booking.models import Booking
|
||||
|
||||
from .models import ListingClick
|
||||
from .serializers import ListingClickSerializer, TrackListingClickSerializer
|
||||
from .services import record_adventure_listing_click, record_equipment_listing_click, record_listing_click_from_payload
|
||||
|
||||
|
||||
def _parse_range(request):
|
||||
from_raw = request.query_params.get("from")
|
||||
to_raw = request.query_params.get("to")
|
||||
if not from_raw or not to_raw:
|
||||
return None, None, Response(
|
||||
{"detail": "Query params 'from' and 'to' are required (ISO-8601 datetimes, UTC)."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
start = parse_datetime(from_raw)
|
||||
end = parse_datetime(to_raw)
|
||||
if not start or not end:
|
||||
return None, None, Response({"detail": "Invalid 'from' or 'to' datetime."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if timezone.is_naive(start):
|
||||
start = timezone.make_aware(start, timezone.utc)
|
||||
if timezone.is_naive(end):
|
||||
end = timezone.make_aware(end, timezone.utc)
|
||||
if end <= start:
|
||||
return None, None, Response({"detail": "'to' must be after 'from'."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return start, end, None
|
||||
|
||||
|
||||
class TrackListingClickView(APIView):
|
||||
"""Explicit click-through when the client does not refetch the public detail endpoint."""
|
||||
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
def post(self, request):
|
||||
ser = TrackListingClickSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
listing_type = ser.validated_data["listing_type"]
|
||||
user = request.user if request.user.is_authenticated else None
|
||||
if listing_type == ListingClick.ListingType.EQUIPMENT:
|
||||
item = ser.validated_data["_equipment_item"]
|
||||
click = record_listing_click_from_payload(
|
||||
listing_type=listing_type,
|
||||
equipment_item=item,
|
||||
utm_source=ser.validated_data.get("utm_source", ""),
|
||||
utm_medium=ser.validated_data.get("utm_medium", ""),
|
||||
utm_campaign=ser.validated_data.get("utm_campaign", ""),
|
||||
utm_term=ser.validated_data.get("utm_term", ""),
|
||||
utm_content=ser.validated_data.get("utm_content", ""),
|
||||
gclid=ser.validated_data.get("gclid", ""),
|
||||
fbclid=ser.validated_data.get("fbclid", ""),
|
||||
referrer=ser.validated_data.get("referrer", ""),
|
||||
user=user,
|
||||
)
|
||||
else:
|
||||
offering = ser.validated_data["_adventure_offering"]
|
||||
click = record_listing_click_from_payload(
|
||||
listing_type=listing_type,
|
||||
adventure_offering=offering,
|
||||
utm_source=ser.validated_data.get("utm_source", ""),
|
||||
utm_medium=ser.validated_data.get("utm_medium", ""),
|
||||
utm_campaign=ser.validated_data.get("utm_campaign", ""),
|
||||
utm_term=ser.validated_data.get("utm_term", ""),
|
||||
utm_content=ser.validated_data.get("utm_content", ""),
|
||||
gclid=ser.validated_data.get("gclid", ""),
|
||||
fbclid=ser.validated_data.get("fbclid", ""),
|
||||
referrer=ser.validated_data.get("referrer", ""),
|
||||
user=user,
|
||||
)
|
||||
return Response({"marketing_click_id": click.id, "traffic_type": click.traffic_type}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class VendorListingClickListView(generics.ListAPIView):
|
||||
serializer_class = ListingClickSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if not user.is_vendor or not hasattr(user, "vendor_profile"):
|
||||
raise PermissionDenied("Only vendors can list listing clicks.")
|
||||
vp = user.vendor_profile
|
||||
qs = ListingClick.objects.filter(vendor=vp).select_related("equipment_item", "adventure_offering")
|
||||
start, end, err = _parse_range(self.request)
|
||||
if err:
|
||||
return ListingClick.objects.none()
|
||||
qs = qs.filter(created_at__gte=start, created_at__lt=end)
|
||||
tt = self.request.query_params.get("traffic_type")
|
||||
if tt in (ListingClick.TrafficType.ORGANIC, ListingClick.TrafficType.MARKETING):
|
||||
qs = qs.filter(traffic_type=tt)
|
||||
lt = self.request.query_params.get("listing_type")
|
||||
if lt in (ListingClick.ListingType.EQUIPMENT, ListingClick.ListingType.ADVENTURE):
|
||||
qs = qs.filter(listing_type=lt)
|
||||
campaign = self.request.query_params.get("utm_campaign")
|
||||
if campaign:
|
||||
qs = qs.filter(utm_campaign=campaign)
|
||||
return qs
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
start, end, err = _parse_range(request)
|
||||
if err:
|
||||
return err
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
class VendorMarketingSummaryView(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
if not user.is_vendor or not hasattr(user, "vendor_profile"):
|
||||
raise PermissionDenied("Only vendors can view marketing summary.")
|
||||
start, end, err = _parse_range(request)
|
||||
if err:
|
||||
return err
|
||||
vp = user.vendor_profile
|
||||
|
||||
clicks_qs = ListingClick.objects.filter(vendor=vp, created_at__gte=start, created_at__lt=end)
|
||||
click_counts = clicks_qs.values("traffic_type").annotate(c=Count("id"))
|
||||
clicks_by_traffic = {row["traffic_type"]: row["c"] for row in click_counts}
|
||||
organic_clicks = clicks_by_traffic.get(ListingClick.TrafficType.ORGANIC, 0)
|
||||
marketing_clicks = clicks_by_traffic.get(ListingClick.TrafficType.MARKETING, 0)
|
||||
|
||||
bookings_qs = Booking.objects.filter(vendor=vp, created_at__gte=start, created_at__lt=end)
|
||||
attributed = bookings_qs.filter(listing_click__isnull=False)
|
||||
unattributed_bookings = bookings_qs.filter(listing_click__isnull=True).count()
|
||||
|
||||
organic_bookings = attributed.filter(listing_click__traffic_type=ListingClick.TrafficType.ORGANIC).count()
|
||||
marketing_bookings = attributed.filter(listing_click__traffic_type=ListingClick.TrafficType.MARKETING).count()
|
||||
|
||||
def rate(bookings: int, clicks: int) -> float | None:
|
||||
if clicks == 0:
|
||||
return None
|
||||
return round(bookings / clicks, 6)
|
||||
|
||||
campaign_rows = (
|
||||
clicks_qs.exclude(utm_campaign="")
|
||||
.values("utm_source", "utm_medium", "utm_campaign")
|
||||
.annotate(clicks=Count("id"))
|
||||
.order_by("-clicks")[:50]
|
||||
)
|
||||
campaign_bookings = (
|
||||
attributed.filter(listing_click__utm_campaign__gt="")
|
||||
.values(
|
||||
"listing_click__utm_source",
|
||||
"listing_click__utm_medium",
|
||||
"listing_click__utm_campaign",
|
||||
)
|
||||
.annotate(bookings=Count("id"))
|
||||
)
|
||||
booking_key_counts = {
|
||||
(
|
||||
row["listing_click__utm_source"] or "",
|
||||
row["listing_click__utm_medium"] or "",
|
||||
row["listing_click__utm_campaign"] or "",
|
||||
): row["bookings"]
|
||||
for row in campaign_bookings
|
||||
}
|
||||
|
||||
campaigns = []
|
||||
for row in campaign_rows:
|
||||
key = (row["utm_source"] or "", row["utm_medium"] or "", row["utm_campaign"] or "")
|
||||
c_clicks = row["clicks"]
|
||||
c_bookings = booking_key_counts.get(key, 0)
|
||||
campaigns.append(
|
||||
{
|
||||
"utm_source": row["utm_source"],
|
||||
"utm_medium": row["utm_medium"],
|
||||
"utm_campaign": row["utm_campaign"],
|
||||
"clicks": c_clicks,
|
||||
"bookings": c_bookings,
|
||||
"conversion_rate": rate(c_bookings, c_clicks),
|
||||
}
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"from": start,
|
||||
"to": end,
|
||||
"clicks": {
|
||||
"organic": organic_clicks,
|
||||
"marketing": marketing_clicks,
|
||||
"total": organic_clicks + marketing_clicks,
|
||||
},
|
||||
"bookings_attributed": {
|
||||
"organic": organic_bookings,
|
||||
"marketing": marketing_bookings,
|
||||
"total_attributed": organic_bookings + marketing_bookings,
|
||||
"unattributed": unattributed_bookings,
|
||||
"total_all": organic_bookings + marketing_bookings + unattributed_bookings,
|
||||
},
|
||||
"conversion_rate_click_to_booking": {
|
||||
"organic": rate(organic_bookings, organic_clicks),
|
||||
"marketing": rate(marketing_bookings, marketing_clicks),
|
||||
},
|
||||
"campaigns": campaigns,
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user