Automat skończony (FSM) w Django

in #strimi6 years ago (edited)

Idąc za wikipedią automat skończony to:

abstrakcyjny, matematyczny, iteracyjny model zachowania systemu dynamicznego oparty na tablicy dyskretnych przejść między jego kolejnymi stanami

Na pierwszy rzut oka niewiele nam to mówi jednak w praktyce zastosowanie tego modelu umożliwia łatwą i przejrzystą kontrolę nad zmianami stanów obiektów. Najprostszy przykład? Statusy artykułu. Powiedzmy, że artykuł może mieć status - nieopublikowany, publiczny albo usunięty. Chciałbyś uniemożliwić bezpośrednie przejście z statusu nieopublikowany na usunięty. Jak to zrobić? Można oczywiście wyciągnąć z bazy poprzedni stan jeszcze przed zapisaniem zmian i porównać stan obiektu zmodyfikowanego z oryginalnym, jednak w praktyce takie rozwiązanie jest nieeleganckie i przy bardziej skomplikowanych przejściach istnieje duże prawdopodobieństwo wygenerowania spaghetti code. Dobry programista to leniwy programista dlatego warto skorzystać z automatu skończonego a dokładniej z konkretnej djangowej implementacji - django-fsm.

Do dzieła!

Instalacja

Wystarczy tylko zainstalować pakiet, żadna inna konfiguracja nie jest wymagana.

pip install django-fsm

Implementacja

Na potrzeby artykułu stworzyłem prosty model "Project" z tytułem, statusem oraz dodatkowym polem śmieciowym, którego przeznaczenie poznasz później.

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

class Project(models.Model):
    DRAFT = 0 
    ACCEPTED = 1 
    PUBLISHED = 2 
    REMOVED = 3 

    STATUS_CHOICES = ( 
        (DRAFT, 'Draft'),
        (ACCEPTED, 'Accepted'),
        (PUBLISHED, 'Published'),
        (REMOVED, 'Removed'),
    )   

    title = models.CharField(max_length=255)
    status = FSMIntegerField(choices=STATUS_CHOICES, default=DRAFT)
    cond = models.IntegerField()

    def __str__(self):
        return self.title

Teraz powiedzmy, że chcesz uniemożliwić przejście z statusu DRAFT na REMOVED. W takiej sytuacji wystarczy zaimplementować poniższą metodę w modelu:

    @transition(field=status, source=[PUBLISHED, ACCEPTED], target=REMOVED)
    def removed(self):
        first_action()
        second_action()                                 
        print("Status changed to removed")

Dzięki dekoratorowi transition django-fsm wie, że właśnie ta metoda a nie inna jest odpowiedzialna za zmianę stanów. Argument field określa pole modelu, którego stan ma być śledzony, source określa dozwoloną listę stanów lub pojedynczy stan z którego ma nastąpić przejście, a target definiuje stan docelowy. W metodzie możemy zapisać akcje związane z zmianą stanu na przykład powiadomienie czytelników mailowo o publikacji, wyczyszczenie cache'u czy wysłanie sygnału. W moim przypadku zostanie wyświetlony błąd ponieważ zmiana statusu z DRAFT na REMOVED jest zabroniona. Sprawdźmy:

obj = Project.objects.create(title="Tytuł", status=Project.DRAFT, cond=2)

Wywołanie metody

obj.removed()

wyrzuca następujący błąd:

TransitionNotAllowed: Can't switch from state '0' using method 'removed'

Czyli wszystko działa dokładnie tak jak powinno! Oczywiście można bezpośrednio zmienić wartość pola:

obj.status = 3
obj.save()
obj.__dict__
{'_state': <django.db.models.base.ModelState at 0x110d82198>,
 'cond': 2,
 'id': 2,
 'status': 3,
 'title': 'Tytuł'}

Jednak korzystając z django-fsm przyjmujemy konwencję, że programiści nie będą tego praktykować a zamiast tego będą wywoływać odpowiednią metodę w modelu. Jeśli nie ufasz programistom z swojego zespołu to możesz zablokować możliwość bezpośredniej edycji pola modelu ustawiając atrybut protected na true.

status = FSMIntegerField(choices=STATUS_CHOICES, default=DRAFT, protected=True)

Efekt?

obj.status = 2
AttributeError: Direct status modification is not allowed

Na koniec pojawia się pytanie czy można określić dodatkowe warunki zmiany stanów. Oczywiście, że tak :) W tym celu wystarczy dodać argument conditions z listą funkcji/metod w dekoratorze zmiany stanu

    @transition(field=status, source=ACCEPTED, target=PUBLISHED, conditions=[f])
    def published(self):
        print("Status changed to published")

Funkcja f sprawdza wartość pola cond i jeśli jest równa 2 zwraca true

def f(inst):
    if inst.cond == 2:
        return True
    else:
        return False

# można krócej i bardziej python way
def f(inst):
    return inst.cond == 2
        
obj.cond = 3
obj.published()
> TransitionNotAllowed: Transition conditions have not been met for method 'published'
obj.cond = 2
obj.published()
# komunikat sukcesu
> Status changed to published
# django-fsm nie zapisuje automatycznie danych do bazy
obj.save()

See ya

ps. cały plik jest dostępny tutaj

Sort:  

Niewiele rozumiem, ale powodzenia! :D

Coin Marketplace

STEEM 0.19
TRX 0.14
JST 0.030
BTC 60907.24
ETH 3249.66
USDT 1.00
SBD 2.45