3

I've a huge model with a lot of attributes including multiple ManyToManyMapping. Most of the addition/update in app is via REST API, but for minor correction I have used Django Admin Form. This admin form also has multiple inline formset.

I want to publish some event to Kafka(publish_event) after the model is updated either through form or REST API. And I want this to happen when the transaction is committed to DB so that services listening to Kafka events don't end up fetching stale data from DB.

I referred this SO post but it appears to be doing it on every transaction not on per model basis and having on_commit poses problems of things getting called twice(more below).

Things I've tried so far:

  1. Signals: Rejected since due to adding ManyToManyMapping, model.save() needs to be called twice which ended up with 2 events published. Also, it operates on model save, not transaction commit, so in case of rollback, I will still end up with publishing an event.

  2. Overriding model's save(self, *args, **kwargs): method: Rejected for same reason as model.save() is called twice.

  3. Overriding ModelAdmin's save_model: This is one of the first things to be called when we hit Save on form, so overriding this is not helping because formset's are still not processed yet. So, full state including M2M mappings are not committed in DB.

def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) publish_event()

  1. Overriding ModelAdmin's save_related: This seemed to be the solution at first, but again transaction is not yet committed to DB. def save_related(self, request, form, formsets, change): form.save_m2m() for formset in formsets: self.save_formset(request, form, formset, change=change) publish_event() So far I'm yet to figure out any callback triggered post transaction commit.
Aniket Sinha
  • 6,001
  • 6
  • 37
  • 50

2 Answers2

4

TLDR: Override change_view


After digging into the source code file django.contrib.admin.option.py, it appears that saving model and related M2M is being triggered by this code within _changeform_view:

if all_valid(formsets) and form_validated:
    self.save_model(request, new_object, form, not add)
    self.save_related(request, form, formsets, not add)
    change_message = self.construct_change_message(request, form, formsets, add)

which is getting called by changeform_view which sets the atomic transaction. This is what I wanted to override so that I can execute publish_event once things are committed to DB:

@csrf_protect_m
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
    with transaction.atomic(using=router.db_for_write(self.model)):
        return self._changeform_view(request, object_id, form_url, extra_context)

This code in turn is called by change_view and add_view.

def change_view(self, request, object_id, form_url='', extra_context=None):
    return self.changeform_view(request, object_id, form_url, extra_context)

Since I'm making only updates(not create) via form, I overrode change_view to explicitly call publish_event:

def change_view(self, request, object_id, form_url='', extra_context=None):
    change_resp = super(MySampleModelAdmin, self).change_view(request, object_id, form_url, extra_context)
    if request.method != 'GET': # since GET also call this and we don't want event published on GET
        publish_event()
    return change_resp

As soon as change_resp = super(MySampleModelAdmin, self).change_view(request, object_id, form_url, extra_context) is done with execution, transaction is committed, so it's safe to call publish_event at this step. After this change_view just expects a response in return.


EDIT: Tried on_commit, and this seems to work too. This is based on signals.

from django.db import transaction

@receiver(post_save, sender='app.MySampleModel')
def send_model_save_event(sender, instance=None, created=False, **kwargs):
    if instance is None:
        log.info('Instance is Null')
        return
    transaction.on_commit(lambda: handle_model_after_save(instance.id))
Aniket Sinha
  • 6,001
  • 6
  • 37
  • 50
0

transaction.on_commit(lambda: handle_model_after_save(instance.id))

saved my life!

  • 1
    Hi and welcome to Stack Overflow! Thanks for answering but can you also add an explanation on how your code solves the issue? Also check the [help center](https://stackoverflow.com/editing-help) for info on how to format code – Tyler2P Aug 13 '21 at 12:10