Declarative model lifecycle hooks, an alternative to Signals.

Overview

Django Lifecycle Hooks

Package version Python versions Python versions PyPI - Django Version

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

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

In short, you can write model code like this:

from django_lifecycle import LifecycleModel, hook, BEFORE_UPDATE, AFTER_UPDATE


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

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

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

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

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

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

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

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

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


Changelog

0.8.1 (January 2021)

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

0.8.0 (October 2020)

  • Significant performance improvements. Thanks @dralley!

0.7.7 (August 2020)

  • Fixes issue with GenericForeignKey. Thanks @bmbouter!

0.7.6 (May 2020)

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

0.7.5 (April 2020)

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

0.7.1 (January 2020)

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

0.7.0 (December 2019)

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

0.6.1 (November 2019)

  • Remove variable type annotation for Python 3.5 compatability.

0.6.0 (October 2019)

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

0.5.0 (September 2019)

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

0.4.2 (July 2019)

  • Fixes missing README.md issue that broke install.

0.4.1 (June 2019)

0.4.0 (May 2019)

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

0.3.2 (February 2019)

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

0.3.1 (August 2018)

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

0.3.0 (April 2018)

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

0.2.4 (April 2018)

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

0.2.3 (April 2018)

  • Removes residual mixin methods from earlier implementation.

0.2.2 (April 2018)

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

0.2.1 (April 2018)

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

0.2.0 (April 2018)

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

Testing

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

License

See License.

Comments
  • Order in which hooks are executed

    Order in which hooks are executed

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

    My use case is something like this:

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

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

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

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

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

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

    Feature: hooked methods cached on class

    Motivation

    1. _get_model_property_names

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

    2. _potentially_hooked_methods cached on instance

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

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

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

    3. depth of searching in _potentially_hooked_methods

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

    Solution

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

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

    Questions

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

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

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

    Lifecycle hook not triggered

    Hi,

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

    My model looks like this:

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

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

    Conversely, this works:

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

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

    opened by sondrelg 6
  • Implement priority to hooks

    Implement priority to hooks

    ...as discussed in #95

    What do you think of this approach?

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

    Feel free to comment, suggest or edit whatever

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

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

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

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

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

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

    Make django-lifecycle much, much faster

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

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

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

    Using `only` queryset method leads to a RecursionError

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

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

    Watching for ForeignKey value changes seems to not trigger the hook

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

    Assuming a model set up like this:

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

    When we then perform the following code, nothing prints:

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

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

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

    Skip GenericForeignKey fields

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

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

    closes #42

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

    README.md not included in dist?

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

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

    AFTER_DELETE hook on ManyToMany relationships

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

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

    If I write a hook on ProductImageRelationship model like this:

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

    the hook is never triggered when I do

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

    However, If I add a receiver like this:

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

    The receiver is triggered as is expected.

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

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

    Reset initial state using a on_commit transaction

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

    Fixes #117

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

    select_related doesn't work with ForeignKey or OneToOneField

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

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

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

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

    fix(sec): upgrade Django to 4.0.6

    What happened?

    There are 1 security vulnerabilities found in Django 3.2.8

    What did I do?

    Upgrade Django from 3.2.8 to 4.0.6 for vulnerability fix

    What did you expect to happen?

    Ideally, no insecure libs should be used.

    The specification of the pull request

    PR Specification from OSCS

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

    Should has_changed/initial_value work with on_commit hooks?

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

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

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

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

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

    [django_lifecycle/mixins.py#L177]

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

    To reproduce the issue you can use this case:

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

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

    opened by alb3rto269 5
Releases(1.0.0)
Owner
Robert Singer
Tech lead at The ABIS Group.
Robert Singer
django app that allows capture application metrics by each user individually

Django User Metrics django app that allows capture application metrics by each user individually, so after you can generate reports with aggregation o

Reiner Marquez 42 Apr 28, 2022
Getdp-project - A Django-built web app that generates a personalized banner of events to come

getdp-project https://get-my-dp.herokuapp.com/ A Django-built web app that gener

CODE 4 Aug 01, 2022
Django admin CKEditor integration.

Django CKEditor NOTICE: django-ckeditor 5 has backward incompatible code moves against 4.5.1. File upload support has been moved to ckeditor_uploader.

2.2k Jan 02, 2023
Django-Audiofield is a simple app that allows Audio files upload, management and conversion to different audio format (mp3, wav & ogg), which also makes it easy to play audio files into your Django application.

Django-Audiofield Description: Django Audio Management Tools Maintainer: Areski Contributors: list of contributors Django-Audiofield is a simple app t

Areski Belaid 167 Nov 10, 2022
Pipeline is an asset packaging library for Django.

Pipeline Pipeline is an asset packaging library for Django, providing both CSS and JavaScript concatenation and compression, built-in JavaScript templ

Jazzband 1.4k Jan 03, 2023
Bringing together django, django rest framework, and htmx

This is Just an Idea There is no code, this README just represents an idea for a minimal library that, as of now, does not exist. django-htmx-rest A l

Jack DeVries 5 Nov 24, 2022
Simple Login Logout System using Django, JavaScript and ajax.

Djanog-UserAuthenticationSystem Technology Use #version Python 3.9.5 Django 3.2.7 JavaScript --- Ajax Validation --- Login and Logout Functionality, A

Bhaskar Mahor 3 Mar 26, 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
Stream Framework is a Python library, which allows you to build news feed, activity streams and notification systems using Cassandra and/or Redis. The authors of Stream-Framework also provide a cloud service for feed technology:

Stream Framework Activity Streams & Newsfeeds Stream Framework is a Python library which allows you to build activity streams & newsfeeds using Cassan

Thierry Schellenbach 4.7k Jan 02, 2023
A django model and form field for normalised phone numbers using python-phonenumbers

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

Stefan Foulis 1.3k Dec 31, 2022
This is a repository for a web application developed with Django, built with Crowdbotics

assignment_32558 This is a repository for a web application developed with Django, built with Crowdbotics Table of Contents Project Structure Features

Crowdbotics 1 Dec 29, 2021
Sampling profiler for Python programs

py-spy: Sampling profiler for Python programs py-spy is a sampling profiler for Python programs. It lets you visualize what your Python program is spe

Ben Frederickson 9.5k Jan 01, 2023
Django Advance DumpData

Django Advance Dumpdata Django Manage Command like dumpdata but with have more feature to Output the contents of the database from given fields of a m

EhsanSafir 7 Jul 25, 2022
Django REST Client API

Django REST Client API Client data provider API.

Ulysses Monteiro 1 Nov 08, 2021
The little ASGI framework that shines. 🌟

✨ The little ASGI framework that shines. ✨ Documentation: https://www.starlette.io/ Community: https://discuss.encode.io/c/starlette Starlette Starlet

Encode 7.7k Dec 31, 2022
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 API written in Python using FastAPI to store and retrieve Books and Authors.

Simple API made with Python FastAPI WIP: Deploy in AWS with Terraform Simple API written in Python using FastAPI to store and retrieve Books and Autho

Caio Delgado 9 Oct 26, 2022
Basic Form Web Development using Python, Django and CSS

thebookrain Basic Form Web Development using Python, Django and CSS This is a basic project that contains two forms - borrow and donate. The form data

Ananya Dhulipala 1 Nov 27, 2021
Tweak the form field rendering in templates, not in python-level form definitions. CSS classes and HTML attributes can be altered.

django-widget-tweaks Tweak the form field rendering in templates, not in python-level form definitions. Altering CSS classes and HTML attributes is su

Jazzband 1.8k Jan 02, 2023
Djangoblog - A blogging platform built on Django and Python.

djangoblog 👨‍💻 A blogging platform built on Django and Python

Lewis Gentle 1 Jan 10, 2022