Time has accelerated and I've done nothing to mark its passage.

None of this stuff is particularly hard, it’s just taking me a while because I’m very lazy.

Implemented without further comment

  • Rebuilding the task “source” text from the task nodes in the DB
  • Marking tasks as complete
    • Caveat: this forces a re-render of the whole tasklist when marking a task as complete, and therefore a network transaction and decently-sized server-side template re-rendering. Really should consider moving a bunch of task display stuff over to the client-side, or maybe abuse HTMX morph algorithms?

Notes on performance

Probably useful to start to taxonomise some of the computation stages we see reoccurring so we can do some basic reasoning about performance. I’m playing fast and loose here, but hopefully I can back up my assumptions with proper performance profiling down the line.

Ranked in descending order of “most likely to be killing performance” (in the context of my Django hypermedia-style app specifically, where the server is keeping 95% of the state and doing some fairly complicated stuff):

  1. View logic and template rendering: this is where actual algorithms live, so the potential for macro-optimisations (i.e. big-O improvements) is quite high. The underlying Django template engine seems pretty fast but scales with complexity; need to pay close attention to how complex the templates are; these tend to be closely coupled with view logic and should be considered side-by-side. Switching off debug mode should give a reasonable boost for free – we’ll see.
  2. Network transaction (i.e. HTTP request): small(-ish) but constant latency, plus latency proportional to payload size and client bandwidth. Should be negligible on fast connections with a few tens of kB per transaction (max). Add client-side UI cues that work is being done (on the server) to placate users. Keep an eye on request frequency and sizes, but probably not worth byte-pinching. (Get gzip to help.)
  3. JS on browser: introduces latency proportional to client compute, depending on code complexity. For this application specifically, unlikely to be an issue.
  4. Database transaction: should be fast. (Can we pull up sqlite3 benchmarking?) Latency necessarily scales with amount of data on system. Also, obviously making lots of these will be slow – see view logic & template rendering. Caching?
  5. Various systems software (e.g. nginx, uWSGI): should be pretty fast, although worth checking that the configuration is tuned correctly/is not in debug mode, etc.
  6. URL routing: should be fast, mostly constant.

With this, I’m mostly just trying to convince myself that I should worry less about sending as little data down the wire as possible and more about writing sound, maintainable, efficient template and view code.

Take a walk on the client side

Most of my work has been server-side, issuing HTTP requests (using HTMX) and putting all the fancy business logic into template rendering. The next thing I want to get done, though, is being able to show/hide completed tasks with the click of a checkbox. While I’ve been abusing server-side rendering a lot, using it for this feature would be a bridge too far, so we’re going to bite the bullet and write some Javascript again.

hypermedia.systems has a chapter on client-side scripting in hypermedia applications, and they reference RSJS: Reasonable System for Javascript Structure as a way of writing maintainable VanillaJS code, which seems to fit our use case, so let’s try doing that.

(time passes)

So a problem I keep running into with client-side scripting is that the elements that I’m trying to script around aren’t necessarily on the page on initial load time due to HTMX swapping things in and out. I kind of solved this by putting <script> tags adjacent to the elements themselves rather than in the header, but realise now that I should be setting event listeners for the ‘htmx:load’ event and rechecking if the element exists on that event firing instead. Remind me to fix this later.

Anyway, showing/hiding tasks works now. The Web Storage API (which I’m using to persistently keep track of the state of the checkbox) is exceedingly simple and effortless to use. The only thing that’s bugging me at the moment is that if you have the page set to hide completed tasks, completing a task causes all completed tasks to briefly flash back up before settling because the whole rendered task div (including the show-completed script) gets swapped in and reloaded and there’s a brief delay before the Javascript that hides the completed tasks runs again. Fixing the <script> placement as mentioned above would probably help. It would also be nice to dynamically set the stylesheet rules with Javascript, which should be persistent across re-rendering (and therefore keep hidden tasks hidden), but the API for doing so is a pain. insertRule() and deleteRule()? Managing the indices of the stylesheet rules? No thanks. (Although if I just started working on it instead of complaining here I might be half done already…)

Jottings

Annoying case-bashing for inserting a task without reparsing the whole task text. I had to make a flowchart and everything!

Minor problem that requires us to make a fairly important decision: marking a task as complete effectively removes it from the entire dependency network of tasks. This means we determine which tasks are available to schedule immediately by taking all tasks that do not have any dependencies. However, we might wish to keep the completed task in the dependency network for the sake of being able to review project progress at the top level; I’m imagining having a graph visualisation of the task DAG down the line.

I think for the time being I will not store any task information in the database that isn’t already explicitly or implicitly contained within the text representation of the same list of tasks. This might change in the future, but I want to be able to keep the ability to “pop the hood” of the task list and modify the text directly (without worrying about introducing state inconsistencies) for as long as I can reasonably do so.

To clarify to future self: completing a task currently removes it from the flow of task dependencies (within the same task hierarchy), but does not remove it from the flow of subtasks and parent tasks. A completed task still has a parent task, which may also be completed, etc. This might have implications for task flattening when we do scheduling.

Writing tests

Theory

I have put off writing tests until now because tests are generally pretty tedious to write and all the implementation details have been relatively straightforward; just trying out features on the frontend in an ad-hoc manner has worked fine. But the function I just wrote is 104 lines and has a bunch of if-else statements that I don’t trust myself to have gotten 100% correct on the first try – I caught myself making some cringeworthy mistakes multiple times in this middle of writing it this week – so now would be a good time to figure out how to test it.

I want to write some basic white-box tests for the backend function in question. Thing is, it inserts a task after an existing task, which requires there to already be tasks associated with a user in the database, so now I’m thinking I should write tests for the view that generates a task from task text first. Chain of operations:

  • Have an existing authorised user in the test database somehow
    • There is currently no way to register a new user within the app, since it’s just me at this point and I don’t want randos signing up. I added my superuser account via the command line and additional test accounts via the admin site.
    • I should be able to register a new test user on the backend directly though
    • Not a view test (yet), just part of the setup for all other tests
  • Have that user create a task list
    • Can be done directly, or via the view
    • Call this “TaskListCreationViewTests” or something
  • Have that user edit and render the task list text to get some task nodes in the DB
    • Again, can be done directly, or via the view
    • Call this “TaskListSourceEditViewTests”
  • Finally, test the task insertion backend and/or view
    • We want to verify that dependencies are modified/set correctly
    • If we assume that the task list rendering from task text works correctly, then we just need to check that we get the same results rendering task text with a given task added compared to rendering task text without the task and adding the task on via the newly implemented route, for lots of different task arrangements
      • Could do fuzzing or something??? Probably not worth it at this point

Testing HTMX adds some wrinkles that someone used to testing SPAs or backend JSON APIs might not be used to. If you’re testing a JSON “REST”[1] API, you can just check that the data values packaged up in the responses are what you expect. If you’re looking at fragments of returned HTML, though, you’re probably going to need to pull out a HTML parser or something. (Beautiful Soup?) Fortunately, the Django view testing framework (built on top of Python’s unittest module) allows you to inspect the values rendered in the template context directly, which should keep the testing machinery we have to do ourselves reasonably lightweight.

Practice

Wow, having tests suddenly makes this whole thing actually feel somewhat like a professional operation! The peace of mind is great. I found myself checking what my code actually does in order to write tests for most of the simple cases, though, which isn’t exactly the epitome of test-driven development. At least it’s a way of quickly checking that adding a new feature doesn’t break any of the old ones.

Ran into some minor PUT/POST trouble. I’m feeling expository today, so here’s the deal:

  • PUT and POST are two request methods that are part of HTTP (the HyperText Transfer Protocol), which is the protocol used to request and deliver web pages. The most common method is probably GET. When you load a web URL into your address bar and hit enter, what your browser is doing on your behalf on a base level is issuing a GET request over HTTP to that URL and then rendering the HTML document returned by the server in your browser.
  • POST is supposed to be used whenever you’re performing a web action that sends data to the server, like when you submit a form or post a comment. GET requests should never meaningfully change server state with respect to the user; POST requests typically should (the server should keep a record of the data you submitted and do something with it).
  • PUT is very similar: it is also used to send data to a web server. The difference is that PUT is idempotent: making the multiple identical PUT requests in a row shouldn’t be meaningfully different from making just one of those requests.
    • POST requests are not expected to be idempotent. For example, if you make a POST request that corresponds to posting a comment, making multiple of the same request will result in multiple identical comments being posted to the server.
    • Examples of PUT requests could be setting some value in settings to a specific value, or editing a message to contain a different message, or renaming a resource. These actions are all idempotent; they might meaningfully change server state the first time, but not any time after that. If I ask you to change the name of resource with ID 4 to “Ganymede”, it won’t do anything if the resource’s name is already Ganymede.
  • However! This is a semantic difference. There is a more significant practical difference: POST has first-class support in HTML, whereas PUT requests can only be made using the assistance of Javascript. You cannot, in 2025, make a <form> element submit a PUT request with pure HTML. Personally, I think this is a bit loopy! (See the Petros and Gross proposal Support PUT, PATCH and DELETE in HTML Forms.) This has the knock-on effect of Django providing easy shortcuts for handling POST requests, but requiring slightly more dev attention for handling PUT requests.

Case in point with the last item: the Django test client’s POST method automatically urlencodes the request data, but you have to urlencode explicitly with django.utils.http.urlencode for PUT. Took a while to figure out why my test was failing. This is the price one must pay for committing to proper HTTP REST semantics.

Anyway, writing tests is pretty boring, but they’re important. Uncovered quite a few bugs running tests!

Accessibility

Doing progressive enhancement would be cool, but will be a fair amount of work if I’m going to be serious about it, so we’ll shelve this for later.

Conclusion and next steps

Still stuck in test writing hell, but this post has been sitting in my drafts for long enough. We’ll finish up tests, implement subtask addition, maybe add insert_before (we only have append and insert_after), and then finally work on app settings. Then we can work on the scheduling stuff… yay…

Footnotes

[1] See How Did REST Come To Mean The Opposite of REST?.