Matthias Kestenholz: Posts about Djangohttps://406.ch/writing/category-django/2024-11-20T12:00:00ZMatthias KestenholzWeeknotes (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>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>The Django admin is a CMShttps://406.ch/writing/the-django-admin-is-a-cms/2024-03-27T12:00:00Z2024-03-27T12:00:00Z<h1 id="the-django-admin-is-a-cms"><a class="toclink" href="#the-django-admin-is-a-cms">The Django admin is a CMS</a></h1>
<p>The post <a href="https://www.coderedcorp.com/blog/why-is-the-django-admin-ugly/">Why is the Django Admin “Ugly”?</a> and the <a href="https://hachyderm.io/@paulox@fosstodon.org/111298440425647176">discussion on
Mastodon</a> around it finally motivated me to write down my
thoughts regarding the recurring theme in Django land that the <a href="https://docs.djangoproject.com/en/5.0/ref/contrib/admin/">Django
administration interface</a> isn’t a CMS (Content Management
System).</p>
<p>I think that this is misguided and needlessly limits the discourse around what
the admin’s current functionality is and the ideas what it could be and already
is.</p>
<p><a href="https://en.wikipedia.org/wiki/Web_content_management_system">A web content management system</a> is about website authoring for users
who do not need to be web programming experts in their own rights. <a href="https://en.wikipedia.org/wiki/Django_(web_framework)">Django was
created at the Lawrence Journal-World newspaper</a>. The admin itself was
created to allow quickly spinning up new websites, where the admin interface
was used by content managers to fill in the content while programmers finalized
the rest of the website. So obviously the admin interface was a system used to
manage content<sup id="fnref:words"><a class="footnote-ref" href="#fn:words">1</a></sup> from the beginning.</p>
<p>Sure, the <a href="https://docs.djangoproject.com/en/5.0/ref/contrib/admin/">Django admin site documentation</a> states:</p>
<blockquote>
<p>One of the most powerful parts of Django is the automatic admin interface. It reads metadata from your models to provide a quick, model-centric interface where trusted users can manage content on your site. The admin’s recommended use is limited to an organization’s internal management tool. <strong>It’s not intended for building your entire front end around.</strong> [emphasis added]</p>
</blockquote>
<p>In other words, the Django documentation also points out that the admin is
powerful and that it allows trusted users to manage content<sup id="fnref2:words"><a class="footnote-ref" href="#fn:words">1</a></sup>.</p>
<p>Yes, it will be very painful if you try to do everything on top of the Django
admin site. The warnings against using the Django admin for more than it was
designed to are necessary and I totally support them. As soon as you’re getting
into workflows, into complex permission scenarios (sad noises) or similar
things the admin definitely isn’t for you. But, the admin nicely solves 90% of
the problems with 10% of the effort. And it’s very good at that.</p>
<p>And sure, if you try building your own frontend on top of the Django admin
you’re in for a bumpy ride, but that much should be obvious.</p>
<p>Many third party apps for Django actually target the Django admin interface
itself, and not one of the (excellent!) Django-based CMS such as
<a href="https://wagtail.org/">Wagtail</a>. This means that by building on the Django
admin instead of one of the CMS you’re <a href="https://406.ch/writing/run-less-code-in-production-or-youll-end-up-paying-the-price-later/">running less code</a>, by using
more libraries instead of frameworks (on top of frameworks) you’re <a href="https://406.ch/writing/low-maintenance-software/">keeping
maintenance lower</a>, and you’re a part of a larger
community<sup id="fnref:community"><a class="footnote-ref" href="#fn:community">2</a></sup>, which brings the potential benefit of being able to
profit more from the general Django packages ecosystem.</p>
<p>Since you’re depending on smaller pieces of additional software it will
generally be possible to upgrade to new Django versions quicker. This isn’t
true for all packages of course, and I’m a reluctant maintainer of some of
them. Anecdotes aren’t data, but I see that some larger CMS systems are
definitely having a hard time keeping up with Django’s release schedule.</p>
<p>I’m not trying to say that the Django admin is a better CMS than other
Django-based CMS, or any other CMS. I’m saying it’s a trade off and you should
be mindful of the downsides of choosing a larger system. And I’m saying that
the people who tell you that you shouldn’t be using the Django admin interface
are wrong in the first approximation.</p>
<p>The fact that it’s so easy to spin up an additional site and with minimal
effort and still be able to work with clean database schemas and all the great
tools Django (and Python) offers is important for those of us who are working
on many different projects with limited financial resources, because the
website often is for example just a small part of a campaign.</p>
<div class="footnote">
<hr />
<ol>
<li id="fn:words">
<p>Sorry-not-sorry for my choice of words. <a class="footnote-backref" href="#fnref:words" title="Jump back to footnote 1 in the text">↩</a><a class="footnote-backref" href="#fnref2:words" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:community">
<p>The assumption that the communities of these Django-based CMS projects are a subset of the Django community itself shouldn’t be too controversial. <a class="footnote-backref" href="#fnref:community" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
</ol>
</div>blacknoise – ASGI app for static file servinghttps://406.ch/writing/blacknoise-asgi-app-for-static-file-serving/2024-03-20T12:00:00Z2024-03-20T12:00:00Z<h1 id="blacknoise-asgi-app-for-static-file-serving"><a class="toclink" href="#blacknoise-asgi-app-for-static-file-serving">blacknoise – ASGI app for static file serving</a></h1>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>This blog post consists of the <a href="https://github.com/matthiask/blacknoise">blacknoise
README</a> at the time of publishing.</p>
<p>I have released blacknoise 1.0 in the meantime and believe that it’s
actually good.</p>
</div>
<p>blacknoise is an <a href="https://asgi.readthedocs.io/en/latest/">ASGI</a> app for static
file serving inspired by <a href="https://github.com/evansd/whitenoise/">whitenoise</a>
and following the principles of <a href="https://406.ch/writing/low-maintenance-software/">low maintenance
software</a>.</p>
<p><strong>This is pre-alpha software and everything is subject to change. I’m not even
sure if blacknoise should exist at all or if the energy wouldn’t be better
spent improving whitenoise or other tools. Feedback and contributions are very
welcome though!</strong></p>
<h2 id="using-blacknoise-with-django-to-serve-static-files"><a class="toclink" href="#using-blacknoise-with-django-to-serve-static-files">Using blacknoise with Django to serve static files</a></h2>
<p>Install blacknoise into your Python environment:</p>
<div class="chl"><pre><span></span><code><span class="go">pip install blacknoise</span>
</code></pre></div>
<p>Wrap your ASGI application with the <code>BlackNoise</code> app:</p>
<div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">blacknoise</span> <span class="kn">import</span> <span class="n">BlackNoise</span>
<span class="kn">from</span> <span class="nn">django.core.asgi</span> <span class="kn">import</span> <span class="n">get_asgi_application</span>
<span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="n">BASE_DIR</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span>
<span class="n">application</span> <span class="o">=</span> <span class="n">BlackNoise</span><span class="p">(</span><span class="n">get_asgi_application</span><span class="p">())</span>
<span class="n">application</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">BASE_DIR</span> <span class="o">/</span> <span class="s2">"static"</span><span class="p">,</span> <span class="s2">"/static"</span><span class="p">)</span>
</code></pre></div>
<p><code>BlackNoise</code> will automatically handle all paths below the prefixes added, and
either return the files or return 404 errors if files do not exist. The files
are added on server startup, which also means that <code>BlackNoise</code> only knows
about files which existed at that particular point in time.</p>
<h2 id="improving-performance"><a class="toclink" href="#improving-performance">Improving performance</a></h2>
<p><code>BlackNoise</code> has worse performance than when using an optimized webserver such
as nginx and others. Sometimes it doesn’t matter much if the app is behind a
caching reverse proxy or behind a content delivery network anyway. To further
support this use case <code>BlackNoise</code> can be configured to serve media files with
far-future expiry headers and has support for serving compressed assets.</p>
<p>Compressing is possible by running:</p>
<div class="chl"><pre><span></span><code><span class="go">python -m blacknoise.compress static/</span>
</code></pre></div>
<p><code>BlackNoise</code> will try compress non-binary files using gzip or brotli (if the
<a href="ttps://pypi.org/project/Brotli/">Brotli</a> library is available), and will serve
the compressed version if the compression actually results in (significantly)
smaller files and if the client also supports it.</p>
<p>Far-future expiry headers can be enabled by passing the <code>immutable_file_test</code>
callable to the <code>BlackNoise</code> constructor:</p>
<div class="chl"><pre><span></span><code><span class="k">def</span> <span class="nf">immutable_file_test</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
<span class="k">return</span> <span class="kc">True</span> <span class="c1"># Enable far-future expiry headers for all files</span>
<span class="n">application</span> <span class="o">=</span> <span class="n">BlackNoise</span><span class="p">(</span>
<span class="n">get_asgi_application</span><span class="p">(),</span>
<span class="n">immutable_file_test</span><span class="o">=</span><span class="n">immutable_file_test</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div>
<p>Maybe you want to add some other logic, for example check if the path contains
a hash based upon the contents of the static file. Such hashes can be added by
Django’s <code>ManifestStaticFilesStorage</code> or by appropriately configuring bundlers
such as <code>webpack</code> and others.</p>
<h2 id="license"><a class="toclink" href="#license">License</a></h2>
<p><code>blacknoise</code> is distributed under the terms of the <a href="https://spdx.org/licenses/MIT.html">MIT</a> license.</p>Podcasts I like listening tohttps://406.ch/writing/podcasts-i-like-listening-to/2024-03-18T12:00:00Z2024-03-18T12:00:00Z<h1 id="podcasts-i-like-listening-to"><a class="toclink" href="#podcasts-i-like-listening-to">Podcasts I like listening to</a></h1>
<p>I discovered listening to podcasts about one year ago. Previously, I never knew
why anyone would want to listen to people talk when they could listen to music
or nothing, but that has changed a bit.</p>
<p>So, here’s a list of podcasts I’m currently listening to on a regular basis.</p>
<h2 id="tech-wont-save-us"><a class="toclink" href="#tech-wont-save-us"><a href="https://www.techwontsave.us/">Tech Won’t Save Us</a></a></h2>
<p>I have recently stumbled over Tech Won’t Save Us, a Podcast which is critical
of the technological “progress” offered by Silicon Valley elites. It’s a great
antidote for the generative AI hype.</p>
<p>I still have some stupid hope that AGI will solve our problems because maybe
people will trust a computer more than scientists that something has to be done
about the combined crisis we’re facing as humans. Who knows. I do not want to
be too negative about it though, there are positive news if you’re looking in
the right places. I myself do not write about those bigger issues <a href="https://406.ch/writing/category-climate/">as
much</a> <a href="https://406.ch/writing/category-politik/">as I used
to</a>. I support those more important
issues elsewhere.</p>
<p>Generative AI is certainly not helping <em>me</em> in my work. I do not want a
computer to generate code which I have to review and maintain when I still
enjoy writing code myself. Maybe that will change at some point in the future.
When that happens I’ll probably retire and become a gardener or something.</p>
<h2 id="the-ezra-klein-show"><a class="toclink" href="#the-ezra-klein-show"><a href="ttps://www.nytimes.com/column/ezra-klein-podcast">The Ezra Klein Show</a></a></h2>
<p>This show seems to be a favorite of many people. I joined for the (relatively)
critical perspectives on AI and stayed for the insights into politics and into
the near east conflict. I think it’s great that people from different sides get
a voice on the show, even if Ezra doesn’t agree with them.</p>
<p>I sometimes wish the questions they ask on the show were a little bit more
critical.</p>
<h2 id="hard-fork"><a class="toclink" href="#hard-fork"><a href="https://www.nytimes.com/column/hard-fork">Hard Fork</a></a></h2>
<p>I like the perspectives and the bantering on this podcast. It’s a good way for
me to stay informed about what’s going on in AI/ML land, among other things.
It’s a lighthearted listen I often look forward to.</p>
<h2 id="django-chat"><a class="toclink" href="#django-chat"><a href="https://djangochat.com/">Django Chat</a></a></h2>
<p>I work with Django all the time and Django Chat is a great way to learn more
about Django, about the people involved and also about the surrounding
ecosystem. A wholehearted recommendation!</p>
<h2 id="talk-python-to-me"><a class="toclink" href="#talk-python-to-me"><a href="https://talkpython.fm/">Talk Python To Me</a></a></h2>
<p>Good interviews, interesting guests and topics. Just a great way to learn about
libraries, tools etc. which I cannot wait to use in my own projects.</p>
<h2 id="datenschutzplaudereien"><a class="toclink" href="#datenschutzplaudereien"><a href="https://podcast.datenschutzpartner.ch/">Datenschutzplaudereien</a></a></h2>
<p>Required listening for people working with customer data in Switzerland. Swiss
german knowledge required.</p>
<h2 id="others"><a class="toclink" href="#others">Others</a></h2>
<p>A few other podcasts I’m listening to more or less regularly:</p>
<ul>
<li><a href="https://www.publiceye.ch/de/wir-muessen-reden-public-eye-spricht-klartext">Wir müssen reden. Public Eye spricht
Klartext</a>:
The podcast from public eye, an organization which uncovers human right
violations perpetrated by Swiss companies around the globe. I’m not yet sure
if I like the podcast, but I like and support the organization very much.</li>
<li><a href="https://hanselminutes.com/">Hanselminutes</a>: The more polifor the wide and
diverse array of guests.tical podcasts have displaced Hanselminutes in my
listening schedule, but I’d definitely recommend it to anyone who wants to
learn more about tech. Recommended because of the diverse array of guests,
among many other reasons.</li>
<li><a href="https://syntax.fm/">Syntax</a>: At times very interesting, at times not much
new for me. I still recommend it.</li>
<li><a href="https://www.weirdstudies.com/">Weird Studies</a>: Really weird. I liked the
discussion of Hellraiser and The Thing, and have listened to some of the
other episodes. Religiosity and spiritualism was always around me when I was
a child, and it’s interesting to revisit some of the ideas about reality from
a different angle after years and years of not engaging with these questions
at all. (Writing this down does sound a bit like a midlife crisis on the
horizon, but I don’t think that’s it. I have studied a little bit of
philosophy and ethics and have stayed interested in these topics ever since.)</li>
<li>I don’t think the german “Welt” is a good newspaper at all. I do like the
«Aha! History» and «Aha! Zehn Minuten Alltagswissen» podcasts because they
are short and often examine topics I don’t know much about.</li>
<li><a href="https://www.srf.ch/play/tv/sendung/sternstunde-philosophie?id=b7705a5d-4b68-4cb1-9404-03932cd8d569">Sternstude
Philosophie</a>:
It’s a bit slow and definitely more geared towards viewers than listeners,
but definitely interesting.</li>
</ul>Weeknotes (2024 week 11)https://406.ch/writing/weeknotes-2024-week-11/2024-03-16T12:00:00Z2024-03-16T12:00:00Z<h1 id="weeknotes-2024-week-11"><a class="toclink" href="#weeknotes-2024-week-11">Weeknotes (2024 week 11)</a></h1>
<h2 id="estimates"><a class="toclink" href="#estimates">Estimates</a></h2>
<p><a href="https://jacobian.org/2024/mar/11/breaking-down-tasks/">Jacob wrote an excellent post on breaking down tasks</a>. I did like the post a lot. Maybe I’ll write a longer reply later, but for now just this. <a href="https://hachyderm.io/@jacob@jacobian.org/112081126379604868">There definitely are good reasons for the pushback against estimation</a>, and it’s really not just that some people lack professionalism.</p>
<h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2>
<ul>
<li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.14.1</a>: Mini
release containing a Turkish translation. It’s always nice if software is
used.</li>
<li><a href="https://pypi.org/project/feincms3/">feincms3 4.6</a>: Fixed a bug where the
move form wouldn’t use a potentially overridden <code>ModelAdmin.get_queryset</code>
method.</li>
<li><a href="https://pypi.org/project/form-designer/">form-designer 0.24</a>: Updated the
package for django-recaptcha 4.0.</li>
<li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.3.1</a>: Fixed an
edge case sanitization bug (luckily without security implications).</li>
<li><a href="https://pypi.org/project/django-content-editor/">django-content-editor
6.4.2</a>:
django-content-editor now again supports transitioning plugin fieldsets when
opening <em>and</em> closing thanks to CSS grid’s ability to animate the maximum
height of an element. Also, the initialization in 6.4 was badly broken.</li>
<li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.2</a>: <a href="https://406.ch/writing/django-prose-editor-prose-editing-component-for-the-django-admin/">See the announcement blog post from Wednesday</a>.</li>
</ul>