Automatically changing the value of a model field upon a certain time

Let’s say I have a model like this:

class Event(models.Model):
    PLANNED = 0
    OPEN = 1
    CLOSED = 2

    EVENT_STATES = (
        (PLANNED, "Planned"),
        (OPEN, "Open"),
        (CLOSED, "Closed"),
    )

    begin_timestamp = models.DateTimeField(null=True, blank=True)
    end_timestamp = models.DateTimeField(null=True, blank=True)
    state = models.PositiveIntegerField(choices=EVENT_STATES)
    # more fields

What I’d like to do is for the state field to go from PLANNED to OPEN when the current timestamp is equal to the value of begin_timestamp for a given model instance, as well as for it to go from OPEN to CLOSED upon reaching end_timestamp.

This could actually be achieved easily by making state a property rather than a field, but there’s a catch: the value of state must be able to be modified manually too, so it can’t just be a computed property.

For example, I might want to close an event before its end timestamp.

I really don’t want to have to restort to using celery or any complicated stuff for this, so here’s what I thought:

class Event(models.Model):
    PLANNED = 0
    OPEN = 1
    CLOSED = 2

    EVENT_STATES = (
        (PLANNED, "Planned"),
        (OPEN, "Open"),
        (CLOSED, "Closed"),
    )

    begin_timestamp = models.DateTimeField(null=True, blank=True)
    end_timestamp = models.DateTimeField(null=True, blank=True)
    _state = models.PositiveIntegerField(choices=EVENT_STATES)
    # more fields

   @property
   def state(self):
      if self._state == Event.PLANNED and self.begin_timestamp >= now:
           self._state = Event.OPEN
           self.save()
      
      if self._state == Event.OPEN and self.end_timestamp <= now:
          self._state = Event.CLOSED
          self.save()
    
      return self._state
  
   @state.setter
   def state(self, value):
       self._state = value

This way, the very first time an Event is accessed in any way after its begin timestamp, its state field will get updated and the correct value will be shown. Same thing for accessing an instance after its end timestamp.

I know that, in general, property getters should not have any side effects, but I can’t seem to find any drawbacks that would come with this approach.

Is this okay to do? Is there a better way without giving up on the simplicity? Thank you all.

In the current implementation, state is only going to change if you’re constantly triggering the save somehow. I’m assuming that’s not what you want.

I’m guessing that you want single database column for the potential override “state_override” and a calculated property state. If state_override is null, calculate the value off the time. Otherwise use the override. The business logic of the property should act as the single source of truth.

If you wanted to get extra fancy, you could create your StateField that encapsulated this logic.

Not really? With the getter I defined, state changes when I access the field.

I’m thinking of using this implementation in the context of a DRF app. Let’s say I have an event with id 22 which has state PLANNED, but the current time is past its set begin timestamp.

Now, if somebody accesses the event via my API, say using the URL /events/22, the serializer for my model will retrieve the model instance and, by accessing its state property, will trigger the code inside the property getter, which will cause the value of the internal _state field to be changed. Ultimately, the returned value will be OPEN.

Now, if somebody were to PUT/PATCH to the URL /events/22 with a value of CLOSED for the object’s state, that would cause the field _state to be changed to CLOSED, which would also become the return value of the getter, as per the code above.

I’m mainly asking about possible drawbacks of having a getter with side effects like this in the context of a django application.

Oops sorry yeah you’re right. In terms of drf specifically:

  • if you made a list endpoint, this code would suffer from n+1 query performance problems
  • Retrieve requests are better if they’re idempotent

Generally you’re probably going to write code that makes more unnecessary saves. It might lead to more race condition type bugs that would require select_for_update type locking, but this depends on requirements/usage.

Nothing here is necessarily django specific but just about designing performant web applications using databases.

Hope it helps!