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
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 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 04, 2023
pdm-django: Django command shortcuts for PDM

pdm-django: Django command shortcuts for PDM A plugin that gives you command shortcuts for developing with PDM. pdm run python manage.py runserver -

Neutron Sync 2 Aug 11, 2022
Django-Text-to-HTML-converter - The simple Text to HTML Converter using Django framework

Django-Text-to-HTML-converter This is the simple Text to HTML Converter using Dj

Nikit Singh Kanyal 6 Oct 09, 2022
Django Girls Tutorial Workshop

Django Girls Tutorial Workshop A log of activities during the workshop. this is an H2 git remote add origin https://github.com/ahuimanu/django_girls_t

Jeffry Babb 1 Oct 27, 2021
Automatically deletes old file for FileField and ImageField. It also deletes files on models instance deletion.

Django Cleanup Features The django-cleanup app automatically deletes files for FileField, ImageField and subclasses. When a FileField's value is chang

Ilya Shalyapin 838 Dec 30, 2022
Twitter Bootstrap for Django Form

Django bootstrap form Twitter Bootstrap for Django Form. A simple Django template tag to work with Bootstrap Installation Install django-bootstrap-for

tzangms 557 Oct 19, 2022
Serve files with Django.

django-downloadview django-downloadview makes it easy to serve files with Django: you manage files with Django (permissions, filters, generation, ...)

Jazzband 328 Dec 07, 2022
Twitter Bootstrap for Django Form - A simple Django template tag to work with Bootstrap

Twitter Bootstrap for Django Form - A simple Django template tag to work with Bootstrap

tzangms 557 Oct 19, 2022
Simple application TodoList django with ReactJS

Django & React Django We basically follow the Django REST framework quickstart guide here. Create backend folder with a virtual Python environment: mk

Flavien HUGS 2 Aug 07, 2022
A web app which allows user to query the weather info of any place in the world

weather-app This is a web app which allows user to get the weather info of any place in the world as soon as possible. It makes use of OpenWeatherMap

Oladipo Adesiyan 3 Sep 20, 2021
Flashback is an awesome, retro IRC based app built using Django

Flashback Flashback is an awesome, retro IRC based app built using Django (and the Django Rest Framework) for the backend as well as React for the fro

Unloading Gnat 1 Dec 22, 2021
A starter template for building a backend with Django and django-rest-framework using docker with PostgreSQL as the primary DB.

Django-Rest-Template! This is a basic starter template for a backend project with Django as the server and PostgreSQL as the database. About the templ

Akshat Sharma 11 Dec 06, 2022
Django Fett is an incomplete code generator used on several projects

Django Fett Django Fett is an incomplete code generator used on several projects. This is an attempt to clean it up and make it public for consumption

Jeff Triplett 6 Dec 31, 2021
Django React Project Setup

Django-React-Project-Setup INSTALLATION: python -m pip install drps USAGE: in your cmd: python -m drps Starting fullstack project with Django and Reac

Ghazi Zabalawi 7 Feb 06, 2022
A calendaring app for Django. It is now stable, Please feel free to use it now. Active development has been taken over by bartekgorny.

Django-schedule A calendaring/scheduling application, featuring: one-time and recurring events calendar exceptions (occurrences changed or cancelled)

Tony Hauber 814 Dec 26, 2022
django-compat-lint

django_compat_lint -- check Django compatibility of your code Django's API stability policy is nice, but there are still things that change from one v

James Bennett 40 Sep 30, 2021
A beginner django project and also my first Django project which involves shortening of a longer URL into a short one using a unique id.

Django-URL-Shortener A beginner django project and also my first Django project which involves shortening of a longer URL into a short one using a uni

Rohini Rao 3 Aug 08, 2021
A quick way to add React components to your Django templates.

Django-React-Templatetags This django library allows you to add React (16+) components into your django templates. Features Include react components u

Fröjd Agency 408 Jan 08, 2023
simple project management tool for educational purposes

Taskcamp This software is used for educational and demonstrative purposes. It's a simple project management tool powered by Django Framework Install B

Ilia Dmitriev 6 Nov 08, 2022