From 3315c41846b0fae750b035f32fbddeb48b9ed818 Mon Sep 17 00:00:00 2001 From: Ke-vin-S Date: Sat, 17 May 2025 15:35:57 +0530 Subject: [PATCH 1/3] Settings fix --- .idea/misc.xml | 2 +- entrypoint.sh | 2 +- logistics_core/settings.py | 5 +++++ requirements.txt | 6 +++++- 4 files changed, 12 insertions(+), 3 deletions(-) mode change 100644 => 100755 entrypoint.sh diff --git a/.idea/misc.xml b/.idea/misc.xml index 18690fd..846dc6d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh old mode 100644 new mode 100755 index e30b760..1b1af3c --- a/entrypoint.sh +++ b/entrypoint.sh @@ -7,4 +7,4 @@ echo "Running migrate..." python manage.py migrate --noinput echo "Starting Django server on port ${DJANGO_PORT}..." -python manage.py runserver 0.0.0.0:${DJANGO_PORT} +python manage.py runserver 0.0.0.0:${DJANGO_PORT} || python manage.py runserver 8001 diff --git a/logistics_core/settings.py b/logistics_core/settings.py index bedafec..9ca5b9b 100644 --- a/logistics_core/settings.py +++ b/logistics_core/settings.py @@ -12,7 +12,11 @@ from pathlib import Path import os +from dotenv import load_dotenv +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -26,6 +30,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False +print(os.getenv('ALLOWED_HOSTS')) ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',') diff --git a/requirements.txt b/requirements.txt index 91625e4..4870216 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ absl-py==2.2.2 asgiref==3.8.1 confluent-kafka==2.10.0 -Django==5.2 +Django~=5.2.1 django-cors-headers==4.7.0 django-filter==25.1 djangorestframework==3.16.0 @@ -24,3 +24,7 @@ six==1.17.0 sqlparse==0.5.3 tzdata==2025.2 uritemplate==4.1.1 + +kafka~=1.3.5 +requests~=2.32.3 +django-environ~=0.12.0 \ No newline at end of file From 7bd714f5702e0c6e59d1b1b6d3a3d30283ec7270 Mon Sep 17 00:00:00 2001 From: Ke-vin-S Date: Sat, 17 May 2025 15:36:34 +0530 Subject: [PATCH 2/3] All vehicles endpoint and location history endpoint --- fleet/admin.py | 6 +- .../0006_remove_vehicle_name_vehicle_model.py | 22 +++ fleet/models/core.py | 2 +- fleet/serializers/__init__.py | 31 +++-- fleet/serializers/vehicle.py | 65 ++++++++- fleet/tests/test_vehicle.py | 4 +- fleet/tests/test_vehicle_api.py | 125 ++++++++++++------ fleet/views/fuel.py | 2 +- fleet/views/trip.py | 2 +- fleet/views/vehicle.py | 42 ++++-- 10 files changed, 224 insertions(+), 77 deletions(-) create mode 100644 fleet/migrations/0006_remove_vehicle_name_vehicle_model.py diff --git a/fleet/admin.py b/fleet/admin.py index 8af2ac4..6adb333 100644 --- a/fleet/admin.py +++ b/fleet/admin.py @@ -5,17 +5,17 @@ @admin.register(Vehicle) class VehicleAdmin(admin.ModelAdmin): list_display = ( - 'vehicle_id', 'name', 'capacity', 'status', 'fuel_type', + 'vehicle_id', 'model', 'capacity', 'status', 'fuel_type', 'depot_id', 'depot_latitude', 'depot_longitude', 'last_location_update' ) list_filter = ('status', 'fuel_type') - search_fields = ('vehicle_id', 'name', 'plate_number', 'depot_id') + search_fields = ('vehicle_id', 'model', 'plate_number', 'depot_id') readonly_fields = ('created_at', 'updated_at', 'last_location_update') fieldsets = ( ('Basic Information', { 'fields': ( - 'vehicle_id', 'name', 'plate_number', + 'vehicle_id', 'model', 'plate_number', 'year_of_manufacture', 'status' ) }), diff --git a/fleet/migrations/0006_remove_vehicle_name_vehicle_model.py b/fleet/migrations/0006_remove_vehicle_name_vehicle_model.py new file mode 100644 index 0000000..5a2d5e0 --- /dev/null +++ b/fleet/migrations/0006_remove_vehicle_name_vehicle_model.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.1 on 2025-05-17 08:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fleet', '0005_vehicle_depot_latitude_vehicle_depot_longitude'), + ] + + operations = [ + migrations.RemoveField( + model_name='vehicle', + name='name', + ), + migrations.AddField( + model_name='vehicle', + name='model', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/fleet/models/core.py b/fleet/models/core.py index 0b7675b..4bc14d4 100644 --- a/fleet/models/core.py +++ b/fleet/models/core.py @@ -21,7 +21,7 @@ class Vehicle(models.Model): ] vehicle_id = models.CharField(max_length=20, unique=True) - name = models.CharField(max_length=100, blank=True) + model = models.CharField(max_length=100, blank=True) capacity = models.PositiveIntegerField(help_text="Capacity in kilograms") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='available') fuel_type = models.CharField(max_length=20, choices=FUEL_TYPE_CHOICES, default='diesel') diff --git a/fleet/serializers/__init__.py b/fleet/serializers/__init__.py index 7d545dc..c89c8c2 100644 --- a/fleet/serializers/__init__.py +++ b/fleet/serializers/__init__.py @@ -1,7 +1,16 @@ -from .vehicle import VehicleSerializer, VehicleLocationSerializer +from .vehicle import ( + VehicleSerializer, + VehicleSummarySerializer, + VehicleLocationDetailSerializer, + LocationPointSerializer +) from django.conf import settings +# Always import base detail serializer +from .vehicle import VehicleDetailSerializer as BaseVehicleDetailSerializer + +# If extended mode is enabled, override VehicleDetailSerializer if settings.ENABLE_FLEET_EXTENDED_MODELS: from .maintenance import ( MaintenanceRecordSerializer, @@ -9,18 +18,16 @@ ) from .fuel import FuelRecordSerializer from .trip import TripRecordSerializer - from rest_framework import serializers - class VehicleDetailSerializer(VehicleSerializer): + class VehicleDetailSerializer(BaseVehicleDetailSerializer): maintenance_records = MaintenanceRecordSerializer(many=True, read_only=True) fuel_records = serializers.SerializerMethodField() trip_records = serializers.SerializerMethodField() - location_history = serializers.SerializerMethodField() - class Meta(VehicleSerializer.Meta): - fields = VehicleSerializer.Meta.fields + [ - 'maintenance_records', 'fuel_records', 'trip_records', 'location_history' + class Meta(BaseVehicleDetailSerializer.Meta): + fields = BaseVehicleDetailSerializer.Meta.fields + [ + 'maintenance_records', 'fuel_records', 'trip_records' ] def get_fuel_records(self, obj): @@ -30,13 +37,5 @@ def get_fuel_records(self, obj): def get_trip_records(self, obj): records = obj.trip_records.all()[:5] return TripRecordSerializer(records, many=True).data - - def get_location_history(self, obj): - records = obj.location_history.all()[:10] - return VehicleLocationSerializer(records, many=True).data - else: - class VehicleDetailSerializer(VehicleSerializer): - """Fallback when extended models are disabled.""" - class Meta(VehicleSerializer.Meta): - fields = VehicleSerializer.Meta.fields + VehicleDetailSerializer = BaseVehicleDetailSerializer diff --git a/fleet/serializers/vehicle.py b/fleet/serializers/vehicle.py index 292fdad..d396a1d 100644 --- a/fleet/serializers/vehicle.py +++ b/fleet/serializers/vehicle.py @@ -11,7 +11,7 @@ class Meta: fields = [ 'id', 'vehicle_id', - 'name', + 'model', 'capacity', 'status', 'fuel_type', @@ -33,7 +33,64 @@ class Meta: read_only_fields = ['created_at', 'updated_at', 'last_location_update'] -class VehicleLocationSerializer(serializers.ModelSerializer): +class LocationPointSerializer(serializers.Serializer): + latitude = serializers.DecimalField(max_digits=9, decimal_places=6) + longitude = serializers.DecimalField(max_digits=9, decimal_places=6) + location_name = serializers.CharField() + timestamp = serializers.DateTimeField() + + +class VehicleLocationDetailSerializer(serializers.Serializer): + plate_number = serializers.CharField() + truck_id = serializers.CharField(source='vehicle_id') + model = serializers.CharField() + status = serializers.SerializerMethodField() + + def get_status(self, obj): + history_qs = VehicleLocation.objects.filter(vehicle=obj).order_by('-timestamp') + current_location = None + location_history = [] + + for i, loc in enumerate(history_qs): + loc_data = { + 'latitude': loc.latitude, + 'longitude': loc.longitude, + 'timestamp': loc.timestamp, + 'location_name': self.get_mock_location_name(loc.latitude, loc.longitude) + } + + if i == 0: + current_location = loc_data + else: + location_history.append(loc_data) + + return { + "current_location": current_location, + "location_history": location_history + } + + def get_mock_location_name(self, lat, lon): + # This should be replaced with a geocoding service if needed + if lat > 6.926: + return "Colombo Fort" + elif lat > 6.923: + return "Slave Island" + elif lat > 6.921: + return "Kollupitiya" + else: + return "Bambalapitiya" + +# 🔹 Base serializer for all cases +class VehicleDetailSerializer(VehicleSerializer): + location_detail = serializers.SerializerMethodField() + + class Meta(VehicleSerializer.Meta): + fields = VehicleSerializer.Meta.fields + ['location_detail'] + + def get_location_detail(self, obj): + return VehicleLocationDetailSerializer(obj).data + +class VehicleSummarySerializer(serializers.ModelSerializer): class Meta: - model = VehicleLocation - fields = ['timestamp', 'latitude', 'longitude', 'speed', 'heading'] + model = Vehicle + fields = ['vehicle_id', 'plate_number', 'model', 'status'] diff --git a/fleet/tests/test_vehicle.py b/fleet/tests/test_vehicle.py index 932d803..6d0d0b7 100644 --- a/fleet/tests/test_vehicle.py +++ b/fleet/tests/test_vehicle.py @@ -9,7 +9,7 @@ class VehicleModelTest(TestCase): def setUp(self): self.vehicle = Vehicle.objects.create( vehicle_id="TRK001", - name="Test Truck 1", + model="Test Truck 1", capacity=1000, status="available", fuel_type="diesel", @@ -22,7 +22,7 @@ def test_vehicle_fields_and_defaults(self): """Test vehicle creation and default values.""" v = self.vehicle self.assertEqual(v.vehicle_id, "TRK001") - self.assertEqual(v.name, "Test Truck 1") + self.assertEqual(v.model, "Test Truck 1") self.assertEqual(v.capacity, 1000) self.assertEqual(v.status, "available") self.assertEqual(v.fuel_type, "diesel") diff --git a/fleet/tests/test_vehicle_api.py b/fleet/tests/test_vehicle_api.py index 502acb4..a0cba59 100644 --- a/fleet/tests/test_vehicle_api.py +++ b/fleet/tests/test_vehicle_api.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from rest_framework.test import APIClient from django.test import TestCase from rest_framework import status @@ -10,44 +12,46 @@ class VehicleAPITest(TestCase): def setUp(self): self.client = APIClient() self.vehicle1 = Vehicle.objects.create( - vehicle_id="TRK001", name="Truck 1", capacity=1000, + vehicle_id="TRK001", model="Truck 1", capacity=1000, status="available", fuel_type="diesel" ) self.vehicle2 = Vehicle.objects.create( - vehicle_id="TRK002", name="Truck 2", capacity=500, + vehicle_id="TRK002", model="Truck 2", capacity=500, status="maintenance", fuel_type="petrol" ) self.vehicle3 = Vehicle.objects.create( - vehicle_id="TRK003", name="Truck 3", capacity=750, + vehicle_id="TRK003", model="Truck 3", capacity=750, status="assigned", fuel_type="diesel" ) - def test_get_all_vehicles(self): - """GET /api/fleet/vehicles/ should return all vehicles.""" + def test_list_summary_fields(self): response = self.client.get('/api/fleet/vehicles/') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 3) + self.assertIn("vehicles", response.data) + self.assertEqual(len(response.data["vehicles"]), 3) + for item in response.data["vehicles"]: + self.assertIn("vehicle_id", item) + self.assertIn("model", item) + self.assertIn("plate_number", item) + self.assertIn("status", item) + self.assertNotIn("capacity", item) - def test_filter_vehicles_by_status(self): - """GET /api/fleet/vehicles/?status=available should return only available vehicles.""" - response = self.client.get('/api/fleet/vehicles/', {'status': 'available'}) + def test_filter_by_status(self): + response = self.client.get('/api/fleet/vehicles/', {'status': 'assigned'}) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['vehicle_id'], "TRK001") + self.assertEqual(len(response.data["vehicles"]), 1) + self.assertEqual(response.data["vehicles"][0]['vehicle_id'], "TRK003") - def test_filter_vehicles_by_min_capacity(self): - """GET /api/fleet/vehicles/?min_capacity=800 should return vehicles with capacity >= 800.""" + def test_filter_by_min_capacity(self): response = self.client.get('/api/fleet/vehicles/', {'min_capacity': 800}) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['vehicle_id'], "TRK001") + self.assertEqual(len(response.data["vehicles"]), 1) + self.assertEqual(response.data["vehicles"][0]['vehicle_id'], "TRK001") - def test_filter_vehicles_by_fuel_type(self): - """GET /api/fleet/vehicles/?fuel_type=diesel should return vehicles with diesel fuel.""" + def test_filter_by_fuel_type(self): response = self.client.get('/api/fleet/vehicles/', {'fuel_type': 'diesel'}) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 2) - vehicle_ids = [v['vehicle_id'] for v in response.data] + vehicle_ids = [v['vehicle_id'] for v in response.data["vehicles"]] self.assertIn("TRK001", vehicle_ids) self.assertIn("TRK003", vehicle_ids) @@ -55,7 +59,7 @@ def test_create_vehicle_successfully(self): """POST /api/fleet/vehicles/ should create a new vehicle.""" payload = { "vehicle_id": "TRK004", - "name": "Truck 4", + "model": "Truck 4", "capacity": 1200, "status": "available", "fuel_type": "electric", @@ -69,7 +73,7 @@ def test_create_vehicle_successfully(self): def test_patch_update_vehicle_status(self): """PATCH /api/fleet/vehicles/{id}/ should update vehicle status.""" response = self.client.patch( - f'/api/fleet/vehicles/{self.vehicle1.id}/', + f'/api/fleet/vehicles/{self.vehicle1.vehicle_id}/', {"status": "maintenance"}, format='json' ) @@ -84,7 +88,7 @@ def test_update_vehicle_location(self): "speed": 65.5 } response = self.client.post( - f'/api/fleet/vehicles/{self.vehicle1.id}/update_location/', + f'/api/fleet/vehicles/{self.vehicle1.vehicle_id}/update_location/', payload, format='json' ) @@ -99,37 +103,32 @@ def test_update_vehicle_location(self): self.assertAlmostEqual(float(history[0].speed), 65.5) self.assertAlmostEqual(float(history[0].latitude), 42.123456) - def test_list_all_vehicles(self): - response = self.client.get("/api/fleet/vehicles/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 3) - - def test_filter_by_status(self): - response = self.client.get("/api/fleet/vehicles/?status=assigned") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['vehicle_id'], "TRK003") + def test_invalid_status_filter(self): + response = self.client.get('/api/fleet/vehicles/', {'status': 'nonexistent'}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_ordering_by_updated_at(self): + """Test that vehicles can be ordered by updated_at even if it's not returned.""" response = self.client.get("/api/fleet/vehicles/?ordering=-updated_at") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data) >= 1) - self.assertIn('updated_at', response.data[0]) + self.assertIn("vehicles", response.data) + self.assertTrue(len(response.data["vehicles"]) >= 1) + self.assertIn("vehicle_id", response.data["vehicles"][0]) # Confirm summary structure def test_mark_vehicle_available(self): - response = self.client.post(f"/api/fleet/vehicles/{self.vehicle3.id}/mark_available/") + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle3.vehicle_id}/mark_available/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.vehicle3.refresh_from_db() self.assertEqual(self.vehicle3.status, 'available') def test_mark_vehicle_assigned(self): - response = self.client.post(f"/api/fleet/vehicles/{self.vehicle1.id}/mark_assigned/") + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle1.vehicle_id}/mark_assigned/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.vehicle1.refresh_from_db() self.assertEqual(self.vehicle1.status, 'assigned') def test_change_status_to_available(self): - response = self.client.post(f"/api/fleet/vehicles/{self.vehicle2.id}/change_status/", { + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle2.vehicle_id}/change_status/", { "status": "available" }, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -137,7 +136,57 @@ def test_change_status_to_available(self): self.assertEqual(self.vehicle2.status, "available") def test_change_status_invalid(self): - response = self.client.post(f"/api/fleet/vehicles/{self.vehicle1.id}/change_status/", { + response = self.client.post(f"/api/fleet/vehicles/{self.vehicle1.vehicle_id}/change_status/", { "status": "nonexistent" }, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_location_overview_success(self): + # Create location history for vehicle1 + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9271, longitude=79.8612) + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9250, longitude=79.8600) + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9225, longitude=79.8590) + + response = self.client.get(f"/api/fleet/vehicles/{self.vehicle1.vehicle_id}/location_overview/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.data + self.assertEqual(data["truck_id"], "TRK001") + self.assertIn("status", data) + self.assertIn("current_location", data["status"]) + self.assertIn("location_history", data["status"]) + + self.assertEqual(len(data["status"]["location_history"]), 2) + self.assertEqual(data["status"]["current_location"]["latitude"], Decimal('6.922500')) + + def test_location_overview_empty_history(self): + response = self.client.get(f"/api/fleet/vehicles/{self.vehicle2.vehicle_id}/location_overview/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertIsNone(response.data["status"]["current_location"]) + self.assertEqual(response.data["status"]["location_history"], []) + + def test_location_overview_history_ordering(self): + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9200, longitude=79.8580) + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9250, longitude=79.8600) + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9271, longitude=79.8612) + + response = self.client.get(f"/api/fleet/vehicles/{self.vehicle1.vehicle_id}/location_overview/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + locs = response.data["status"]["location_history"] + self.assertGreaterEqual(locs[0]["timestamp"], locs[-1]["timestamp"]) + + def test_location_overview_has_mock_location_names(self): + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9271, longitude=79.8612) + VehicleLocation.objects.create(vehicle=self.vehicle1, latitude=6.9225, longitude=79.8590) + + response = self.client.get(f"/api/fleet/vehicles/{self.vehicle1.vehicle_id}/location_overview/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + current = response.data["status"]["current_location"] + history = response.data["status"]["location_history"] + + self.assertIn("location_name", current) + self.assertIn("location_name", history[0]) + self.assertTrue(current["location_name"]) # mock name string diff --git a/fleet/views/fuel.py b/fleet/views/fuel.py index 05e18b8..48a354d 100644 --- a/fleet/views/fuel.py +++ b/fleet/views/fuel.py @@ -73,7 +73,7 @@ def consumption_stats(self, request): vehicle_stats.append({ 'vehicle_id': vehicle.vehicle_id, - 'name': vehicle.name, + 'name': vehicle.model, 'records_count': vehicle_totals['count'], 'total_cost': vehicle_totals['total_cost'], 'total_amount': vehicle_totals['total_amount'] diff --git a/fleet/views/trip.py b/fleet/views/trip.py index f38e174..e23c549 100644 --- a/fleet/views/trip.py +++ b/fleet/views/trip.py @@ -163,7 +163,7 @@ def stats(self, request): vehicle_stats.append({ 'vehicle_id': vehicle.vehicle_id, - 'name': vehicle.name, + 'name': vehicle.model, 'trip_count': v_trip_count, 'total_distance': v_total_distance, 'total_duration': v_total_duration, diff --git a/fleet/views/vehicle.py b/fleet/views/vehicle.py index 8fb24b0..15b16cf 100644 --- a/fleet/views/vehicle.py +++ b/fleet/views/vehicle.py @@ -1,18 +1,18 @@ import os import django +from fleet.serializers.vehicle import VehicleSummarySerializer, \ + VehicleLocationDetailSerializer from fleet.services.status_services import mark_vehicle_assigned, mark_vehicle_available, update_vehicle_status os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'logistics_core.settings') django.setup() from django.db.models import Sum, Count -from django.utils import timezone -from rest_framework import viewsets, status, filters +from rest_framework import viewsets, filters from rest_framework.decorators import action from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend -from datetime import datetime from fleet.models import Vehicle, VehicleLocation from django.conf import settings @@ -29,12 +29,23 @@ class VehicleViewSet(viewsets.ModelViewSet): """ queryset = Vehicle.objects.all() serializer_class = VehicleSerializer + lookup_field = 'vehicle_id' + lookup_url_kwarg = 'vehicle_id' filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ['status', 'fuel_type', 'depot_id'] search_fields = ['vehicle_id', 'name', 'plate_number', 'depot_id'] ordering_fields = ['vehicle_id', 'capacity', 'status', 'created_at'] ordering = ['vehicle_id'] + def list(self, request, *args, **kwargs): + """ + Override default list to return limited truck data only. + """ + queryset = self.filter_queryset(self.get_queryset()) + + serializer = VehicleSummarySerializer(queryset, many=True) + return Response({'vehicles': serializer.data}) + def get_serializer_class(self): if self.action == 'retrieve': return VehicleDetailSerializer @@ -64,20 +75,20 @@ def get_queryset(self): return queryset @action(detail=True, methods=['post']) - def mark_available(self, request, pk=None): + def mark_available(self, request, vehicle_id=None): vehicle = self.get_object() mark_vehicle_available(vehicle) return Response({'vehicle_id': vehicle.vehicle_id, 'status': 'available'}) @action(detail=True, methods=['post']) - def mark_assigned(self, request, pk=None): + def mark_assigned(self, request, vehicle_id=None): vehicle = self.get_object() mark_vehicle_assigned(vehicle) return Response({'vehicle_id': vehicle.vehicle_id, 'status': 'assigned'}) # Admin only @action(detail=True, methods=['post']) - def change_status(self, request, pk=None): + def change_status(self, request, vehicle_id=None): vehicle = self.get_object() new_status = request.data.get('status') @@ -88,8 +99,9 @@ def change_status(self, request, pk=None): update_vehicle_status(vehicle, new_status) return Response({'vehicle_id': vehicle.vehicle_id, 'status': new_status}) + @action(detail=True, methods=['post']) - def update_location(self, request, pk=None): + def update_location(self, request, vehicle_id=None): vehicle = self.get_object() latitude = request.data.get('latitude') longitude = request.data.get('longitude') @@ -113,10 +125,10 @@ def update_location(self, request, pk=None): return Response({'error': str(e)}, status=400) @action(detail=True, methods=['post']) - def assign_depot(self, request, pk=None): - """ + def assign_depot(self, request, vehicle_id=None): + f""" Assign or update a vehicle's depot. - POST /api/fleet/vehicles/{id}/assign_depot/ + POST /api/fleet/vehicles/{vehicle_id}/assign_depot/ { "depot_id": "WHS001", "latitude": 6.9271, @@ -171,7 +183,6 @@ def stats(self, request): 'maintenance_count': maintenance_count, 'utilization_rate': utilization_rate }) - @action(detail=False, methods=['get']) def by_depot(self, request): depot_id = request.query_params.get('depot_id') @@ -193,6 +204,15 @@ def depot_stats(self, request): return Response({'by_depot': stats}) + @action(detail=True, methods=['get'], url_path='location_overview') + def location_overview(self, request, vehicle_id=None): + """ + GET /api/fleet/vehicles//location_overview/ + """ + vehicle = self.get_object() + serializer = VehicleLocationDetailSerializer(vehicle) + return Response(serializer.data) + # # To be implemented with maintenance part # @action(detail=True, methods=['post']) # def change_status(self, request, pk=None): From fce8739a19ad74e3da8c03d2627c946a95501234 Mon Sep 17 00:00:00 2001 From: Ke-vin-S Date: Sat, 17 May 2025 16:03:48 +0530 Subject: [PATCH 3/3] Add driver assigned field to vehicle so can filter out which vehicles are not assigned --- .../0007_vehicle_driver_assigned.py | 18 ++++++++++++++ fleet/models/core.py | 3 +++ fleet/serializers/vehicle.py | 3 ++- fleet/tests/test_vehicle_api.py | 24 +++++++++++++++++++ fleet/views/vehicle.py | 2 +- 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 fleet/migrations/0007_vehicle_driver_assigned.py diff --git a/fleet/migrations/0007_vehicle_driver_assigned.py b/fleet/migrations/0007_vehicle_driver_assigned.py new file mode 100644 index 0000000..86a64ef --- /dev/null +++ b/fleet/migrations/0007_vehicle_driver_assigned.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.1 on 2025-05-17 10:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fleet', '0006_remove_vehicle_name_vehicle_model'), + ] + + operations = [ + migrations.AddField( + model_name='vehicle', + name='driver_assigned', + field=models.BooleanField(default=False, help_text='Indicates if a driver is assigned to this vehicle'), + ), + ] diff --git a/fleet/models/core.py b/fleet/models/core.py index 4bc14d4..f53c4b2 100644 --- a/fleet/models/core.py +++ b/fleet/models/core.py @@ -55,6 +55,9 @@ class Vehicle(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + # In Vehicle model + driver_assigned = models.BooleanField(default=False, help_text="Indicates if a driver is assigned to this vehicle") + def __str__(self): return f"{self.vehicle_id} ({self.status})" diff --git a/fleet/serializers/vehicle.py b/fleet/serializers/vehicle.py index d396a1d..3d84105 100644 --- a/fleet/serializers/vehicle.py +++ b/fleet/serializers/vehicle.py @@ -28,7 +28,8 @@ class Meta: 'created_at', 'updated_at', 'is_available', - 'location_is_stale' + 'location_is_stale', + 'driver_assigned' ] read_only_fields = ['created_at', 'updated_at', 'last_location_update'] diff --git a/fleet/tests/test_vehicle_api.py b/fleet/tests/test_vehicle_api.py index a0cba59..8972b49 100644 --- a/fleet/tests/test_vehicle_api.py +++ b/fleet/tests/test_vehicle_api.py @@ -190,3 +190,27 @@ def test_location_overview_has_mock_location_names(self): self.assertIn("location_name", current) self.assertIn("location_name", history[0]) self.assertTrue(current["location_name"]) # mock name string + + def test_filter_by_driver_assigned_true(self): + self.vehicle1.driver_assigned = True + self.vehicle1.save() + + response = self.client.get('/api/fleet/vehicles/?driver_assigned=true') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["vehicles"]), 1) + self.assertEqual(response.data["vehicles"][0]["vehicle_id"], "TRK001") + + def test_filter_by_driver_assigned_false(self): + self.vehicle1.driver_assigned = True + self.vehicle1.save() + self.vehicle2.driver_assigned = False + self.vehicle2.save() + self.vehicle3.driver_assigned = False + self.vehicle3.save() + + response = self.client.get('/api/fleet/vehicles/?driver_assigned=false') + self.assertEqual(response.status_code, status.HTTP_200_OK) + vehicle_ids = [v["vehicle_id"] for v in response.data["vehicles"]] + self.assertIn("TRK002", vehicle_ids) + self.assertIn("TRK003", vehicle_ids) + self.assertNotIn("TRK001", vehicle_ids) diff --git a/fleet/views/vehicle.py b/fleet/views/vehicle.py index 15b16cf..c6e9523 100644 --- a/fleet/views/vehicle.py +++ b/fleet/views/vehicle.py @@ -32,7 +32,7 @@ class VehicleViewSet(viewsets.ModelViewSet): lookup_field = 'vehicle_id' lookup_url_kwarg = 'vehicle_id' filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] - filterset_fields = ['status', 'fuel_type', 'depot_id'] + filterset_fields = ['status', 'fuel_type', 'depot_id', 'driver_assigned'] search_fields = ['vehicle_id', 'name', 'plate_number', 'depot_id'] ordering_fields = ['vehicle_id', 'capacity', 'status', 'created_at'] ordering = ['vehicle_id']