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 API that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format).

"Casares News" API Api that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format). Usage Consume the articles

Andrés Milla 6 May 12, 2022
Exemplo de biblioteca com Django

Bookstore Exemplo de biblioteca feito com Django. Este projeto foi feito com: Python 3.9.7 Django 3.2.8 Django Rest Framework 3.12.4 Bootstrap 4.0 Vue

Regis Santos 1 Oct 28, 2021
Sistema de tratamento e análise de grandes volumes de dados através de técnicas de Data Science

Sistema de tratamento e análise de grandes volumes de dados através de técnicas de data science Todos os scripts, gráficos e relatórios de todas as at

Arthur Quintanilha Neto 1 Sep 05, 2022
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

Daniel Lindsley 4 Dec 23, 2022
Per object permissions for Django

django-guardian django-guardian is an implementation of per object permissions [1] on top of Django's authorization backend Documentation Online docum

3.3k Jan 04, 2023
Neighbourhood - A python-django web app to help the residence of a given neighborhood know their surrounding better

Neighbourhood A python-django web app to help the residence of a given neighborh

Levy Omolo 4 Aug 25, 2022
Keep track of failed login attempts in Django-powered sites.

django-axes Axes is a Django plugin for keeping track of suspicious login attempts for your Django based website and implementing simple brute-force a

Jazzband 1.1k Dec 30, 2022
Built from scratch to replicate some of the Django admin functionality and add some more, to serve as an introspective interface for Django and Mongo.

django-mongonaut Info: An introspective interface for Django and MongoDB. Version: 0.2.21 Maintainer: Jazzband (jazzband.co) This Project is Being Mov

Jazzband 238 Dec 26, 2022
Учебное пособие по основам Django и сопутствующим технологиям

Учебный проект для закрепления основ Django Подробный разбор проекта здесь. Инструкция по запуску проекта на своей машине: Скачиваем репозиторий Устан

Stanislav Garanzha 12 Dec 30, 2022
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
Simple tagging for django

django-taggit This is a Jazzband project. By contributing you agree to abide by the Contributor Code of Conduct and follow the guidelines. django-tagg

Jazzband 3k Jan 02, 2023
DCM is a set of tools that helps you to keep your data in your Django Models consistent.

Django Consistency Model DCM is a set of tools that helps you to keep your data in your Django Models consistent. Motivation You have a lot of legacy

Occipital 59 Dec 21, 2022
Log and View requests made on Django

Django Request Viewer Log and view requests made on your Django App Introduction Recently, @ichtrojan and @toniastro released horus, a request logger

Akere Mukhtar 26 May 29, 2022
Opinionated boilerplate for starting a Django project together with React front-end library and TailwindCSS CSS framework.

Opinionated boilerplate for starting a Django project together with React front-end library and TailwindCSS CSS framework.

João Vítor Carli 10 Jan 08, 2023
Domain-driven e-commerce for Django

Domain-driven e-commerce for Django Oscar is an e-commerce framework for Django designed for building domain-driven sites. It is structured such that

Oscar 5.6k Jan 01, 2023
A pickled object field for Django

django-picklefield About django-picklefield provides an implementation of a pickled object field. Such fields can contain any picklable objects. The i

Gintautas Miliauskas 167 Oct 18, 2022
REST API with Django and SQLite3

REST API with Django and SQLite3

Luis Quiñones Requelme 1 Nov 07, 2021
This "I P L Team Project" is developed by Prasanta Kumar Mohanty using Python with Django web framework, HTML & CSS.

I-P-L-Team-Project This "I P L Team Project" is developed by Prasanta Kumar Mohanty using Python with Django web framework, HTML & CSS. Screenshots HO

1 Dec 15, 2021
A simple REST API to manage postal addresses, written in Python/Django.

A simple REST API to manage postal addresses, written in Python/Django.

Attila Bagossy 2 Feb 14, 2022
A simple Blog Using Django Framework and Used IBM Cloud Services for Text Analysis and Text to Speech

ElhamBlog Cloud Computing Course first assignment. A simple Blog Using Django Framework and Used IBM Cloud Services for Text Analysis and Text to Spee

Elham Razi 5 Dec 06, 2022