Skip to content

django

Async Planning

I've been thinking about this a bit more and this post is to progress detailed planning.

Note

Within this post I refer sometimes to "wrapped" method or function or task. What I mean by that is the (synchronous) function that is "wrapped" by django_q.tasks.async_task().

Issues

There are 5 issues, each of which need to be considered at the level of powercrud as well as of downstream projects:

  1. Conflict detection: maintain records of object ids that are subject to currently running async tasks. Prevent initiation of update (synchronous or asynchronous) if there is a clash. The conflict management framework needs to be shared across both levels. Need to use either (redis) cache or custom database model.
  2. Dashboard: I'd prefer to keep this at the downstream project level if possible, rather than bogging powercrud down with extraneous models required to support this stuff. But if a dashboard (and supporting model) is needed, then we need some way of getting powercrud bulk update async tasks into the dashboard. So need hooks or override methods or something.
  3. Progress Updates: the "easy" pattern is to always write from within the "wrapped" task (ie the synchronous task that is called using async_task()) to the redis cache (or a custom model) and then use htmx polling on the front end where needed. We need front end progress updates probably in powercrud on the modals that pop-up to indicate conflict with existing async task. If we use redis then there's no need to worry about extraneous models.
  4. Launching async task: this is more about having a common pattern for launching, since at both levels there is a need to share common conflict management framework, and possibly also the dashboard and progress frameworks.
  5. Error handling cascades: we need a way to ensure cleanup of problematic task instances, including cleaning up associated progress and conflict and custom dashboard data.

Django Async Task Conflict Prevention for PowerCRUD

By introducing async processing we introduce potential conflicts. Also want to allow downstream project developers to use the same base mechanism for conflict detection, in case they want to run async processes independent of powercrud (eg save() method that updates child and descendant objects) but want overall conflict detection.

Problems to Solve

  1. Lock conflicts - preventing simultaneous updates to same objects
  2. User state confusion - users making decisions based on stale data while async tasks are running
  3. Race conditions - timing-dependent bugs in concurrent operations
  4. Complex dependencies - bulk operations, single saves with descendant updates, and async tasks can all affect overlapping sets of objects
  5. Downstream flexibility - powercrud needs to work with any downstream project's specific object relationships

Debugging 405 Method Not Allowed in Django powercrud Bulk Actions

The Problem: 405 on Toggle All Selection

When attempting to use the "toggle all selection" checkbox in the Django powercrud list view, a 405 Method Not Allowed error was encountered for the POST request to /sample/bigbook/toggle-all-selection/. This indicated that while the URL was being matched, the HTTP POST method was not permitted for the associated view.

Initial Investigation & The "Pretence"

The JavaScript frontend correctly sends a POST request. The Django URL configuration in powercrud/mixins/url_mixin.py for this endpoint explicitly included http_method_names=["post"]:

path(
    f"{cls.url_base}/toggle-all-selection/",
    cls.as_view(
        role=Role.LIST, # The "pretence"
        http_method_names=["post"],
        template_name_suffix="_toggle_all_selection",
    ),
    name=f"{cls.url_base}-toggle-all-selection",
)

The toggle_all_selection_view method in powercrud/mixins/bulk_mixin.py was the intended handler. However, debug messages from this view were not appearing, suggesting the view method itself was never reached.

The core issue stemmed from the role=Role.LIST assignment. While seemingly logical (as the action relates to the list view), neapolitan's Role.LIST enum is designed to handle GET requests for listing ({"get": "list"}). It does not define a POST handler.

The Flow of Failure

  1. Frontend (JavaScript): Sends POST request to /sample/bigbook/toggle-all-selection/.
  2. Django URL Dispatcher: Matches the URL to the path defined in UrlMixin.get_urls.
  3. neapolitan.views.CRUDView.as_view():
    • Receives role=Role.LIST.
    • Consults Role.LIST.handlers(), which only specifies {"get": "list"}.
    • Dynamically sets self.get = self.list on the view instance.
    • Crucially, it does NOT set self.post because Role.LIST.handlers() lacks a post entry. The http_method_names=["post"] parameter is noted by Django's generic View but doesn't override neapolitan's Role-based method mapping.
  4. CRUDView.dispatch():
    • Receives the POST request.
    • Checks for a self.post method.
    • Finds no self.post method (as it was never set by as_view() for Role.LIST).
    • Returns 405 Method Not Allowed.

The toggle_all_selection_view method is never executed because the request is rejected at the dispatch level.

The Solution: Speaking neapolitan's Language

The neapolitan framework provides a clear pattern for extending its functionality with custom actions, as demonstrated by BulkEditRole. Instead of trying to force custom POST actions into existing Role enums (like Role.LIST) that don't have POST handlers, the solution is to define dedicated "Role-like" objects for these custom actions.

We implemented the Single Enum for Core Bulk Actions approach. We created a new enum called BulkActions in powercrud/mixins/bulk_mixin.py. This enum has members for each bulk action (TOGGLE_SELECTION, CLEAR_SELECTION, TOGGLE_ALL_SELECTION). Each enum member defines its own handlers() and URL patterns, similar to how neapolitan.Role itself is structured.

This solution ensures that neapolitan's as_view method receives a role object that correctly defines a POST handler for the specific action, allowing the dispatch method to route the request to the intended view function and resolve the 405 error. This adheres to the framework's design philosophy, leading to more maintainable and robust code.