0

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})
William
  • 171
  • 1
  • 12
  • can you post your view please? – Yellowduck Aug 27 '20 at 18:53
  • Thanks @Yellowduck I've added this now. Whilst I can for the car object work out a derived 'currently_serviced' attribute relatively simply (as seen in the view). I would like to ideally have this available as a field calculated within the model itself to make it easier to use in other use cases. Such as my attempt in the view to output a number of cars 'currently_serviced' at a dealer level. – William Aug 27 '20 at 19:39
  • @Yellowduck Just updated to make it clearer how I intend to use this field : ) – William Aug 28 '20 at 15:34

1 Answers1

0

I worked it out :) thanks to this great answer in another question. Putting in my code in the hope that it helps out someone else.

My code

class Car(models.Model):
car_name = models.CharField(max_length=200)
dealership = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="cars")

@property
def currently_serviced(self):
    last_service = ServiceEvent.objects.filter(car__car_name=self.car_name, service_time__lte=timezone.now()).latest('service_time')
    last_hire = HireEvent.objects.filter(car__car_name=self.desk_name, hire_end_date__lte=timezone.now()).latest('hire_end_time')
    if last_service.service_time < timezone.now() and last_service.service_time > last_hire.hire_end_time:
        return True
    else:
        return False

def __str__(self):
    return self.car_name

Adding in this property into my Car model class, firstly identifies a single instance of a ServiceEvent, taking the latest service time with a filter for the same car via the self object. The same is done for the last hire time.

I then compare the difference in timestamps in an if statement and return True or False depending on the output of the comparison.

William
  • 171
  • 1
  • 12