Matthias Kestenholz: Posts about Djangohttps://406.ch/writing/category-django/2024-12-20T12:00:00ZMatthias KestenholzWeeknotes (2024 week 51)https://406.ch/writing/weeknotes-2024-week-51/2024-12-20T12:00:00Z2024-12-20T12:00:00Z<h1 id="weeknotes-2024-week-51"><a class="toclink" href="#weeknotes-2024-week-51">Weeknotes (2024 week 51)</a></h1>
<h2 id="building-forms-using-django"><a class="toclink" href="#building-forms-using-django">Building forms using Django</a></h2>
<p>I last wrote about this topic <a href="https://406.ch/writing/building-forms-with-the-django-admin/">in April</a>. It has <a href="https://mastodon.social/@webology/113669270531953652">resurfaced on Mastodon this week</a>. I’m thinking about writing a <a href="https://github.com/feincms/feincms3-forms">feincms3-forms</a> demo app, but I already have too much on my plate. I think composing a forms builder on top of <a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> is the way to go, instead of replacing the admin interface altogether – sure, you can always do that, but it’s so much less composable…</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/blacknoise/">blacknoise 1.2</a>: No real changes, added support for Python 3.13 basically without changing anything. It’s always nice when this happens.</li>
<li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.21</a></li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.10</a>: I rebuilt django-prose-editor from the ground up <a href="https://406.ch/writing/rebuilding-django-prose-editor-from-the-ground-up/">and wrote about that two weeks ago</a>. The 0.10 release marks the final point of this particular rewrite.</li>
<li><a href="https://pypi.org/project/django-js-asset/">django-js-asset 3.0</a>: See the blog post from <a href="https://406.ch/writing/object-based-assets-for-django-s-forms-media/">this week</a></li>
</ul>Object-based assets for Django's forms.Mediahttps://406.ch/writing/object-based-assets-for-django-s-forms-media/2024-12-18T12:00:00Z2024-12-18T12:00:00Z<h1 id="object-based-assets-for-djangos-formsmedia"><a class="toclink" href="#object-based-assets-for-djangos-formsmedia">Object-based assets for Django’s forms.Media</a></h1>
<p>The pull request for adding <a href="https://github.com/django/django/pull/18782">object-based script media assets into
Django</a> is in a good state and I
hope it will be merged soon. I have been using object-based assets long before
<a href="https://github.com/django/django/commit/4c76ffc2d6c77">Django actually added support for them in
4.1</a> (<a href="https://github.com/feincms/django-content-editor/commit/82ac91ea7af2409bb3672e11c18871002ddc9753">since
2016</a>,
that’s before Django 1.10!) by using a gross hack. Luckily I have been able to
clean up the code when Django 4.1 landed.</p>
<p>I have been asking myself at times why I haven’t proposed the change to Django
myself despite having been a user of something like this for such a long time.
After all, I have been happily contributing issue reports, bug fixes and tests
to Django. The process of adding new features sometimes is terribly frustrating
though even when looking (and cheering) from the sidelines. It feels bad that
adding another package to the <a href="https://pypi.org/user/matthiask/">list of packages I
maintain</a> so clearly seems to be the better
way to <strong>get things done</strong> compared to proposing a new feature for Django
itself. I hope <a href="https://406.ch/writing/weeknotes-2024-week-49/">processes change
somewhat</a>.</p>
<p>But I digress.</p>
<p>The <code>ProseEditorWidget</code> in
<a href="https://github.com/matthiask/django-prose-editor/">django-prose-editor</a> wants
to ship CSS, JavaScript and some JSON to the browser for the widget. So, of
course I used object-based media assets for this instead of widget HTML
templates. Media assets are deduplicated and sorted by Django. If different
editor presets use differing lists of assets they are smartly merged by
<code>forms.Media</code> using a topological sort. You get those niceties for free when
using <code>forms.Media</code> and everything just works, so what’s not to like?</p>
<p>The only thing which isn’t to like is that Django, at the time of writing,
doesn’t provide any classes helping with this. You can put strings into
<code>forms.Media</code> or you can put objects with a <code>__html__()</code> method in there. The
latter of course is all that’s needed to support more advanced use cases – and
that’s exactly what
<a href="https://pypi.org/project/django-js-asset/">django-js-asset</a> now provides, and
what django-prose-editor uses.</p>
<p><a href="https://pypi.org/project/django-js-asset/">django-js-asset</a> has long supported
a <code>JS</code> class with support for additional
attributes, for example:</p>
<div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">js_asset</span> <span class="kn">import</span> <span class="n">JS</span>
<span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span>
<span class="n">JS</span><span class="p">(</span><span class="s2">"asset.js"</span><span class="p">,</span> <span class="p">{</span><span class="s2">"id"</span><span class="p">:</span> <span class="s2">"asset-script"</span><span class="p">,</span> <span class="s2">"data-answer"</span><span class="p">:</span> <span class="s2">"42"</span><span class="p">}),</span>
<span class="p">])</span>
</code></pre></div>
<p>Since 3.0 the package also comes with a <code>CSS</code> and <code>JSON</code> class:</p>
<div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">js_asset</span> <span class="kn">import</span> <span class="n">CSS</span><span class="p">,</span> <span class="n">JS</span><span class="p">,</span> <span class="n">JSON</span>
<span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span>
<span class="n">JSON</span><span class="p">({</span><span class="s2">"cfg"</span><span class="p">:</span> <span class="mi">42</span><span class="p">},</span> <span class="nb">id</span><span class="o">=</span><span class="s2">"widget-cfg"</span><span class="p">),</span>
<span class="n">CSS</span><span class="p">(</span><span class="s2">"widget/style.css"</span><span class="p">),</span>
<span class="n">CSS</span><span class="p">(</span><span class="s2">"p{color:red;}"</span><span class="p">,</span> <span class="n">inline</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
<span class="n">JS</span><span class="p">(</span><span class="s2">"widget/script.js"</span><span class="p">,</span> <span class="p">{</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"module"</span><span class="p">}),</span>
<span class="p">])</span>
</code></pre></div>
<p>This produces the following HTML:</p>
<div class="chl"><pre><span></span><code><span class="p"><</span><span class="nt">script</span> <span class="na">id</span><span class="o">=</span><span class="s">"widget-cfg"</span> <span class="na">type</span><span class="o">=</span><span class="s">"application/json"</span><span class="p">>{</span><span class="s2">"cfg"</span><span class="o">:</span><span class="w"> </span><span class="mf">42</span><span class="p">}</</span><span class="nt">script</span><span class="p">></span>
<span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">"stylesheet"</span> <span class="na">href</span><span class="o">=</span><span class="s">"/static/widget/style.css"</span><span class="p">></span>
<span class="p"><</span><span class="nt">style</span><span class="p">></span><span class="nt">p</span><span class="p">{</span><span class="k">color</span><span class="p">:</span><span class="kc">red</span><span class="p">;}</</span><span class="nt">style</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">"/static/widget/script.js"</span> <span class="na">type</span><span class="o">=</span><span class="s">"module"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
</code></pre></div>
<p>The code which is proposed for Django supports the JavaScript use case but with
a slightly different API:</p>
<div class="chl"><pre><span></span><code><span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span>
<span class="n">Script</span><span class="p">(</span><span class="s2">"widget/script.js"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="s2">"module"</span><span class="p">),</span>
<span class="p">])</span>
</code></pre></div>
<p>This looks slightly nicer as long as you don’t use e.g. data attributes,
because then you have to do:</p>
<div class="chl"><pre><span></span><code><span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span>
<span class="n">Script</span><span class="p">(</span><span class="s2">"widget/script.js"</span><span class="p">,</span> <span class="o">**</span><span class="p">{</span><span class="s2">"data-cfg"</span><span class="p">:</span> <span class="o">...</span><span class="p">}),</span>
<span class="p">])</span>
</code></pre></div>
<p>I always forget that Python supports passing keyword arguments names which
aren’t valid Python identifiers (but only when using <code>**kwargs</code>). I personally
don’t care much either way, and when my packages can finally drop compatibility
with Django versions which do not support all these functionalities yet I’ll
finally be able to retire
<a href="https://pypi.org/project/django-js-asset/">django-js-asset</a>. That won’t happen
any time soon though, if only because I like supporting old versions of Django
because I have so many Django-based websites running somewhere.</p>Weeknotes (2024 week 49)https://406.ch/writing/weeknotes-2024-week-49/2024-12-06T12:00:00Z2024-12-06T12:00:00Z<h1 id="weeknotes-2024-week-49"><a class="toclink" href="#weeknotes-2024-week-49">Weeknotes (2024 week 49)</a></h1>
<h2 id="django-steering-council-elections"><a class="toclink" href="#django-steering-council-elections">Django Steering Council elections</a></h2>
<p>I have been thinking long and hard about running for the Django Steering Council. I think there are a few things I could contribute since I’ve been using Django for 16 or more years, and have been working on, maintaining and publishing third-party apps almost all this time. I have also contributed a few small features to Django core itself, and contributed my fair share of tests and bugfixes. The reason why I haven’t been more involved was always that I feared the review process with what I perceive to be a too unrestrained perfectionism. Teaching people is good, but I fear that those who teach are self-selected survivors of the process, which come to appreciate the perfectionism a bit too much. It’s somewhat the same as with the Swiss naturalization process – the hurdles are very high, and some of those who weather the process maybe are or grow to be too fond of it.</p>
<p>An important point is that this has nothing to do with being nice (or not). Everybody has always been great, maybe with the exception of myself back when I didn’t understand that the problem wasn’t the individuals but the way everyone has agreed things should be done.</p>
<p>I’m not the only one who thinks that we <a href="https://knowyourmeme.com/memes/we-should-improve-society-somewhat">should improve the process somewhat</a>. So, I’m definitely going to look out for candidates who think this is important.</p>
<p>There are a few reasons why I’m not running myself at this time. A somewhat important reason is that my candidacy wouldn’t help diversity at all. This shouldn’t discourage anyone else with the same background from running – we cannot change the world all at once. More importantly, I have more personal reasons for being hesitant to accept new commitments. That being said, I’m looking forward to be more involved in the community in other ways. And also, it’s not now or never.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.5.4</a>: No functional changes, only code golfing. It’s nice to have a working cookie banner with a solution for embedding third party content only when people consent in less than 4KiB of minified (not compressed!) JavaScript.</li>
<li><a href="https://pypi.org/project/django-admin-ordering/">django-admin-ordering 0.20</a>: Objects can now be reordered using arrow buttons instead of drag drop or manually changing the ordering field’s value. This should make the package more accessible. It’s always a joy when people contribute such useful improvements.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.10a?</a>: See <a href="https://406.ch/writing/rebuilding-django-prose-editor-from-the-ground-up/">the recent blog post</a>.</li>
</ul>Rebuilding django-prose-editor from the ground uphttps://406.ch/writing/rebuilding-django-prose-editor-from-the-ground-up/2024-12-04T12:00:00Z2024-12-04T12:00:00Z<h1 id="rebuilding-django-prose-editor-from-the-ground-up"><a class="toclink" href="#rebuilding-django-prose-editor-from-the-ground-up">Rebuilding django-prose-editor from the ground up</a></h1>
<p>The <a href="https://pypi.org/project/django-prose-editor/">django-prose-editor</a> package provides a HTML editor based upon the <a href="https://prosemirror.net/">ProseMirror toolkit</a> for the Django administration interface and for the frontend.</p>
<p>The package has been extracted from a customer project and open sourced so that it could be used in other projects as well. It followed a very restricted view of how rich text editors should work, which I have initially added to the <a href="https://github.com/feincms/feincms/commit/70cd7a1244438d2ba97852256f77daa2c870c345#diff-556c5559a716059d4fb714ad34de6a9845870e8d55bbd2cb9d77c732eb961388">FeinCMS repository when documenting the design decisions more than 15 years ago</a> <small>(Note that I didn’t edit the paragraph, it’s reproduced here as it was back then, with all the errors and heedlessness.)</small></p>
<blockquote>
<p>All of this convinced me that offering the user a rich text editor with too much capabilites is a really bad idea. The rich text editor in FeinCMS only has bold, italic, bullets, link and headlines activated (and the HTML code button, because that’s sort of inevitable – sometimes the rich text editor messes up and you cannot fix it other than going directly into the HTML code. Plus, if someone really knows what he’s doing, I’d still like to give him the power to shot his own foot).</p>
</blockquote>
<p>My personal views are unchanged. I have to recognize though that forcing this idea upon everyone isn’t workable and that this would mean that I’d have to find a different editor for most projects just because people really want or need more rope. Going back to an editor which allows everything was out of the question, so I had to look around for a way to allow project-specific extensions for the editor.</p>
<p>Of course that’s problematic, since Django packages and Python virtualenvs do not offer a good way of shipping CSS and JavaScript which should be available for a frontend bundler to process. The existing <a href="https://docs.djangoproject.com/en/5.1/ref/contrib/staticfiles/">Django staticfiles app</a> is great, works well, but it’s not a bundler – and it shouldn’t be.</p>
<p>So, I started shopping around for ways to make ProseMirror extensible while keeping extensions clean and well localized. Instead of inventing another plugin ecosystem I settled on <a href="https://tiptap.dev/">Tiptap</a> which uses ProseMirror under the hood. The abstractions are pleasantly leaky – if you know how to work with ProseMirror’s API, you can use Tiptap’s API without any issues. That was important for me, since I already have a somewhat large selection of plugins which I do not want to reimplement from the ground up.</p>
<p>I had already looked at Tiptap a few years back, but ultimately stayed with ProseMirror because I liked some behaviors better (such as not including trailing spaces in marks) and because I didn’t need the extensibility which at the time only made the resulting bundle much bigger.</p>
<p>Now, things have improved a lot, and I’m really happy with Tiptap and the development version of django-prose-editor. <a href="https://github.com/matthiask/django-prose-editor/?tab=readme-ov-file#customization">Writing an editor extension in project code is great</a>, and my editor core stays nice. Also the list of readily available extensions is large, and most of the things just work.</p>Weeknotes (2024 week 47)https://406.ch/writing/weeknotes-2024-week-47/2024-11-20T12:00:00Z2024-11-20T12:00:00Z<h1 id="weeknotes-2024-week-47"><a class="toclink" href="#weeknotes-2024-week-47">Weeknotes (2024 week 47)</a></h1>
<p>I missed a single co-writing session and of course that lead to four weeks of no posts at all to the blog. Oh well.</p>
<h2 id="debugging"><a class="toclink" href="#debugging">Debugging</a></h2>
<p>I want to share a few debugging stories from the last weeks.</p>
<h3 id="pillow-11-and-djangos-get_image_dimensions"><a class="toclink" href="#pillow-11-and-djangos-get_image_dimensions">Pillow 11 and Django’s <code>get_image_dimensions</code></a></h3>
<p>The goal of <a href="https://github.com/matthiask/django-imagefield">django-imagefield</a>
was to deeply verify that Django and Pillow are able to work with uploaded
files; some files can be loaded, their dimensions can be inspected, but
problems happen later when Pillow actually tries resizing or filtering files.
Because of this django-imagefield does more work when images are added to the
system instead of working around it later. (Django doesn’t do this on purpose
because doing all this work up-front could be considered a DoS factor.)</p>
<p>In the last weeks I suddenly got recurring errors from saved files again,
something which shouldn’t happen, but obviously did.</p>
<p>Django wants to read image dimensions when accessing or saving image files (by
the way, always use <code>height_field</code> and <code>width_field</code>, otherwise Django will
open and inspect image files even when you’re only loading Django models from
the database…!) and it uses a smart and wonderful<sup id="fnref:fn1"><a class="footnote-ref" href="#fn:fn1">1</a></sup> hack to do this: It reads a few hundred bytes from the image file, instructs Pillow to inspect the file and if an exception happens it reads more bytes and tries again. This process relies on the exact type of exceptions raised internally though, and the release of Pillow 11 changed the types… for some file types only. Fun times.</p>
<p>The issue had already been reported as
<a href="https://code.djangoproject.com/ticket/33240">#33240</a> and is now tracked as <a href="https://github.com/python-pillow/Pillow/issues/8530">#8530</a> on the Pillow issue tracker. Let’s see what happens.
For now, django-imagefield declares itself to be incompatible with Pillow
11.0.0 so that this error cannot happen.</p>
<h3 id="rspack-and-lightningcss-shuffled-css-properties"><a class="toclink" href="#rspack-and-lightningcss-shuffled-css-properties">rspack and lightningcss shuffled CSS properties</a></h3>
<p><a href="https://rspack.dev/">rspack</a> 1.0 started reordering CSS properties which of course lead to CSS properties overriding each other in the incorrect order. That was a fun one to debug. I tracked the issue down to the switch from the swc CSS minimizer to <a href="https://github.com/parcel-bundler/lightningcss">lightningcss</a> and submitted a reproduction to the <a href="https://github.com/parcel-bundler/lightningcss/issues/805#issuecomment-2358219597">issue tracker</a>. My rust knowledge wasn’t up to the task of attempting to submit a fix myself. Luckily, it has been fixed in the meantime.</p>
<h3 id="rspack-problems"><a class="toclink" href="#rspack-problems">rspack problems</a></h3>
<p>I have another problem with rspack where I haven’t yet tracked down the issue. rspack produces a broken bundle starting with <a href="https://github.com/web-infra-dev/rspack/releases/tag/v1.0.0-beta.2">1.0.0-beta.2</a> when compiling a particular project of mine. I have the suspicion that I have misconfigured some stuff related to import paths and yarn workspaces. I have no idea how anyone could have a complete understanding of these things…</p>
<p><strong>Update:</strong> The problem was <a href="https://github.com/web-infra-dev/rspack/issues/8027">#8027</a>, <code>experiments.css</code> is quite broken for now.</p>
<p>Bundlers are complex beasts, and I’m happy that I mostly can just use them.</p>
<h3 id="closing-thoughts"><a class="toclink" href="#closing-thoughts">Closing thoughts</a></h3>
<p>Debugging is definitely a rewarding activity for me. I like tracking stuff down like this. Unfortunately, problems always tend to crop up when time is scarce already, but what can you do.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<p>Quite a few releases, many of them verifying Python 3.13 and Django 5.1 support (if it hasn’t been added already in previous releases). The nicest part: If I remember correctly I didn’t have to change anything anywhere, everything just continues to work.</p>
<ul>
<li><a href="https://pypi.org/project/django-admin-ordering/">django-admin-ordering 0.19</a>: I added support for automatically renumbering objects on page load. This is mostly useful if you already have existing data which isn’t ordered yet.</li>
<li><a href="https://pypi.org/project/feincms3-data/">feincms3-data 0.7</a>: Made sure that objects are dumped in a deterministic order when dumping. I wanted to compare JSON dumps by hand before and after a big data migration in a customer project and differently ordered dumps made the comparison impossible.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.9</a>: I updated the ProseMirror packages, and put the editor into read-only mode for <code><textarea disabled></code> elements.</li>
<li><a href="https://pypi.org/project/feincms3-language-sites/">feincms3-language-sites 0.4</a>: Finally released the update containing the necessary hook to validate page trees and their unique paths before moving produces integrity errors. Error messages are nicer than internal server errors.</li>
<li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.17.2</a>: The value of the cookie which is used to save the URL where users should be redirected to after authentication wasn’t checked for validity when setting it, only when reading it. This meant that attackers could produce invalid header errors in application servers. No real security problem here when using authlib’s code.</li>
<li><a href="https://pypi.org/project/feincms3/">feincms3 5.3</a>: Minor update which mostly removes support for outdated Python and Django versions.</li>
<li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.20</a>: See above.</li>
</ul>
<div class="footnote">
<hr />
<ol>
<li id="fn:fn1">
<p>wonderfully ugly <a class="footnote-backref" href="#fnref:fn1" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>Weeknotes (2024 week 43)https://406.ch/writing/weeknotes-2024-week-43/2024-10-23T12:00:00Z2024-10-23T12:00:00Z<h1 id="weeknotes-2024-week-43"><a class="toclink" href="#weeknotes-2024-week-43">Weeknotes (2024 week 43)</a></h1>
<p>I had some much needed time off, so this post isn’t that long even though <a href="https://406.ch/writing/weeknotes-2024-week-39/">four weeks have passed since the last entry</a>.</p>
<h2 id="from-webpack-to-rspack"><a class="toclink" href="#from-webpack-to-rspack">From webpack to rspack</a></h2>
<p>I’ve been really happy with <a href="https://rspack.dev/">rspack</a> lately. Converting
webpack projects to rspack is straightforward since it mostly supports the same
configuration, but it’s much much faster since it’s written in Rust. Rewriting
things in Rust is a recurring theme, but in this case it really helps a lot.
Building the frontend of a larger project of ours consisting of several admin
tools and complete frontend implementations for different teaching materials
only takes 10 seconds now instead of several minutes. That’s a big and relevant
difference.</p>
<p>Newcomers should probably still either use <a href="https://rsbuild.dev/">rsbuild</a>,
<a href="https://vite.dev/">Vite</a> or maybe no bundler at all. Vanilla JS and browser
support for ES modules is great. That being said, I like cache busting,
optimized bundling and far-future expiry headers in production and hot module
reloading in development a lot, so learning to work with a frontend bundler is
definitely still worth it.</p>
<h2 id="dark-mode-and-light-mode"><a class="toclink" href="#dark-mode-and-light-mode">Dark mode and light mode</a></h2>
<p>I have been switching themes in my preferred a few times per year in the past. The following ugly bit of vimscript helps switch me the theme each time the sun comes out when working outside:</p>
<div class="chl"><pre><span></span><code><span class="nv">let</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span>
<span class="nv">function</span><span class="o">!</span><span class="w"> </span><span class="nv">FiatLux</span><span class="ss">()</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span>
<span class="w"> </span>:<span class="nv">set</span><span class="w"> </span><span class="nv">background</span><span class="o">=</span><span class="nv">light</span>
<span class="w"> </span><span class="nv">let</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span>
<span class="w"> </span><span class="k">else</span>
<span class="w"> </span>:<span class="nv">set</span><span class="w"> </span><span class="nv">background</span><span class="o">=</span><span class="nv">dark</span>
<span class="w"> </span><span class="nv">let</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span>
<span class="w"> </span><span class="k">endif</span>
<span class="nv">endfunction</span>
<span class="nv">nnoremap</span><span class="w"> </span><span class="o"><</span><span class="nv">F12</span><span class="o">></span><span class="w"> </span>:<span class="k">call</span><span class="w"> </span><span class="nl">FiatLux</span><span class="ss">()</span><span class="o"><</span><span class="nv">CR</span><span class="o">></span>
</code></pre></div>
<p>I’m using the <a href="https://devsuite.app/ptyxis/">Ptyxis</a> terminal emulator
currently, I haven’t investigated yet if there’s a shortcut to toggle dark and
light mode for it as well. Using F10 to open the main menu works fine though,
and using the mouse wouldn’t be painful either.</p>
<h2 id="helping-out-in-the-django-forum-and-the-discord"><a class="toclink" href="#helping-out-in-the-django-forum-and-the-discord">Helping out in the Django forum and the Discord</a></h2>
<p>I have found some pleasure in helping out in the <a href="https://forum.djangoproject.com/">Django
Forum</a> and in the official <a href="https://discord.gg/xcRH6mN4fa">Django
Discord</a>. I sometimes wonder why more people
aren’t reading the Django source code when they hit something which looks like
a bug or something which they do not understand. I find Django’s source code
very readable and I have found many nuggets within it. I’d always recommend
checking the documentation or maybe official help channels first, but the code
is also out there and that fact should be taken advantage of.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.1</a>:
Fixed a bug where the ordering and region fields were handled incorrectly
when they appear on one line in the fieldset. Also improved the presentation
of inlines in unknown regions and clarified the meaning of the move to region
dropdown. Also, released the improvements from previous patch releases as a
new minor release because that’s what I should have been doing all along.</li>
<li><a href="https://pypi.org/project/form-designer/">form-designer 0.27</a>: A user has
been bitten by <code>slugify</code> removing cyrillic characters because it only keeps
ASCII characters around. Here’s the wontfixed bug in the Django issue
tracker: <a href="https://code.djangoproject.com/ticket/8391">#8391</a>. I fixed the
issue by removing the slugification (is that even a word?) when generating
choices.</li>
</ul>Weeknotes (2024 week 39)https://406.ch/writing/weeknotes-2024-week-39/2024-09-25T12:00:00Z2024-09-25T12:00:00Z<h1 id="weeknotes-2024-week-39"><a class="toclink" href="#weeknotes-2024-week-39">Weeknotes (2024 week 39)</a></h1>
<h2 id="css-for-django-forms"><a class="toclink" href="#css-for-django-forms">CSS for Django forms</a></h2>
<p>Not much going on in OSS land. I have been somewhat active in the official
Django forum, discussing ways to add Python-level hooks to allow adding CSS
classes around form fields and their labels. The discussion on the
<a href="https://forum.djangoproject.com/t/proposal-make-it-easy-to-add-css-classes-to-a-boundfield/32022">forum</a>
and on the <a href="https://github.com/django/django/pull/18266">pull request</a> goes in
the direction of allowing using custom <code>BoundField</code> classes per form or even
per project (instead of only per field as is already possible today). This
would allow overriding <code>css_classes</code>, e.g. to add a simple <code>class="field"</code>.
Together with <code>:has()</code> this would probably allow me to skip using custom HTML
templates in 99% of all cases.</p>
<p>I have also been lurking in the Discord, but more to help and less to promote
my packages and ideas :-)</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/django_user_messages/">django-user-messages 1.1</a>:
Added Django 5.0, 5.1 to the CI, and fixed the migrations to no longer
mention <code>index_together</code> at all. It seems that squashing the migrations
wasn’t sufficient, I also had to actually delete the old migrations.</li>
<li><a href="https://pypi.org/project/blacknoise/">blacknoise 1.1</a>:
<a href="https://www.starlette.io/">Starlette</a>’s <code>FileResponse</code> has gained support
for the HTTP Range header, allowing me to remove my homegrown implementation
from the package. The blacknoise implementation is now half as long as it was
in 1.0.</li>
<li><a href="https://pypi.org/project/django_fhadmin/">django-fhadmin 2.3</a>: No new
features, only tweaks to the styling and behavior prompted by updates to
Django’s admin interface.</li>
<li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.17</a>: I have
pruned the CI matrix and accepted a pull request adding a ru translation. I
feel conflicted about that since I strongly believe that everything is
political, but I don’t know if rejecting translations helps anyone.</li>
</ul>django-content-editor now supports nested sectionshttps://406.ch/writing/django-content-editor-now-supports-nested-sections/2024-09-13T12:00:00Z2024-09-13T12:00:00Z<h1 id="django-content-editor-now-supports-nested-sections"><a class="toclink" href="#django-content-editor-now-supports-nested-sections">django-content-editor now supports nested sections</a></h1>
<p><a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> (and
it’s ancestor FeinCMS) has been the Django admin extension for editing content
consisting of reusable blocks since 2009. In the last years we have more and
more often started <a href="https://feincms3.readthedocs.io/en/latest/guides/rendering.html#grouping-plugins-into-subregions">automatically grouping related
items</a>,
e.g. for rendering a sequence of images as a gallery. But, sometimes it’s nice
to give editors more control. This has been possible by using blocks which open
a subsection and blocks which close a subsection for a long time, but it hasn’t
been friendly to content managers, especially when using nested sections.</p>
<p>The content editor now has first-class support for such nested sections. Here’s
a screenshot showing the nesting:</p>
<p><img alt="django-content-editor with sections" src="https://406.ch/assets/20240911-content-editor-sections.png" /></p>
<p>Finally it’s possible to visually group blocks into sections, collapse those
sections as once and drag and drop whole sections into their place instead of
having to select the involved blocks individually.</p>
<p>The best part about it is that the content editor still supports all Django
admin widgets, as long as those widgets have support for the Django
administration interface’s <a href="https://docs.djangoproject.com/en/latest/ref/contrib/admin/javascript/">inline form
events</a>!
Moving DOM nodes around breaks attached JavaScript behaviors, but we do not
actually move DOM nodes around after the initialization – instead, we use
<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Ordering_flex_items">Flexbox
ordering</a>
to visually reorder blocks. It’s a bit more work than using a ready-made
sortable plugin, but – as mentioned – the prize is that we don’t break any
other Django admin extensions.</p>
<h2 id="simple-patterns"><a class="toclink" href="#simple-patterns">Simple patterns</a></h2>
<p>I previously already reacted to a blog post by Lincoln Loop here in my post <a href="https://406.ch/writing/my-reaction-to-the-block-driven-cms-blog-post/">My
reaction to the block-driven CMS blog
post</a>.</p>
<p>The latest blog post, <a href="https://lincolnloop.com/insights/simple-block-pattern-wagtail-cms/">Solving the Messy Middle: a Simple Block Pattern for
Wagtail
CMS</a> was
interesting as well. It dives into the configuration of a
<a href="https://wagtail.org/">Wagtail</a> stream field which allows composing content out
of reusable blocks of content (<a href="https://406.ch/writing/i-just-learned-about-wagtail-s-streamfield/">sounds
familiar!</a>).
The result is saved in a JSON blob in the database with all the advantages and
disadvantages that entails.</p>
<p>Now, django-content-editor is a worthy competitor when you do not want to add
another interface to your website besides the user-facing frontend and the
Django administration interface.</p>
<p>The example from the Lincoln Loop blog post can be replicated quite closely
with django-content-editor by using sections. I’m using the
<a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor</a>
package for the section plugin since it easily allows adding more fields if
some section type needs it.</p>
<p>Here’s an example model definition:</p>
<div class="chl"><pre><span></span><code><span class="c1"># Models</span>
<span class="kn">from</span> <span class="nn">content_editor.models</span> <span class="kn">import</span> <span class="n">Region</span><span class="p">,</span> <span class="n">create_plugin_base</span>
<span class="kn">from</span> <span class="nn">django_json_schema_editor.plugins</span> <span class="kn">import</span> <span class="n">JSONPluginBase</span>
<span class="kn">from</span> <span class="nn">feincms3</span> <span class="kn">import</span> <span class="n">plugins</span>
<span class="k">class</span> <span class="nc">Page</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># You have to define regions; each region gets a tab in the admin interface</span>
<span class="n">regions</span> <span class="o">=</span> <span class="p">[</span><span class="n">Region</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="s2">"content"</span><span class="p">,</span> <span class="n">title</span><span class="o">=</span><span class="s2">"Content"</span><span class="p">)]</span>
<span class="c1"># Additional fields for the page...</span>
<span class="n">PagePlugin</span> <span class="o">=</span> <span class="n">create_plugin_base</span><span class="p">(</span><span class="n">Page</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">RichText</span><span class="p">(</span><span class="n">plugins</span><span class="o">.</span><span class="n">richtext</span><span class="o">.</span><span class="n">RichText</span><span class="p">,</span> <span class="n">PagePlugin</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">Image</span><span class="p">(</span><span class="n">plugins</span><span class="o">.</span><span class="n">image</span><span class="o">.</span><span class="n">Image</span><span class="p">,</span> <span class="n">PagePlugin</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">Section</span><span class="p">(</span><span class="n">JSONPluginBase</span><span class="p">,</span> <span class="n">PagePlugin</span><span class="p">):</span>
<span class="k">pass</span>
<span class="n">AccordionSection</span> <span class="o">=</span> <span class="n">Section</span><span class="o">.</span><span class="n">proxy</span><span class="p">(</span>
<span class="s2">"accordion"</span><span class="p">,</span>
<span class="n">schema</span><span class="o">=</span><span class="p">{</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"object"</span><span class="p">,</span> <span class="p">{</span><span class="s2">"properties"</span><span class="p">:</span> <span class="p">{</span><span class="s2">"title"</span><span class="p">:</span> <span class="p">{</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"string"</span><span class="p">}}}},</span>
<span class="p">)</span>
<span class="n">CloseSection</span> <span class="o">=</span> <span class="n">Section</span><span class="o">.</span><span class="n">proxy</span><span class="p">(</span>
<span class="s2">"close"</span><span class="p">,</span>
<span class="n">schema</span><span class="o">=</span><span class="p">{</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"object"</span><span class="p">,</span> <span class="p">{</span><span class="s2">"properties"</span><span class="p">:</span> <span class="p">{}}},</span>
<span class="p">)</span>
</code></pre></div>
<p>Here’s the corresponding admin definition:</p>
<div class="chl"><pre><span></span><code><span class="c1"># Admin</span>
<span class="kn">from</span> <span class="nn">content_editor.admin</span> <span class="kn">import</span> <span class="n">ContentEditor</span>
<span class="kn">from</span> <span class="nn">django_json_schema_editor.plugins</span> <span class="kn">import</span> <span class="n">JSONPluginInline</span>
<span class="kn">from</span> <span class="nn">feincms3</span> <span class="kn">import</span> <span class="n">plugins</span>
<span class="nd">@admin</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Page</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">PageAdmin</span><span class="p">(</span><span class="n">ContentEditor</span><span class="p">):</span>
<span class="n">inlines</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">plugins</span><span class="o">.</span><span class="n">richtext</span><span class="o">.</span><span class="n">RichTextInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">RichText</span><span class="p">),</span>
<span class="n">plugins</span><span class="o">.</span><span class="n">image</span><span class="o">.</span><span class="n">ImageInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Image</span><span class="p">),</span>
<span class="n">JSONPluginInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">AccordionSection</span><span class="p">,</span> <span class="n">sections</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
<span class="n">JSONPluginInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">CloseSection</span><span class="p">,</span> <span class="n">sections</span><span class="o">=-</span><span class="mi">1</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div>
<p>The somewhat cryptic <code>sections=</code> argument says how many levels of sections
the individual blocks open or close.</p>
<p>To render the content including accordions I’d probably use a <a href="https://feincms3.readthedocs.io/en/latest/guides/rendering.html#using-marks">feincms3
renderer</a>.
At the time of writing the renderer definition for sections is a bit tricky.</p>
<div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">feincms3.renderer</span> <span class="kn">import</span> <span class="n">RegionRenderer</span><span class="p">,</span> <span class="n">render_in_context</span><span class="p">,</span> <span class="n">template_renderer</span>
<span class="k">class</span> <span class="nc">PageRenderer</span><span class="p">(</span><span class="n">RegionRenderer</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
<span class="n">plugins</span> <span class="o">=</span> <span class="n">deque</span><span class="p">(</span><span class="n">plugins</span><span class="p">)</span>
<span class="k">yield from</span> <span class="bp">self</span><span class="o">.</span><span class="n">_handle</span><span class="p">(</span><span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">in_section</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="k">while</span> <span class="n">plugins</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">plugins</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">models</span><span class="o">.</span><span class="n">Section</span><span class="p">):</span>
<span class="n">section</span> <span class="o">=</span> <span class="n">plugins</span><span class="o">.</span><span class="n">popleft</span><span class="p">()</span>
<span class="k">if</span> <span class="n">section</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">"close"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">in_section</span><span class="p">:</span>
<span class="k">return</span>
<span class="c1"># Ignore close section plugins when not inside section</span>
<span class="k">continue</span>
<span class="k">if</span> <span class="n">section</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">"accordion"</span><span class="p">:</span>
<span class="k">yield</span> <span class="n">render_in_context</span><span class="p">(</span><span class="s2">"accordion.html"</span><span class="p">,</span> <span class="p">{</span>
<span class="s2">"title"</span><span class="p">:</span> <span class="n">accordion</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">"title"</span><span class="p">],</span>
<span class="s2">"content"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_handle</span><span class="p">(</span><span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="n">in_section</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span>
<span class="p">})</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">yield</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_plugin</span><span class="p">(</span><span class="n">plugin</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span>
<span class="n">renderer</span> <span class="o">=</span> <span class="n">PageRenderer</span><span class="p">()</span>
<span class="n">renderer</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">RichText</span><span class="p">,</span> <span class="n">template_renderer</span><span class="p">(</span><span class="s2">"plugins/richtext.html"</span><span class="p">))</span>
<span class="n">renderer</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Image</span><span class="p">,</span> <span class="n">template_renderer</span><span class="p">(</span><span class="s2">"plugins/image.html"</span><span class="p">))</span>
<span class="n">renderer</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Section</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
</code></pre></div>
<h2 id="closing-thoughts"><a class="toclink" href="#closing-thoughts">Closing thoughts</a></h2>
<p>Sometimes, I think to myself, I’ll “just” write a “simple” blog post. I get
what I deserve when using those forbidden words. This blog post is neither
short or simple. That being said, the rendering code is a bit tricky, the rest
is quite straightforward. The amount of code in django-content-editor and
feincms3 is reasonable as well. Even though it may look like a lot you’ll still
be <a href="https://406.ch/writing/run-less-code-in-production-or-youll-end-up-paying-the-price-later/">running less code in
production</a>
than when using comparable solutions built using Django.</p>Weeknotes (2024 week 37)https://406.ch/writing/weeknotes-2024-week-37/2024-09-11T12:00:00Z2024-09-11T12:00:00Z<h1 id="weeknotes-2024-week-37"><a class="toclink" href="#weeknotes-2024-week-37">Weeknotes (2024 week 37)</a></h1>
<h2 id="django-debug-toolbar-alpha-with-async-support"><a class="toclink" href="#django-debug-toolbar-alpha-with-async-support">django-debug-toolbar alpha with async support!</a></h2>
<p>I have helped mentoring Aman Pandey who has worked all summer to add async
support to
<a href="https://github.com/jazzband/django-debug-toolbar/">django-debug-toolbar</a>.
<a href="https://github.com/tim-schilling">Tim</a> has released an alpha which contains
all of the work up to a few days ago. Test it! Let’s find the breakages before
the final release.</p>
<h2 id="dropping-python-39-from-my-projects"><a class="toclink" href="#dropping-python-39-from-my-projects">Dropping Python 3.9 from my projects</a></h2>
<p>I have read <a href="https://noumenal.es/posts/the-only-green-python/yLw/">Carlton’s post about the only green Python release</a> and have started dropping Python 3.9 support from many of the packages I maintain. This is such a good point:</p>
<blockquote>
<p>[…] I’m also thinking about it in terms of reducing the number of Python versions we support in CI. It feels like a lot of trees to support 5 full versions of Python for their entire life. 🌳</p>
</blockquote>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-debug-toolbar/5.0.0a0/">django-debug-toolbar 5.0.0a0</a>: See above.</li>
<li><a href="https://pypi.org/project/form-designer/">form-designer 0.26.2</a>: The values
of choice fields are now returned as-is when sending mails or exporting form
submissions instead of only returning the slugified version.</li>
<li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.17.1</a>: The
<a href="https://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/">role-based permissions
backend</a>
had a bug where it wouldn’t return all available permissions in all
circumstances, leading to empty navigation sidebars in the Django
administration. This has been fixed.</li>
<li><a href="https://pypi.org/project/feincms3/">feincms3 5.2.3</a>: Bugfix release, the
page moving interface is no longer hidden by an expanded navigation sidebar.
I almost always turn off the sidebar in my projects so I haven’t noticed
this.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.8.1</a>:
Contains the most recent ProseMirror updates and bugfixes.</li>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor
7.0.10</a>: Now supports
sections. Separate blog post coming up!</li>
</ul>Weeknotes (2024 week 35)https://406.ch/writing/weeknotes-2024-week-35/2024-08-28T12:00:00Z2024-08-28T12:00:00Z<h1 id="weeknotes-2024-week-35"><a class="toclink" href="#weeknotes-2024-week-35">Weeknotes (2024 week 35)</a></h1>
<h2 id="getting-deep-into-htmx-and-django-template-partials"><a class="toclink" href="#getting-deep-into-htmx-and-django-template-partials">Getting deep into htmx and django-template-partials</a></h2>
<p>I have been skeptical about <a href="https://htmx.org/">htmx</a> for some time because basically everything the library does is straightforward to do myself with a few lines of JavaScript. I am a convert now because, really, adding a few HTML attributes is nicer than copy pasting a few lines of JavaScript. Feels good.</p>
<p>The combination of htmx with <a href="https://github.com/carltongibson/django-template-partials/">django-template-partials</a> is great as well. I didn’t know I had been missing template partials until I started using them. Includes are still useful, but replacing some of them with partials makes working on the project much more enjoyable.</p>
<p>I haven’t yet had a use for <a href="https://django-htmx.readthedocs.io/">django-htmx</a> but I may yet surprise myself.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.17</a>: django-authlib bundles <code>authlib.little_auth</code> which offers an user model which uses the email address as the username. I have also introduced the concept of <a href="https://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/">roles instead of permissions</a>; now I have reorganized the user admin fieldset to hide user permissions altogether. Group permissions are still available as are roles. I’m personally convinced that user permissions were a mistake.</li>
<li><a href="https://pypi.org/project/feincms3-forms/">feincms3-forms 0.5</a>: Allowed setting a maximum length for the bundled URL and email fields through the Django administration interface.</li>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.0.7</a>: Fixed a bug where plugins with several fieldsets weren’t collapsed completely.</li>
<li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.19.1</a>: Allowed deactivating autogeneration of thumbnails completely through the <code>IMAGEFIELD_AUTOGENERATE</code> setting. This is very useful for batch processing. Also, documented all available settings.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.8</a>: Added support for translating the interface elements and for restricting the available heading levels in the UI.</li>
<li><a href="https://pypi.org/project/form-designer/">form-designer 0.25</a>: Fixed the type of the author field for the send-to-author processing action.</li>
<li><a href="https://pypi.org/project/feincms3/">feincms3 5.2.2</a>: Added support for embedding <a href="https://www.srf.ch/play/tv">SRF play</a> external content. They do not support oEmbed unfortunately.</li>
<li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.5.3</a>: Added support for SRF play embeddings as well. The difference is that the feincms3-cookiecontrol embedding requires consent before embedding external content.</li>
</ul>How I handle versioninghttps://406.ch/writing/how-i-handle-versioning/2024-08-20T12:00:00Z2024-08-20T12:00:00Z<h1 id="how-i-handle-versioning"><a class="toclink" href="#how-i-handle-versioning">How I handle versioning</a></h1>
<p>I have been reading up on versioning methods a bit, and I noticed that I never
shared my a bit unorthodox versioning method. I previously wrote about my <a href="https://406.ch/writing/my-rules-for-releasing-open-source-software/">rules for releasing open source software</a> but skipped everything related to versioning.</p>
<p>I use something close to the ideas of <a href="https://semver.org/">semantic versioning</a> but it’s not quite that. Note that the versioning scheme has nothing to do with production readiness, it’s more about communicating the state of the project.</p>
<h2 id="00x-everything-breaks"><a class="toclink" href="#00x-everything-breaks">0.0.x – Everything breaks</a></h2>
<p>I don’t trust my choices a lot. Everything may radically change, and I may also abandon the project with no hard feelings.</p>
<h2 id="010-changelogging"><a class="toclink" href="#010-changelogging">0.1.0 – Changelogging</a></h2>
<p>I generally start writing release notes or rather a <a href="https://en.wikipedia.org/wiki/Changelog">Changelog</a>, since writing full release notes is much more work. You absolutely have to test the software and I do not guarantee anything related to backwards compatibility – just that you’ll know about breaking changes when you read the CHANGELOG.</p>
<h2 id="x-bugfixes-and-pure-additions"><a class="toclink" href="#x-bugfixes-and-pure-additions">?.?.x – Bugfixes and pure additions</a></h2>
<p>Strictly speaking, patch version increments are only for bugfixes. However, when I add new features which are purely additional to the package or if there’s no way (famous last words) that anything will break, I’ll upload a patch release.</p>
<p>Sometimes it might be better to increment the minor version, yes. But, being very lazy, I sometimes only want to add a line to the CHANGELOG instead of adding a new heading for the new release. Also, if I don’t quickly release new additions there’s a real danger that I’ll only come back to the project weeks or months later, and I don’t want anyone (myself included) waiting on these updates. Also, if I inadvertently introduced new bugs it’s better to fix them quickly instead of forgetting about it first and having to rediscover the buggy code later.</p>
<h2 id="100-im-happy"><a class="toclink" href="#100-im-happy">1.0.0 – I’m happy</a></h2>
<p>I’m happy with the package and I commit to start using semver more properly. If I have to (or want to) break backwards compatibility you get a migration path, especially if I need it myself. Which is often the case since I’m maintaining dozens of Django-based websites and webapps, and I’m always <a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food">dogfooding</a> my stuff.</p>
<p>The pure additions case from the last section still applies, since slapping a 1.0 version on something doesn’t suddenly make me less lazy.</p>
<h2 id="outliers"><a class="toclink" href="#outliers">Outliers</a></h2>
<p><a href="https://github.com/feincms/feincms">FeinCMS</a> uses a variant of <a href="https://calver.org/">calendar versioning</a>; the version at the time of writing is 24.8.2 which means the second release in August 2024.</p>
<p>There are probably other outliers I forgot about.</p>Weeknotes (2024 week 33)https://406.ch/writing/weeknotes-2024-week-33/2024-08-14T12:00:00Z2024-08-14T12:00:00Z<h1 id="weeknotes-2024-week-33"><a class="toclink" href="#weeknotes-2024-week-33">Weeknotes (2024 week 33)</a></h1>
<h2 id="partying"><a class="toclink" href="#partying">Partying</a></h2>
<p>It’s summer, it’s hot, and it’s dance week. <a href="https://lethargy.ch/">Lethargy</a> is over, <a href="https://www.junglestreetgroove.ch/">Jungle Street Groove</a> is coming up. Good times.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.1</a>: I have finally left the alpha versioning. I’m still not committing to backwards compatibility, but I have started writing a CHANGELOG.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.7.1</a>: Thanks to Carlton’s pull request I have finally cleaned up the CSS somewhat and made overriding the styles more agreeable when using the editor outside the Django administration. The confusing active state of menubar buttons has also been rectified. <a href="https://django-prose-editor.readthedocs.io/">Docs are now available on Read the Docs.</a></li>
<li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.19</a>: django-imagefield can now be used with proxy models. Previously, thumbnails weren’t generated or deleted when saving proxy models because the signal handlers would only be called if the <code>sender</code> matches exactly. I have already debugged this before, but have forgotten about it again. The ticket is really old for this, and fixing it isn’t easy since it’s unclear what should happen (<a href="https://code.djangoproject.com/ticket/9318">#9318</a>).</li>
<li><a href="https://pypi.org/project/django_canonical_domain/">django-canonical-domain 0.11</a>: django-canonical-domain has gained support for excluding additional domains from the canonical domain redirect. django-canonical-domain is used to redirect users to HTTPS (optionally) and to a particular canonical domain (as the name says). But sometimes you have auxiliary domains, e.g. for an API service, which shouldn’t be redirected. The package can now be used in these scenarios as well.</li>
<li><a href="https://pypi.org/project/FeinCMS/">FeinCMS 24.8.2</a>: The venerable FeinCMS, now more than 15 years old. The thumbnailing support had a bug where it tried saving JPEGs using RGBA (which obviously doesn’t work). This has been fixed.</li>
</ul>Weeknotes (2024 week 31)https://406.ch/writing/weeknotes-2024-week-31/2024-07-31T12:00:00Z2024-07-31T12:00:00Z<h1 id="weeknotes-2024-week-31"><a class="toclink" href="#weeknotes-2024-week-31">Weeknotes (2024 week 31)</a></h1>
<p>I have missed almost two months of weeknotes. I’ve got some catching up to do.
I have tried writing a larger piece on my thoughts about CMS, but with
everything going on in my personal and work life I haven’t made much progress.</p>
<p>This weeknotes entry is me trying to get back into the groove of writing (and
publishing!) regularly.</p>
<h2 id="django-prose-editor"><a class="toclink" href="#django-prose-editor"><a href="https://github.com/matthiask/django-prose-editor/">django-prose-editor</a></a></h2>
<p>I have previously written about the
<a href="https://prosemirror.net/">ProseMirror</a>-based editor for Django websites
<a href="https://406.ch/writing/django-prose-editor-prose-editing-component-for-the-django-admin/">here</a>.
I have continued working on the project in the meantime. Apart from bugfixes
the big new feature is the support for showing typographic characters. For now
the editor supports showing non-breaking spaces and soft hyphens. The project
seems to get a little more interest after the deprecation of django-ckeditor
has become more well known and the project has even received a contribution by
someone else. It’s always a lovely moment when this happens.</p>
<h2 id="django-json-schema-editor"><a class="toclink" href="#django-json-schema-editor"><a href="https://github.com/matthiask/django-json-schema-editor">django-json-schema-editor</a></a></h2>
<p>Still alpha. Updated the vendorized JSON editor and fixed the integration into
Django to not throw errors with the newer version.</p>
<p>Foreign key fields now support describing the referenced value similar to the
raw ID fields functionality. Added optional support for using <code>"format":
"prose"</code> to use the django-prose-editor to edit individual fields. JSON
plugins for the content editor are now downcasted into their proxy models
automatically. This is especially useful with the feincms3 changes mentioned
below. (You do not have to use either django-content-editor or feincms3 to use
this package!)</p>
<p>The following screenshot shows the prose editor integration; the mentioned
foreign key field description isn’t visible yet here.</p>
<p><img alt="A screenshot of the JSON editor including the prose editor" src="https://406.ch/assets/20240731-json-editor.png" /></p>
<h2 id="traduire"><a class="toclink" href="#traduire"><a href="https://github.com/matthiask/traduire">Traduire</a></a></h2>
<p>Traduire (french for «translate») is a web-based platform for editing gettext
translations.</p>
<p>It is intended as a replacement for Transifex, Weblate and comparable products.
It is geared towards small teams or agencies which want to allow their
customers and their less technical team members to update translations.</p>
<p>Traduire profits from the great work done on
<a href="https://github.com/mbi/django-rosetta/">django-rosetta</a>. I would still be
using Rosetta if it would work when used with a container orchestator such as
Kubernetes. Since all application storage is ephemeral that doesn’t work,
translation editing and deployment have to be separated.</p>
<p><img alt="A screenshot of the Traduire interface" src="https://406.ch/assets/20240731-traduire.png" /></p>
<p>It is built using Django and relies on <a href="https://pypi.org/project/polib/">polib</a>
to do the heavy lifting.</p>
<p>This is a project which might also be interesting for others. I would
especially appreciate it if someone could contribute an easier way to get it up
and running, e.g. using a Docker Compose configuration or something. I am using
Kubernetes and GitOps to host it, but that’s not straightforward at all.
Really, all that’s needed to run it is a Django host with any database which is
supported by Django. I prefer using PostgreSQL because I have it, but sqlite
etc. work just as well.</p>
<h2 id="releases-since-the-second-week-of-june"><a class="toclink" href="#releases-since-the-second-week-of-june">Releases since the second week of June</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-translated-fields/">django-translated-fields 0.13</a>:
Nothing much except for CI and pre-commit updates. The implementation
continues to be rock-solid and basically unchanged.</li>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.0.6</a>:
Tweaks and fixes to the new interface. Added better scrolling behavior when
dragging content around. The editor now also supports colorized icons which
helps quickly understanding the structure of some content when there are many
plugins.</li>
<li><a href="https://pypi.org/project/blacknoise/">blacknoise 1.0.2</a>:
Fixed a few bugs in the <code>blacknoise.compress</code> utility and started running
the testsuite on GitHub actions.
<a href="https://github.com/evansd/whitenoise/">whitenoise</a> has been friendly-forked
as <a href="https://github.com/Archmonger/ServeStatic">ServeStatic</a> and I’m
definitely having a close look at this project as well, but blacknoise is
simple and works well, so I’m not convinced that switching back to the much
larger project (in terms of amounts of code) is an improvement now.</li>
<li><a href="https://pypi.org/project/django-authlib/">django-authlib</a>:
Minor bugfixes.</li>
<li><a href="https://pypi.org/project/feincms3/">feincms3</a>:
Allowed registering plugin models with the renderer which aren’t supposed to
be fetched from the database. This is especially useful when used together
with JSON plugins, where the individual JSON plugins are created as proxies
for the underlying Django model and fetched all at once. Disabled the version
check on our CKEditor plugin. Still, really stop using CKEditor 4 if you want
to use maintained software.</li>
<li><a href="https://pypi.org/project/FeinCMS/">FeinCMS 24.7.1</a>:
Small bugfixes, and made the Read the Docs build work correctly.</li>
<li><a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar 4.4.x</a>:
The toolbar continues to be a nice project to work on. Fixed a few edge cases
in the new alerts panel.</li>
<li><a href="https://pypi.org/project/django-admin-ordering/">django-admin-ordering 0.18.2</a>:
The value of <code>ordering_field</code> now has additional sanity checks.</li>
<li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.16</a>:
cabinet now supports exporting a folder as a ZIP file while preserving the
structure you see in the CMS instead of the structure on the file system. The
inline upload form has been dropped from the <code>CabinetForeignKey</code> widget
because the folder dropdown slowed down the page a lot when used on a site
with many folders. Using the raw ID fields popup isn’t that bad.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.6.2</a>:
See above.</li>
<li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.0.28</a>:
See above.</li>
</ul>Weeknotes (2024 week 23)https://406.ch/writing/weeknotes-2024-week-23/2024-06-07T12:00:00Z2024-06-07T12:00:00Z<h1 id="weeknotes-2024-week-23"><a class="toclink" href="#weeknotes-2024-week-23">Weeknotes (2024 week 23)</a></h1>
<h2 id="switching-everything-from-pip-to-uv"><a class="toclink" href="#switching-everything-from-pip-to-uv">Switching everything from pip to uv</a></h2>
<p>Enough said. I’m always astonished how fast computers can be.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-admin-ordering/">django-admin-ordering
0.18</a>: Added a database
index to the ordering field since we’re always sorting by it.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.4</a>:
Dropped the jQuery dependency making it possible to use the editor outside
the Django administration interface without annoying JavaScript errors.
Allowed additional heading levels and moved the block type buttons into a
popover.</li>
<li><a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar 4.4.2</a>:
I enjoy working on this important piece of software very much.</li>
<li><a href="https://pypi.org/project/django-email-hosts/">django-email-hosts 0.2.1</a>:
Added a command analogous to <code>./manage.py sendtestemail</code> so that it’s
possible to easily test the different configured email backends.</li>
<li><a href="https://pypi.org/project/feincms3/">feincms3 5.0</a>: I completely reworked the
move node action; previously it opened a new page where you could see all
possible targets; now you can cut a page and paste it somewhere else. The
advantages of the new interface is that you don’t leave the changelist and
can still profit from all its features while moving pages around.</li>
<li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.21</a>: A new
release taking advantage of a new hook in feincms3 7.0 so that the new moving
interface works.</li>
<li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.16.5</a>: authlib
now shows a welcome message when authenticating using admin OAuth2. It’s nice
and helps with debugging strange authentication failures.</li>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.0</a>:
I reworked the UI. The sidebar is gone, instead there are nice buttons in the
place where you can add new plugins; the plugins appear in a nice grid
instead of a list, which looks much better once you have more than just a few
plugin types available. Also, plugin type icons are now shown in the plugin
forms. I think it looks much better than before.</li>
<li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol
1.5.2</a>: I didn’t contribute
anything to this release which is also a nice experience for a change. The
Google consent mode integration has been improved and simplified.</li>
<li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor
0.0.22</a>: Various
small-ish improvements. I should really start using higher version numbers,
but not having to commit to anything also feels great. That being said, the
editor is in active use in several projects, so maybe I’m deceiving myself.</li>
</ul>Workbench: Coffee time!https://406.ch/writing/workbench-coffee-time/2024-05-24T12:00:00Z2024-05-24T12:00:00Z<h1 id="workbench-coffee-time"><a class="toclink" href="#workbench-coffee-time">Workbench: Coffee time!</a></h1>
<p>I have written about <a href="https://406.ch/writing/workbench-the-django-based-agency-software/">the Workbench agency
software</a> a
few weeks back.</p>
<p>Back when we were using Slack at <a href="https://feinheit.ch/">Feinheit</a> we used a
Donut bot to generate randomized invites for a coffee break. <a href="https://406.ch/writing/why-we-switched-from-slack-to-discord-at-work/">We lost that bot
when we switched to
Discord</a>.
I searched some time for an equivalent bot for Discord but couldn’t find any,
so like any self-respecting nerd, break lover and NIH-sufferer I reimplemented
the functionality as a part of our agency software.</p>
<p>The first version generated totally random pairings without any history; I
thought that maybe this would be good enough, but of course I received an
invite for a coffee with the exact same person in the first two invitations.
Even though I did enjoy the break both times this motivated me to put some
effort into a better solution :-)</p>
<p>The result of this work is the <a href="https://github.com/matthiask/workbench/blob/a14d8b9560def7a4e2bbf7531eb6108f734568db/workbench/accounts/tasks.py#L57"><code>coffee_invites()</code> function</a></p>
<p>There are two main parts to the generator:</p>
<ul>
<li>It randomly generates twenty pairings from all participants; it prefers
groups of two except for the last group which may contain three people.</li>
<li>It loads old pairings from the database and gives higher penalties to equal
pairings depending on the time which has passed since the last time.</li>
</ul>
<p>So, it’s not mathematically perfect, but all imbalances will cancel out over a
long time. I experimented with the discounting factor and the number of random
pairings it generates upfront. The values in the code seem to work fine.</p>
<p>Sometimes it’s simple stuff like this which gets me going and reignites the
love for programming. Like a small puzzle.</p>Weeknotes (2024 week 21)https://406.ch/writing/weeknotes-2024-week-21/2024-05-22T12:00:00Z2024-05-22T12:00:00Z<h1 id="weeknotes-2024-week-21"><a class="toclink" href="#weeknotes-2024-week-21">Weeknotes (2024 week 21)</a></h1>
<p>There have been times when work has been more enjoyable than in the last few
weeks. It feels more stressful than at other times, and this mostly has to do
with particular projects. I hope I’ll be able to move on soon.</p>
<h2 id="blacknoise"><a class="toclink" href="#blacknoise">blacknoise</a></h2>
<p>I have released <a href="https://pypi.org/project/blacknoise/">blacknoise 1.0</a>. It’s an
ASGI app for static file serving inspired by
<a href="https://github.com/evansd/whitenoise/">whitenoise</a>.</p>
<p>The 1.0 version number is only a big step in versioning terms, not much has
happened with the code. It’s a tiny little well working piece of software which
has been running in production for some time without any hickups. The biggest
recent change is that I have parallelized the gzip and brotli compression step;
this makes building images using whitenoise more painful because there the wait
is really really long sometimes. <a href="https://github.com/evansd/whitenoise/pull/484">A pull request fixing this
exists</a>, but it hasn’t moved
forwards in months.</p>
<p>I have written a longer post about it earlier this year
<a href="https://406.ch/writing/blacknoise-asgi-app-for-static-file-serving/">here</a>.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.5</a>: Code golfing. Added backwards compatibility with old Django versions so that I can use it for old projects. Also includes optional support for Google consent management.</li>
<li><a href="https://pypi.org/project/django_fast_export/">django-fast-export 0.1.1</a>: This is basically a repackaging of the streaming CSV view from Django’s documentation as a reusable class. I have switched to using an iterator so that I can export even larger datasets.</li>
<li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.0.18</a>: Still alpha versioned but used in production in various projects. I should really release an 1.0 version, but there are no integration tests at all. Mainly visual tweaks in this update.</li>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 6.5</a>: Better handling of templates and regions when a particular editor instance only shows a subset of configured templates. Disallowed adding plugins when in an unknown region. It’s funny how many edge cases exist in software as old as this.</li>
<li><a href="https://pypi.org/project/blacknoise/">blacknoise 1.0</a>: See above.</li>
<li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.4.4</a>: Fixed edge cases with whitespace handling when merging elements.</li>
<li><a href="https://pypi.org/project/feincms3-data/">feincms3-data 0.6.1</a>: Allowed <code>./manage.py f3loaddata -</code> to load JSON data from stdin.</li>
</ul>Weeknotes (2024 week 18)https://406.ch/writing/weeknotes-2024-week-18/2024-05-03T12:00:00Z2024-05-03T12:00:00Z<h1 id="weeknotes-2024-week-18"><a class="toclink" href="#weeknotes-2024-week-18">Weeknotes (2024 week 18)</a></h1>
<h2 id="google-summer-of-code-has-begun"><a class="toclink" href="#google-summer-of-code-has-begun">Google Summer of Code has begun</a></h2>
<p>We have a student helping out with adding async support to the <a href="https://github.com/jazzband/django-debug-toolbar/">Django Debug
Toolbar</a>. It’s great that
someone can spend some concentrated time to work on this. Tim and others have
done all the necessary preparation work, I’m only helping from the sidelines so
don’t thank me.</p>
<h2 id="bike-to-work"><a class="toclink" href="#bike-to-work">Bike to Work</a></h2>
<p>Two teams from my company are participating in the <a href="https://www.biketowork.ch/">Bike to Work Challenge
2024</a>. It’s what I do anyway (if I’m not working
from home) but maybe it helps build others some motivation to get on the
bicycle once more. Public transports in the city where I live are great but
I’ll always take the bike when I can. I also went on my first mountain bike
ride in a few months yesterday, good fun.</p>
<h2 id="json-blobs-and-referential-integrity"><a class="toclink" href="#json-blobs-and-referential-integrity">JSON blobs and referential integrity</a></h2>
<p>The <a href="https://github.com/matthiask/django-json-schema-editor/">django-json-schema-editor</a> has gained support for referencing Django models. Here’s an example schema excerpt:</p>
<div class="chl"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="o">...</span>
<span class="w"> </span><span class="s">"articles"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s">"type"</span><span class="p">:</span><span class="w"> </span><span class="s">"array"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"format"</span><span class="p">:</span><span class="w"> </span><span class="s">"table"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"title"</span><span class="p">:</span><span class="w"> </span><span class="nx">_</span><span class="p">(</span><span class="s">"articles"</span><span class="p">),</span>
<span class="w"> </span><span class="s">"minItems"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="s">"maxItems"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span>
<span class="w"> </span><span class="s">"items"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s">"type"</span><span class="p">:</span><span class="w"> </span><span class="s">"string"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"title"</span><span class="p">:</span><span class="w"> </span><span class="nx">_</span><span class="p">(</span><span class="s">"article"</span><span class="p">),</span>
<span class="w"> </span><span class="s">"format"</span><span class="p">:</span><span class="w"> </span><span class="s">"foreign_key"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"options"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s">"url"</span><span class="p">:</span><span class="w"> </span><span class="s">"/admin/articles/article/?_popup=1&_to_field=id"</span><span class="p">,</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="o">...</span>
<span class="p">}</span>
</code></pre></div>
<p>The ID field is stringly typed; using an integer directly wouldn’t work because
the empty string isn’t a valid integer.</p>
<p>The problem with referencing models in this way is that there’s no way to know
if the referenced object is still around or not, or even to protect it against
deletion. The bundled django-content-editor <code>JSONPlugin</code> now supports
automatically generating a <code>ManyToManyField</code> with a <code>through</code> model which
protects articles from deletion as long as they are referenced from a
<code>JSONPlugin</code> instance. The <code>register_reference</code> line creates the mentioned
model with an <code>on_delete=models.PROTECT</code> foreign key to articles and a
<code>post_save</code> handler which updates said references.</p>
<div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django_json_schema_editor.plugins</span><span class="w"> </span><span class="kn">import</span><span class="w"> </span><span class="n">JSONPluginBase</span><span class="p">,</span><span class="w"> </span><span class="n">register_reference</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">articles.models</span><span class="w"> </span><span class="kn">import</span><span class="w"> </span><span class="n">Article</span>
<span class="k">class</span><span class="w"> </span><span class="nc">JSONPlugin</span><span class="p">(</span><span class="n">JSONPluginBase</span><span class="p">,</span><span class="w"> </span><span class="o">...</span><span class="p">):</span>
<span class="w"> </span><span class="k">pass</span>
<span class="n">register_reference</span><span class="p">(</span><span class="n">JSONPlugin</span><span class="p">,</span><span class="w"> </span><span class="s2">"articles"</span><span class="p">,</span><span class="w"> </span><span class="n">Article</span><span class="p">)</span>
</code></pre></div>
<h2 id="releases-since-the-beginning-of-april"><a class="toclink" href="#releases-since-the-beginning-of-april">Releases since the beginning of April</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.0.14</a>: See above. Also, some styling work and a patch update to the vendorized json-editor.</li>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 6.4.6</a>: Many small stylistic fixes. The target indicator when dragging plugins is now also shown when plugins are collapsed. It’s now possible to directly drag a plugin to the end, and not just to the second to last position.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.3.4</a>: Switched to the nh3 sanitizer because it’s faster and because ProseMirror never emits HTML which has to be cleaned up first. Stopped generating menu items for nodes and marks which aren’t in the schema. Added the possibility to reduce the functionality per editor instance. Small tweaks and fixes.</li>
<li><a href="https://pypi.org/project/django-tree-queries/">django-tree-queries 0.19</a>: Added support for pre-filtering the tree (much more efficient when only querying a part of the tree). Added support for adding additional fields to the CTE so that you can collect values from ancestors for other fields than the default fields too.</li>
<li><a href="https://pypi.org/project/FeinCMS/">FeinCMS 24.4.2</a>: Added support for webp images. Fixed a few of the admin list filters to work with Django 5.</li>
<li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.14.3</a>: Fixed the support for the <code>extra_context</code> argument to our <code>changelist_view</code> implementation.</li>
</ul>Workbench, the Django-based agency softwarehttps://406.ch/writing/workbench-the-django-based-agency-software/2024-04-24T12:00:00Z2024-04-24T12:00:00Z<h1 id="workbench-the-django-based-agency-software"><a class="toclink" href="#workbench-the-django-based-agency-software">Workbench, the Django-based agency software</a></h1>
<p>I get the impression that there’s a lot of interesting but unknown software in Django land. I don’t know if there’s any interest in some of the packages I have been working on; if not this blog post is for myself only.</p>
<h2 id="history-time"><a class="toclink" href="#history-time">(Hi)story time</a></h2>
<p>As people may know I work at <a href="https://feinheit.ch/">Feinheit</a>, an agency which specializes in digital communication services for SMEs, campaigns for referendums, and website and webapp development. At the time of writing we are a team of about 20-25 communication experts, graphic designers, programmers and project managers.</p>
<p>We have many different clients and are working on many different projects at the same time and are billing by the hour<sup id="fnref:bythehour"><a class="footnote-ref" href="#fn:bythehour">1</a></sup>. Last year my own work has been billed to more than 50 different customers. In the early days we used a shared file server with spreadsheet files to track our working hours. Luckily we didn’t often overwrite the edits others made but that was definitely something which happened from time to time.</p>
<p>We knew of another agency who had the same problems and used a FileMaker-based software. Their solution had several problems, among them the fact that it became hard to evolve and that it got slower and slower as more and more data was entered into it over the years. They had the accounting know how and we had the software engineering know how so we wrote a webapp based on the Django framework. As always, it was much more work than the initial estimate, but if we as programmers didn’t underestimate the effort needed we wouldn’t have started many of the great projects we’re now getting much value and/or enjoyment from, hopefully both. The product of that work was <a href="https://www.fineware.ch/">Metronom</a>. The first release happened a little bit later than <a href="https://www.getharvest.com/about">harvest</a> but it already came with full time tracking including an annual working time calculator, absence management, offers, invoices including PDF generation etc, so it was quite a bit more versatile while still being easier to use than “real” business software solutions.</p>
<p>I personally was of the opinion that the product was good enough to try selling it, but for a variety of reasons (which I don’t want to go into here) this never happened and <a href="https://feinheit.ch/">we</a> decided that we didn’t want to be involved anymore.</p>
<p>However, this meant that we were dead-end street with a software that didn’t belong to us anymore, which wasn’t evolving to our changing requirements. I also didn’t enjoy working on it anymore. Over the years I have tried replacing it several times but that never came to pass until some time after the introduction of <a href="https://www.holacracy.org/">Holacracy</a> at our company. I noticed that I didn’t have to persuade everyone but that I, as the responsible person for this particular decision, could “just”<sup id="fnref:just"><a class="footnote-ref" href="#fn:just">2</a></sup> move ahead with a broad interpretation of the purpose and accountabilities of one of my roles.</p>
<h2 id="workbench"><a class="toclink" href="#workbench"><a href="https://github.com/matthiask/workbench">Workbench</a></a></h2>
<p><img alt="Screenshot" src="https://406.ch/assets/20240424-workbench.png" /></p>
<p><a href="https://github.com/matthiask/workbench">Workbench</a> is the product of a few long nights of hacking. The project was started as an experiment in 2015 and was used for sending invoices but that wasn’t really the intended purpose. After long periods of lying dormant I have brought the project to a good enough state and switched Feinheit away from Metronom in 2019.</p>
<p>I have thought long and hard about switching to one of the off-the-shelf products and it could very well be that one of them would work well for us. Also, we wouldn’t have to pay (in form of working hours) for the maintenance and for enhancements ourselves. On the other hand, we can use a tool which is tailored to our needs. Is it worth the effort? That’s always hard to answer. The tool certainly works well for the few companies which are using it right now, so there’s no reason to agonize over that.</p>
<p>At the time of writing, Workbench offers projects and services, offers and invoices incl. PDF generation, recurring invoices, an address book, a logbook for rendered services, annual working time reports, an acquisition funnel, a stupid project planning and resource management tool and various reports. It has not only replaced Metronom but also a selection of SaaS (for example Pipedrive and TeamGantt) we were using previously.</p>
<p>The whole thing is open source because I don’t want to try making agency software into a business anymore, I only want to solve the problems <em>we</em> have. That being said, if someone else finds it useful then that’s certainly alright as well.</p>
<p>The license has been MIT for the first few years but I have switched to the GPL because I wanted to integrate the excellent <a href="https://pypi.org/project/qrbill/">qrbill</a> module which is also licensed under the GPL. As I wrote elsewhere, I have released almost everything under the GPL in my first few open source years but have switched to BSD/MIT later when starting to work mainly with Python and Django because I thought that this license is a better fit<sup id="fnref:license"><a class="footnote-ref" href="#fn:license">3</a></sup> for the ecosystem. That being said, for a product such as this the GPL is certainly an excellent fit.</p>
<h2 id="final-words"><a class="toclink" href="#final-words">Final words?</a></h2>
<p>There are a few things I could write about to make this into a series. I’m putting a few ideas here, not as an announcement, just as a reminder for myself.</p>
<ul>
<li>Better time tracking</li>
<li>Patterns for creating recurring invoices</li>
<li>Feature switches and configurability</li>
<li><a href="https://406.ch/writing/workbench-coffee-time/">Coffee time</a></li>
</ul>
<div class="footnote">
<hr />
<ol>
<li id="fn:bythehour">
<p>Rather, in six minute increments. It’s even worse. <a class="footnote-backref" href="#fnref:bythehour" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:just">
<p>Of course it’s never that simple, because the responsibility is a lot. The important point is: It would have been a lot of work anyways, the big difference is that it was sufficient to get consent from people; no consensus required. <a class="footnote-backref" href="#fnref:just" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:license">
<p>That’s not meant as a criticism in any way! <a class="footnote-backref" href="#fnref:license" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>Building forms with the Django adminhttps://406.ch/writing/building-forms-with-the-django-admin/2024-04-12T12:00:00Z2024-04-12T12:00:00Z<h1 id="building-forms-with-the-django-admin"><a class="toclink" href="#building-forms-with-the-django-admin">Building forms with the Django admin</a></h1>
<p>The title of this post was shamelessly copied from <a href="https://mastodon.social/@webology/112235938469045649">Jeff Triplett’s post on
Mastodon</a>.</p>
<h2 id="why"><a class="toclink" href="#why">Why?</a></h2>
<p>Many websites need a simple way of embedding forms, for example as a contact
form or for simple surveys to collect some data or inputs from visitors.
<a href="https://docs.djangoproject.com/en/5.0/topics/forms/">Django’s forms library</a>
makes building such forms straightforward but changing those forms requires
programming skills and programmer time. Both of them may not be readily
available. More importantly, sometimes it’s just nice to give more tools to web
publishers.</p>
<p>The simple way to build something like this is to use a form builder such as
Google Forms, Typeform, Paperform or anything of the sort. Those options work
nicely. The downsides are that embedded forms using those services load slowly,
look differently, cost a lot or collect a lot of data on users, or all of those
options. Because of that there’s still a place for building such functionality
locally.</p>
<p>If I wanted to use PHP and WordPress I could just use
<a href="https://wpforms.com/">WPForms</a> and call it a day. Since I do not actually want
that this blog post is a bit longer.</p>
<h2 id="the-early-days-form-designer"><a class="toclink" href="#the-early-days-form-designer">The early days: form-designer</a></h2>
<p>One of the first Django-based third party apps I published was the <a href="https://github.com/feincms/form-designer">form-designer</a>. The first version was uploaded to PyPI in 2012 but it had already been used in production for more than two years at that point in time. I had used <a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules">Git submodules</a> for the deployment back then, before switching to <a href="https://virtualenv.pypa.io/">Python virtualenvs</a> some time later (and never looking back!)</p>
<p>The form-designer is still maintained actively. Because of Django’s stability and because of the fact that the app doesn’t do all that much it doesn’t require much development at all.</p>
<p><img alt="A screenshot of the admin interface" src="https://406.ch/assets/20240410-form-designer.png" /></p>
<p>The form designer supports a selection of standard HTML5 input fields out of the box and also has an optional <a href="https://github.com/django-recaptcha/django-recaptcha">django-recaptcha</a> integration. All fields support some basic configuration such as setting a title, a help text, marking the field as required etc. Submissions can be sent to a configurable email address and can be saved in the database and later exported as an XLSX file. It’s also possible to define your own actions.</p>
<h2 id="more-flexibility-needed-feincms3-forms"><a class="toclink" href="#more-flexibility-needed-feincms3-forms">More flexibility needed: feincms3-forms</a></h2>
<p>A few years back I mentioned <a href="https://406.ch/writing/weeknotes-2021-week-13-and-14/">feincms3-forms in a weeknotes entry</a>. The reasons why form-designer wasn’t sufficient for a project back then are outlined in the blog post:</p>
<blockquote>
<h3 id="feincms3-forms-a-new-forms-builder-for-the-django-admin-interface"><a class="toclink" href="#feincms3-forms-a-new-forms-builder-for-the-django-admin-interface">feincms3-forms – A new forms builder for the Django admin interface</a></h3>
<p>For a current project <a href="https://feinheit.ch/">we</a> needed a forms builder with the following constraints:</p>
<ul>
<li>Simple fields (text, email, checkboxes, dropdowns etc.)</li>
<li>Custom validation and processing logic</li>
<li>It should be possible to add other content, e.g. headings and explanations between form fields</li>
</ul>
<p>The <a href="https://github.com/feincms/form_designer">form_designer</a> fulfilled a few of these requirements but not all. It still works well but I wanted a forms builder based on <a href="https://github.com/matthiask/django-content-editor">django-content-editor</a> for a long time already. Also, I really like the feincms3 pattern where the third party app only provides abstract models. Yeah, it is much more work to start with but the flexibility and configurability is worth it – especially since it’s possible to write straightforward code to handle special cases[^2] instead of configuring even more settings.</p>
<p>The humble beginnings are here in the <a href="https://github.com/matthiask/feincms3-forms/">feincms3-forms</a> repository. The <a href="https://github.com/matthiask/feincms3-forms/tree/main/tests/testapp">test suite already shows how things work together</a> but as of now no documentation exists and no release has been made yet. I hope it will be ready for a first beta release in the next few weeks 😄</p>
</blockquote>
<p>Since then I have used feincms3-forms more often than form-designer, for building simple forms and also to build multi-step form wizards with custom fields, custom validation, configurable steps etc. The <a href="https://github.com/feincms/feincms3-forms?tab=readme-ov-file#feincms3-forms">README</a> now actually explains why the project exists and how it could be used.</p>
<p>It still doesn’t come close to WPForms in terms of included functionality; a big feature which is missing is conditional logic because I haven’t yet had a use for it.</p>
<p><img alt="The feincms3-forms admin interface" src="https://406.ch/assets/20240410-feincms3-forms.png" /></p>
<p>The feincms3-forms forms support all types of content between form fields (basically everything <a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> supports). Plugins for form fields are more flexible and can add as many input fields to the form as they want, you’re not restricted to single values or single input fields.</p>
<h2 id="packages"><a class="toclink" href="#packages">Packages</a></h2>
<ul>
<li><a href="https://github.com/feincms/form-designer">form-designer</a></li>
<li><a href="https://github.com/feincms/feincms3-forms">feincms3-forms</a></li>
</ul>Weeknotes (2024 week 14)https://406.ch/writing/weeknotes-2024-week-14/2024-04-06T12:00:00Z2024-04-06T12:00:00Z<h1 id="weeknotes-2024-week-14"><a class="toclink" href="#weeknotes-2024-week-14">Weeknotes (2024 week 14)</a></h1>
<p>I’m having a bit of a slow week with the easter weekend and a wisdom tooth
extraction. I’m recovering quite quickly it seems and I’m glad about it.</p>
<p>This weeknotes entry is short and quick. I’m trying to get back into the habit
of writing them after a mediocre start this year.</p>
<h2 id="20th-anniversary-celebration-of-young-greens-switzerland"><a class="toclink" href="#20th-anniversary-celebration-of-young-greens-switzerland">20th Anniversary Celebration of Young Greens Switzerland</a></h2>
<p>I have attended the celebration of Young Greens Switzerland. I have been a
founding member and have been active for close to ten years. A lot of time has
passed since then. It has been great to reminisce about old times with friends
and, more importantly, to see how the torch is carried on.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/blacknoise/">blacknoise 0.0.5</a>: blacknoise is an
ASGI app for static file serving inspired by
<a href="https://whitenoise.readthedocs.io/en/latest/">whitenoise</a>. It only supports
a very limited subset of whitenoise’s functionality, but it supports async.</li>
<li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.4.1</a>: The lxml
library moved the HTML cleaner into its own package,
<a href="https://pypi.org/project/lxml-html-clean/">lxml-html-clean</a>; this release
adds support for that. I didn’t know that the HTML cleaner is viewed as being
problematic by the lxml maintainers. I’m having another look at
<a href="https://github.com/messense/nh3">nh3</a> and will maybe switch html-sanitizer’s
guts from lxml to nh3 in the future.</li>
<li><a href="https://pypi.org/project/django-tree-queries/">django-tree-queries 0.18</a>:
django-tree-queries now supports ordering siblings by multiple fields and
even allows descending orderings.</li>
<li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.14.2</a>: This
release fixes the CKEditor 4 filebrowser popup when using Django 5 or better.</li>
</ul>