Giter Site home page Giter Site logo

django-fsm-log's Introduction

Django Finite State Machine Log

test suite codecov Jazzband pre-commit.ci status Documentation Status

Provides persistence of the transitions of your fsm's models. Backed by the excellent Django FSM package.

Logs can be accessed before a transition occurs and before they are persisted to the database by enabling a cached backend. See Advanced Usage

Changelog

4.0.0 (not released)

  • remove support for django 2.2 & 4.0

3.1.0 (2023-03-23)

  • fsm_log_description now accepts a default description parameter
  • Document fsm_log_description decorator
  • Add support for Django 4.1
  • Add compatibility for python 3.11

3.0.0 (2022-01-14)

  • Switch to github actions (from travis-ci)
  • Test against django 3.2 and 4.0, then python 3.9 and 3.10
  • Drop support for django 1.11, 2.0, 2.1, 3.0, 3.1
  • Drop support for python 3.4, 3.5, 3.6
  • allow using StateLogManager in migrations #95

2.0.1 (2020-03-26)

  • Add support for django3.0
  • Drop support for python2

1.6.2 (2019-01-06)

  • Address Migration history breakage added in 1.6.1

1.6.1 (2018-12-02)

  • Make StateLog.description field nullable

1.6.0 (2018-11-14)

  • Add source state on transitions
  • Fixed get_state_display with FSMIntegerField (#63)
  • Fixed handling of transitions if target is None (#71)
  • Added fsm_log_description decorator (#1, #67)
  • Dropped support for Django 1.10 (#64)

1.5.0 (2017-11-29)

  • cleanup deprecated code.
  • add codecov support.
  • switch to pytest.
  • add Admin integration to visualize past transitions.

1.4.0 (2017-11-09)

  • Bring compatibility with Django 2.0 and drop support of unsupported versions of Django: 1.6, 1.7, 1.9.

Compatibility

  • Python 2.7 and 3.4+
  • Django 1.8+
  • Django-FSM 2+

Installation

First, install the package with pip. This will automatically install any dependencies you may be missing

pip install django-fsm-log

Register django_fsm_log in your list of Django applications:

INSTALLED_APPS = (
    ...,
    'django_fsm_log',
    ...,
)

Then migrate the app to create the database table

python manage.py migrate django_fsm_log

Usage

The app listens for the django_fsm.signals.post_transition signal and creates a new record for each transition.

To query the log:

from django_fsm_log.models import StateLog
StateLog.objects.all()
# ...all recorded logs...

Disabling logging for specific models

By default transitions get recorded for all models. Logging can be disabled for specific models by adding their fully qualified name to DJANGO_FSM_LOG_IGNORED_MODELS.

DJANGO_FSM_LOG_IGNORED_MODELS = ('poll.models.Vote',)

for_ Manager Method

For convenience there is a custom for_ manager method to easily filter on the generic foreign key:

from my_app.models import Article
from django_fsm_log.models import StateLog

article = Article.objects.all()[0]

StateLog.objects.for_(article)
# ...logs for article...

by Decorator

We found that our transitions are commonly called by a user, so we've added a decorator to make logging this easy:

from django.db import models
from django_fsm import FSMField, transition
from django_fsm_log.decorators import fsm_log_by

class Article(models.Model):

    state = FSMField(default='draft', protected=True)

    @fsm_log_by
    @transition(field=state, source='draft', target='submitted')
    def submit(self, by=None):
        pass

With this the transition gets logged when the by kwarg is present.

article = Article.objects.create()
article.submit(by=some_user) # StateLog.by will be some_user

description Decorator

Decorator that allows to set a custom description (saved on database) to a transitions.

from django.db import models
from django_fsm import FSMField, transition
from django_fsm_log.decorators import fsm_log_description

class Article(models.Model):

    state = FSMField(default='draft', protected=True)

    @fsm_log_description(description='Article submitted')  # description param is NOT required
    @transition(field=state, source='draft', target='submitted')
    def submit(self, description=None):
        pass

article = Article.objects.create()
article.submit()  # logged with "Article submitted" description
article.submit(description="Article reviewed and submitted")  # logged with "Article reviewed and submitted" description

.. TIP:: The "description" argument passed when calling ".submit" has precedence over the default description set in the decorator

The decorator also accepts a allow_inline boolean argument that allows to set the description inside the transition method.

from django.db import models
from django_fsm import FSMField, transition
from django_fsm_log.decorators import fsm_log_description

class Article(models.Model):

    state = FSMField(default='draft', protected=True)

    @fsm_log_description(allow_inline=True)
    @transition(field=state, source='draft', target='submitted')
    def submit(self, description=None):
        description.set("Article submitted")

article = Article.objects.create()
article.submit()  # logged with "Article submitted" description

Admin integration

There is an InlineForm available that can be used to display the history of changes.

To use it expand your own AdminModel by adding StateLogInline to its inlines:

from django.contrib import admin
from django_fsm_log.admin import StateLogInline


@admin.register(FSMModel)
class FSMModelAdmin(admin.ModelAdmin):
    inlines = [StateLogInline]

Advanced Usage

You can change the behaviour of this app by turning on caching for StateLog records. Simply add DJANGO_FSM_LOG_STORAGE_METHOD = 'django_fsm_log.backends.CachedBackend' to your project's settings file. It will use your project's default cache backend by default. If you wish to use a specific cache backend, you can add to your project's settings:

DJANGO_FSM_LOG_CACHE_BACKEND = 'some_other_cache_backend'

The StateLog object is now available after the django_fsm.signals.pre_transition signal is fired, but is deleted from the cache and persisted to the database after django_fsm.signals.post_transition is fired.

This is useful if:

  • you need immediate access to StateLog details, and cannot wait until django_fsm.signals.post_transition has been fired
  • at any stage, you need to verify whether or not the StateLog has been written to the database

Access to the pending StateLog record is available via the pending_objects manager

from django_fsm_log.models import StateLog
article = Article.objects.get(...)
pending_state_log = StateLog.pending_objects.get_for_object(article)

Contributing

Running tests

pip install tox
tox

Linting with pre-commit

We use ruff, black and more, all configured and check via pre-commit. Before committing, run the following:

pip install pre-commit
pre-commit install

django-fsm-log's People

Contributors

alcad avatar arnaudlimbourg avatar blueyed avatar codingjoe avatar dependabot[bot] avatar fjcapdevila avatar hggh avatar hugovk avatar imomaliev avatar jacobh avatar jonasvp avatar knaperek avatar lorenzomorandini avatar mrigal avatar neostimul avatar orf avatar pre-commit-ci[bot] avatar rajiteh avatar scotta avatar stueken avatar ticosax avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-fsm-log's Issues

Remove configuration no longer supported in Django 4.1

default_app_config was deprecated in 3.2, and is removed in 4.1.

  /usr/local/lib/python3.10/site-packages/django/apps/registry.py:91: RemovedInDjango41Warning: 'django_fsm_log' defines default_app_config = 'django_fsm_log.apps.DjangoFSMLogAppConfig'. Django now detects this configuration automatically. You can remove default_app_config.

DataError: integer out of range

Getting an integer out of range error -- any ideas how to debug? I'm using the FSMIntegerField for my states and django-model-utils backed by a Choices field. I was able to run tests with FSMIntegerField and Choices with no problems, so not sure what's happening...

---------------------------------------------------------------------------
DataError                                 Traceback (most recent call last)
<ipython-input-4-7a7130d4660d> in <module>()
----> 1 c.start()

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django_fsm_log/decorators.pyc in wrapped(*args, **kwargs)
     10             instance.by = kwargs['by']
     11 
---> 12         out = func(instance, *arg_list, **kwargs)
     13 
     14         if kwargs.get('by', False):

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django_fsm/__init__.pyc in _change_state(instance, *args, **kwargs)
    510         @wraps(func)
    511         def _change_state(instance, *args, **kwargs):
--> 512             return fsm_meta.field.change_state(instance, func, *args, **kwargs)
    513 
    514         if not wrapper_installed:

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django_fsm/__init__.pyc in change_state(self, instance, method, *args, **kwargs)
    335             raise
    336         else:
--> 337             post_transition.send(**signal_kwargs)
    338 
    339         return result

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/dispatch/dispatcher.pyc in send(self, sender, **named)
    190 
    191         for receiver in self._live_receivers(sender):
--> 192             response = receiver(signal=self, sender=sender, **named)
    193             responses.append((receiver, response))
    194         return responses

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django_fsm_log/backends.pyc in post_transition_callback(sender, instance, name, source, target, **kwargs)
     57             state=target,
     58             transition=name,
---> 59             content_object=instance,
     60         )
     61 

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/manager.pyc in manager_method(self, *args, **kwargs)
    120         def create_method(name, method):
    121             def manager_method(self, *args, **kwargs):
--> 122                 return getattr(self.get_queryset(), name)(*args, **kwargs)
    123             manager_method.__name__ = method.__name__
    124             manager_method.__doc__ = method.__doc__

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/query.pyc in create(self, **kwargs)
    399         obj = self.model(**kwargs)
    400         self._for_write = True
--> 401         obj.save(force_insert=True, using=self.db)
    402         return obj
    403 

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/base.pyc in save(self, force_insert, force_update, using, update_fields)
    706 
    707         self.save_base(using=using, force_insert=force_insert,
--> 708                        force_update=force_update, update_fields=update_fields)
    709     save.alters_data = True
    710 

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/base.pyc in save_base(self, raw, force_insert, force_update, using, update_fields)
    734             if not raw:
    735                 self._save_parents(cls, using, update_fields)
--> 736             updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
    737         # Store the database on which the object was saved
    738         self._state.db = using

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/base.pyc in _save_table(self, raw, cls, force_insert, force_update, using, update_fields)
    818 
    819             update_pk = bool(meta.has_auto_field and not pk_set)
--> 820             result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
    821             if update_pk:
    822                 setattr(self, meta.pk.attname, result)

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/base.pyc in _do_insert(self, manager, using, fields, update_pk, raw)
    857         """
    858         return manager._insert([self], fields=fields, return_id=update_pk,
--> 859                                using=using, raw=raw)
    860 
    861     def delete(self, using=None, keep_parents=False):

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/manager.pyc in manager_method(self, *args, **kwargs)
    120         def create_method(name, method):
    121             def manager_method(self, *args, **kwargs):
--> 122                 return getattr(self.get_queryset(), name)(*args, **kwargs)
    123             manager_method.__name__ = method.__name__
    124             manager_method.__doc__ = method.__doc__

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/query.pyc in _insert(self, objs, fields, return_id, raw, using)
   1037         query = sql.InsertQuery(self.model)
   1038         query.insert_values(fields, objs, raw=raw)
-> 1039         return query.get_compiler(using=using).execute_sql(return_id)
   1040     _insert.alters_data = True
   1041     _insert.queryset_only = False

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/models/sql/compiler.pyc in execute_sql(self, return_id)
   1058         with self.connection.cursor() as cursor:
   1059             for sql, params in self.as_sql():
-> 1060                 cursor.execute(sql, params)
   1061             if not (return_id and cursor):
   1062                 return

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/backends/utils.pyc in execute(self, sql, params)
     77         start = time()
     78         try:
---> 79             return super(CursorDebugWrapper, self).execute(sql, params)
     80         finally:
     81             stop = time()

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/backends/utils.pyc in execute(self, sql, params)
     62                 return self.cursor.execute(sql)
     63             else:
---> 64                 return self.cursor.execute(sql, params)
     65 
     66     def executemany(self, sql, param_list):

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/utils.pyc in __exit__(self, exc_type, exc_value, traceback)
     93                 if dj_exc_type not in (DataError, IntegrityError):
     94                     self.wrapper.errors_occurred = True
---> 95                 six.reraise(dj_exc_type, dj_exc_value, traceback)
     96 
     97     def __call__(self, func):

/Users/dbinetti/.virtualenvs/barberscore/lib/python2.7/site-packages/django/db/backends/utils.pyc in execute(self, sql, params)
     62                 return self.cursor.execute(sql)
     63             else:
---> 64                 return self.cursor.execute(sql, params)
     65 
     66     def executemany(self, sql, param_list):

DataError: integer out of range


In [5]: 

Support FSMIntegerField in StateLogInline

Using StateLogInline, I noticed that a FSMIntegerField renders the underlying (integer) database value, which carries no business meaning.
It would be very useful if StateLogInline could render FSMIntegerFields as their display value!

'django_fsm_log' have changes that are not yet reflected in a migration, and so won't be applied

Constantly getting a message about an unapplied migration see below, not sure if set something wrong up. I run migrate and no migrations are applied.

Operations to perform:
 Apply all migrations: admin, auth, contenttypes, django_celery_beat, django_fsm_log, rest_api, sessions
Running migrations:
No migrations to apply.
 Your models in app(s): 'django_fsm_log' have changes that are not yet reflected in a migration, and so won't be applied.
 Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.

 160 static files copied to '/usr/src/app/django/staticfiles'.
Watching for file changes with StatReloader
Watching for file changes with StatReloader
 Performing system checks...

 System check identified no issues (0 silenced).
 June 10, 2023 - 18:20:12
 Django version 4.2.2, using settings 'django_project.settings'
 Starting ASGI/Channels version 3.0.5 development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

python = "^3.11"
Django = "^4.2.2"
django-fsm = "^2.8.1"
django-fsm-log = "^3.1.0"


INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django_fsm",
    "django_fsm_log",
    ...
]

root@353cd9925eef:/usr/src/app/django# python manage.py makemigrations
Migrations for 'django_fsm_log':
  /usr/local/lib/python3.11/site-packages/django_fsm_log/migrations/0005_alter_statelog_id.py
    - Alter field id on statelog
root@353cd9925eef:/usr/src/app/django# cat /usr/local/lib/python3.11/site-packages/django_fsm_log/migrations/0005_alter_statelog_id.py
# Generated by Django 4.2.2 on 2023-06-10 18:20

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("django_fsm_log", "0004_auto_20190131_0341"),
    ]

    operations = [
        migrations.AlterField(
            model_name="statelog",
            name="id",
            field=models.BigAutoField(
                auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
            ),
        ),
    ]

Handle multiple state fields

Currently the field keyword argument in the signal handler is not being handled.

Therefore, if multiple state fields (e.g. "state" and "second_state") are being used those cannot be easily distinguished in the logs.

Do you think it makes sense to add "field_name" to StateLog, where kwargs['field'].name would be stored into?

This could also be used in __str__ then, but is typically redundant then (since often only a single state field is used probably).

Log instance caching timeout is too low

Hi,

While debugging an application using django-fsm-log, an error occurs at the end of the transition process, about the cached log instance is None, as the timeout only 10 seconds, which is too low for a "normal" debugging session.

I' would be nice to make this timeout configurable (for debugging purpose), and to get it from django settings.

I'll propose a PR for this...

Thanks !

Null Object_ID

One of the situations I ran into is running trial transitions on non-permanent objects (i.e. not saved in the DB only memory) so they have no pk as of yet, however the fsm-log signal/creation that runs doesn't check for a pk before running manager.create(**values).

Is there a way of disabling the fsm_log decorator when there is no pk or some other way of manually saving without it running?

Alternatively, would a change that checks for a pk before running create() be a reasonable adjustment?

Just after posting, I realized this is happening on my transition methods that aren't even decorated by the fsm_log decorator so I'm at a bit of a loss as to how to disable it.

integer out of range when pk is uuid

get error when pk is uuid.

ERROR 2021-03-28 17:29:30,102 log 1274593 140551966910016 Internal Server Error: /en-us/manager/trades/order/f1f522e9-4e00-4401-8700-0ce6ba6879db/change/
Traceback (most recent call last):
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/fsm_admin/mixins.py", line 164, in _do_transition
    trans_func(request=request, by=request.user)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm/__init__.py", line 520, in _change_state
    return fsm_meta.field.change_state(instance, func, *args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm/__init__.py", line 326, in change_state
    result = method(instance, *args, **kwargs)
TypeError: obsolete() got an unexpected keyword argument 'request'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/fsm_admin/mixins.py", line 168, in _do_transition
    trans_func(by=request.user)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm/__init__.py", line 520, in _change_state
    return fsm_meta.field.change_state(instance, func, *args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm/__init__.py", line 326, in change_state
    result = method(instance, *args, **kwargs)
TypeError: obsolete() got an unexpected keyword argument 'by'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
psycopg2.errors.NumericValueOutOfRange: integer out of range
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/lib/python3.9/contextlib.py", line 79, in inner
    return func(*args, **kwds)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/contrib/admin/options.py", line 614, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/contrib/admin/sites.py", line 233, in inner
    return view(request, *args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/contrib/admin/options.py", line 1656, in change_view
    return self.changeform_view(request, object_id, form_url, extra_context)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/contrib/admin/options.py", line 1534, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/contrib/admin/options.py", line 1580, in _changeform_view
    self.save_model(request, new_object, form, not add)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/fsm_admin/mixins.py", line 188, in save_model
    self._do_transition(transition, request, obj, form, fsm_field)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/fsm_admin/mixins.py", line 171, in _do_transition
    trans_func()
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm/__init__.py", line 520, in _change_state
    return fsm_meta.field.change_state(instance, func, *args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm/__init__.py", line 345, in change_state
    post_transition.send(**signal_kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/dispatch/dispatcher.py", line 177, in send
    return [
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/dispatch/dispatcher.py", line 178, in <listcomp>
    (receiver, receiver(signal=self, sender=sender, **named))
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm_log/backends.py", line 82, in post_transition_callback
    return _pre_transition_callback(sender, instance, name, source, target, StateLog.objects, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django_fsm_log/backends.py", line 28, in _pre_transition_callback
    manager.create(**values)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/query.py", line 447, in create
    obj.save(force_insert=True, using=self.db)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/base.py", line 753, in save
    self.save_base(using=using, force_insert=force_insert,
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/base.py", line 790, in save_base
    updated = self._save_table(
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/base.py", line 895, in _save_table
    results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/base.py", line 933, in _do_insert
    return manager._insert(
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/query.py", line 1254, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1397, in execute_sql
    cursor.execute(sql, params)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/debug_toolbar/panels/sql/tracking.py", line 198, in execute
    return self._record(self.cursor.execute, sql, params)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/debug_toolbar/panels/sql/tracking.py", line 133, in _record
    return method(sql, params)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 98, in execute
    return super().execute(sql, params)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/yswtrue/Code/seatower_erp/.venv/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
django.db.utils.DataError: integer out of range

Django 4 support

It crashes before starting.

  File "/usr/local/lib/python3.9/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/usr/local/lib/python3.9/site-packages/django/apps/registry.py", line 91, in populate
    app_config = AppConfig.create(entry)
  File "/usr/local/lib/python3.9/site-packages/django/apps/config.py", line 123, in create
    mod = import_module(mod_path)
  File "/usr/local/lib/python3.9/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/usr/local/lib/python3.9/site-packages/django_fsm_log/apps.py", line 6, in <module>
    from django_fsm.signals import pre_transition, post_transition
  File "/usr/local/lib/python3.9/site-packages/django_fsm/__init__.py", line 11, in <module>
    from django_fsm.signals import pre_transition, post_transition
  File "/usr/local/lib/python3.9/site-packages/django_fsm/signals.py", line 4, in <module>
    pre_transition = Signal(providing_args=['instance', 'name', 'source', 'target'])
TypeError: __init__() got an unexpected keyword argument 'providing_args'

New maintainers wanted

For various reasons, new maintainers are sought for django-fsm-log. Candidates should post a comment here supporting their appointment, and I'll grant ownership of the repo once suitable candidates are found.

I'd like to take this opportunity to thank @ticosax for his numerous contributions over the last 2½ years. Every contribution whether comment, critique, or pull request is appreciated (but especially the pull requests). @ticosax has given more than his fair share of each, and for this I say thankyou.

On with the show... who's up next?

Need to select with models are logged

I'm finding that for some models, fsm-log is adequate, for others, it's not; because some other metadata needs to be recorded along with the state change.

Maybe we could introduce a setting, and/or model mixin, to indicate which models should/shouldn't be logged?

Drop support for Django 1.6

This would help simplify the codebase.

Developers still using Django 1.6 could still use the version of django-fsm-log prior to this being released.

Nested transitions with `@fsm_log_by` cause AttributeError

If you use nested transitions with Django FSM Admin and @fsm_log_by decorator on both transitions, an AttributeError is thrown because the fsm_log_by decorator on the inner transition calls delattr(instance, 'by') however when fsm_log_by decorate tries to delete the by attribute on the instance -- it fails with an AttributeError because it's already been deleted. In short, the fsm_log_by decorator deletes the by attribute in the instance prematurely when calling a transition from another transition within the same object.

We have fixed this in our own code base by writing a new decorator. Code to follow below. Would you accept a pull request for this? If so, let me know and we'll prepare a pull request.

from functools import wraps


def fsm_log_by(func):
    """
    Fixes issue with nested transitions and logging causes `instance.by` to be deleted too soon (in the nested inner
    transition and then tried to be deleted again in the outer transition.
    """

    @wraps(func)
    def wrapped(*args, **kwargs):
        arg_list = list(args)
        instance = arg_list.pop(0)
        by_set = False  # Track if this decorator was the one that set `by` in the instance

        if kwargs.get('by', None) and not hasattr(instance, 'by'):
            instance.by = kwargs['by']
            by_set = True

        out = func(instance, *arg_list, **kwargs)

        # Only delete the `by` if the function I decorate was the one that set `by` in the instance
        # Otherwise, the outer transition will take care of cleaning things up
        if hasattr(instance, 'by') and by_set:
            delattr(instance, 'by')

        return out

    return wrapped

Deleting user object in multiple database environment, raises exception

Env:

  • Python 2.7
  • Django==1.7.10
  • django-fsm==2.6.0
  • django-fsm-log==1.3.0

Scenario:

  • A django project with two Apps (app1 & app2).
  • We have two databases(DB1, DB2) configured as well (check below DB router configuration).
  • Tables which are part of app2, will be created in the DB2.
    (python manage.py migrate app2 --database=DB2)
  • And app1 tables in DB1 database, which is a default database.
    (python manage.py migrate app1)
  • Both DBs have the user, group, permission and auth tables.
  • Installdjango_fsm_log in 2nd DB only (i.e. DB2).
    (python manage.py migrate django_fsm_log --database=DB2)

Now try to delete a user object in the app1, This user object is coming from DB1. you will get an Error saying,
ProgrammingError: (1146, "Table 'DB1.django_fsm_log_statelog' doesn't exist") which is True.
we don't have that table in DB1 but in DB2.

After investigating we found that it tries to delete user.statelog__set and fail.

Source code
DB router configuration: database_router.py

class DB2Router(object):
    APP_LABEL_LIST = ['app2, 'django_fsm_log']

    def db_for_read(self, model, **hints):
        if model._meta.app_label in self.APP_LABEL_LIST:
            return DB2
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label in self.APP_LABEL_LIST:
            return DB2
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if set(self.APP_LABEL_LIST) & set([obj1._meta.app_label, obj2._meta.app_label]):
            return obj1._meta.app_label == obj2._meta.app_label
        return None

    def allow_migrate(self, db, model):
        if model._meta.app_label in self.APP_LABEL_LIST:
            return db == DB2
        return None

And in settings

DATABASE_ROUTERS = [
    'database_router.DB2Router',
]

Even after having database_router configured, it is happening.
What can be done here? is there any way in our library to remove this constraint or set ON_DELETE attribute dynamically.

Error Traceback:

File "/home/local/lib/python2.7/site-packages/django/db/models/base.py", line 738, in delete
File "/home/local/lib/python2.7/site-packages/django/db/models/deletion.py", line 198, in collect
File "/home/local/lib/python2.7/site-packages/django/db/models/query.py", line 145, in __nonzero__
File "/home/local/lib/python2.7/site-packages/django/db/models/query.py", line 966, in _fetch_all
File "/home/local/lib/python2.7/site-packages/django/db/models/query.py", line 265, in iterator
File "/home/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 701, in results_iter
File "/home/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 787, in execute_sql
File "/home/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 65, in execute
File "/home/local/lib/python2.7/site-packages/django/db/utils.py", line 94, in __exit__
File "/home/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 65, in execute
File "/home/local/lib/python2.7/site-packages/django/db/backends/mysql/base.py", line 129, in execute
File "/home/local/lib/python2.7/site-packages/MySQLdb/cursors.py", line 205, in execute
File "/home/local/lib/python2.7/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler

Push new release to PyPI

should get around to this sometime soon.

Interested in doing this @dinie? just saw you pushing releases for something else :)

Stop using cache to stash StateLog instances

When using DJANGO_FSM_LOG_STORAGE_METHOD = 'django_fsm_log.backends.CachedBackend', a cache is used to stash unsaved log instances created during the pre transition callback. These unsaved StateLog instances are then retrieved and saved as part of the post transition callback.

Problems arise when DJANGO_FSM_LOG_CACHE_BACKEND points to a shared cache, since the PendingStateLogManager._get_cache_key_for_object method doesn't differentiate cache keys between between different requests. This results in StateLog instances being lost if multiple concurrent requests change the state of a single object. This could be solved by either including request identifying details in the cache key, or by using a specialised cache backend (such as django-request-cache). Better yet, the StateLog instance should be stashed on the related object itself.

Easy enough to fix, however...

I've seen code that relies on current cache semantics. More specifically, I have code that approximates the behaviour of queryset.select_for_update() to ensure pending StateLog instances are written to the DB before proceeding:

# wait for pending StateLog instances to be written to the DB before querying
# to find the most recent instances
while(StateLog.pending_objects.get_for_object(instance) is not None):
    sleep(0.05)

Changing current cache semantics would break code like this. This wouldn't be so bad except that it seems to be supported behaviour.

I'm not sure how to proceed, any help would be appreciated.

Having a StateLog on object creation with the default state?

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.

Implement Jazzband guidelines for django-fsm-log

This issue tracks the implementation of the Jazzband guidelines for the project django-fsm-log

It was initiated by @ticosax who was automatically assigned in addition to the Jazzband roadies.

See the TODO list below for the generally required tasks, but feel free to update it in case the project requires it.

Feel free to ping a Jazzband roadie if you have any question.

TODOs

Project details

Description Automatic logging for Django FSM
Homepage
Stargazers 146
Open issues 10
Forks 63
Default branch master
Is a fork False
Has Wiki True
Has Pages False

`fsm_log_by` decorator does not persist `by`

model:

@fsm_log_by
@transition(field=status, source=Status.PENDING_CODE_REVIEW.name, target=Status.CODE_REJECTED.name)
def reject_code(self, by=None):
        print('REJECTING...')
        print(by.id)
        pass

(Successfully prints user's id but doesn't persist by to StateLog)

views.py:

def post(self, request, job_id):
     job = get_object_or_404(Job, pk=job_id)
     if not can_proceed(job.reject_code):
          raise PermissionDenied
     job.reject_code(self.request.user)
     job.save()

DB:

postgres=# select * from django_fsm_log_statelog;
 id |           timestamp           |     state     |  transition  | object_id | by_id | content_type_id
----+-------------------------------+---------------+--------------+-----------+-------+-----------------
  1 | 2018-02-06 14:40:30.159429+00 | QUEUED        | approve_code |         2 |       |               1
  2 | 2018-02-06 14:58:45.591487+00 | DELETED       | delete       |         1 |       |               1
  3 | 2018-02-06 15:28:10.691508+00 | QUEUED        | approve_code |         3 |       |               1
  4 | 2018-02-07 14:08:30.8747+00   | CODE_REJECTED | reject_code  |         4 |       |               1
  5 | 2018-02-07 14:09:42.174577+00 | CODE_REJECTED | reject_code  |         5 |       |               1
  6 | 2018-02-07 14:32:12.451424+00 | CODE_REJECTED | reject_code  |         6 |       |               1
(6 rows)

REPL:

>>> StateLog.objects.get(pk=6).by
>>> StateLog.objects.get(pk=6).by_id

This is with the latest pip release, 1.5.0

Initial migration for django 1.7+ is incomplete.

I've been running around upgrading from django 1.6 and this came up:

kursitet@precise64:~/kursitet-platform$ ./manage.py makemigrations django_fsm_log -v3 --dry-run
Migrations for 'django_fsm_log':
  0002_auto_20150620_1353.py:
    - Change Meta options on statelog
Full migrations file '0002_auto_20150620_1353.py':
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

    dependencies = [
        ('django_fsm_log', '0001_initial'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='statelog',
            options={'get_latest_by': 'timestamp'},
        ),
    ]

Basically, it appears that the meta options are not reflected in the migration when Django wants them to be. This is only a thing in Django 1.8, I believe, as when upgrading to 1.7 the warning about model changes not being reflected in migrations did not come up.

Not logging source state

I'm not sure why the target state of the transition is logged but not the source state. The target can be inferred from the transition, since every transition can only have one target. The source is ambiguous, however, since it can be a list of states (https://github.com/kmmbvnr/django-fsm#usage).

I'll be happy to provide a pull request for saving the source to the StateLog model as well - or is there a reason it's currently not being logged?

Wont work with Django 1.9

File "..venv\lib\site-packages\django_fsm_log\backends.py", line 2, in
from django.core.cache import get_cache
ImportError: cannot import name 'get_cache'

get_cache is not supported in django 1.9

Add templatetag for transition logs

Would it make sense to provide a templatetag for the transition logs, or include a section in the README?

Here's a working example:

myproject/myapp/subapp/templatetags/__init__.py:
Don't forget to register the templatetags folder as module. I've placed the templatetags inside the nested subapp that has django_fsm transitions.

myproject/myapp/templatetags/transitions.py

from django import template
from django_fsm_log.models import StateLog
register = template.Library()

@register.inclusion_tag('tx_logs.html', takes_context=False)
def tx_logs(obj):
    """Renders the FSM transition logs for an object."""
    return {'logs': [log for log in StateLog.objects.for_(obj)]}

A very basic template myproject/templates/tx_logs.html - note my user model has a field name:

{% for log in logs %}
<div><strong>{{ log.timestamp|date:"r" }}</strong> {{ log.state }} by {{ log.by.name }}</div>
{% endfor %}

Feel free to place the template in a better place, just make sure to adjust the path in the inclusion tag accordingly.

In this example, I'm overriding the admin change form with a custom myproject/myapp/templates/admin/change_form.html,
inserting the logs underneath the messages block. YMMV.

{% extends "admin/change_form.html" %}
{% load i18n admin_urls transitions %} {# load templatetag library transitions #}
{% block messages %}
    {{ block.super }}
    {% if original %}
    {% tx_logs original %} {# the object "original" has a django_fsm status field #}
    {% endif %}
{% endblock %}

As I'm overriding an admin change form, the object is available in the template as original.

Happy to send PR if appropriate.

Request for reason_for_state_change

Hi,
We're using django-fsm-log at Songtrust (thanks!) and I was wondering if you would be open to adding a reason_for_state_change field on the FSM log. This would be helpful for us and we'd be willing to submit a patch for this if you're interested in incorporating it in the library.

Thanks
Alex Kane

IntegrityError: null value in column "description" violates not-null constraint

I just encountered the following error after upgrading from 1.6.0 to 1.6.1 when running a transition:

IntegrityError: null value in column "description" violates not-null constraint
DETAIL:  Failing row contains (529, 2018-12-10 10:29:01.030241+00, active, activate, 683, 80, 36, null, unpublished).

This should not have happened, because the description field was made nullable in migration 0005 added in PR #84.

You also added the field alteration to migration 0003. AFAIK, since I already had this migration applied from a previous version of django-fsm-log, Django skipped the AlterField action in 0005 because it thought the removal of the not-null constrained had already been applied in 0003.

Fortunately I encountered this in a testing environment, manually rolling back to 0002 and forwarding to 0005 again fixed the issue. This would NOT have been a suitable solution in a production due to data loss and would require manual database manipulation.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.