jazzband / django-fsm-log

Automatic logging for Django FSM

Home Page:https://django-fsm-log.readthedocs.io/en/latest/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Having a StateLog on object creation with the default state?

ddahan opened this issue · comments

In my app, I'm trying to show the full life cycles of objects using FSM.
While I love django-fsm-log for that purpose, the object creation itself is missing from these logs, as it's not a transition.
But after all, we could see this first step as a transition from the "None state" to the "created (default) state".

That's why I was wondering what is the best way, to create a StateLog instance when my object is created with the default state.

For example:

class SomeObject(models.Model):

    state = FSMField(
        choices=SomeObjectState.choices,
        default="created",
        protected=True,
    )


some_obj = SomeObject.objects.create()
StateLog.objects.for_(some_obj).count() # I would love to have 1 item here!

Thanks.

django-fsm-log offers persistence of the transition. When we initialize a state machine, there is no transition called, per nature I would say.
What you could do, is to call a transition yourself that can be triggered from a post_create signal handler.
I don't think it's a functionality that needs to live in django-fsm-log, but it could be a documented pattern.

@ticosax Thanks for the reply. I was thinking of this kind of solution too.
Except that since signals are often considered to be a bad practice for code readability, I guess I would override the save() method for that purpose.
Then, I would put that code in a mixin, sothat it can be used for any model that have a state field.

I'll try this and let you know about the result on this thread.

Yep, it seems to work as expected. I'll detail my solution here to potentially:

  • help people browsing the issues and having a similar need
  • get some additional tips to improve this piece of code.

The mixin

# Reusable enum choices
INITIALIZED = "INITIALIZED", "initialisé"
CREATED = "CREATED", "créé"


class FSMTransitionOnCreateMixin(models.Model):
    """
    Add an automatic new transition (from INITIALIZED to CREATED) for a class
    with a `state` attribute.
    The purpose is to have a transition log as soon as an object is created.
    """

    class Meta:
        abstract = True

    @fsm_log_by
    @transition(
        field="state",
        source=INITIALIZED[0],
        target=CREATED[0],
    )
    def to_created(self, by=None, description=None):
        pass

    def save(self, *args, **kwargs):
        """
        - super() needs to be run before, as django fsm needs object_id to build the log
        - adding state must be recorded before calling super(), as it would become false
        anyway after calling super().
        """
        adding = self._state.adding  # save the state
        super().save(*args, **kwargs)
        if adding is True:
            self.to_created()

Example usage in a class with a state

from core.mixins.fsm import CREATED, INITIALIZED, FSMTransitionOnCreateMixin

class MandateState(models.TextChoices):
    INITIALIZED = INITIALIZED  # avoid repetition
    CREATED = CREATED  # avoid repetition
    SENT = "SENT", "envoyé au client"
    SIGNED = "SIGNED", "signé par le client"
    ONGOING_SEARCH = "ONGOING_SEARCH", "recherche en cours"
    PAUSED_SEARCH = "PAUSED_SEARCH", "recherche en pause"
    ENDED = "ENDED", "recherche terminée"


class Mandate(FSMTransitionOnCreateMixin, models.Model):
    # ...

    state = FSMField(
        "statut",
        choices=MandateState.choices,
        default=MandateState.INITIALIZED,
        protected=True,
    )

Creating an Mandate will immediately get this StateLog:
image

Or you can define the initial transition in a signal:

from django.db.models.signals import post_save
from django.dispatch import receiver
from django_fsm.signals import post_transition

from project.foobar.models import Entity


@receiver(post_save, sender=Entity)
def trigger_initial_transition(sender, instance, created, **kwargs):
    if created:
        instance.baz()

Assuming you have a model like so:

from django.db import models
from django_fsm import FSMIntegerField
from django_fsm import transition


class Entity(models.Model):
    ...
    class State(models.IntegerChoices):    
            _NONE = 0, "-"
            FOO = 1, "foo"
            BAR = 2, "bar"
            ...
            
    state = FSMIntegerField(
        default=State._NONE,
        choices=State.choices,
        blank=True,
    )

    @transition(
        state,
        source=[State._NONE],
        target=State.FOO,
    )
    def baz(self):
        """"""
    ...