In my Django project, I am attempting to create a custom model field that relies on comparing the values of two reverse foreign key timestamps.
In this toy example, I operate some car dealerships. Cars are hired out to folks but need to be 'serviced' every time before they are hired out again. I am trying to add a 'currently_serviced' field into my Car model. A car is considered currently serviced if it has had an associated 'ServiceEvent' after the latest 'HireEvent'.
I have been following the documentation on custom model fields but this doesn't help me navigate the trickiness of the reverse foreign keys and identifying the latest (before current time as I want to ignore future hires) timestamps.
Usage
I would then be able to utilise this field 'car.currently_serviced' in my views, such as by having a simple individual car page, showing the name of the car {{ car.car_name }} as well as it's service status {{ car.currently_serviced }} or in more complex pages such as outputting the '# of serviced cars by dealership' on a dealership page.
Current progress
I am stuck in trying to declare this field within the model, see my efforts in models.py where I am declaring a function 'currently_serviced'. I have tried writing model manager classes and writing the logic into my views with F() comparisons, although would prefer as a DRY approach to declare the 'currently_serviced' attribute on the model itself as it would dynamically update as and when cars are hired and serviced.
The approach shown currently leads to an attribute error:
'RelatedManager' object has no attribute 'service_time'.
Thank you!
models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class Dealer(models.Model):
dealer_name = models.CharField(max_length=200)
def __str__(self):
return self.dealer_name
class Car(models.Model):
car_name = models.CharField(max_length=200)
dealership = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="cars")
# This section should return a Boolean field accessible from the Car model.
def currently_serviced(self):
# This was another failed approach.
# Car.objects.filter(serviceevent__service_time__lte=timezone.now(), serviceevent__service_time__lte= F('hireevent__hire_time')).count()
if self.service_events.service_time.latest() > self.hire_events.hire_end_time.latest() and self.hire_events.hire_end_time < timezone.now():
return True
else:
return False
def __str__(self):
return self.car_name
class HireEvent(models.Model):
hire_id = models.AutoField(primary_key=True)
car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name="hire_events")
user = models.ForeignKey(User, on_delete=models.CASCADE)
hire_end_time = models.DateTimeField(null=True,blank=True)
def __str__(self):
return self.hire_id
class ServiceEvent(models.Model):
event_id = models.AutoField(primary_key=True)
car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name="service_events")
service_time = models.DateTimeField(null=True,blank=True)
def __str__(self):
return self.event_id
views.py
from django.shortcuts import get_object_or_404, render
from .models import Dealer, Car, HireEvent, ServiceEvent
from django.db.models import Count, When, Sum, Case, IntegerField, BooleanField, F, Max, Window
import json
from django.http import JsonResponse
from django.utils import timezone
from datetime import timedelta
def car(request, car_name):
car_name = car_name.replace('-', ' ')
car = Car.get(car_name__iexact=car_name)
# Commented out as this approach does not work and ideally I'd like to not have to work out the currently
# serviced status every time in the views, but rather pull it from the model dynamically.
# dealership = Dealer.objects.annotate(
# currently_serviced = Sum(Case(
# When(
# cars__hire_events__hire_end_time__lte=timezone.now(),
# cars__service_events__service_time__gte = Max(F('hire_events__end_date')),
# then=1),
# output_field=IntegerField(),))
# ).get(cars__car_name__iexact=car_name)
all_hire_events = HireEvent.objects.filter(car = car)
last_hire_event = all_hire_events.filter(hire_end_time__lte=timezone.now()).order_by('hire_end_time').first()
service_events = ServiceEvent.objects.filter(car = car, service_time__lte=timezone.now())
last_service = service_events.filter(car = car, service_time__lte=timezone.now()).order_by('service_time').first()
if last_service is not None:
if last_hire_event.hire_end_time < last_service.service_time:
car.currently_serviced = True
else:
car.currently_serviced = False
else:
car.currently_serviced = False
return render(request, 'dealership_app/car.html', {'car': car, "all_hire_events":all_hire_events, "service_events":service_events})