Declarative model lifecycle hooks, an alternative to Signals.

Overview

Django Lifecycle Hooks

Package version Python versions Python versions PyPI - Django Version

This project provides a @hook decorator as well as a base model and mixin to add lifecycle hooks to your Django models. Django's built-in approach to offering lifecycle hooks is Signals. However, my team often finds that Signals introduce unnecessary indirection and are at odds with Django's "fat models" approach.

Django Lifecycle Hooks supports Python 3.5, 3.6, 3.7 and 3.8, Django 2.0.x, 2.1.x, 2.2.x and 3.0.x.

In short, you can write model code like this:

from django_lifecycle import LifecycleModel, hook, BEFORE_UPDATE, AFTER_UPDATE


class Article(LifecycleModel):
    contents = models.TextField()
    updated_at = models.DateTimeField(null=True)
    status = models.ChoiceField(choices=['draft', 'published'])
    editor = models.ForeignKey(AuthUser)

    @hook(BEFORE_UPDATE, when='contents', has_changed=True)
    def on_content_change(self):
        self.updated_at = timezone.now()

    @hook(AFTER_UPDATE, when="status", was="draft", is_now="published")
    def on_publish(self):
        send_email(self.editor.email, "An article has published!")

Instead of overriding save and __init__ in a clunky way that hurts readability:

    # same class and field declarations as above ...
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._orig_contents = self.contents
        self._orig_status = self.status
        
        
    def save(self, *args, **kwargs):
        if self.pk is not None and self.contents != self._orig_contents:
            self.updated_at = timezone.now()

        super().save(*args, **kwargs)

        if self.status != self._orig_status:
            send_email(self.editor.email, "An article has published!")

Documentation: https://rsinger86.github.io/django-lifecycle

Source Code: https://github.com/rsinger86/django-lifecycle


Changelog

0.8.1 (January 2021)

  • Added missing return to delete() method override. Thanks @oaosman84!

0.8.0 (October 2020)

  • Significant performance improvements. Thanks @dralley!

0.7.7 (August 2020)

  • Fixes issue with GenericForeignKey. Thanks @bmbouter!

0.7.6 (May 2020)

  • Updates to use constants for hook names; updates docs to indicate Python 3.8/Django 3.x support. Thanks @thejoeejoee!

0.7.5 (April 2020)

  • Adds static typed variables for hook names; thanks @Faisal-Manzer!
  • Fixes some typos in docs; thanks @tomdyson and @bmispelon!

0.7.1 (January 2020)

  • Fixes bug in utils._get_field_names that could cause recursion bug in some cases.

0.7.0 (December 2019)

  • Adds changes_to condition - thanks @samitnuk! Also some typo fixes in docs.

0.6.1 (November 2019)

  • Remove variable type annotation for Python 3.5 compatability.

0.6.0 (October 2019)

  • Adds when_any hook parameter to watch multiple fields for state changes

0.5.0 (September 2019)

  • Adds was_not condition
  • Allow watching changes to FK model field values, not just FK references

0.4.2 (July 2019)

  • Fixes missing README.md issue that broke install.

0.4.1 (June 2019)

0.4.0 (May 2019)

  • Fixes initial_value(field_name) behavior - should return value even if no change. Thanks @adamJLev!

0.3.2 (February 2019)

  • Fixes bug preventing hooks from firing for custom PKs. Thanks @atugushev!

0.3.1 (August 2018)

  • Fixes m2m field bug, in which accessing auto-generated reverse field in before_create causes exception b/c PK does not exist yet. Thanks @garyd203!

0.3.0 (April 2018)

  • Resets model's comparison state for hook conditions after save called.

0.2.4 (April 2018)

  • Fixed support for adding multiple @hook decorators to same method.

0.2.3 (April 2018)

  • Removes residual mixin methods from earlier implementation.

0.2.2 (April 2018)

  • Save method now accepts skip_hooks, an optional boolean keyword argument that controls whether hooked methods are called.

0.2.1 (April 2018)

  • Fixed bug in _potentially_hooked_methods that caused unwanted side effects by accessing model instance methods decorated with @cache_property or @property.

0.2.0 (April 2018)

  • Added Django 1.8 support. Thanks @jtiai!
  • Tox testing added for Python 3.4, 3.5, 3.6 and Django 1.8, 1.11 and 2.0. Thanks @jtiai!

Testing

Tests are found in a simplified Django project in the /tests folder. Install the project requirements and do ./manage.py test to run them.

License

See License.

Comments
  • Order in which hooks are executed

    Order in which hooks are executed

    Is it possible to control somehow the order in which hooks are executed?

    My use case is something like this:

    class Festival(LifecycleModelMixin, models.Model):
        name = models.CharField(max_length=200)
        slug = models.SlugField(unique=True, null=True, blank=True)
    
        @hook(BEFORE_CREATE)
        def set_slug(self):
            self.slug = generate_slug(self.name)
    
        @hook(BEFORE_CREATE)
        def do_something_with_slug(self):
            print(f"Here we want to use our slug, but it could be None: {self.slug}")
    
    opened by EnriqueSoria 6
  • [Question] how is this different from django-fsm and can I use this in conjunction with it?

    [Question] how is this different from django-fsm and can I use this in conjunction with it?

    i have been using django-lifecycle for a while to merely store the status. But I am realizing that I am building towards something like a Finite State machine.

    So not sure if this library and https://github.com/viewflow/django-fsm overlap or I can use them in conjunction

    I do find the idea of eschewing Signals for Hooks in this library for greater readability to be appealing.

    opened by simkimsia 6
  • Feature: hooked methods cached on class

    Feature: hooked methods cached on class

    Motivation

    1. _get_model_property_names

    Utility function _get_model_property_names is currently used for getting attribute names of properties to avoid potential side-effects during getting them. This workaround is great, but it isn't covering all possible properties types (e.g. functools.cached_property), only builtin property and cached_property from Django.

    2. _potentially_hooked_methods cached on instance

    Since _potentially_hooked_methods use cached_property, the results are cached on an instance, not on class -- but in my opinion, it's useless to have them valid for instance. Except for edge cases (dynamic definition of a method with hook during runtime) are the hooked methods the same for all instances of one model class. Because of that, _potentially_hooked_methods is evaluated 1000 times in this code:

    [ModelInheritedFromLifecycleMixin() for _ in range(1000)]
    

    That's measurable and unnecessary performance effect on model runtime (especially in combination with first point).

    3. depth of searching in _potentially_hooked_methods

    This method is currently using dir(self) to inspect all possible attributes with a hook -- that means scanning all delivered attributes from base DjangoModels and this is really unnecessary since @hook could be only on user's code, not on code from Django.

    Solution

    This PR contains refactoring of @hook decorator and part of LifecycleMixin code to use class-based cache to avoid problems mentioned above (evaluation of _potentially_hooked_methods for each new instance of model and evaluation of not known property types). Methods for scanning for possible hooks are now taken only from children's classes, not from Django Models.

    PR is without BC break IMHO, if you don't use internals (accessing ._hooked manually, relying on the order of hooks evaluation or using @hook higher in the class tree than LifecycleMixin).

    Questions

    1. order of hook evaluation hooked methods in tests https://github.com/rsinger86/django-lifecycle/blob/a77e05c3376707b06dc765911968ad5fa37b168c/tests/testapp/models.py#L72-L93 and surrounding test method https://github.com/rsinger86/django-lifecycle/blob/a77e05c3376707b06dc765911968ad5fa37b168c/tests/testapp/tests/test_user_account.py#L88-L102

    The test is currently expecting a specific order of hooks evaluation since both hooks are on the same attribute. Is it a wanted feature? I don't think so, hooked methods should not affect each other, and hooks shall have undeterminable order of evaluation. This PR also changes the way of working with excluded attributes internally, now is used sets and not lists (and that's the problem for the hooks order evaluation).

    1. _get_model_descriptor_names There is no test for this method, respectively in all test cases this method returns empty iterable. What's the use case for this functionality?
    opened by thejoeejoee 6
  • Lifecycle hook not triggered

    Lifecycle hook not triggered

    Hi,

    I've just tried implementing django-lifecycle into my project, but I'm having a hard time getting started.

    My model looks like this:

    class MyModel(LifecycleModel):
        ...
        model = models.CharField(max_length=200)
        ...
    
        @hook('before_update', when='model', has_changed=True)
        def on_content_change(self):
            self.model = 'test'
    

    for some reason, the hook doesn't seem to be triggered. I've also tried stacking decorators to include other moments with the same result.

    Conversely, this works:

    class MyModel(LifecycleModel):
        ...
        model = models.CharField(max_length=200)
        ...
    
        def save(self, *args, **kwargs):
            self.model = 'test'
            super(MyModel, self).save(*args, **kwargs)
    

    Am I missing something in my implementation? I'm on Django 2.2.8, python 3.7, and django-lifecycle 0.7.1.

    opened by sondrelg 6
  • Implement priority to hooks

    Implement priority to hooks

    ...as discussed in #95

    What do you think of this approach?

    I have chosen that priority=0 is maximum priority, also I have added some priorities to constant values so end users doesn't have to think about the implementation (DEFAULT_PRIORITY, HIGHEST_PRIORITY, etc...)

    Feel free to comment, suggest or edit whatever

    opened by EnriqueSoria 5
  • "atomic"-ness of hooks should be configureable or removed

    First of all, thanks for this library. The API is really well done.

    However, today I discovered that in #85 the change was made to force hooks to run inside of a transaction, which for many cases is desirable behavior, however one of my uses for lifecycle hooks is to queue background jobs in AFTER_SAVE assuming any calls to the model's save() will either observe the default django orm autocommit behavior or abide by whatever the behavior set by its current context will be.

    Forcing a transaction / savepoint that wraps all model hooks using the atomic decorator means you can't safely make a call to an external service(or queue a celery / rq job) and assume the model changes will be visible. For example, it is not unusual for a background job to begin execution before the transaction that queued the job commits.

    I'd be happy to open a PR that either reverts #85 or makes the current behavior configureable in some way depending no your preference if you are open to it.

    opened by amcclosky 5
  • Make django-lifecycle much, much faster

    Make django-lifecycle much, much faster

    Some of the work that the lifecycle mixin is during the initialization of new model objects is very expensive and unnecessary. It's calculating (and caching) field names and foreign key models per-object, rather than per-class / model. All instances of a model are going to have the same field names and foreign key model types so this work actually only needs to be done once per model type.

    Replacing cached methods with cached classmethods yields a very sizable performance improvement when creating a bunch of new model instances.

    opened by dralley 5
  • Using `only` queryset method leads to a RecursionError

    Using `only` queryset method leads to a RecursionError

    I tried to query a model using LifecycleModel class and when doing an only('id') I got a RecursionError

        res = instance.__dict__[self.name] = self.func(instance)
      File "/app/.heroku/python/lib/python3.7/site-packages/django_lifecycle/mixins.py", line 169, in _watched_fk_model_fields
        for method in self._potentially_hooked_methods:
      File "/app/.heroku/python/lib/python3.7/site-packages/django/utils/functional.py", line 80, in __get__
        res = instance.__dict__[self.name] = self.func(instance)
      File "/app/.heroku/python/lib/python3.7/site-packages/django_lifecycle/mixins.py", line 152, in _potentially_hooked_methods
        attr = getattr(self, name)
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/query_utils.py", line 135, in __get__
        instance.refresh_from_db(fields=[self.field_name])
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/base.py", line 628, in refresh_from_db
        db_instance = db_instance_qs.get()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/query.py", line 402, in get
        num = len(clone)
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/query.py", line 256, in __len__
        self._fetch_all()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/query.py", line 1242, in _fetch_all
        self._result_cache = list(self._iterable_class(self))
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/query.py", line 55, in __iter__
        results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1127, in execute_sql
        sql, params = self.as_sql()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 474, in as_sql
        extra_select, order_by, group_by = self.pre_sql_setup()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 54, in pre_sql_setup
        self.setup_query()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 45, in setup_query
        self.select, self.klass_info, self.annotation_col_map = self.get_select()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 219, in get_select
        cols = self.get_default_columns()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 641, in get_default_columns
        only_load = self.deferred_to_columns()
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1051, in deferred_to_columns
        self.query.deferred_to_data(columns, self.query.get_loaded_field_names_cb)
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/query.py", line 680, in deferred_to_data
        add_to_dict(seen, model, field)
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/sql/query.py", line 2167, in add_to_dict
        data[key] = {value}
      File "/app/.heroku/python/lib/python3.7/site-packages/django/db/models/fields/__init__.py", line 508, in __hash__
        return hash(self.creation_counter)
    RecursionError: maximum recursion depth exceeded while calling a Python object
    
    opened by andresmachado 5
  • Watching for ForeignKey value changes seems to not trigger the hook

    Watching for ForeignKey value changes seems to not trigger the hook

    Using the below example in version 0.6.0, I am expecting to print a message any time someone changes a user's first name that is saved in SomeModel. From what I can tell from the documentation, I am setting everything up correctly. Am I misunderstanding how this works?

    Assuming a model set up like this:

    from django.conf import settings
    from django.db import models
    from django_lifecycle import LifecycleModel, hook
    
    class SomeModel(LifecycleModel):
        user = models.ForeignKey(
            on_delete=models.CASCADE,
            to=settings.AUTH_USER_MODEL
        )
        # More fields here...
    
        @hook('after_update', when='user.first_name', has_changed=True)
        def user_first_name_changed(self):
            print(
                f"User's first_name has changed from "
                f"{self.initial_value('user.first_name')} to {user.first_name}!"
            )
    

    When we then perform the following code, nothing prints:

    from django.contrib.auth import get_user_model
    
    # Create a test user (Jane Doe)
    get_user_model().objects.create_user(
        username='test', 
        password=None, 
        first_name='Jane', 
        last_name='Doe'
    )
    
    # Create an instance
    SomeModel.objects.create(user=user)
    
    # Retrieve a new instance of the user
    user = get_user_model().objects.get(username='test')
    
    # Change the name (John Doe)
    user.first_name = 'John'
    user.save()
    
    # Nothing prints from the hook
    

    In the tests, I see that it is calling user_account._clear_watched_fk_model_cache() explicitly after changing the Organization.name. However, from looking at the code, I do not see this call anywhere except for the overridden UserAccount.save() method. Thus, saving the Organization has no way to notify the UserAccount that a change has been made, and therefore, the hook cannot possibly be fired. The only reason that I can see that the test is passing is because of the explicit call to user_account._clear_watched_fk_model_cache().

        def test_has_changed_is_true_if_fk_related_model_field_has_changed(self):
            org = Organization.objects.create(name="Dunder Mifflin")
            UserAccount.objects.create(**self.stub_data, organization=org)
            user_account = UserAccount.objects.get()
    
            org.name = "Dwight's Paper Empire"
            org.save()
            user_account._clear_watched_fk_model_cache()
            self.assertTrue(user_account.has_changed("organization.name"))
    
    opened by michaeljohnbarr 5
  • Skip GenericForeignKey fields

    Skip GenericForeignKey fields

    The GenericForeignKey field does not provide a get_internal_type method so when checking if it's a ForeignKey or not an AttributeError is raised.

    This adjusts the code to ignore this AttributeError which effectively un-monitors the GenericForeignKey itself. However, it does leave the underlying ForeignKey to the ContentType table and the primary key storage field indexing into that table monitored. This does not enable support for hooking on the name of the GenericForeignKey, but hooking on the underlying fields that support that GenericForeignKey should still be possible.

    closes #42

    opened by bmbouter 4
  • README.md not included in dist?

    README.md not included in dist?

    I ran into this earlier and it looks like maybe your README.md is not being included in 0.4.1:

    Collecting django-lifecycle
      Using cached https://files.pythonhosted.org/packages/d4/ab/9daddd333fdf41bf24da744818a00ce8caa8e39d93da466b752b291ce412/django-lifecycle-0.4.1.tar.gz
        ERROR: Complete output from command python setup.py egg_info:
        ERROR: Traceback (most recent call last):
          File "<string>", line 1, in <module>
          File "/private/var/folders/pb/j_dpdd4n0858j1ym98g3884r0000gn/T/pip-install-x3_d5nyd/django-lifecycle/setup.py", line 30, in <module>
            long_description=readme(),
          File "/private/var/folders/pb/j_dpdd4n0858j1ym98g3884r0000gn/T/pip-install-x3_d5nyd/django-lifecycle/setup.py", line 7, in readme
            with open("README.md", "r") as infile:
          File "/Users/jefftriplett/.pyenv/versions/3.6.5/lib/python3.6/codecs.py", line 897, in open
            file = builtins.open(filename, mode, buffering)
        FileNotFoundError: [Errno 2] No such file or directory: 'README.md'
        ----------------------------------------
    ERROR: Command "python setup.py egg_info" failed with error code 1 in /private/var/folders/pb/j_dpdd4n0858j1ym98g3884r0000gn/T/pip-install-x3_d5nyd/django-lifecycle/
    
    opened by jefftriplett 4
  • AFTER_DELETE hook on ManyToMany relationships

    AFTER_DELETE hook on ManyToMany relationships

    Hi, I'm writing this issue because I think the AFTER_DELETE hook does not work as expected on m2m relationships. If we have something like that:

    class Product(LifecycleModel):
    	title = models.Charfield(max_length=100)
    	images = models.ManyToManyField(
            to="ProductImage", through="ProductImageRelationship", related_name="products"
        )
    
    class ProductImageRelationship(LifecycleModel):
        product = models.ForeignKey("Product", on_delete=models.CASCADE)
        image = models.ForeignKey("ProductImage", on_delete=models.CASCADE)
        order = models.IntegerField(default=0, help_text="Lower number, higher priority")
    
    class ProductImage(LifecycleModel):
    	field_name = models.CharField(max_length=40)
    

    If I write a hook on ProductImageRelationship model like this:

        @hook(AFTER_DELETE, on_commit=True)
        def deleting_image(self):
            print("Image deleted...")
    

    the hook is never triggered when I do

    p = Product.objects.get(pk=123)
    i = p.images.first()
    # to remove image from product do
    p.images.remove(i)
    # or do this
    i.products.remove(p)
    

    However, If I add a receiver like this:

    @receiver(post_delete, sender=ProductImageRelationship)
    def deleting_image(sender, instance, **kwargs):
        print("Image deleted...")
    

    The receiver is triggered as is expected.

    I think I'm doing it correctly :confused: but I'm not sure completely.

    opened by mateocpdev 0
  • Reset initial state using a on_commit transaction

    Reset initial state using a on_commit transaction

    After saving an instance, reset the _initial_state using a on_commit callback. This makes the has_changed and initial_value API work with hooks that run with on_commit=True.

    Fixes #117

    opened by alb3rto269 5
  • select_related doesn't work with ForeignKey or OneToOneField

    select_related doesn't work with ForeignKey or OneToOneField

    When you use the dot notation in @hook decorator for the related fields (ForeignKey or OneToOneField) it hits the database for every object separately. It doesn't matter if you use select_related or not. Here are the models to test:

    from django.contrib.auth.models import User
    from django.db import models
    
    from django_lifecycle import LifecycleModel, hook, AFTER_SAVE
    
    
    class Organization(models.Model):
        name = models.CharField(max_length=250)
    
    
    class Profile(LifecycleModel):
        user = models.OneToOneField(User, on_delete=models.CASCADE, null=True)
        employer = models.ForeignKey(Organization, on_delete=models.SET_NULL, null=True)
        bio = models.TextField(null=True, blank=True)
        age = models.PositiveIntegerField(null=True, blank=True)
    
        @hook(AFTER_SAVE, when='user.first_name', has_changed=True)
        @hook(AFTER_SAVE, when='user.last_name', has_changed=True)
        def user_changed(self):
            print('User was changed')
    
        @hook(AFTER_SAVE, when='employer.name', has_changed=True)
        def employer_changed(self):
            print('Employer was changed')
    

    What I got when tried to fetch profiles (with db queries logging):

    >>> from main.models import Profile
    >>> queryset = Profile.objects.all()[:10]
    >>> queryset
    (0.000) SELECT "main_profile"."id", "main_profile"."user_id", "main_profile"."employer_id", "main_profile"."bio", "main_profile"."age" FROM "main_profile" LIMIT 10; args=(); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 2 LIMIT 21; args=(2,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 3 LIMIT 21; args=(3,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 4 LIMIT 21; args=(4,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 5 LIMIT 21; args=(5,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 6 LIMIT 21; args=(6,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 7 LIMIT 21; args=(7,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 8 LIMIT 21; args=(8,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 9 LIMIT 21; args=(9,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 10 LIMIT 21; args=(10,); alias=default
    <QuerySet [<Profile: Profile object (1)>, <Profile: Profile object (2)>, <Profile: Profile object (3)>, <Profile: Profile object (4)>, <Profile: Profile object (5)>, <Profile: Profile object (6)>, <Profile: Profile object (7)>, <Profile: Profile object (8)>, <Profile: Profile object (9)>, <Profile: Profile object (10)>]>
    
    >>> queryset = Profile.objects.select_related('user')[:10]
    >>> queryset
    (0.001) SELECT "main_profile"."id", "main_profile"."user_id", "main_profile"."employer_id", "main_profile"."bio", "main_profile"."age", "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "main_profile" LEFT OUTER JOIN "auth_user" ON ("main_profile"."user_id" = "auth_user"."id") LIMIT 10; args=(); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 2 LIMIT 21; args=(2,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 3 LIMIT 21; args=(3,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 4 LIMIT 21; args=(4,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 5 LIMIT 21; args=(5,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 6 LIMIT 21; args=(6,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 7 LIMIT 21; args=(7,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 8 LIMIT 21; args=(8,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 9 LIMIT 21; args=(9,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 10 LIMIT 21; args=(10,); alias=default
    <QuerySet [<Profile: Profile object (1)>, <Profile: Profile object (2)>, <Profile: Profile object (3)>, <Profile: Profile object (4)>, <Profile: Profile object (5)>, <Profile: Profile object (6)>, <Profile: Profile object (7)>, <Profile: Profile object (8)>, <Profile: Profile object (9)>, <Profile: Profile object (10)>]>
    
    >>> queryset = Profile.objects.select_related('user', 'employer')[:10]
    >>> queryset
    (0.001) SELECT "main_profile"."id", "main_profile"."user_id", "main_profile"."employer_id", "main_profile"."bio", "main_profile"."age", "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined", "main_organization"."id", "main_organization"."name" FROM "main_profile" LEFT OUTER JOIN "auth_user" ON ("main_profile"."user_id" = "auth_user"."id") LEFT OUTER JOIN "main_organization" ON ("main_profile"."employer_id" = "main_organization"."id") LIMIT 10; args=(); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 2 LIMIT 21; args=(2,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 3 LIMIT 21; args=(3,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 4 LIMIT 21; args=(4,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 5 LIMIT 21; args=(5,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 6 LIMIT 21; args=(6,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 7 LIMIT 21; args=(7,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 8 LIMIT 21; args=(8,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 9 LIMIT 21; args=(9,); alias=default
    (0.000) SELECT "main_organization"."id", "main_organization"."name" FROM "main_organization" WHERE "main_organization"."id" = 1 LIMIT 21; args=(1,); alias=default
    (0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 10 LIMIT 21; args=(10,); alias=default
    <QuerySet [<Profile: Profile object (1)>, <Profile: Profile object (2)>, <Profile: Profile object (3)>, <Profile: Profile object (4)>, <Profile: Profile object (5)>, <Profile: Profile object (6)>, <Profile: Profile object (7)>, <Profile: Profile object (8)>, <Profile: Profile object (9)>, <Profile: Profile object (10)>]>
    
    opened by SimaDovakin 0
  • fix(sec): upgrade Django to 4.0.6

    fix(sec): upgrade Django to 4.0.6

    What happened?

    There are 1 security vulnerabilities found in Django 3.2.8

    What did I do?

    Upgrade Django from 3.2.8 to 4.0.6 for vulnerability fix

    What did you expect to happen?

    Ideally, no insecure libs should be used.

    The specification of the pull request

    PR Specification from OSCS

    opened by 645775992 0
  • Should has_changed/initial_value work with on_commit hooks?

    Should has_changed/initial_value work with on_commit hooks?

    First of all, thanks for this project. I used to rely a lot on the built-in django signals. However, I have a project that is growing fast and django-lifecycle is helping us to bring some order to all these before_* and after_* actions.

    One of my use-cases requires 2 features that django-lifecycle offers:

    • The ability to compare against the initial state, i.e. obj.has_changed('field_name').
    • Running hooks on commit to trigger background tasks.

    Both features work well by separate. However calling has_changed or initial_value from a on_commit hook compares against the already saved state.

    Looking into the code I noticed that the reason is that the save method resets the _inital_state just before returning:

    [django_lifecycle/mixins.py#L177]

    @transaction.atomic
    def save(self, *args, **kwargs):
        # run before_* hooks
        save(...)
        # run after_* hooks
    
        self._initial_state = self._snapshot_state()
    

    To reproduce the issue you can use this case:

    from django_lifecycle import LifecycleModel, AFTER_UPDATE, hook
    
    
    class MyModel(LifecycleModel):
        foo = models.CharField(max_length=3)
    
        @hook(AFTER_UPDATE, on_commit=True)
        def my_hook(self):
            assert self.has_changed('foo')   # <-- fails
    
    obj = MyModel.objects.create(foo='bar')
    obj.foo = 'baz'
    obj.save()
    

    I think It is arguable if this behavior is expected or if it is a bug. If it is expected, probably we should add a note in the docs mentioning that has_changed and initial_state does not make sense with on_commit=True. If it is a bug, any idea how to address it? I can contribute with a PR if necessary and if we agree on a solution.

    opened by alb3rto269 5
Releases(1.0.0)
Owner
Robert Singer
Tech lead at The ABIS Group.
Robert Singer
django-reversion is an extension to the Django web framework that provides version control for model instances.

django-reversion django-reversion is an extension to the Django web framework that provides version control for model instances. Requirements Python 3

Dave Hall 2.8k Jan 2, 2023
Django model mixins and utilities.

django-model-utils Django model mixins and utilities. django-model-utils supports Django 2.2+. This app is available on PyPI. Getting Help Documentati

Jazzband 2.4k Jan 4, 2023
Store model history and view/revert changes from admin site.

django-simple-history django-simple-history stores Django model state on every create/update/delete. This app supports the following combinations of D

Jazzband 1.8k Jan 8, 2023
A reusable Django model field for storing ad-hoc JSON data

jsonfield jsonfield is a reusable model field that allows you to store validated JSON, automatically handling serialization to and from the database.

Ryan P Kilby 1.1k Jan 3, 2023
Improved Django model inheritance with automatic downcasting

Polymorphic Models for Django Django-polymorphic simplifies using inherited models in Django projects. When a query is made at the base model, the inh

null 1.4k Jan 3, 2023
A django model and form field for normalised phone numbers using python-phonenumbers

django-phonenumber-field A Django library which interfaces with python-phonenumbers to validate, pretty print and convert phone numbers. python-phonen

Stefan Foulis 1.3k Dec 31, 2022
Store model history and view/revert changes from admin site.

django-simple-history django-simple-history stores Django model state on every create/update/delete. This app supports the following combinations of D

Jazzband 1.8k Jan 6, 2023
MAC address Model Field & Form Field for Django apps

django-macaddress MAC Address model and form fields for Django We use netaddr to parse and validate the MAC address. The tests aren't complete yet. Pa

null 49 Sep 4, 2022
A django model and form field for normalised phone numbers using python-phonenumbers

django-phonenumber-field A Django library which interfaces with python-phonenumbers to validate, pretty print and convert phone numbers. python-phonen

Stefan Foulis 1.3k Dec 31, 2022
Django Pickled Model

Django Pickled Model Django pickled model provides you a model with dynamic data types. a field can store any value in any type. You can store Integer

Amir 3 Sep 14, 2022
Open source platform for the machine learning lifecycle

MLflow: A Machine Learning Lifecycle Platform MLflow is a platform to streamline machine learning development, including tracking experiments, packagi

MLflow 13.3k Jan 4, 2023
PrimaryBid - Transform application Lifecycle Data and Design and ETL pipeline architecture for ingesting data from multiple sources to redshift

Transform application Lifecycle Data and Design and ETL pipeline architecture for ingesting data from multiple sources to redshift This project is composed of two parts: Part1 and Part2

Emmanuel Boateng Sifah 1 Jan 19, 2022
:fishing_pole_and_fish: List of `pre-commit` hooks to ensure the quality of your `dbt` projects.

pre-commit-dbt List of pre-commit hooks to ensure the quality of your dbt projects. BETA NOTICE: This tool is still BETA and may have some bugs, so pl

Offbi 262 Nov 25, 2022
Cross-platform .NET Core pre-commit hooks

dotnet-core-pre-commit Cross-platform .NET Core pre-commit hooks How to use Add this to your .pre-commit-config.yaml - repo: https://github.com/juan

Juan Odicio 5 Jul 20, 2021
Git Hooks Tutorial.

Git Hooks Tutorial My public talk about this project at Sberloga: Git Hooks Is All You Need 1. Git Hooks 101 Init git repo: mkdir git_repo cd git_repo

Dani El-Ayyass 17 Oct 12, 2022
A collection of pre-commit hooks for handling text files.

texthooks A collection of pre-commit hooks for handling text files. In particular, hooks for handling unicode characters which may be undesirable in a

Stephen Rosen 5 Oct 28, 2022
Hooks for VCOCO

Verbs in COCO (V-COCO) Dataset This repository hosts the Verbs in COCO (V-COCO) dataset and associated code to evaluate models for the Visual Semantic

Saurabh Gupta 131 Nov 24, 2022
Provides guideline on how to configure pre-commit hooks in your own python project

Pre-commit Configuration Guide The main aim of this repository is to act as a guide on how to configure the pre-commit hooks in your existing python p

Faraz Ahmed Khan 2 Mar 31, 2022
MLflow App Using React, Hooks, RabbitMQ, FastAPI Server, Celery, Microservices

Katana ML Skipper This is a simple and flexible ML workflow engine. It helps to orchestrate events across a set of microservices and create executable

Tom Xu 8 Nov 17, 2022