Implementing the Four-Eyes Principle in Django (Approvals? Moderation?)

Not sure if this is the right category/subcategory, but here we go:

I’ve been asked to implement the Four-Eyes Principle for every object that is created, updated, or deleted; i.e., any time a user wants to make a change, that change needs to be approved by another user before the change is actually implemented and made visible for all users. This is similar to moderation, but with more bells and whistles.

New object? Not visible for most users until the supervisor approves (author and supervisor can view).
Existing & approved object that’s updated? Pre-update values are displayed for users until the supervisor approves.
Supervisor rejects? Back to the reportee for review, perhaps with a comment.

I’ve looked at django-moderation, but m2m relations in models are not supported, and there are plenty of models with m2m relations that should be included in this process. Additionally, the last update was 2 years ago, and the highest version of Django it’s tested against is 3.2, which is losing support at the end of this month. So that’s out.

I’ve also explored django-approval, but I’ve found that this also has a shortcoming with m2m models.

Does anyone have a library or tool that they use & recommend for handling such a particular request?

Failing that, does anyone have any recommendations/guidance on how to proceed and implement my own bespoke solution to this problem?

I don’t know about any packages for this, but we effectively implemented this principle in a “workflow manager”. All changes go through a workflow of submission / approval.

We looked for workflow packages and solutions, but none had the degree of control that we needed, so we decided to write our own.

Every “Managed Model” has a status field associated with it, identifying its current position within the workflow. (Status may be things like “created”, “saved”, “submitted”, “approved”, “rejected”, etc.)

When a person in an appropriate role makes some type of change, the status of that data is set to “submitted”. The people in the roles that can approve those changes can see the items with that status and then approve or reject them. (Additionally, a person who submitted a change can withdraw the submission if it has not been approved, make edits, and resubmit the change.)

ManyToManyRelationships are managed by the status on the “through” model in that relationship. For example, there is an M2M relationship between Staff and Tasks. Assigning a staff member to a task is done by creating an entry in the through model. (Doing this does not require a change to either the Staff or the Task.)

The workflows themselves are stored in models, making them easy to customize for different installations. Also, it’s possible to define a workflow to bypass the approval step for functions specifically managed by particular rows. (For example, a “Task Manager” is allowed to create tasks without external approval.)

I won’t deny - it was a lot of work, with a lot of edge cases that needed to be addressed, but overall we’re quite satisfied with the results.

Ugh, this is my concern.

In your implementation, when a person in an appropriate role made a change to an object, was that object hidden from other users until the status was, e.g., ‘approved’? Or were the pre-change fields displayed until the changes themselves were approved?

I’m considering retrofitting either simple-history or reversion to display old versions until changes are approved, but this will complicate a lot of the existing code and probably be an absolute pain.

Failing that, are there any workflow managers that you recommend? Django-viewflow looks highly-regarded. In your experience, do you think this would fit my use-case? Or might it be best to just bite the bullet and create my own workflow manager?

It depends upon how the workflow is defined for that object. Objects in any state can be seen by any roles granted the authority to review objects in that state.

Basically, the mindset is that a workflow consists of an object occupying a state. At every state, there is a list of activities that can be performed on that object, and the roles that can perform those activities. So there really isn’t anything special about any state. If a general user is defined to be able to view an object in a state, then it can do so - otherwise not.

This is enforced by the model managers being used in concert with custom authorization tests created for the user model.

(Side note - in many cases, new data is added - data in a “completed” or “finished” state isn’t modified. If those objects need to be modified, then a new instance is created. The existing instance remains until the new version is complete - at which point the original is moved to an “obsolete” or “expired” state. I’m paraphrasing a bit, but I think you get the idea.)

No, I really can’t. The last we looked at this as a topic was about 2017/2018, and none at that time provided us with the flexibility and level of control that we needed. (For example, we have the ability to define access rights to objects based on time-of-day or day-of-week type requirements.) Once we decided to implement our own, I haven’t followed developments in that area since then.

Nor would it be fair for me to comment on the suitability of any of these packages for your use. Only you (and your team) are going to know what all you need this system to handle and what the edge cases you may be facing.