Designing a simple audit log
This blog post is an update of my journey in Djangonaut Space , an 8-week mentoring program for future open source developers in the Django ecosystem.
Series:
- 3… 2… 1… becoming a Djangonaut
- Djangonaut Log 1: Lift-off and first week in Djangonaut Space
- Debugging a flaky test — how I methodologically tamed an annoying bug
- Assumptions are sneaky
- Djangonaut Log 2: Leaving the atmosphere and first merged contributions
- Djangonaut Log 3: Into the orbit!
- Djangonaut Log 4: Avoiding asteroids
- Designing a simple audit log (you are here)
One of the first bigger fixes I was interested in from the issues list when I started working on Django Girls+ project in Djangonaut Space was a bug fix for admins being able to change the RSVP status of an applicant from an admin view . In the beginning, I got sidetracked many times to fix other things so that we could get the development environment all good to go for new contributors (including me).
The situation
Over the past 3 weeks, I’ve been experimenting with this ticket here and there while waiting for reviews on other PRs.
When a person applies to a Django Girls event, they are added to a list of applications that the organisers then go through to decide who gets a spot. If an applicant is accepted, they receive an email with links to RSVP either yes or no. Sometimes, people click the wrong link accidentally or something comes up later and they need to change their RSVP.
The event organisers have a view listing all the applications and their status and there was a dropdown for changing the RSVP on behalf of the applicant but something had gone awry and it didn’t work anymore, hence the ticket.
The original, now broken implementation was a Boostrap/jQuery dropdown from a previous era of web development. I had a chat with Anna, the maintainer and our Djangonaut Space Navigator for the project, and we came to a conclusion that this is a good moment to not only fix the bug but to start taking out the old Bootstrap/jQuery bits and rewrite them in more modern means.
I rewrote the HTML and Javascript responsible for the interactions on that
page, ripping out the old solutions and replacing them with native
select
elements and vanilla Javascript event listeners and DOM manipulation. It was
an interesting journey into seeing how much the tooling has improved over the
past decade or so after the heights of jQuery/Bootstrap dropdowns. To be
honest, I was also very happy I didn’t have to debug the old solution and
learn how to write decade old Boostrap code.
As I was working on the fix, I started thinking ways to mitigate potential future issues: what if an organiser accidentally clicks on someone’s RSVP turning it from “yes” to “no” and nobody notices until the person appears in the workshop and they weren’t counted to appear. Alternatively, what if someone does it maliciously? I’ve been an event organiser for long enough to know these things happen every now and then and it’s nice to able to figure them out.
My proposed solution I started working on was a simple audit log: a record of every change for both the RSVP status and the general application status. A database entry that states what the state was, what it was changed to and how/by whom.
The different states and transitions
For the application status, we have five different states:
- Application submitted (initial state)
- Application accepted
- Application rejected
- Application on waiting list
- Applicant declined
When an applicant fills in a form on the website, a new application is created with the first status of being submitted. An organiser can then accept, reject or put the applicant to a waiting list. Or the applicant can decline/withdraw their application.
For the RSVP status, we have three different states:
- Waiting for response (initial state)
- Confirmed attendance / yes
- Rejected invitation / no
When an applicant is accepted, they are emailed a link to either accept or reject the invitation. Alternatively, an admin can change it from the organiser applications page if for example someone accepts and later withdraws or accidentally reject when they meant to accept.
When thinking about the audit log, we have two items (RSVP and application status), 3 or 5 different states they can be in and a couple of different sources (automatic, applicant or organiser) for how they can change.
(The systems guy inside my head would be thrilled to turn this into some sort of a finite state machine as well but that feels a bit overkill — maybe some day?)
Analysis paralysis and fear of perceived skill level
There are so many ways one can build a system like this (and even more alternative solutions that don’t involve an idea of an audit log). It’s one thing to build such a thing for a personal project or anything private but exposing your ideas on a public forum like GitHub for an open source project can lead to all sorts of feelings.
Am I making a “right decision” and am I implementing it the “right way”? What will others think of my proposal? What if I make a mistake?
In general, this can be a problem especially in open source projects where (based on my experience and understanding) a lot of the discussion about ideas are based on proposals and implementation ideas on an open forum.
In my specific case, a lot of those mental barriers have been bulldozed by Djangonaut Space. I still have them nagging at the back of my head but the fact that I’ve gotten to know Anna, the lead maintainer, so well and have a direct line to discuss ideas with her makes such a big difference. I don’t have to worry about what Anna would think of my ideas — I’ve learned to trust that we can have a great constructive discussion and go back and forth to find a good solution. That’s also why smaller fixes were a great starting point to get into the project as that allows me to learn the “ways of working” of the project and people responsible.
I’m not sure if I’m any kind of expert to give advice on this but two things I try to keep in mind to battle my own worries:
- Ask questions, be open to discussion and take small steps. I probably should have followed that even more with this feature myself because I found myself going around circles with my thoughts a few times and it would have been beneficial to bring early ideas to the discussion earlier.
- Treat a PR as a proposal, a start of a discussion, rather than a perfect, ready solution. Of course, it’s a good and polite habit to do your best and explore options and ideas and test implementations before dumping them to the maintainers but there’s a lot of spectrum in between.
A fellow Djangonaut Goodluck touched on this in his blog post this week as well:
Early in the review process, Johannes Maron dropped a piece of Python wisdom that completely changed how I view open-source collaboration: EAFP (Easier to Ask for Forgiveness than Permission). Instead of waiting around asking if an approach is okay in theory, push the commit. Give reviewers a concrete diff to react to, and trust the review pipeline to refine it.
This summarises what I was trying to say in a very nice and concise way.
Building the audit log
I spent a lot of brain cycles in trying to figure out how to balance
generality (one
AuditLogEntry
for everything) and specificity (one for each combination of source and state,
ie
ApplicantChangedRSVP
or
OrganiserChangedApplicationStatus
as an extreme example).
A generic one makes it easy to list them: we can show all logs for a specific application with one query. On the other hand, it can make the model itself complex if you have to have different fields for different things and check which ones have been filled.
A specific one makes the models themselves simpler but then you need to combine objects from different models and implement a lot of the branching logic when reading and rendering the data.
I got sidetracked a bit with rabbit holes
The other question that popped up — and made my remember why it’s so valuable to make small changes one step at a time — was if we should inform the applicant when changes have happened, kind of like receipts so that they’d know and would be able to react to something. I decided that it was a rabbit hole that could lead to never-ending considerations and that could be done later and especially not part of this fix.
When I was pondering that, I thought it would have been better to just fix the bug in the organiser view and leave the audit log as a separate ticket to be implemented as well but since I was already so long down the road, I decided to finish it as part of this fix even if it went a little bit against my general idea of bug fixes only fixing the bug at question.
It’s more important to get a broken thing fixed than try to solve all related problems that often require a lot more discussions and design.
Back to the audit log implementation
One of the reasons why I kept going in circles was that we have two different types of sources for changes: unauthenticated applicants RSVPing and the authenticated organisers making changes on behalf of the applicants.
I ended up with a simple model:
class AuditLogCategory(models.TextChoices):
STATE = "STATE", _("Application State")
RSVP = "RSVP", _("RSVP Status")
class ApplicationAuditLog(models.Model):
"""An audit log to keep track of changes to
application's state and RSVP status."""
application = models.ForeignKey(Application, on_delete=models.CASCADE)
category = models.CharField(choices=AuditLogCategory.choices, default=AuditLogCategory.STATE, max_length=255)
old_value = models.CharField(blank=True, max_length=255)
new_value = models.CharField(blank=True, max_length=255)
user_source = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
other_source = models.CharField(blank=True, max_length=255)
created = models.DateTimeField(auto_now_add=True)
When creating a log entry, you can optionally attach it to a user (an event organiser) in a way that helps keep track of things.
The audit log can then be viewed by event organisers and admins on the application’s details page.
The pull request for this change still requires some work and cleaning up but I’m really glad for how it turned out in the big picture and that I managed to ground myself and make a decision to steer myself out of the roundabout I had been going around in my head.
Is this the best possible design and implementation? Probably not but it’s a functional one and one that I can stand behind and make arguments for. Most importantly, it’s a step forward in a progress and one that can hopefully act as a starting point for future improvements either by me or someone else who contributes to the project.
It’s still an open PR: an invitation for the maintainers and contributors to review, request changes and suggest improvements. By the power of teamwork, it will shape into a great new addition to the project and a long-awaited bug fix for the community organisers.
If something above resonated with you, let's start a discussion about it! Email me at juhis@hamatti.org and share your thoughts . This year, I want to have more deeper discussions with people from around the world and I'd love if you'd be part of that.