Matthias Kestenholz: Posts about Djangohttps://406.ch/writing/category-django/2023-09-20T12:00:00ZMatthias KestenholzKeep content managers' admin access up-to-date with role-based permissionshttps://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/2023-09-20T12:00:00Z2023-09-20T12:00:00Z<h1>Keep content managers’ Django admin access up-to-date with role-based permissions</h1>
<p><a href="https://docs.djangoproject.com/en/4.2/topics/auth/default/#permissions-and-authorization">Django’s built-in permissions
system</a>
is great if you want fine-grained control of the permissions content
managers should have. The allowlist-based approach where users have no
permissions by default and must be granted each permission individually makes a
lot of sense to me and is easy to understand.</p>
<p>When we build a CMS at <a href="https://feinheit.ch/">Feinheit</a> we often use the Django administration panel as a CMS.
Unfortunately, Django doesn’t provide a way to specify that content managers
should have all permissions in the <code>pages</code> and <code>articles</code> app (just as an
example). Adding all current permissions in a particular app is straightforward when using the
<a href="https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.filter_horizontal"><code>filter_horizontal</code></a>
interface but keeping the list up-to-date later isn’t. When we add an
additional <a href="https://406.ch/writing/my-reaction-to-the-block-driven-cms-blog-post/">content block
plugin</a>
we always have to remember to also update the permissions after deploying the
change – and often, deployment happens some time after the code has been
written, e.g. because clients want to approve the change first. What happens
all too often is that the manual step of updating permissions gets forgotten.</p>
<p>This has annoyed me (intermittently) for a long time and my preferred solution
has always been to give superuser permissions to everyone and trust them to
not make changes which they aren’t supposed to according to the <em>Trusted Users
Editing Structured Content</em> principle which was mentioned in a Django book I
read early in my Django journey.</p>
<h2>The basic ideas of my role-based permissions implementation</h2>
<p>A recent project has resurfaced this annoyance and I did finally bite the
bullet and implement a solution for this in the form of a
<a href="https://github.com/matthiask/django-authlib/">django-authlib</a> extension. The basic ideas are:</p>
<p><strong>All users are assigned a single role</strong>: Single roles sound inflexible, but is
good enough for my default use case. Examples for roles could be <em>default</em> (no
additional permissions granted), <em>content managers</em> (grant access to the pages
and articles apps) or maybe <em>deny auth</em> (deny access to users, groups and
permissions).</p>
<p><strong>The permission check is implemented using a single callable</strong>: A custom
backend is provided whose only job is to call the correct callable for the
user’s current role.</p>
<p><strong>The callable either returns a boolean or raises <code>PermissionDenied</code> to prevent
other backends from granting access</strong>: No new ideas here, it’s exactly what
<a href="https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#handling-authorization-in-custom-backends">Django’s authentication backends are supposed to
do</a>.</p>
<p><strong>Permission checkers for the most common scenarios are bundled</strong>:
django-authlib only ships one permission checker right now, <code>allow_deny_globs</code>,
which allows specifying a list of permission name globs to allow and to deny.
Deny overrides allow as is probably expected.</p>
<h2>Using roles in your own project</h2>
<p>Specify the available roles in your settings and add the authentication backend:</p>
<div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">partial</span>
<span class="kn">from</span> <span class="nn">authlib.roles</span> <span class="kn">import</span> <span class="n">allow_deny_globs</span>
<span class="kn">from</span> <span class="nn">django.utils.translation</span> <span class="kn">import</span> <span class="n">gettext_lazy</span> <span class="k">as</span> <span class="n">_</span>
<span class="n">AUTHLIB_ROLES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"default"</span><span class="p">:</span> <span class="p">{</span><span class="s2">"title"</span><span class="p">:</span> <span class="n">_</span><span class="p">(</span><span class="s2">"default"</span><span class="p">)},</span>
<span class="s2">"staff"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"title"</span><span class="p">:</span> <span class="n">_</span><span class="p">(</span><span class="s2">"editorial staff"</span><span class="p">),</span>
<span class="s2">"callback"</span><span class="p">:</span> <span class="n">partial</span><span class="p">(</span>
<span class="n">allow_deny_globs</span><span class="p">,</span>
<span class="n">allow</span><span class="o">=</span><span class="p">{</span>
<span class="s2">"pages.*"</span><span class="p">,</span>
<span class="s2">"articles.*"</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">),</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="n">AUTHENTICATION_BACKENDS</span> <span class="o">=</span> <span class="p">(</span>
<span class="c1"># This is the necessary additional backend</span>
<span class="s2">"authlib.backends.PermissionsBackend"</span><span class="p">,</span>
<span class="c1"># Maybe you want to use authlib's email authentication ...</span>
<span class="s2">"authlib.backends.EmailBackend"</span><span class="p">,</span>
<span class="c1"># ... or the standard username & password combination:</span>
<span class="s2">"django.contrib.auth.backends.ModelBackend"</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div>
<p>You have to extend your user model (you have to use <a href="https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#specifying-custom-user-model">a custom user model</a> if you’re not using django-authlib’s <code>little_user.User</code>):</p>
<div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">authlib.roles</span> <span class="kn">import</span> <span class="n">RoleField</span>
<span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">AbstractUser</span><span class="p">):</span>
<span class="c1"># ...</span>
<span class="n">role</span> <span class="o">=</span> <span class="n">RoleField</span><span class="p">()</span>
</code></pre></div>
<p>And that’s basically it.</p>
<p>Of course the globbing is flexible, you could also allow users to view all objects:</p>
<div class="codehilite"><pre><span></span><code><span class="n">partial</span><span class="p">(</span><span class="n">allow_deny_globs</span><span class="p">,</span> <span class="n">allow</span><span class="o">=</span><span class="p">{</span><span class="s2">"*.view_*"</span><span class="p">})</span>
</code></pre></div>
<p>Or you could block users from deleting anything:</p>
<div class="codehilite"><pre><span></span><code><span class="n">partial</span><span class="p">(</span><span class="n">allow_deny_globs</span><span class="p">,</span> <span class="n">deny</span><span class="o">=</span><span class="p">{</span><span class="s2">"*.delete_*"</span><span class="p">})</span>
</code></pre></div>
<p>And as mentioned above, you can also combine <code>allow</code> and <code>deny</code> (<code>deny</code> wins
over <code>allow</code>) or even provide your own callables. If you provide your own
callable it must accept <code>user</code>, <code>perm</code> and <code>obj</code> (which may be <code>None</code>) as
keyword arguments. Implementing such a callable is probably less work than
implementing an authentication backend yourself; I had to do more work than
initially expected because only implementing <code>.has_perm</code> isn’t sufficient if
you want to see any apps and models in the admin index page. The current
<code>allow_deny_globs</code> implementation is nice and short:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">allow_deny_globs</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">perm</span><span class="p">,</span> <span class="n">obj</span><span class="p">,</span> <span class="n">allow</span><span class="o">=</span><span class="p">(),</span> <span class="n">deny</span><span class="o">=</span><span class="p">()):</span>
<span class="k">for</span> <span class="n">rule</span> <span class="ow">in</span> <span class="n">deny</span><span class="p">:</span>
<span class="k">if</span> <span class="n">fnmatch</span><span class="p">(</span><span class="n">perm</span><span class="p">,</span> <span class="n">rule</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">PermissionDenied</span>
<span class="k">return</span> <span class="nb">any</span><span class="p">(</span><span class="n">fnmatch</span><span class="p">(</span><span class="n">perm</span><span class="p">,</span> <span class="n">rule</span><span class="p">)</span> <span class="k">for</span> <span class="n">rule</span> <span class="ow">in</span> <span class="n">allow</span><span class="p">)</span>
</code></pre></div>My reaction to the block-driven CMS blog posthttps://406.ch/writing/my-reaction-to-the-block-driven-cms-blog-post/2023-08-23T12:00:00Z2023-08-23T12:00:00Z<h1>My reaction to the block-driven CMS blog post</h1>
<p>This morning I read an interesting post on the Lincoln Loop blog called <a href="https://lincolnloop.com/insights/block-driven-cms-is-critical-build-a-future-proof/">Building a Future-Proof Platform with Block-Driven CMS</a>. It shouldn’t come as a surprise to those (few 😄) who know my work in the area of content management systems that the post resonated with me. I found the description of the advantages of block-based CMS editing very clear and I like the emphasis on structuring data well so that it can be reused for multiple distribution channels.</p>
<p>Of course <a href="https://www.django-cms.org/">django CMS</a> isn’t the only way to implement a block-driven CMS using Django. Since its inception <a href="https://406.ch/writing/the-future-of-feincms/">FeinCMS</a> was always the smaller, faster and nimbler counterpart to it, achieving the same <em>basic</em> goals with a fraction of the code and maintenance headaches. django CMS always seems to trail the official releases of Django. django-content-editor and feincms3 are almost always compatible with the development version of Django by way of running the tests with the <code>main</code> branch as well. This allows me to be an early adopter of upcoming Django releases with a software stack that’s already well tested, or also to report bugs to the Django project itself. All that probably wouldn’t be possible if feincms3 and its dependencies supported all the things django CMS does, but it doesn’t have to to be useful.</p>
<p><a href="https://406.ch/writing/the-other-future-of-feincms-django-content-editor-and-feincms3/">django-content-editor and feincms3</a> are the legacy of FeinCMS in an even smaller, even more maintainable and even more composable package and while I’m definitely always checking out other Django-based CMS I’m persuaded that sticking with feincms3 is a good choice.</p>Weeknotes (2023 week 33)https://406.ch/writing/weeknotes-2023-week-33/2023-08-20T12:00:00Z2023-08-20T12:00:00Z<h1>Weeknotes</h1>
<p>I’m not sure if I should call these posts weeknotes when I see the posting schedule, but oh well. Keep expectations up but also practice forgiveness when not meeting them, it’s fine really.</p>
<h2><code>py_modules</code> using hatchling</h2>
<p>I converted <a href="https://github.com/matthiask/speckenv/">speckenv</a> and <a href="https://github.com/matthiask/django-sitemaps/">django-sitemaps</a> after finding the following very helpful post on packaging projects consisting of Python modules without any packages: <a href="https://www.stefaanlippens.net/single-python-module-packaging-hatch.html">Packaging of single Python module projects with Hatch/Hatchling</a>. It’s very easy in hindsight, but that’s basically always the case.</p>
<p>The relevant part is including the files in the build:</p>
<div class="codehilite"><pre><span></span><code><span class="k">[tool.hatch.build]</span><span class="w"></span>
<span class="n">include</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="w"></span>
<span class="w"> </span><span class="s">"speckenv.py"</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="s">"speckenv_django.py"</span><span class="p">,</span><span class="w"></span>
<span class="w"> </span><span class="s">"speckenv_django_patch.py"</span><span class="p">,</span><span class="w"></span>
<span class="p">]</span><span class="w"></span>
</code></pre></div>
<p>That’s all.</p>
<h2>django-debug-toolbar and tracing the cause of DB queries in an async world</h2>
<p>I have also started investigating what would have to be changed in django-debug-toolbar to make it fully support async Django. We currently patch Django’s database cursors per thread, which works fine in sync Django land to attribute SQL queries to a particular request/response cycle.</p>
<p>Since async Django executes DB queries in a thread pool executor and the rest of the work happens inside awaitables (async land) I don’t immediately see a way how we could do the same thing. It doesn’t seem possible to find out which task spawned another task (without dropping down to C?) but maybe there’s something I’m overlooking. I hope that someone smarter than me finds a way :-) or that I find the time and motivation to either find a way using Python or using C/Rust/whatever.</p>
<h2>Releases</h2>
<ul>
<li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.16</a>: I added basic support for <code>i18n_patterns</code> when using feincms3-sites with its <code>default_language_middleware</code> (which allows setting a default language per site in case there is no other mechanism overriding it, such as <code>i18n_patterns</code>).</li>
<li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.4.1</a>: The privacy policy is now linked inside the banner text instead of adding a link after the text. Looks much nicer.</li>
<li><a href="lhttps://pypi.org/project/speckenv/">speckenv 5.0</a>: Finally released changes made a long time ago which make one edge case when parsing settings less surprising.</li>
<li><a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar 4.2</a>: I didn’t do much work here again, mostly code reviews, some changes to the ruff configuration and general polishing. I also didn’t do the release itself, that was handled by Tim. Thanks!</li>
<li><a href="https://pypi.org/project/FeinCMS/">FeinCMS 23.8</a>: Fixes for Pillow 10, and some feincms3 / django-content-editor interoperability improvements which make it easier to reuse plugins/content types.</li>
<li><a href="https://pypi.org/project/feincms3/">feincms3 4.1</a>: Some basic support for using the apps middleware with async Django. Not documented yet and not deployed anywhere but it basically works. Some documentation edits and changes to the inline CKEditor styling because of the recent changes to Django admin’s CSS.</li>
</ul>Composition over inheritance: The case for function-based viewshttps://406.ch/writing/composition-over-inheritance-the-case-for-function-based-views/2023-08-11T12:00:00Z2023-08-11T12:00:00Z<h1>Composition over inheritance: The case for function-based views</h1>
<p><a href="https://hachyderm.io/@matthiask/110814846128940975">A recent conversation with Carlton on Mastodon</a> prompted me to write down some of my thoughts re. function- vs class-based views in Django.</p>
<h2>The early days</h2>
<p>When I started using Django some time after 0.96 and 1.0 all views were
function based. Except when you added a class with a <code>def __call__()</code> method
yourself – that was always possible but not really comparable to today’s
class-based views.</p>
<h2>The introduction of class-based views</h2>
<p>Class based views (both generic versions and the base <code>View</code>) were introduced to Django in 2010. Judging from the <a href="https://code.djangoproject.com/ticket/6735">ticket tracker</a> the main motivation was to avoid adding yet another argument to the generic function-based views (GFBV) which were available in Django back then.</p>
<p>The GFBV’s argument count was impressive. Two examples follow:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">object_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">queryset</span><span class="p">,</span> <span class="n">object_id</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">slug</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">slug_field</span><span class="o">=</span><span class="s1">'slug'</span><span class="p">,</span> <span class="n">template_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">template_name_field</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">template_loader</span><span class="o">=</span><span class="n">loader</span><span class="p">,</span> <span class="n">extra_context</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">context_processors</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">template_object_name</span><span class="o">=</span><span class="s1">'object'</span><span class="p">,</span>
<span class="n">mimetype</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="o">...</span>
<span class="k">def</span> <span class="nf">archive_month</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">year</span><span class="p">,</span> <span class="n">month</span><span class="p">,</span> <span class="n">queryset</span><span class="p">,</span> <span class="n">date_field</span><span class="p">,</span>
<span class="n">month_format</span><span class="o">=</span><span class="s1">'%b'</span><span class="p">,</span> <span class="n">template_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">template_loader</span><span class="o">=</span><span class="n">loader</span><span class="p">,</span>
<span class="n">extra_context</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">allow_empty</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">context_processors</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">template_object_name</span><span class="o">=</span><span class="s1">'object'</span><span class="p">,</span> <span class="n">mimetype</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">allow_future</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="o">...</span>
</code></pre></div>
<p>The GFBVs were immediately deprecated when GCBVs were introduced and later removed in 2012.</p>
<p>Class-based views have to be adapted by calling the <code>View.as_view()</code> method; <code>as_view()</code> returns arguably the thing which is viewed (sorry) as the view by Django, it’s the thing which gets called with a request and is expected to return a response. This thing in turn instantiates the view object once per request; this means that <code>self</code> can be used to save request-specific data such as <code>self.request</code>, <code>self.args</code> but also custom attributes.</p>
<p>The GCBV code is extremely factored and decomposed. The <a href="https://ccbv.co,ul/">Classy Class-Based Views</a> site mentions that the <code>UpdateView</code> has 10 separate ancestors and its code is spread across three files. But, the view code for instantiating a model object and handling a form really isn’t that complex. Most of the complexity is handled by Django itself, in the request handler and in the <code>django.forms</code> package. So, what’s the reason for all this?</p>
<h2>Generic views could be simple</h2>
<p>I wish that the existing generic views had better building blocks instead of a big hierarchy of mixins and multiple inheritance which is probably not understood by anyone without checking and re-checking the documentation, the code, or the excellent <a href="https://ccbv.co.uk/">Classy Class-Based Views</a>. Certainly not by me.</p>
<p>In my ideal world, generic views would be composed of small reusable and composable functions which wuld cover 80% of use cases with 20% of the code. And if not, you could copy the whole code of the view, change or introduce a line or two and leave it at that. And since the functions do one thing (but do that well) you can immediately see what they are doing and why. You’d avoid the Hollywood Principle (Don’t call us, we’ll call you) in your code. Sure, your view is called by Django but you don’t have to introduce more and more layers of indirection.</p>
<p>The internet is full of advice that you should prefer composition over inheritance. Let’s try to outline what generic views could look like if they followed the composition paradigm. Note that the goal isn’t to gain points by showing that the resulting code is shorter. One important goal is maintainability by being easier to understand. Another important goal is showing a better path from a beginner’s use of views to an experts understanding of everything underneath it by bridging the gap using more powerful building blocks which don’t leave all the minutiae to you if the defaults don’t work.</p>
<p>Some repetition caused by copy pasting is fine. Not all identical three lines of code are the same. The <a href="https://wiki.c2.com/?ThreeStrikesAndYouRefactor">Three Strikes And You Refactor</a> rule<sup id="fnref:wet"><a class="footnote-ref" href="#fn:wet">1</a></sup> leads to better and more maintainable code than following an extreme interpretation of the DRY (Don’t Repeat Yourself) principle.</p>
<h3>ListView and DetailView</h3>
<p>I’m going to profit from Django’s shortcuts module and also from <a href="https://feincms3.readthedocs.io/en/latest/ref/shortcuts.html">feincms3’s shortcuts module</a> which offers functions for rendering pages for single objects or lists of objects. The <code>render_list</code> and <code>render_detail</code> functions implement the same way of determining the template paths as the generic views use (for example <code><app_name>/<model_name>_detail.html</code>) and the same way of naming context variables (<code>object</code> and <code><model_name></code> for the object, <code>object_list</code> and <code><model_name>_list</code> for the list) as well as pagination but nothing more.</p>
<p>Here’s a possible minimal implementation of a list and detail object generic view:</p>
<div class="codehilite"><pre><span></span><code><span class="c1"># _get_queryset runs ._default_manager.all() on models and returns</span>
<span class="c1"># everything else as-is. It's the secret sauce which allows using models,</span>
<span class="c1"># managers or querysets with get_object_or_404 and friends.</span>
<span class="kn">from</span> <span class="nn">django.shortcuts</span> <span class="kn">import</span> <span class="n">get_object_or_404</span><span class="p">,</span> <span class="n">_get_queryset</span>
<span class="kn">from</span> <span class="nn">feincms3.shortcuts</span> <span class="kn">import</span> <span class="n">render_list</span><span class="p">,</span> <span class="n">render_detail</span>
<span class="k">def</span> <span class="nf">object_list</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">model</span><span class="p">,</span> <span class="n">paginate_by</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">return</span> <span class="n">render_list</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">_get_queryset</span><span class="p">(</span><span class="n">model</span><span class="p">),</span> <span class="n">paginate_by</span><span class="o">=</span><span class="n">paginate_by</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">object_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">model</span><span class="p">,</span> <span class="n">slug</span><span class="p">,</span> <span class="n">slug_field</span><span class="o">=</span><span class="s2">"slug"</span><span class="p">):</span>
<span class="nb">object</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="o">**</span><span class="p">{</span><span class="n">slug_field</span><span class="p">:</span> <span class="n">slug</span><span class="p">})</span>
<span class="k">return</span> <span class="n">render_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="nb">object</span><span class="p">)</span>
</code></pre></div>
<p>You want to change the way a single object is retrieved? You could do that easily but not by adding configuration-adjacent values in your URLconf but rather by adding a view yourself:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">article_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">year</span><span class="p">,</span> <span class="n">slug</span><span class="p">):</span>
<span class="nb">object</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">published</span><span class="p">(),</span> <span class="n">year</span><span class="o">=</span><span class="n">year</span><span class="p">,</span> <span class="n">slug</span><span class="o">=</span><span class="n">slug</span><span class="p">)</span>
<span class="k">return</span> <span class="n">render_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="nb">object</span><span class="p">)</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="o">...</span>
<span class="n">path</span><span class="p">(</span><span class="s2">"articles/<year:int>/<slug:slug>/"</span><span class="p">,</span> <span class="n">article_detail</span><span class="p">,</span> <span class="n">name</span><span class="o">=...</span><span class="p">),</span>
<span class="o">...</span>
<span class="p">]</span>
</code></pre></div>
<p>I don’t think that was much harder than a hypothetical alternative:</p>
<div class="codehilite"><pre><span></span><code><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="o">...</span>
<span class="n">path</span><span class="p">(</span>
<span class="s2">"articles/<year:int>/<slug:slug>/"</span><span class="p">,</span>
<span class="n">object_detail</span><span class="p">,</span>
<span class="p">{</span>
<span class="s2">"model"</span><span class="p">:</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">published</span><span class="p">(),</span>
<span class="s2">"object_kwargs"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"year"</span><span class="p">,</span> <span class="s2">"slug"</span><span class="p">],</span>
<span class="p">},</span>
<span class="p">),</span>
<span class="o">...</span>
<span class="p">]</span>
</code></pre></div>
<p>And think about the internal implementation of the <code>object_detail</code> view. Viewed one additional feature at a time it may be fine but when adding up everything it would probably be quite gross.</p>
<p>The additional benefit is that it shows beginners the way to intermediate skills – writing views isn’t hard, and shouldn’t be.</p>
<p>Finally, the official way of overriding <code>DetailView.get_object()</code> (I think!) doesn’t look that good compared to the <code>def article_detail()</code> view above:</p>
<div class="codehilite"><pre><span></span><code><span class="k">class</span> <span class="nc">ArticleDetailView</span><span class="p">(</span><span class="n">generic</span><span class="o">.</span><span class="n">DetailView</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">if</span> <span class="n">queryset</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">queryset</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">return</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">queryset</span><span class="p">,</span> <span class="n">year</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="p">[</span><span class="s2">"year"</span><span class="p">],</span> <span class="n">slug</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="p">[</span><span class="s2">"slug"</span><span class="p">])</span>
</code></pre></div>
<p>Did you know that <code>get_object()</code> has an optional queryset argument? I certainly didn’t. It seems to be used by the date-based generic views but they also have their own <code>get_object()</code> implementation so who knows, really.</p>
<h2>Detail view with additional behavior</h2>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">article_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">year</span><span class="p">,</span> <span class="n">slug</span><span class="p">):</span>
<span class="nb">object</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">published</span><span class="p">(),</span> <span class="n">year</span><span class="o">=</span><span class="n">year</span><span class="p">,</span> <span class="n">slug</span><span class="o">=</span><span class="n">slug</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="p">,</span> <span class="n">request</span><span class="o">.</span><span class="n">FILES</span><span class="p">)</span> <span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s2">"POST"</span> <span class="k">else</span> <span class="p">()</span>
<span class="n">form</span> <span class="o">=</span> <span class="n">CommentForm</span><span class="p">(</span><span class="o">*</span><span class="n">data</span><span class="p">)</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">is_valid</span><span class="p">():</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">article</span> <span class="o">=</span> <span class="nb">object</span>
<span class="n">form</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="s2">".#comments"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">render_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="nb">object</span><span class="p">,</span> <span class="p">{</span><span class="s2">"comment_form"</span><span class="p">:</span> <span class="n">form</span><span class="p">})</span>
</code></pre></div>
<p>A counterexample would be to move the endpoint which accepts a comment POST
request somewhere else. But then you’d also have to keep the different
<code>CommentForm</code> instantiations in sync.</p>
<p>You could also override <code>get_context_data()</code> to add the comment form to the
context and override <code>post()</code> to instantiate check the form’s validity. But
then you’d have to make sure that an eventual invalid form is handled correctly
by <code>get_context_data()</code>. It’s not hard but it certainly isn’t as
straightforward as the example above either.</p>
<p>The custom view is the most obvious way of keeping the form instantiation in
one place.</p>
<h2>Form views</h2>
<p>Generic create and update views could look something like this, again reusing the shortcuts mentioned above:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">save_and_redirect_to_object</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="nb">object</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">return</span> <span class="n">redirect</span><span class="p">(</span><span class="nb">object</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_form_instance</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">model</span><span class="p">,</span> <span class="n">form_class</span><span class="p">,</span> <span class="n">instance</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">assert</span> <span class="n">model</span> <span class="ow">or</span> <span class="n">form_class</span><span class="p">,</span> <span class="s2">"Provide at least one of model and form_class"</span>
<span class="k">if</span> <span class="n">form_class</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">modelform_factory</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="p">,</span> <span class="n">request</span><span class="o">.</span><span class="n">FILES</span><span class="p">)</span> <span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s2">"POST"</span> <span class="k">else</span> <span class="p">()</span>
<span class="k">return</span> <span class="n">form_class</span><span class="p">(</span><span class="o">*</span><span class="n">data</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">object_create</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">form_class</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">form_valid</span><span class="o">=</span><span class="n">save_and_redirect_to_object</span><span class="p">):</span>
<span class="n">form</span> <span class="o">=</span> <span class="n">get_form_instance</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="n">model</span><span class="p">,</span> <span class="n">form_class</span><span class="o">=</span><span class="n">form_class</span><span class="p">)</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">is_valid</span><span class="p">():</span>
<span class="k">return</span> <span class="n">form_valid</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="p">)</span>
<span class="k">return</span> <span class="n">render_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">,</span> <span class="p">{</span><span class="s2">"form"</span><span class="p">:</span> <span class="n">form</span><span class="p">},</span> <span class="n">template_name_suffix</span><span class="o">=</span><span class="s2">"_form"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">object_update</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">model</span><span class="p">,</span> <span class="n">slug</span><span class="p">,</span> <span class="n">slug_field</span><span class="o">=</span><span class="s2">"slug"</span><span class="p">,</span> <span class="n">form_class</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">form_valid</span><span class="o">=</span><span class="n">save_and_redirect_to_object</span><span class="p">):</span>
<span class="nb">object</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="o">**</span><span class="p">{</span><span class="n">slug_field</span><span class="p">:</span> <span class="n">slug</span><span class="p">})</span>
<span class="n">form</span> <span class="o">=</span> <span class="n">get_form_instance</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="nb">object</span><span class="o">.</span><span class="vm">__class__</span><span class="p">,</span> <span class="n">form_class</span><span class="o">=</span><span class="n">form_class</span><span class="p">,</span> <span class="n">instance</span><span class="o">=</span><span class="nb">object</span><span class="p">)</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">is_valid</span><span class="p">():</span>
<span class="k">return</span> <span class="n">form_valid</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="p">)</span>
<span class="k">return</span> <span class="n">render_detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="p">,</span> <span class="p">{</span><span class="s2">"form"</span><span class="p">:</span> <span class="n">form</span><span class="p">},</span> <span class="n">template_name_suffix</span><span class="o">=</span><span class="s2">"_form"</span><span class="p">)</span>
</code></pre></div>
<p>You want to redirect to a different URL and maybe emit a success message? Easy:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">article_form_valid</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">form</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">_</span><span class="p">(</span><span class="s2">"Successfully updated the article."</span><span class="p">))</span>
<span class="k">return</span> <span class="n">redirect</span><span class="p">(</span><span class="s2">"articles:list"</span><span class="p">)</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="o">...</span>
<span class="n">path</span><span class="p">(</span>
<span class="s2">"<slug:slug>/update/"</span><span class="p">,</span>
<span class="n">object_update</span><span class="p">,</span>
<span class="p">{</span><span class="s2">"model"</span><span class="p">:</span> <span class="n">Article</span><span class="p">,</span> <span class="s2">"form_valid"</span><span class="p">:</span> <span class="n">article_form_valid</span><span class="p">},</span>
<span class="n">name</span><span class="o">=...</span>
<span class="p">),</span>
<span class="o">...</span>
<span class="p">]</span>
</code></pre></div>
<p>Yes, these generic views wouldn’t allow overriding the case when a form was invalid. But, I’d assume that displaying the form with error messages is the right thing to do in 90% of the cases. And if not, write your own specific or generic view? After all, with the mentioned tools it won’t take up more than a few lines of straightforward code. (If the code was tricky it would be different. But views shouldn’t be tricky.)</p>
<p>Adding more <code>form_valid</code> handlers should be mostly painless. A few examples inspired by <a href="https://docs.djangoproject.com/en/4.2/topics/class-based-views/generic-editing/">Django’s generic editing documentation</a>:</p>
<div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">save_and_redirect_to</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">fn</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">form</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">return</span> <span class="n">redirect</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">return</span> <span class="n">fn</span>
<span class="k">def</span> <span class="nf">send_mail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">form</span><span class="o">.</span><span class="n">send_email</span><span class="p">()</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="s2">"/thanks/"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">set_author_and_save</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">created_by</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">user</span>
<span class="nb">object</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">return</span> <span class="n">redirect</span><span class="p">(</span><span class="nb">object</span><span class="p">)</span>
</code></pre></div>
<p>You could also couple the form a bit to the request and do something like:</p>
<div class="codehilite"><pre><span></span><code><span class="nv">def</span> <span class="nv">process_form</span><span class="ss">(</span><span class="nv">request</span>, <span class="nv">form</span><span class="ss">)</span>:
<span class="k">return</span> <span class="nv">form</span>.<span class="nv">process</span><span class="ss">(</span><span class="nv">request</span><span class="ss">)</span>
</code></pre></div>
<p>Sure, forms probably shouldn’t know much about requests. But then, Django is a framework for perfectionists <em>with deadlines</em> and sometimes practicality beats purity.</p>
<h2>Date-based generic views</h2>
<p>I think I would want to offer a few analyzers which allow easily returning a
data structure suitable for rendering links for yearly, monthly, weekly or even
daily (who writes that much?) archives. The <a href="https://docs.djangoproject.com/en/4.2/ref/models/querysets/#dates"><code>.dates()</code> queryset
method</a>
method should be a big help there.</p>
<p>The archive views themselves are straightforward adaptations of the
<code>object_list</code> view above.</p>
<p>It may feel like leaving out the actually hard part but I’d have to be
convinced that this is actually a hard problem and not just a problem of making
basically arbitrary choices which people then adapt to and then think that this
is the way things should be since it’s the way things are.</p>
<h2>Wrapping up</h2>
<p>Some points this post could have made or tried to make are made much better by
Luke Plant in the guide <a href="https://spookylukey.github.io/django-views-the-right-way/">Django Views - The Right
Way</a>. I don’t
generally think that class-based views never make sense. I also don’t think
that people shouldn’t use the available tools. I just think that I, myself,
don’t want to use them, and I also think that I’m still happier with <code>lambda
request: HttpResponseRedirect(...)</code> than with
<code>generic.RedirectView.as_view(url=...)</code>. The point isn’t to compare the
character count. The point is: Does the <code>RedirectView</code> cause a permanent or a
temporary redirect? I had to look it up for a long time, and then it changed.
The former is completely obvious.</p>
<h2>Closing words</h2>
<p>I know that people have strong opinions. I’m not interested in all of them. I’m
mostly interested in design critiques and arguments regarding the beginner to
intermediate skills argument. It’s fine if CBVs work fine for you, and there’s
no need to feel challenged by this post.</p>
<p>Thanks for reading!</p>
<div class="footnote">
<hr />
<ol>
<li id="fn:wet">
<p>Also called the WET rule (Write Everything Twice). (Not coined by me.) <a class="footnote-backref" href="#fnref:wet" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>Weeknotes (2023 week 30)https://406.ch/writing/weeknotes-2023-week-30/2023-07-28T12:00:00Z2023-07-28T12:00:00Z<h1>Weeknotes</h1>
<h2>Async Django</h2>
<p>I have used <a href="https://channels.readthedocs.io/">Django Channels</a> successfully in a few projects from 2017 to 2019. A few months back I have worked with <a href="https://www.starlette.io/">Starlette</a>. And now I have finally started digging into using Django itself with an ASGI server, and not just for one or two views but also including the middleware stack etc since I also need authentication, not just an endpoint forwarding requests to a remote server. I have looked at <a href="https://github.com/emmett-framework/granian">Granian</a>, an RSGI/ASGI server written in Rust. But for now I am using <a href="https://www.uvicorn.org/">uvicorn</a>.</p>
<p>Django truly has come a long way but there’s much left to do. Django 5.0 is looking great already, but 4.2 misses many pieces still. I am really really glad Django wants to stay backwards compatible but I wish I could wave a magic wand and upgrade everything to async. Adding <code>a</code> prefixes everywhere for the async version is certainly a good compromise and probably the way to go but it’s just not that nice.</p>
<p>I have been playing around with making <a href="https://feincms3.readthedocs.io/">feincms3</a>’s applications middleware async compatible because I want the full middleware stack to be async. The code is already released but undocumented and not even mentioned in the changelog. So, feel free to play around with it but it’s not supposed to be stable or supported yet.</p>
<h2>Releases</h2>
<ul>
<li><a href="https://pypi.org/project/feincms3/">feincms3 4.1</a>: Switched to hatchling and ruff. Updated the feincms3-sites docs. Some async updates mentioned above. A Django 4.2 admin CSS update for the inline CKEditor.</li>
<li><a href="https://pypi.org/project/feincms3-forms/">feincms3-forms 0.4</a>: Switched to hatchling and ruff. Started defining default icons for the form fields <a href="https://django-content-editor.readthedocs.io/">content editor</a> plugins.</li>
<li><a href="https://pypi.org/project/django-ckeditor/">django-ckeditor 6.7</a>: I’m still maintaining the CKEditor 4 integration for Django even though CKEditor 4 itself isn’t supported anymore. Minor updates to the editor itself and Pillow compatibility updates.</li>
<li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.3.2</a>: The cookie banner doesn’t generate an empty <code><div class="f3cc"></code> element anymore if there’s nothing to add inside (e.g. if the user only accepted necessary cookies).</li>
</ul>How ruff changed my Python programming habitshttps://406.ch/writing/how-ruff-changed-my-python-programming-habits/2023-07-26T12:00:00Z2023-07-26T12:00:00Z<h1>How ruff changed my Python programming habits</h1>
<p><a href="https://beta.ruff.rs/">ruff</a> isn’t just a faster replacement for flake8, isort
and friends.</p>
<p>With other Python-based formatters and linters there’s always a trade off
between development speed (waiting on <code>git commit</code> is very boring) and
strictness.</p>
<p>ruff is so fast that enabling additional rules is practically free in terms of
speed; the only question is if those rules lead to better, or maybe just to
more correct and consistent code.</p>
<p>I have long been using <a href="https://pre-commit.com/">pre-commit</a>, and even longer
flake8, black, isort. I have written a piece about <a href="https://406.ch/writing/flake8-and-value-standards/">flake8 and the value of
standards</a> almost 9 years
ago and have continued moving in the mentioned direction ever since.</p>
<p>These days I have enabled a wide variety of rules. I’m not sold on all of them
(looking at you, pylint) and I’m definitely not of the opinion that rules which
I’m not using currently are worthless. I didn’t even know most of these rules
before starting to use ruff, and ruff making them easy and painless to use
(without a measureable performance penalty) has certainly lead to me annoying
my coworkers with a growing set of enabled rules.</p>
<h2>Rules</h2>
<p>The current ruleset and some justifications for it follows.</p>
<h3>pyflakes, pycodestyle</h3>
<p>No justification necessary, really.</p>
<h3>mccabe</h3>
<p>I like the cyclomatic complexity checker, but I have relaxed it a bit. I find it very useful to avoid complex code, but some code is totally straightforward (e.g. building a queryset from a wide variety of query parameters) but still has many <code>if</code> statements. I’d rather allow more complexity instead of sprinkling the code with <code># noqa</code> statements.</p>
<h3>isort</h3>
<p>Sorted imports are great.</p>
<h3>pep8-naming</h3>
<p>Mostly great except when it flags Django’s migration files. The filenames
always start with numbers and that’s obviously not a valid Python module name,
but it’s not supposed to be.</p>
<h3>pyupgrade</h3>
<p>pyupgrade is totally awesome.</p>
<h3>flake-2020</h3>
<p>Avoiding non future-proof uses of <code>sys.version</code> and <code>sys.version_info</code> is a good idea, no questions about that.</p>
<h3>flake8-boolean-trap</h3>
<p>Sometimes annoying, mostly useful. I don’t like that the plugin flags e.g. <code>with_tree_fields(True)</code> or <code>with_tree_fields(False)</code> because I don’t think this could be possibly misread. But, apart from these edge cases it really is a good idea, especially since keyword-only arguments exist and those aren’t flagged by this rule.</p>
<h3>flake8-bugbear</h3>
<p>Mostly useful. I have disabled the <code>zip()</code> without <code>strict=</code> warning.</p>
<h3>flake8-comprehensions</h3>
<p>Checks for unnecessary conversions between generators and lists, sets, tuples or dicts.</p>
<h3>flake8-django</h3>
<p>I actually like consistency. I also like flagging <code>fields = "__all__"</code>, but this check shouldn’t trigger in admin <code>ModelForm</code> classes, really. I probably have to add another entry to <code>[tool.ruff.per-file-ignores]</code> for this.</p>
<h3>flake8-pie</h3>
<p>Quite a random assortment of rules. I like the <code>no-unnecessary-pass</code> and <code>no-pointless-statements</code> rules, among others.</p>
<h3>flake8-simplify</h3>
<p>Nice simplifications. I’m not sure if ternary opeartors are always a plus, especially since they hide the branching from <a href="https://pypi.org/project/coverage/">coverage</a>.</p>
<h3>flake8-gettext</h3>
<p>Enormously useful and important. I don’t know how many times I have encountered broken code like <code>gettext("Hello {name}".format(name=name))</code> instead of <code>gettext("Hello {name}").format(name=name)</code>.</p>
<h3>pygrep-hooks</h3>
<p>Avoids <code>eval()</code>. Avoids blanket <code># noqa</code> rules (always be specific!)</p>
<h3>pylint</h3>
<p>I have been using all pylint rules for some time. The pylint refactoring rules (<code>PLR</code>) did prove to be very annoying so I have reverted to only enabling errors and warnings.</p>
<p>The two main offenders were PLR0913 (too many arguments) and PLR2004 (magic value comparison). The former would be fine if it would count keyword-only arguments differently; it’s certainly a good idea to avoid too many positional parameters, I don’t think keyword-only parameters are that bad. The latter is bad because often the magic value is really obvious. If you’re writing code for the web you shouldn’t have to use constants for the <code>200</code> or <code>404</code> status codes; one can assume that they are well known.</p>
<h3>RUF100</h3>
<p>Ruff is able to automatically remove <code># noqa</code> statements which don’t actually silence any warnings. That’s a great feature to have.</p>
<h2>Line length</h2>
<p>Yes, let’s go there. I still don’t use longer lines than +/- 80 characters, but
I have disabled all line length warnings these days. I don’t want to be warned
because I didn’t break a string across lines.</p>
<h2>Rules I don’t like</h2>
<ul>
<li>flake8-builtins: Too many boring warnings. I didn’t even want to know that
<code>copyright</code> is a Python builtin.</li>
<li>flake8-logging-format: Not generally helpful. Avoiding different exception strings so that e.g. <a href="https://sentry.io/welcome/">Sentry</a> can group exceptions more easily is a good idea, but the rule generated so many false positives as to be not useful anymore.</li>
</ul>
<h2>Final words (for now)</h2>
<p>I really hope that Black is integrated into ruff one day.</p>
<p>Also, I hope that ESLint and prettier will be replaced by a faster tool. I have my eyes on a few alternatives, but they are not there yet for my use cases.</p>Weeknotes (2023 week 29)https://406.ch/writing/weeknotes-2023-week-29/2023-07-21T12:00:00Z2023-07-21T12:00:00Z<h1>Weeknotes</h1>
<p>I have mainly done work in private projects this week. Not much to talk about.
Except for the ZIP file <code>content-type</code> bug which was interesting enough to
justify <a href="https://406.ch/writing/serving-zip-files-using-django/">its own blog post</a>.</p>
<h2>Releases</h2>
<ul>
<li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.13</a>: I converted
the package to use ruff, hatchling; started running CI tests using Python
3.11. The internals of the Django admin’s filters have changed to allow
multi-valued filters, this has required some changes to the implementation of
the folder filter. I opted to using a relatively ugly <code>django.VERSION</code>
hack; but that’s not too bad since such branches will be automatically
removed by the awesome
<a href="https://github.com/adamchainz/django-upgrade">django-upgrade</a>. I would have
tried finding other ways in the past but now that old compatibility code can
be removed by a single run of <code>django-upgrade</code> (respectively
<code>pre-commit</code>) there really is no point to doing it in a different way.</li>
</ul>Serving ZIP files using Djangohttps://406.ch/writing/serving-zip-files-using-django/2023-07-18T12:00:00Z2023-07-18T12:00:00Z<h1>Serving ZIP files using Django</h1>
<p>I have generated ZIP files on the fly and served them using Django for a time.
Serving ZIP files worked well until it didn’t and browsing StackOverflow etc.
didn’t produce clear answers either. The development server worked fine, but
gunicorn/nginx didn’t.</p>
<p>In the end, I had to change <code>content_type="application/zip"</code> to
<code>content_type="application/x-zip-compressed"</code>. I still don’t know what changed
and I have only theories why that’s necessary, but maybe it helps someone else.
Sometimes it’s better to be dumber about it.</p>Weeknotes (2023 week 28)https://406.ch/writing/weeknotes-2023-week-28/2023-07-12T12:00:00Z2023-07-12T12:00:00Z<h1>Weeknotes (2023 week 28)</h1><h2>Releases</h2>
<ul>
<li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.2</a>: Made the
sanitizer’s configuration initialization more strict. Strings cannot be used
anymore in places where the sanitizer expects a set (resp. any iterable).
It’s useful that strings are iterable in Python and I wouldn’t want to change
that, but the fact that <code>("class")</code> is a string and not a tuple makes me sad.
The fact that tuples are created by <code>,</code> and not by <code>()</code> will always trip up
people.</li>
<li><a href="https://pypi.org/project/feincms3-language-sites/">feincms3-language-sites
0.1</a>: The version number
is wrong but whatever. I’m certainly happy with the state of things. The big
change in 0.1 is that <code>Page.get_absolute_url</code> no longer generates
protocol-relative URLs. Depending on the value of <code>SECURE_SSL_REDIRECT</code> it
automatically prepends either <code>http:</code> or <code>https:</code>.</li>
<li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.15</a>:
django-authlib’s admin Single Sign On module now supports a hook to
automatically create staff users when a matching user doesn’t exist already.
I don’t plan to use this functionality myself and I have recommended people
to implement the functionality themselves using the tools in django-authlib
if they need it, but the change was so small and well-contained that adding
it to the core made sense to me.</li>
</ul>
<h2>pipx inject</h2>
<p>We learned that <a href="https://pypa.github.io/pipx/">pipx</a> seems to remember injected
packages even across <code>pipx reinstall</code> invocations. Not too surprising now that
we know it, but we certainly spent some time scratching our heads. <code>pipx
uninject</code> was the thing we needed to stop pipx from installing an old version
of a dependency instead of the one being specified in <code>pyproject.toml</code>.</p>
<h2>hatchling and data files</h2>
<p>I’m very confused by the way <a href="https://hatch.pypa.io/">hatchling</a> sometimes
includes data files and sometimes it doesn’t. I had to add <code>[tool.hatch.build]
include=["authlib/"]</code> to <a href="https://github.com/matthiask/django-authlib/commit/67d4673e4039eac277b5d2557c0736c1f01442ac">django-authlib’s <code>pyproject.toml</code>
file</a>
to make it include HTML files from subpackages. Maybe the subpackages are the
reason, but I’m not sure.</p>
<h2>Payment providers that must not be named</h2>
<p>I have spent hours and hours battling with the badly documented, incomplete,
inconsistent and confusing API of a (not that well known) payment provider
based in Switzerland. I’m surprised that this still happens years and years
after Stripe started offering a really well thought out and documented API
geared towards programmers. It’s really sad because when the same structure is
named with differing naming conventions (e.g. <code>snake_case</code> vs. <code>camelCase</code>) in
different parts of the API you just know that somebody spent too much time
writing too much code instead of reusing already existing functionality.</p>Weeknotes (2023 week 26)https://406.ch/writing/weeknotes-2023-week-26/2023-06-30T12:00:00Z2023-06-30T12:00:00Z<h1>Weeknotes (2023 week 26)</h1><h2>Releases</h2>
<p>I released updates to a few of my packages; I have continued converting packages to <a href="https://hatch.pypa.io">hatchling</a> and <a href="https://github.com/astral-sh/ruff">ruff</a> while doing that.</p>
<p>New releases in the last two weeks include:</p>
<ul>
<li><a href="https://pypi.org/project/django-tree-queries/">django-tree-queries 0.15</a>: Added a new function, <code>.without_tree_fields()</code> to the queryset which can be used to avoid the <code>.with_tree_fields(False)</code> boolean trap warning.</li>
<li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.3.1</a>: This small update allows replacing the feincms3 <a href="https://noembed.com">noembed.com</a> oEmbed code using other libraries such as <a href="https://github.com/coleifer/micawber/">micawber</a> which support a wider range of URLs while still gating the embed behind users’ explicit consent.</li>
<li><a href="https://pypi.org/project/feincms3-downloads/">feincms3-downloads 0.5.3</a>: Updated translations.</li>
<li><a href="https://pypi.org/project/django-ckeditor/">django-ckeditor 6.6.1</a>: Updated the bundled CKEditor 4 and merged a pull request adding better integration with Django admin’s dark mode.</li>
<li><a href="https://pypi.org/project/django-js-asset/">django-js-asset 2.1</a>: Just basic maintainability and packaging updates. The <code>JS()</code> implementation itself is untouched since February 2022.</li>
<li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.0</a>: Not really a backwards incompatible change (at least not according to the tests); I just wanted to avoid <code>1.10</code> and go directly to <code>2.0</code> this time.</li>
</ul>
<h2>GitHub projects</h2>
<p>We are using GitHub project boards more and more. It definitely isn’t the most versatile way of managing projects but it sort-of hits the sweet spot for us. I’m mostly happy with it, and it seems to me that applying <a href="https://en.wikipedia.org/wiki/Rule_of_least_power">the rule of least power</a> to project management software may not be such a bad idea after all.</p>
<p>The built-in workflows are a bit boring and limited; especially the fact that it seems impossible to automatically add issues to the project when using multiple repositories. Luckily, <a href="https://github.com/actions/add-to-project">actions/add-to-project</a> exists so that’s not really a big problem.</p>
<h2>To cloud or not</h2>
<p>I had a long discussion with a colleague about containerization, Kubernetes, self-hosting, etc. etc. and I still don’t know where I stand. I can honestly say that the old way of hosting (ca. 2010) still works fine. I worry about the deterioriation of service quality we’re seeing and sometimes I really would like to have root to apply quick fixes where now I have to jump to hoops just to get what I already know I need. Annoying. But migrations are annoying as well.</p>
<h2>Scheduled publishing</h2>
<p>I augmented the script generating this website with scheduled publishing support while again reducing the number of lines in the file. The code is still formatted using black and ruff, while only ignoring line-length errors (I do this everywhere now to avoid breaking up long strings, not to put much code onto single lines) and allowing named lambdas. The weeknotes from two weeks ago where published by GitHub actions’ cron scheduling support.</p>
<h2>I like programming more than writing (even though I like writing)</h2>
<p>I notice that writing is the first thing I start skipping when I have to
prioritize. Programming, biking, gardening come first. That’s fine, really. But
I’m still a bit sad that I do not manage to at least put out a short weekly
weeknotes entry.</p>FeinCMS is a dead end (but feincms3 is not)https://406.ch/writing/feincms-is-a-dead-end-but-feincms3-is-not/2023-06-19T12:00:00Z2023-06-19T12:00:00Z<h1>FeinCMS is a dead end (but feincms3 is not)</h1>
<p>I wouldn’t encourage people to start new sites with FeinCMS. Five years ago I wrote that <a href="https://406.ch/writing/the-future-of-feincms/">FeinCMS is used in a few flagship projects which we’re still actively developing, which means that FeinCMS won’t be going away for years to come.</a> That’s still true but less and less so. We’re actively moving away from FeinCMS where we can, mostly towards feincms3 and django-content-editor.</p>
<p><a href="https://406.ch/writing/the-other-future-of-feincms-django-content-editor-and-feincms3/">FeinCMS lives on in django-content-editor and feincms3</a>; not only in spirit but also in (code) history, since django-content-editor contains the whole history of FeinCMS up to and including the beginning of 2016.</p>
<p>The implementation of FeinCMS is too expensive to clean up without breaking backwards compatibility. I still wish I had pursued an incremental way back then which would have allowed us to evolve old projects to the current best way of doing things (tm), but it didn’t happen and I’m not shedding too many tears about that since I’m quite happy with where we’re at today.</p>
<p>That basically means that I won’t put any effort into <a href="https://406.ch/writing/bringing-feincms-and-django-content-editorfeincms3-closer-together/">bringing FeinCMS and django-content-editor closer together</a>. I haven’t spent much time on that anyway but now my mind is made up that this wouldn’t be time well spent. That being said, some of the items mentioned in the blog post linked above are available in django-content-editor now.</p>Weeknotes (2023 week 24)https://406.ch/writing/weeknotes-2023-week-24/2023-06-16T12:00:00Z2023-06-16T12:00:00Z<h1>Weeknotes (2023 week 24)</h1><p>Life happened and I missed a month of weeknotes. Oh well.</p>
<h2>django-debug-toolbar 4.1</h2>
<p>We have released <a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar
4.1</a>. Another cycle where I
mostly contributed reviews and not much else. Feels great :-)</p>
<h2>Going all in on hatch and hatchling</h2>
<p>I got to know hatch because django-debug-toolbar was converted to it. I was
confused as probably anyone else with the new state of packaging in Python
world. After listening to a few Podcasts (for example <a href="https://talkpython.fm/episodes/show/408/hatch-a-modern-python-workflow">Hatch: A Modern Python
Workflow</a>)
I did bite the bullet and started converting projects to hatch as mentioned
<a href="https://406.ch/writing/weeknotes-2023-week-13-and-14/">some time ago</a>. I have
converted a few other projects in the meantime because the development
experience is nicer. Not much, but enough to make it worthwile.
<a href="https://pypi.org/project/feincms3-sites/">feincms3-sites</a> is the latest
package I converted.</p>
<h2>CKEditor 5’s new license and django-ckeditor</h2>
<p>The pressure is on to maybe switch away from CKEditor 4 since it probably will not be supported after <a href="https://support.ckeditor.com/hc/en-us/articles/115005281629-How-long-will-CKEditor-4-be-supported-">June 2023</a>. It’s totally understandable that the CKEditor 5 license isn’t the same as before, but I’m not sure what that means for the Django integration <a href="https://github.com/django-ckeditor/django-ckeditor/issues/482">django-ckeditor</a> which I’m maintaining since a few years. I don’t actually like the new capabilities of CKEditor all that much and don’t intend to use them; maybe it would be better to use a build of <a href="">ProseMirror</a> in the CMS since <a href="https://django-content-editor.readthedocs.io/en/latest/#about-rich-text-editors">we’re intentionally only using a very small subset of the features most rich text editors offer</a>.</p>
<h2>Mountain biking.</h2>
<p>My mountain bike is repaired, I’m back on the trail.</p>CSS variables and immutabilityhttps://406.ch/writing/css-variables-and-immutability/2023-06-14T12:00:00Z2023-06-14T12:00:00Z<h1>Using CSS variables<sup id="fnref:variables"><a class="footnote-ref" href="#fn:variables">1</a></sup> to ship customizable CSS in Django apps</h1>
<p>I have been working with <a href="https://sass-lang.com/">SASS</a> for a long time but
have been moving towards writing CSS with a few <a href="https://postcss.org/">PostCSS</a>
goodies in the last years. At first, I just replaced the <code>$...</code> with
<code>var(--...)</code> and didn’t think much about it. The realization that CSS variables
can be more than that came later. Edit basic values directly in the browser and
immediately see the results! Change CSS depending on media queries or the
cascade!</p>
<p>With all that power came back the wish to not just ship backend and HTML code
in Django apps I (help) maintain but also reusable CSS, with a few overrideable
CSS variables for basic changes to the visual style. Loading <code>.scss</code> files from
somewhere inside <code>venv/lib/python3.11/site-packages/<package>/styles/</code> would of
course have been possible, but very obscure. Also, not everyone puts their
virtualenv at <code>venv</code>, the <code>README</code> instructions for those packages would
quickly have become unwieldy. CSS variables paved the way for shipping CSS as a
Django static file while still allowing customizability by leveraging the
functionality of the browser itself instead of the frontend build toolchain.</p>
<h2>Patterns for overrideable values</h2>
<p>A pattern for defining defaults for CSS variables is to always define the
fallback (the example is intentionally bad but inspired by real world
experiences when developing
<a href="https://github.com/feinheit/feincms3-cookiecontrol">feincms3-cookiecontrol</a>):</p>
<div class="codehilite"><pre><span></span><code><span class="p">.</span><span class="nc">box</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="k">background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-background</span><span class="p">,</span><span class="w"> </span><span class="mh">#222</span><span class="p">);</span><span class="w"></span>
<span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-foreground</span><span class="p">,</span><span class="w"> </span><span class="mh">#fff</span><span class="p">);</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
</code></pre></div>
<h2>Less repetition (but trouble awaits)</h2>
<p>If <code>--box-background</code> isn’t set the <code>var()</code> function falls back to the second
argument, <code>#222</code>. Repeating this value over and over gets annoying quickly, so
you define a few defaults on the <code>:root</code> element and use those variables in the
code, without specifying the default again:</p>
<div class="codehilite"><pre><span></span><code><span class="p">:</span><span class="nd">root</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-background</span><span class="p">:</span><span class="w"> </span><span class="mh">#222</span><span class="p">;</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-foreground</span><span class="p">:</span><span class="w"> </span><span class="mh">#fff</span><span class="p">;</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
<span class="p">.</span><span class="nc">box</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="k">background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-background</span><span class="p">);</span><span class="w"></span>
<span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-foreground</span><span class="p">);</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
</code></pre></div>
<p>The project can now override the default background color using:</p>
<div class="codehilite"><pre><span></span><code><span class="p">:</span><span class="nd">root</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-background</span><span class="p">:</span><span class="w"> </span><span class="mh">#444</span><span class="p">;</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-foreground</span><span class="p">:</span><span class="w"> </span><span class="mh">#ccc</span><span class="p">;</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
</code></pre></div>
<p>Of course now you’re back at the mercy of CSS loading order. If the app’s CSS
is loaded first, everything works. If not, your custom value is immediately
overwritten. You could avoid this by overwriting the default lower in the cascade:</p>
<div class="codehilite"><pre><span></span><code><span class="p">.</span><span class="nc">box</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-background</span><span class="p">:</span><span class="w"> </span><span class="mh">#444</span><span class="p">;</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-foreground</span><span class="p">:</span><span class="w"> </span><span class="mh">#ccc</span><span class="p">;</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
</code></pre></div>
<p>Great, everything works again!</p>
<p>Later, the box also contains a button which uses a different background but the
same foreground, so of course we add more variables in the package:</p>
<div class="codehilite"><pre><span></span><code><span class="p">:</span><span class="nd">root</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-background</span><span class="p">:</span><span class="w"> </span><span class="mh">#222</span><span class="p">;</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-foreground</span><span class="p">:</span><span class="w"> </span><span class="mh">#fff</span><span class="p">;</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-button-background</span><span class="p">:</span><span class="w"> </span><span class="mh">#333</span><span class="p">;</span><span class="w"></span>
<span class="w"> </span><span class="nv">--box-button-foreground</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-foreground</span><span class="p">);</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
</code></pre></div>
<p>What happens now when overwriting the <code>--box-foreground</code> variable just for the
<code>.box</code> element?</p>
<p>You’re not sure? I certainly wasn’t and am not. But what I remember happening
was that the overridden foreground color was just applied to the text and not
to the button itself. I was confused (it seems clearer in hindsight…)</p>
<h2>A better way</h2>
<p>If values are supposed to be overridden and only used inside components, a
better way is to define local CSS for components by following a convention
(underscore prefix for local/private variables):</p>
<div class="codehilite"><pre><span></span><code><span class="c">/* Defined on .box, not :root */</span><span class="w"></span>
<span class="p">.</span><span class="nc">box</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="nv">--_background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-background</span><span class="p">,</span><span class="w"> </span><span class="mh">#222</span><span class="p">);</span><span class="w"></span>
<span class="w"> </span><span class="nv">--_foreground</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-foreground</span><span class="p">,</span><span class="w"> </span><span class="mh">#fff</span><span class="p">);</span><span class="w"></span>
<span class="w"> </span><span class="nv">--_button-background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-button-background</span><span class="p">,</span><span class="w"> </span><span class="mh">#333</span><span class="p">);</span><span class="w"></span>
<span class="w"> </span><span class="nv">--_button-foreground</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--box-button-foreground</span><span class="p">,</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--_foreground</span><span class="p">));</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
</code></pre></div>
<p>And then you only use the prefixed versions inside the component:</p>
<div class="codehilite"><pre><span></span><code><span class="p">.</span><span class="nc">box</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="k">background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--_background</span><span class="p">);</span><span class="w"></span>
<span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--_foreground</span><span class="p">);</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
<span class="p">.</span><span class="nc">box__button</span><span class="w"> </span><span class="p">{</span><span class="w"></span>
<span class="w"> </span><span class="k">background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--_button-background</span><span class="p">);</span><span class="w"></span>
<span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--_button-foreground</span><span class="p">);</span><span class="w"></span>
<span class="p">}</span><span class="w"></span>
</code></pre></div>
<p>The <code>--box-*</code> variables are undefined by default; the only time when they are
set is when the user of the package wants to override those values. If you only
overide the box foreground the button also inherits the new foreground color.
And while there would certainly be a way to achieve the same thing with the old
way above it’s certainly not as simple to explain.</p>
<p>The reason why it’s simple to explain is <strong>immutability</strong>. The CSS variables
which are overrideable by the user are only ever read by the package, never
written to.</p>
<div class="footnote">
<hr />
<ol>
<li id="fn:variables">
<p>Custom properties would probably be the more correct naming, but CSS variables is nicer to say. <a class="footnote-backref" href="#fnref:variables" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>Weeknotes (2023 week 17)https://406.ch/writing/weeknotes-2023-week-17/2023-04-28T12:00:00Z2023-04-28T12:00:00Z<h1>Weeknotes (2023 week 17)</h1><h2>Birthday</h2>
<p>Another year achieved. Feels the same as last year. I’m glad.</p>
<h2>feincms3-cookiecontrol</h2>
<p>I have released <a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol
1.3</a>. Mostly cleanups since
1.2, but also a new translation (already announced here). The script size has
been reduced from 4519 bytes to 4228 bytes (-6.5%) while keeping all features
intact. The reduction is totally meaningless but it was fun to do.</p>
<h2>oEmbed</h2>
<p>I have been digging into the oEmbed spec a bit. I didn’t even know that a
central list of <a href="https://oembed.com/providers.json">providers</a> exists.
<a href="https://noembed.com/">Noembed</a> still works great to embed many different types
of content but I worry more and more about its maintenance state.
Reimplementing the interesting parts shouldn’t be that hard, but maybe I don’t
have to do this myself. <a href="https://github.com/attakei-lab/oEmbedPy/">oEmbedPy</a>
looks nice, I hope I get a chance to play around with it.</p>Weeknotes (2023 week 16)https://406.ch/writing/weeknotes-2023-week-16/2023-04-21T12:00:00Z2023-04-21T12:00:00Z<h1>Weeknotes (2023 week 16)</h1>
<h2>Experiments with Stable Diffusion</h2>
<p>A friend and myself threw a few scripts together to automatically finetune a Stable Diffusion model using images downloaded from Google Image search. It’s terrifying how easy and fast generating fake news including photorealistic images can be and will be already. And seeing how fast those models improve it’s just a matter of time until we can trust photos even less than now. Manipulating images has been possible for a long time of course, but it hasn’t been a “commodity” until now.</p>
<p>I definitely also see upsides in the new machine learning technologies but I fear that there’s a real danger to trust, and in extension to democracy.</p>
<p>This technology and what we did will be a part of an upcoming <a href="https://www.srf.ch/play/tv/sendung/kulturplatz?id=d70e9bb9-0cee-46b6-8d87-7cbd8317a9c7">SRF Kulturplatz</a> broadcast, or so I hope. It’s high time that the public knows what’s possible. It’s about <strong>,edia literacy</strong> really.</p>
<p>I’m not that pessimistic though. I just hope that this time the thoughtfulness will prevail over pure profit seeking. (Did I really write that.)</p>
<h2><a href="https://github.com/django-ckeditor/django-ckeditor">django-ckeditor</a></h2>
<p>Many people are noticing that the CKEditor 4 integration for Django doesn’t work that well when using the dark color scheme of the Django admin panel. That’s not surprising. What does surprise me is the number of reports and the total absence of pull requests. Subjectively, most other packages I help maintain have a comfortable ratio of issues and pull requests.</p>
<p>I’m not complaining and people aren’t complaining, almost everyone is nice and tries to be helpful. Since I’m not a heavy user of django-ckeditor anymore I don’t really find the motivation to fix this issue myself since it’s not all that enjoyable work to me. I would just hope that <a href="https://github.com/django-ckeditor/django-ckeditor/issues/670">after all this time</a> someone would finally come and do the necessary work – if they cared enough.</p>
<h2>django-mptt</h2>
<p>I have marked <a href="https://github.com/django-mptt/django-mptt">django-mptt</a> as unmaintained two years ago. I’m still looking at pull requests from time to time but without feeling an obligation to do so and without feeling bad when I miss something.</p>
<p>I wonder if there are still many people out there who still depend on this library and if any of them would be willing to pick up the maintenance? On the off chance that someone is out there who has the time, ability and motivation and just didn’t know that django-mptt could use some love: Here’s your invite!</p>The insides of my static site generatorhttps://406.ch/writing/the-insides-of-my-static-site-generator/2023-04-19T12:00:00Z2023-04-19T12:00:00Z<h1>The insides of my static site generator</h1>
<p><a href="https://406.ch/writing/static-site-generation/">Last sunday I wrote that I’m now using a hacky ~200 LOC Python script to
generate this blog.</a> The ~200
LOC became a challenge to myself immediately and I started refactoring the code
while adding additional features, adding a licensing comment at the top and
further reducing the lines of code in there.</p>
<p>I don’t intend to stop working on it, but I’m really happy with the result <a href="https://github.com/matthiask/406.ch/blob/e6402d0927c92c6d426db1dd44de6002940f28b7/generate.py">as it is currently</a>. The script is less than 190 lines long and supports:</p>
<ul>
<li>Generating individual HTML files for the front page, the category pages and posts</li>
<li>Generating <code>robots.txt</code> and <code>sitemap.xml</code></li>
<li>Generating Atom feeds for all posts and posts of each category</li>
<li>Minifying HTML and CSS using
<a href="https://pypi.org/project/minify-html/">minify-html</a> and
<a href="https://pypi.org/project/rcssmin/">rcssmin</a>; the CSS is outputted as a
single file and includes the content hash in the filename for better
cacheability</li>
<li>Keeping the link structure of the old Django-based website</li>
</ul>
<p>I used Django’s <code>feedgenerator.py</code> module at first to generate the Atom feed;
I have since switched to directly working with the <a href="https://docs.python.org/3/library/xml.etree.elementtree.html#module-xml.etree.ElementTree">ElementTree
API</a>.
Yes, it’s probably less efficient since it has to keep the whole XML tree in
memory but who cares when the largest file’s file size is under 100 KiB at the
time of writing.</p>
<p>I’m using tox to generate the site locally; the local build includes published
and draft posts. The production build uses GitHub actions and automatically
deploys to GitHub pages, while only including published posts. There is no
incremental build right now but rebuilding the whole site using tox (with an
initialized virtualenv) takes less than one second, so that’s not really a pain
point for me right now.</p>
<p>A difficulty was that I have used URLs ending in slashes in the past, not just
for the browsable pages but also for the Atom feeds themselves. nginx only
serves <code>index.html</code> in folders by default so I couldn’t just add a
<code>index.xml</code> file in those folders. Luckily enough the internet is made of
lots and lots of duct tape and saving the atom feed as <code>.../feed/index.html</code>
actually works. It seems that RSS readers, aggregators and some libraries such
as <a href="https://pypi.org/project/feedparser/">feedparser</a> do not really need the
correct HTTP headers.</p>
<p>I have licensed the script under the <a href="http://www.wtfpl.net/">WTFPL</a>, so if
you’re interested you can do what you want with it, without any obligations. I
would certainly enjoy hearing about it though!</p>Weeknotes (2023 week 15)https://406.ch/writing/weeknotes-2023-week-15/2023-04-16T12:00:00Z2023-04-16T12:00:00Z<h1>Weeknotes (2023 week 15)</h1><h2>Romansh translations for feincms3-cookiecontrol and django-fineforms</h2>
<p>The feincms3 cookie control banner and django-fineforms have received a small update: Support for the <a href="https://en.wikipedia.org/wiki/Romansh_language">Romansh</a> language. I would be surprised if any readers of this blog even knew about this language at all. Switzerland has four official languages: German, French, Italian and the mentioned Romansh.</p>
<p>It’s not a coincidence of course that those two packages have received an update at the same time. Both packages are used for an upcoming campaign site where people may express their support for political action to preserve or protect the <a href="https://en.wikipedia.org/wiki/Biodiversity">biodiversity</a>. It’s at the same time laughable and horrifying that this is even a thing though: Who in their right mind could NOT agree that preserving biodiversity is important? It’s incomprehensible. Maybe I’m just a romantic <a href="https://en.wikipedia.org/wiki/Gutmensch">Gutmensch</a> after all…</p>
<h2>django-ckeditor</h2>
<p>I have set up the <a href="https://github.com/actions/stale">stale</a> GitHub action for the <a href="https://github.com/django-ckeditor/django-ckeditor">django-ckeditor</a> repository. So many support requests, so little time and almost no actual collaborators. Also, many support requests actually concern CKEditor itself, not its Django integration. I shouldn’t complain though, CKEditor has served me well and still does, <a href="https://github.com/matthiask/feincms3/blob/main/feincms3/inline_ckeditor.py">especially when it’s being used with a very minimal configuration</a> which basically makes most pain points of HTML editors go away.</p>
<h2>django-debug-toolbar</h2>
<p>I have reviewed and merged a few changes to <a href="https://github.com/jazzband/django-debug-toolbar">django-debug-toolbar</a> in the last week. Still a fun project, especially since it’s so widely used and loved.</p>
<h2>Meta</h2>
<p>Blogging with vim is fun.</p>Static site generationhttps://406.ch/writing/static-site-generation/2023-04-15T12:00:00Z2023-04-15T12:00:00Z<h1>Static site generation</h1>
<p><a href="https://406.ch/writing/weeknotes-2023-week-10/">I did what I threatened (myself) to do:</a> I replaced the Django code base for this weblog with a static site generator.</p>
<p>My main goal was to preserve as much as possible of the existing structure, including the Atom feeds and the IDs of posts so that the rewrite wouldn’t flood any aggregators.</p>
<p>The end result is a hacky ~200 LOC Python script which uses Markdown, Jinja2 and minify-html. Markdown is great for blogging and I have been using it for a long time, basically since I started this website in 2005. I don’t like it that much for documentation, but that’s a story for another day.</p>
<p>For now I still deploy the blog to a VPS but there’s nothing stopping me from uploading it somewhere else. I’m thinking about using GitHub actions for the deployment, but I can do that another day.</p>Run less code in production or you’ll end up paying the price laterhttps://406.ch/writing/run-less-code-in-production-or-youll-end-up-paying-the-price-later/2023-04-07T12:00:00Z2023-04-07T12:00:00Z<h1>Run less code in production or you’ll end up paying the price later</h1>
<p>At <a href="https://feinheit.ch/">work</a> we do have the problem of dependencies which aren’t maintained anymore. That’s actually a great problem to have because it means that the app or website ended up running for a long time, maybe longer than expected initially. I think that websites have a lifetime of 3-5 years<sup id="fnref:lifetime"><a class="footnote-ref" href="#fn:lifetime">1</a></sup> which is already much longer than the lifetime of major versions of some rapid development frameworks, especially frontend frameworks. We have been using <a href="https://get.foundation/">Zurb Foundation</a> in the past. It has served us well and I don’t want to dunk on it – after all it is free, it has great documentation and it works well. It has many features and many components, but as changes happen, not just in the framework itself but also in the tooling it uses upgrades stay hard and things start to break somewhere down the road (for example when <a href="https://github.com/sass/dart-sass">Dart Sass 2.0 will be released</a>. And when that happens you have to pick up the parts and maintain them yourself – having grown your codebase by a considerable amount practically overnight.</p>
<p>Of course it’s also hard to argue for starting to write all HTML, CSS and JavaScript from scratch. <a href="https://406.ch/writing/flake8-and-value-standards/">Standards are very helpful.</a> Building without some sort of standardized tooling will not only lead to rank growth but also means that you have to make many many small and (relatively) irrelevant decisions <em>and you have to justify those decisions when working in a team</em> because you could just as well have decided differently.</p>
<p>So after all that, should you use frontend frameworks? The answer is – as always – it depends.</p>
<p>You should definitely use a framework when prototyping.</p>
<p>But already when working on a MVP it gets less and less clear. You’re kidding yourself if you think that sometime in the future you’ll have the time to clean up your code base; <strong>of course</strong> you’re always refactoring and <strong>of course</strong> you’ll try to always leave each part behind a little bit nicer than it was before, but even then the big rewrite didn’t happen and you’ll continue using at least some aspects of the code that was hastily written for the initial release.</p>
<p>The exceptions to the rule are frameworks and tools which have a proven track record of maintaining stability over time. So, despite the fact that <a href="https://www.djangoproject.com/">the Django framework</a> has more than 100‘000 lines of code I feel good about using it. Django was released as Open Source in July 2005 and has been steadily maintained over the last nearly 18 years. <a href="https://www.djangoproject.com/fundraising/">Funding</a> is always a problem (contribute if you can!) but at least the Django software foundation manages to employ two Django fellows which are certainly key in driving the framework forward.</p>
<p>Again the picture gets less clear when third party apps are involved. Some third party apps were actively maintained several years ago and now they aren’t. I have personally taken over the maintenance of several such apps or more often than not reimplemented them with less functionality and much less code. Sometimes it’s easier to maintain a little more of your own code instead of much more of someone else’s code.</p>
<p>So, what’s the take away?</p>
<p>Maybe pay attention to the following points:</p>
<ul>
<li>Recent activity on the repository, recent releases and good docs are always a good sign for the health of a project.</li>
<li>Take the time to read at least a part of the code of third party projects before starting to rely on them. It’s a bad idea to just read the documentation and nothing else. When it comes to Django apps I always at least read the model files to understand how the data’s modeled and skim the views and forms.</li>
<li>Have a look at issues and pull/merge requests. Don’t worry about usage questions etc but mainly about “hard problem” issues.</li>
<li>Try to stay away from packages which are too comprehensive; reusing these packages is hard even for their authors and when they are abandoned the amount of responsibility transferred to you will be so much bigger.</li>
</ul>
<div class="footnote">
<hr />
<ol>
<li id="fn:lifetime">
<p>Citation required obviously, don’t take my word on it. Data is certainly more long lived, but I have my doubts regarding the code running many small sites. <a class="footnote-backref" href="#fnref:lifetime" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>Weeknotes (2023 week 13 and 14)https://406.ch/writing/weeknotes-2023-week-13-and-14/2023-04-05T12:00:00Z2023-04-05T12:00:00Z<h1>Weeknotes (2023 week 13 and 14)</h1>
<h2>My son will be a teenager soon</h2>
<p>My eldest is now 12 years old and will be a teenager soon. We had a good time and two nice Birthday parties, one with his friends and one with family and our friends. Good times.</p>
<h2>django-debug-toolbar 4.0</h2>
<p><a href="https://www.djangoproject.com/weblog/2023/apr/03/django-42-released/">Django 4.2 was released</a>, <a href="https://github.com/pypa/hatch/pull/762">Hatch gained support for the Django 4.2 Trove classifier</a> and <a href="https://pypi.org/project/django-debug-toolbar/">we released django-debug-toolbar 4.0</a>, with support for Django 4.2, psycopg 3 and all the existing goodies.</p>
<h2>feincms3-cookiecontrol</h2>
<p><a href="https://github.com/feinheit/feincms3-cookiecontrol/">feincms3-cookiecontrol</a> has gained support for consciously embedding stuff via oEmbed. It can now use <a href="https://noembed.com/">Noembed</a> (via <a href="https://github.com/matthiask/feincms3">feincms3</a>’s external plugin) and only actually embed the third party content if users consented explicitly.</p>
<p>I have since learned through the <a href="https://podcast.datenschutzpartner.ch/">Datenschutz-Plaudereien</a> podcast that laws regarding consent are not that strict in Switzerland compared to the European Union, also not when the <a href="https://www.admin.ch/gov/de/start/dokumentation/medienmitteilungen.msg-id-90134.html">DSG</a> is put into effect in September. What’s right and what’s legal are two different things and while I don’t really like the ubiquitous cookie banners (especially not when they aren’t actually doing anything) I like the idea of explicit consent and of not sending data unnecessarily to third party providers. The additional click isn’t that bad.</p>
<h2>Diving into hatch for Python packaging</h2>
<p>I listened to the TalkPython podcast episode with Ofek Lev on <a href="https://talkpython.fm/episodes/show/408/hatch-a-modern-python-workflow">his Hatch packaging tool</a>. After a long period of uncertainty and waiting I bit the bullet and started to migrate a few of my Python packages from setuptools and <code>setup.py</code> to hatch and <code>pyproject.toml</code>, until now <a href="https://github.com/feinheit/feincms3-cookiecontrol">feincms3-cookiecontrol</a> and <a href="https://github.com/matthiask/feincms3">feincms3</a>. It was surprisingly painless.</p>
<h2>ruff</h2>
<p>I started learning Rust during the last <a href="https://adventofcode.com/">Advent of Code</a>; it’s a nice language. <a href="https://beta.ruff.rs/">ruff</a> is a linter and (more and more) formatter for Python code written in Rust. After years of working with Python and Python-based tools it’s surprisingly fast, almost worryingly so. It’s true what they say: ruff finishes so fast that I’m always left wondering if it even did anything at all.</p>
<p>I’m configuring ruff through <code>pyproject.toml</code>, so switching from setuptools to hatch (see above) also helped in this regard. The main trouble I had was that I’m running Python 3.11 locally but Python 3.10 in the server environment (no dev-prod parity…), and rebuilding <code>requirements.txt</code> locally of course didn’t add TOML support because it’s built into Python 3.11, but Python 3.10 needs an external package. So of course I broke the build. That’s not all bad though: If stuff broke it definitely helps with remembering the reasons later.</p>
<h2>Meta</h2>
<p>Co-writing still works really well for me. Expressed differently: I seem to be unable to write without the (slight) pressure of writing together.</p>