Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Having a StateLog on object creation with the default state? #145

Open
ddahan opened this issue Sep 12, 2022 · 4 comments
Open

Having a StateLog on object creation with the default state? #145

ddahan opened this issue Sep 12, 2022 · 4 comments

Comments

@ddahan
Copy link

ddahan commented Sep 12, 2022

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.

@ticosax
Copy link
Member

ticosax commented Sep 13, 2022

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.

@ddahan
Copy link
Author

ddahan commented Sep 13, 2022

@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.

@ddahan
Copy link
Author

ddahan commented Sep 14, 2022

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

@samuel-andres
Copy link

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):
        """"""
    ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants