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
🔥 Campus-Run Django Server🔥

🏫 Campus-Run Campus-Run is a 3D racing game set on a college campus. Designed this service to comfort university students who are unable to visit the

Youngkwon Kim 1 Feb 08, 2022
Login System Django

Login-System-Django Login System Using Django Tech Used Django Python Html Run Locally Clone project git clone https://link-to-project Get project for

Nandini Chhajed 6 Dec 12, 2021
A slick ORM cache with automatic granular event-driven invalidation.

Cacheops A slick app that supports automatic or manual queryset caching and automatic granular event-driven invalidation. It uses redis as backend for

Alexander Schepanovski 1.7k Jan 03, 2023
django social media app with real time features

django-social-media django social media app with these features: signup, login and old registered users are saved by cookies posts, comments, replies,

8 Apr 30, 2022
Radically simplified static file serving for Python web apps

WhiteNoise Radically simplified static file serving for Python web apps With a couple of lines of config WhiteNoise allows your web app to serve its o

Dave Evans 2.1k Dec 15, 2022
Cached file system for online resources in Python

Minato Cache & file system for online resources in Python Features Minato enables you to: Download & cache online recsources minato supports the follo

Yasuhiro Yamaguchi 10 Jan 04, 2023
A Django app for managing robots.txt files following the robots exclusion protocol

Django Robots This is a basic Django application to manage robots.txt files following the robots exclusion protocol, complementing the Django Sitemap

Jazzband 406 Dec 26, 2022
An insecure login and registration website with Django.

An insecure login and registration website with Django.

Luis Quiñones Requelme 1 Dec 05, 2021
Modular search for Django

Haystack Author: Daniel Lindsley Date: 2013/07/28 Haystack provides modular search for Django. It features a unified, familiar API that allows you to

Haystack Search 3.4k Jan 08, 2023
Probably the best abstract model / admin for your tree based stuff.

django-treenode Probably the best abstract model / admin for your tree based stuff. Features Fast - get ancestors, children, descendants, parent, root

Fabio Caccamo 360 Jan 05, 2023
Intellicards-backend - A Django project bootstrapped with django-admin startproject mysite

Intellicards-backend - A Django project bootstrapped with django-admin startproject mysite

Fabrizio Torrico 2 Jan 13, 2022
Template for Django Project Using Docker

You want a Django project who use Docker and Docker-compose for Development and for Production ? It's for you !

1 Dec 17, 2021
django-quill-editor makes Quill.js easy to use on Django Forms and admin sites

django-quill-editor django-quill-editor makes Quill.js easy to use on Django Forms and admin sites No configuration required for static files! The ent

lhy 139 Dec 05, 2022
Full featured redis cache backend for Django.

Redis cache backend for Django This is a Jazzband project. By contributing you agree to abide by the Contributor Code of Conduct and follow the guidel

Jazzband 2.5k Jan 03, 2023
The friendly PIL fork (Python Imaging Library)

Pillow Python Imaging Library (Fork) Pillow is the friendly PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lund

Pillow 10.4k Jan 03, 2023
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
Phoenix LiveView but for Django

Reactor, a LiveView library for Django Reactor enables you to do something similar to Phoenix framework LiveView using Django Channels. What's in the

Eddy Ernesto del Valle Pino 526 Jan 02, 2023
PEP-484 stubs for Django

pep484 stubs for Django This package contains type stubs and a custom mypy plugin to provide more precise static types and type inference for Django f

TypedDjango 1.1k Dec 30, 2022
A collection of models, views, middlewares, and forms to help secure a Django project.

Django-Security This package offers a number of models, views, middlewares and forms to facilitate security hardening of Django applications. Full doc

SD Elements 258 Jan 03, 2023
Django API without Django REST framework.

Django API without DRF This is a API project made with Django, and without Django REST framework. This project was done with: Python 3.9.8 Django 3.2.

Regis Santos 3 Jan 19, 2022