Matthias Kestenholz: Posts about feincmshttps://406.ch/writing/category-feincms/2023-10-18T12:00:00ZMatthias KestenholzWeeknotes (2023 week 42)https://406.ch/writing/weeknotes-2023-week-42/2023-10-18T12:00:00Z2023-10-18T12:00:00Z<h1>Weeknotes (2023 week 42)</h1><h2>Vacation in Italy</h2> <p>We have spent a wonderful family week in Italy. The voyage by train was very comfortable and we had a great time there. I have lived close to lakes all my life but the sea is always something else. Now I enjoy the cold temperatures of fall.</p> <h2>Going back (forward) to GitJournal</h2> <p>I have tried several note taking apps but I&rsquo;m now back using <a href="https://gitjournal.io/">GitJournal</a> with a Git repository filled with Markdown notes. It works well enough. I just wish that there was a way to make notes more distinguishable and I wish that the editor was more forgiving when encountering badly formatted checklists.</p> <h2>Analog blogging</h2> <p>I have long wanted to write about <a href="https://406.ch/writing/why-we-switched-from-slack-to-discord-at-work/">our switch from Slack to Discord</a>. I have started to write this post with pen and paper. I find that I think better when using pen and paper than when using the computer keyboard. One factor is certainly that the computer offers more distractions, but I suspect that another, more important factor is that as a fast typist the fingers and the thinking are always getting out of step, and this happens less when using a slower method of writing. This actually isn&rsquo;t an idea I had myself, but I don&rsquo;t remember where I got it from.</p> <h2>Zero-based versioning: Good or bad?</h2> <p>I discovered <a href="https://0ver.org/">ZeroVer</a> sometime in the last few days. I have many many Django packages with zero-based versions. Some of them have been used in production for years now. I sometimes wonder if staying with <code>0.</code> is unprofessional and I should just release 1.0 and be done with it or if it doesn&rsquo;t really matter at all.</p> <p>If I evaluate software packages more often than not I don&rsquo;t look at the version number or the version numbering scheme (except when a package is still using <code>0.0.</code>) when deciding whether to rely on it or not. The documentation and the code itself are much more important to me.</p> <h2>Releases</h2> <p>I haven&rsquo;t uploaded any releases in the last 14 days. That&rsquo;s good: I&rsquo;m one of those people who have made their passion their job (which is great) but that sometimes makes it hard to not work at all since I can always tell myself that I&rsquo;m not working, that it&rsquo;s just a hobby.</p>Weeknotes (2023 week 40)https://406.ch/writing/weeknotes-2023-week-40/2023-10-04T12:00:00Z2023-10-04T12:00:00Z<h1>Weeknotes (2023 week 40)</h1><h2>More work on hosting several websites from a single Django application server using feincms3-sites</h2> <p>I have mentioned feincms3-sites last week in my last weeknotes entry; I have again given this package a lot of attention in the last days, so another update is in order.</p> <p>It is now possible to override the list of languages available on each site. That&rsquo;s especially useful for an upcoming campaign site where the umbrella group&rsquo;s site is available in three languages, but (most?) individual group sites (hosted on subdomains) will only have a subset of languages. Since I live in a country with four national languages (english isn&rsquo;t one of them, but is spoken by many!) supporting more than one language, or even many languages is totally commonplace. It&rsquo;s great that Django has good support for internationalization. For the sake of an example, I have the following sites:</p> <ul> <li><code>example.com</code>: The default. The host has to match exactly.</li> <li><code>subdomain.example.com</code>: One individual group&rsquo;s site. The host has to match the regex <code>^subdomain\.</code> (sorry, I actually do like regexes).</li> </ul> <h3>Overriding configured hosts for local development</h3> <p>One thing which always annoyed me when using <code>django.contrib.sites</code> was that &ldquo;just&rdquo; pulling the database from production to the local development environment always produced links pointing back to the remote host instead of working locally (when producing absolute URLs). This problem was shared by feincms3-sites as well. I have now found a very ugly but perfectly workable solution: Overwrite <code>Site.get_host()</code> locally:</p> <div class="chl"><pre><span></span><code><span class="k">if</span> <span class="n">DEBUG</span><span class="p">:</span> <span class="n">domain</span> <span class="o">=</span> <span class="s2">&quot;example.com&quot;</span> <span class="c1"># Or whatever</span> <span class="n">_get_host</span> <span class="o">=</span> <span class="k">lambda</span> <span class="n">site</span><span class="p">:</span> <span class="n">site</span><span class="o">.</span><span class="n">host</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">domain</span><span class="p">,</span> <span class="s2">&quot;localhost:8000&quot;</span><span class="p">)</span> <span class="n">FEINCMS3_SITES_SITE_GET_HOST</span> <span class="o">=</span> <span class="n">_get_host</span> </code></pre></div> <p>This works especially well when using <code>example.com</code> and maybe subdomains of <code>example.com</code>: All absolute links will point to <code>localhost:8000</code> or <code>subdomain.localhost:8000</code>. Since <code>*.localhost</code> always resolves to the local IP the browser knows where it should connect to, and since <code>subdomain.localhost:8000</code> also matches the <code>^subdomain\.</code> regex mentioned above, the site selection logic works as well.</p> <p>Of course if you have more domains, not just subdomains, you could adapt the <code>get_host</code> override and the relevant regexes to those use cases.</p> <h3>Closing words</h3> <p>We&rsquo;re at 100% code coverage now when running the test suite. That&rsquo;s really nice.</p> <h2>Logging into the Django admin using your Google account</h2> <p>This functionality has long been provided by <a href="https://pypi.org/project/django-authlib/">django-admin-sso</a>; however, as mentioned a long time ago this package still uses a deprecated OAuth2 library. <a href="https://github.com/matthiask/django-authlib/">django-authlib</a> supports using a Google account to authenticate with the Django admin since 2017. I have now fixed a small problem with it: If you are logged into a single Google account, and this account&rsquo;s email address doesn&rsquo;t match the configured admin login rule, you were out of luck: There was no way to add another account at that time because the library didn&rsquo;t request the account selection. That has changed now, if the first login attempt doesn&rsquo;t work, it now explicitly tells Google to let the user select their Google account. A small quality of life improvement for those using more than one Google account (voluntarily or not).</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/feincms3/">feincms3 4.4.3</a>: Polished the CKEditor integration a little bit. Re-enabled the source button now that we&rsquo;re back to using the classic iframe-based editor again.</li> <li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.19.3</a>: See above.</li> <li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.16.4</a>: See above.</li> </ul>Weeknotes (2023 week 39)https://406.ch/writing/weeknotes-2023-week-39/2023-09-28T12:00:00Z2023-09-28T12:00:00Z<h1>Weeknotes (2023 week 39)</h1><p>Again a few weeks have passed since the last weeknotes entry :-)</p> <h2>Moving feincms3 repositories into the feincms organization</h2> <p>The <a href="https://github.com/feincms">feincms</a> GitHub organization has seen more active days when FeinCMS 1.x was still actively developed. Since my interest has moved to feincms3 some years ago I haven&rsquo;t kept the organization up to date. That has changed this week, and I have moved most feincms3-related repositories into the organization.</p> <p>This move doesn&rsquo;t change much though, but it certainly feels more official now.</p> <h2>Adding scheduled tests</h2> <p>I have started using the cronjob schedule feature of GitHub actions to ensure that tests run at least once a month in a few important projects. I want to get notified of changes in Django@main affecting my packages not only when actively working on them. I try to keep up with Django@main in all packages I maintain.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.18.2</a>: Many releases in the last weeks. Stopped using permanent redirects in DEBUG mode. Avoid migrations when Django adds more languages. Added utilities which allow restricting model relations to objects in the same site (trickier than it sounds). Added utilities for building full URLs to other sites without taxing the database as much.</li> <li><a href="https://pypi.org/project/feincms3-language-sites/">feincms3-language-sites 0.2.0</a>: No biggie. No permanent redirects in DEBUG mode anymore.</li> <li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.4.5</a>: Reduced the byte size of the CSS and JavaScript some more. Added spanish translations.</li> <li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.16.3</a>: I have published a post last week describing the new <a href="https://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/">role-based permissions feature</a>.</li> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.17</a>: The <code>process_imagefields</code> management command now allows specifying globs. If you wanted to prerender all imagefields in the pages app you can use <code>./manage.py process_imagefields pages.*</code> now instead of listing all image fields&rsquo; labels explicitly.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 4.4.1</a>: I&rsquo;m enormously unhappy but I had to go back to the classic CKEditor instead of using the inline editor. The latter looked much nicer but overriding the Django admin CSS was very very painful. Also, I can totally understand why CKEditor 5 is completely different and why CKEditor 4 is only maintained in a paid LTS plan. It still is making me look for alternatives.</li> <li><a href="https://pypi.org/project/django-mptt/">django-mptt 0.15</a>: I unfortunately am still using this despite the fact that I have marked it as officially unmaintained since march 2021. I did a mediocre job of making the library run on Django@main again. Parts of the library do not work, but since I&rsquo;m not using them I don&rsquo;t care too much. I&rsquo;m still wondering if someone wants to take over maintenance of the library since it still seems to be actively used in projects of others as well. When I don&rsquo;t have to use django-mptt I&rsquo;m still really happy with <a href="https://406.ch/writing/django-tree-queries/">django-tree-queries</a>.</li> <li><a href="https://pypi.org/project/form-designer/">form-designer 0.22</a>: This is probably my oldest actively developed project these days. 13 years! (Except for django-content-editor of course.) I have modernized the package, switched to hatchling and put out a new release.</li> </ul>Keep 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&rsquo; 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&rsquo;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&rsquo;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&rsquo;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 &ndash; 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&rsquo;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&rsquo;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&rsquo;s exactly what <a href="https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#handling-authorization-in-custom-backends">Django&rsquo;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="chl"><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">&quot;default&quot;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="n">_</span><span class="p">(</span><span class="s2">&quot;default&quot;</span><span class="p">)},</span> <span class="s2">&quot;staff&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="n">_</span><span class="p">(</span><span class="s2">&quot;editorial staff&quot;</span><span class="p">),</span> <span class="s2">&quot;callback&quot;</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">&quot;pages.*&quot;</span><span class="p">,</span> <span class="s2">&quot;articles.*&quot;</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">&quot;authlib.backends.PermissionsBackend&quot;</span><span class="p">,</span> <span class="c1"># Maybe you want to use authlib&#39;s email authentication ...</span> <span class="s2">&quot;authlib.backends.EmailBackend&quot;</span><span class="p">,</span> <span class="c1"># ... or the standard username &amp; password combination:</span> <span class="s2">&quot;django.contrib.auth.backends.ModelBackend&quot;</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&rsquo;re not using django-authlib&rsquo;s <code>little_user.User</code>):</p> <div class="chl"><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&rsquo;s basically it.</p> <p>Of course the globbing is flexible, you could also allow users to view all objects:</p> <div class="chl"><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">&quot;*.view_*&quot;</span><span class="p">})</span> </code></pre></div> <p>Or you could block users from deleting anything:</p> <div class="chl"><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">&quot;*.delete_*&quot;</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&rsquo;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="chl"><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&rsquo;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&rsquo;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&rsquo;s already well tested, or also to report bugs to the Django project itself. All that probably wouldn&rsquo;t be possible if feincms3 and its dependencies supported all the things django CMS does, but it doesn&rsquo;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&rsquo;m definitely always checking out other Django-based CMS I&rsquo;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 (2023 week 33)</h1><p>I&rsquo;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&rsquo;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&rsquo;s very easy in hindsight, but that&rsquo;s basically always the case.</p> <p>The relevant part is including the files in the build:</p> <div class="chl"><pre><span></span><code><span class="k">[tool.hatch.build]</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="s2">&quot;speckenv.py&quot;</span><span class="p">,</span> <span class="w"> </span><span class="s2">&quot;speckenv_django.py&quot;</span><span class="p">,</span> <span class="w"> </span><span class="s2">&quot;speckenv_django_patch.py&quot;</span><span class="p">,</span> <span class="p">]</span> </code></pre></div> <p>That&rsquo;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&rsquo;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&rsquo;t immediately see a way how we could do the same thing. It doesn&rsquo;t seem possible to find out which task spawned another task (without dropping down to C?) but maybe there&rsquo;s something I&rsquo;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&rsquo;t do much work here again, mostly code reviews, some changes to the ruff configuration and general polishing. I also didn&rsquo;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&rsquo;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 &ndash; that was always possible but not really comparable to today&rsquo;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&rsquo;s argument count was impressive. Two examples follow:</p> <div class="chl"><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">&#39;slug&#39;</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">&#39;object&#39;</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">&#39;%b&#39;</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">&#39;object&#39;</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&rsquo;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&rsquo;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&rsquo;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&rsquo;d avoid the Hollywood Principle (Don&rsquo;t call us, we&rsquo;ll call you) in your code. Sure, your view is called by Django but you don&rsquo;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&rsquo;s try to outline what generic views could look like if they followed the composition paradigm. Note that the goal isn&rsquo;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&rsquo;s use of views to an experts understanding of everything underneath it by bridging the gap using more powerful building blocks which don&rsquo;t leave all the minutiae to you if the defaults don&rsquo;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&rsquo;t Repeat Yourself) principle.</p> <h3>ListView and DetailView</h3> <p>I&rsquo;m going to profit from Django&rsquo;s shortcuts module and also from <a href="https://feincms3.readthedocs.io/en/latest/ref/shortcuts.html">feincms3&rsquo;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>&lt;app_name&gt;/&lt;model_name&gt;_detail.html</code>) and the same way of naming context variables (<code>object</code> and <code>&lt;model_name&gt;</code> for the object, <code>object_list</code> and <code>&lt;model_name&gt;_list</code> for the list) as well as pagination but nothing more.</p> <p>Here&rsquo;s a possible minimal implementation of a list and detail object generic view:</p> <div class="chl"><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&#39;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">&quot;slug&quot;</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="chl"><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">&quot;articles/&lt;year:int&gt;/&lt;slug:slug&gt;/&quot;</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&rsquo;t think that was much harder than a hypothetical alternative:</p> <div class="chl"><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">&quot;articles/&lt;year:int&gt;/&lt;slug:slug&gt;/&quot;</span><span class="p">,</span> <span class="n">object_detail</span><span class="p">,</span> <span class="p">{</span> <span class="s2">&quot;model&quot;</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">&quot;object_kwargs&quot;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&quot;year&quot;</span><span class="p">,</span> <span class="s2">&quot;slug&quot;</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 &ndash; writing views isn&rsquo;t hard, and shouldn&rsquo;t be.</p> <p>Finally, the official way of overriding <code>DetailView.get_object()</code> (I think!) doesn&rsquo;t look that good compared to the <code>def article_detail()</code> view above:</p> <div class="chl"><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">&quot;year&quot;</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">&quot;slug&quot;</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&rsquo;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="chl"><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">&quot;POST&quot;</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">&quot;.#comments&quot;</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">&quot;comment_form&quot;</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&rsquo;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&rsquo;s validity. But then you&rsquo;d have to make sure that an eventual invalid form is handled correctly by <code>get_context_data()</code>. It&rsquo;s not hard but it certainly isn&rsquo;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="chl"><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">&quot;Provide at least one of model and form_class&quot;</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">&quot;POST&quot;</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">&quot;form&quot;</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">&quot;_form&quot;</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">&quot;slug&quot;</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">&quot;form&quot;</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">&quot;_form&quot;</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="chl"><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">&quot;Successfully updated the article.&quot;</span><span class="p">))</span> <span class="k">return</span> <span class="n">redirect</span><span class="p">(</span><span class="s2">&quot;articles:list&quot;</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">&quot;&lt;slug:slug&gt;/update/&quot;</span><span class="p">,</span> <span class="n">object_update</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;model&quot;</span><span class="p">:</span> <span class="n">Article</span><span class="p">,</span> <span class="s2">&quot;form_valid&quot;</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&rsquo;t allow overriding the case when a form was invalid. But, I&rsquo;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&rsquo;t take up more than a few lines of straightforward code. (If the code was tricky it would be different. But views shouldn&rsquo;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&rsquo;s generic editing documentation</a>:</p> <div class="chl"><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">&quot;/thanks/&quot;</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="chl"><pre><span></span><code>def process_form(request, form): return form.process(request) </code></pre></div> <p>Sure, forms probably shouldn&rsquo;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&rsquo;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&rsquo;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&rsquo;t generally think that class-based views never make sense. I also don&rsquo;t think that people shouldn&rsquo;t use the available tools. I just think that I, myself, don&rsquo;t want to use them, and I also think that I&rsquo;m still happier with <code>lambda request: HttpResponseRedirect(...)</code> than with <code>generic.RedirectView.as_view(url=...)</code>. The point isn&rsquo;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&rsquo;m not interested in all of them. I&rsquo;m mostly interested in design critiques and arguments regarding the beginner to intermediate skills argument. It&rsquo;s fine if CBVs work fine for you, and there&rsquo;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.)&#160;<a class="footnote-backref" href="#fnref:wet" title="Jump back to footnote 1 in the text">&#8617;</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&rsquo;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&rsquo;s just not that nice.</p> <p>I have been playing around with making <a href="https://feincms3.readthedocs.io/">feincms3</a>&rsquo;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&rsquo;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&rsquo;m still maintaining the CKEditor 4 integration for Django even though CKEditor 4 itself isn&rsquo;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&rsquo;t generate an empty <code>&lt;div class="f3cc"&gt;</code> element anymore if there&rsquo;s nothing to add inside (e.g. if the user only accepted necessary cookies).</li> </ul>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&rsquo;s configuration initialization more strict. Strings cannot be used anymore in places where the sanitizer expects a set (resp. any iterable). It&rsquo;s useful that strings are iterable in Python and I wouldn&rsquo;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&rsquo;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&rsquo;s admin Single Sign On module now supports a hook to automatically create staff users when a matching user doesn&rsquo;t exist already. I don&rsquo;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&rsquo;m very confused by the way <a href="https://hatch.pypa.io/">hatchling</a> sometimes includes data files and sometimes it doesn&rsquo;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&rsquo;s <code>pyproject.toml</code> file</a> to make it include HTML files from subpackages. Maybe the subpackages are the reason, but I&rsquo;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&rsquo;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&rsquo;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&rsquo; 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&rsquo;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&rsquo;t the most versatile way of managing projects but it sort-of hits the sweet spot for us. I&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo; 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&rsquo;s fine, really. But I&rsquo;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&rsquo;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&rsquo;s still true but less and less so. We&rsquo;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&rsquo;t happen and I&rsquo;m not shedding too many tears about that since I&rsquo;m quite happy with where we&rsquo;re at today.</p> <p>That basically means that I won&rsquo;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&rsquo;t spent much time on that anyway but now my mind is made up that this wouldn&rsquo;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&rsquo;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&rsquo;s totally understandable that the CKEditor 5 license isn&rsquo;t the same as before, but I&rsquo;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&rsquo;m maintaining since a few years. I don&rsquo;t actually like the new capabilities of CKEditor all that much and don&rsquo;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&rsquo;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&rsquo;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&rsquo;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/&lt;package&gt;/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="chl"><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="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="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="p">}</span> </code></pre></div> <h2>Less repetition (but trouble awaits)</h2> <p>If <code>--box-background</code> isn&rsquo;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="chl"><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="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="nv">--box-foreground</span><span class="p">:</span><span class="w"> </span><span class="mh">#fff</span><span class="p">;</span> <span class="p">}</span> <span class="p">.</span><span class="nc">box</span><span class="w"> </span><span class="p">{</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="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="p">}</span> </code></pre></div> <p>The project can now override the default background color using:</p> <div class="chl"><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="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="nv">--box-foreground</span><span class="p">:</span><span class="w"> </span><span class="mh">#ccc</span><span class="p">;</span> <span class="p">}</span> </code></pre></div> <p>Of course now you&rsquo;re back at the mercy of CSS loading order. If the app&rsquo;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="chl"><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="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="nv">--box-foreground</span><span class="p">:</span><span class="w"> </span><span class="mh">#ccc</span><span class="p">;</span> <span class="p">}</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="chl"><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="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="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="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="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="p">}</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&rsquo;re not sure? I certainly wasn&rsquo;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&hellip;)</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="chl"><pre><span></span><code><span class="c">/* Defined on .box, not :root */</span> <span class="p">.</span><span class="nc">box</span><span class="w"> </span><span class="p">{</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="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="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="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="o">--</span><span class="n">_foreground</span><span class="p">));</span> <span class="p">}</span> </code></pre></div> <p>And then you only use the prefixed versions inside the component:</p> <div class="chl"><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="k">background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">_background</span><span class="p">);</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="o">--</span><span class="n">_foreground</span><span class="p">);</span> <span class="p">}</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="k">background</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">_button</span><span class="nv">-background</span><span class="p">);</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="o">--</span><span class="n">_button</span><span class="nv">-foreground</span><span class="p">);</span> <span class="p">}</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&rsquo;s certainly not as simple to explain.</p> <p>The reason why it&rsquo;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.&#160;<a class="footnote-backref" href="#fnref:variables" title="Jump back to footnote 1 in the text">&#8617;</a></p> </li> </ol> </div>Weeknotes (2023 week 18 and 19)https://406.ch/writing/weeknotes-2023-week-18-and-19/2023-05-12T12:00:00Z2023-05-12T12:00:00Z<h1>Weeknotes (2023 week 18 and 19)</h1> <p>Not much programming this week :-(</p> <h2>oEmbed</h2> <p><a href="https://406.ch/writing/weeknotes-2023-week-17/">Still occupying myself with oEmbed.</a> I have been looking at a few libraries and have rediscovered <a href="https://github.com/coleifer/micawber/">micawber</a> which probably does everything I need. Yay, another reinvented wheel avoided. I haven&rsquo;t tested it yet but I&rsquo;m quite sure that replacing the <a href="https://github.com/matthiask/feincms3/blob/main/feincms3/plugins/external.py">oEmbed functionality in feincms3&rsquo;s external plugin</a> with micawber would be a good idea.</p> <h2>Vacation</h2> <p>Vacation last week. Was nice. We went for a few hikes. I have learned that I&rsquo;m not totally free from giddiness anymore. Oh well.</p>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&rsquo;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&rsquo;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&rsquo;t be that hard, but maybe I don&rsquo;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 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&rsquo;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&rsquo;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&rsquo;s incomprehensible. Maybe I&rsquo;m just a romantic <a href="https://en.wikipedia.org/wiki/Gutmensch">Gutmensch</a> after all&hellip;</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&rsquo;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&rsquo;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&rsquo;s so widely used and loved.</p> <h2>Meta</h2> <p>Blogging with vim is fun.</p>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>&rsquo;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&rsquo;s right and what&rsquo;s legal are two different things and while I don&rsquo;t really like the ubiquitous cookie banners (especially not when they aren&rsquo;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&rsquo;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&rsquo;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&rsquo;s surprisingly fast, almost worryingly so. It&rsquo;s true what they say: ruff finishes so fast that I&rsquo;m always left wondering if it even did anything at all.</p> <p>I&rsquo;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&rsquo;m running Python 3.11 locally but Python 3.10 in the server environment (no dev-prod parity&hellip;), and rebuilding <code>requirements.txt</code> locally of course didn&rsquo;t add TOML support because it&rsquo;s built into Python 3.11, but Python 3.10 needs an external package. So of course I broke the build. That&rsquo;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>Weeknotes (2023 week 11 and 12)https://406.ch/writing/weeknotes-2023-week-11-and-12/2023-03-24T12:00:00Z2023-03-24T12:00:00Z<h1>Weeknotes (2023 week 11 and 12)</h1> <h2>Mail user agents being mail user agents</h2> <p><a href="https://pypi.org/project/django-authlib/">django-authlib</a> is my collection of utilities for implementing passwordless authentication, either using OAuth2 or by sending magic links by email.</p> <p>The latter functionality has existed for a long time in <a href="https://pypi.org/project/django-registration/">django-registration</a> (which is great!) but I wanted a way to generate links without the need for a storage somewhere. Django has utilities for <a href="https://docs.djangoproject.com/en/4.1/topics/signing/">Cryptographic signing</a> built-in as the <code>django.core.signing</code> module. I&rsquo;m using this module to send a signed version of the email address to users, and when I&rsquo;m able to successfully verify the email addresses signature I can be sure (enough) that those links have actually been generated by my code.</p> <p>The form of the generated verification URL is as follows: <code>https://example.com/.../test@example.com:1pXJtx:aypKOlb5zaCg.../</code>; the part before the first colon contains the data-to-be-verified, the short string between colons is the timestamp and the rest is the signature, everything generated by <code>django.core.signing</code>.</p> <p>So far so boring.</p> <p>But then mail user agents happened. Some MUAs insist to butcher the URL in various ways, e.g. by removing the signature or by making only the embedded email address clickable. This is &ldquo;interesting&rdquo; behavior and certainly unexpected.</p> <p>Last week I implemented a change where I&rsquo;m now also base64 encoding the email address itself. So, the URL is even longer and even more opaque now but it seems to work well. I also added a HTML version of the mail to hopefully prevent mail programs from creating their own worse version. I could have done that a long time ago but I really like <code>text/plain</code> mails so I did hold off on that as long as I could. Oh well.</p> <h2>feincms3-data and unique fields</h2> <p>I did a longer post on <a href="https://406.ch/writing/moving-data-including-deletions-between-the-same-django-app-running-in-different-environments/">feincms3-data</a> some time ago which explains what it is for.</p> <p>A recurring problem was dumping and loading data in the presence of unique constraints. For example, website managers deleted data with a specific (unique) slug and created new data with the same slug. feincms3-data and also Django itself were unable to handle this. The former because it loads new data before deleting old data, the latter because it doesn&rsquo;t even have a concept of data which should be deleted.</p> <p>feincms3-data now has an ugly workaround for this problem: It generates random data for a list of explicitly specified fields (which is very unlikely to cause collisions) and only updates the records with the correct data after the cleanup step. <a href="https://github.com/matthiask/feincms3-data/commit/9324605dc8dcb32ee4118adca0668643597ec130">The code wasn&rsquo;t that hairy to write</a> and I was again very happy that unittesting is a thing.</p> <h2>ProseMirror hacking</h2> <p>I&rsquo;m back to hacking on <a href="https://prosemirror.net/">ProseMirror</a> plugins. I still feel really well about picking it as a basis for all those use cases when using the venerable <a href="https://ckeditor.com/ckeditor-4/">CKEditor 4</a> isn&rsquo;t a good fit.</p> <p>I did like the fact a lot that ProseMirror was written in JavaScript. Now that it has been rewritten in TypeScript I&rsquo;m reevaluating my resistance against TypeScript, but I just don&rsquo;t know if it&rsquo;s worth it. Why write down all those annotations when the code also works without? I still don&rsquo;t really see the point. Yes I know, catching bugs early and all that. But still.</p>Weeknotes (2023 week 10)https://406.ch/writing/weeknotes-2023-week-10/2023-03-10T12:00:00Z2023-03-10T12:00:00Z<h1>Weeknotes (2023 week 10)</h1> <h2>FeinCMS bugs</h2> <p><a href="https://github.com/feincms/feincms/">FeinCMS</a> is really stable (since there isn&rsquo;t much going on) but this week a Django upgrade surfaced bugs in the <a href="https://github.com/feincms/feincms/blob/main/feincms/extensions/datepublisher.py">datepublisher</a> extension. The extension uses a <code>granular_now</code> utility to determine if a page should be shown or not; the time is rounded to a 5-minute boundary which makes some sorts of caching more effective.</p> <p>On one hand it&rsquo;s a bit sad that activity on the project has practically ground to a halt, on the other hand it&rsquo;s great that the project requires practically no changes when a new version of Django is released. Stability is awesome.</p> <h2>15 years of using GitHub</h2> <p>I used <a href="https://subversion.apache.org/">Subversion</a> and <a href="http://darcs.net/">darcs</a> for probably not much more than a year each before discovering <a href="https://git-scm.com/">Git</a> probably some time in spring 2006. It was a very archaic version with a much worse CLI than today. Everything was archaic, not just the CLI itself but also the repository browsers and the various web UIs. But everything was also very interesting to me. I especially found the emphasis on clean data structures instead of code very inspiring. And here we are, basically all the tools have been rewritten but the commits from back then are still around.</p> <p>Yesterday marks the 15 anniversary of my GitHub account. I was invited to join the private beta. GitHub was probably the first Git-related tool I encountered which actually looked nice. Hard to believe that so much time has passed.</p> <p>(No, I don&rsquo;t find Git hard to use. And I have basically stopped losing work since I started using Git, something which has happened too often before.)</p> <h2>Static Site generation</h2> <p>I played around with several static site generators to replace my blogging software. I checked out several generators:</p> <ul> <li><a href="https://getpelican.com/">Pelican</a> looks interesting because it uses Python and Jinja2 which should make it easier to reuse the Django templates (or so I thought)</li> <li><a href="https://gohugo.io/">Hugo</a> seems to be very widely used and looks like a good idea to bet on</li> </ul> <p>This site runs on Django and I&rsquo;m using a markdown editor based on <a href="https://codemirror.net/">CodeMirror</a> in the admin. I do like CodeMirror a lot but I would like it even more if I could use neovim in the console. I know that vim bindings for CodeMirror exist, but I didn&rsquo;t have a good experience with the vim bindings for VisualStudio Code so I&rsquo;m wary (and too lazy).</p> <p>The problem with all those generators are that they have different ideas for how to structure the URLs and RSS/Atom feeds. <a href="https://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id">I have since learned the reason why Pelican doesn&rsquo;t use the permalink of posts as their Atom ID</a> (from the <code>feedgenerator</code> module) but I&rsquo;m just not convinced that flooding aggregators with old posts is a net benefit to everyone. Yes, it&rsquo;s only once but&hellip;</p> <p>Of course I&rsquo;m now thinking about writing my own static site generator, for fun.</p> <h2>Thanks, Tim</h2> <p>I have been coworking with <a href="https://hachyderm.io/@CodenameTim@fosstodon.org/109807763335807167">Tim</a> to write more. Tim and I have been maintaining the <a href="https://jazzband.co/projects/django-debug-toolbar">django-debug-toolbar</a> together in the last few years and have been pairing up every one or two weeks to write together (mostly) in silence as described by <a href="https://jacobian.org/2021/mar/9/coworking-to-write-more/">Jacob Kaplan-Moss in his blog entry</a> a few times. Today I really needed the additional motivation brought by committing to work together. If not for this arrangement I would probably have skipped it and who knows if I had managed to restart the weeknotes streak next week on my own&hellip;!</p>Weeknotes (2023 week 8 and 9)https://406.ch/writing/weeknotes-2023-week-8-and-9/2023-03-03T12:00:00Z2023-03-03T12:00:00Z<h1>Weeknotes (2023 week 8 and 9)</h1> <p>Last week was a short work week. We spent a few nights in the mountains. Not much snow though. Enough for some skiing and sledging.</p> <p>This work week was &hellip; not normal. Many meetings, not enough time to work on projects. That shortens these week notes a lot.</p> <h2>Django Software Foundation membership</h2> <p>I found out through Tim that I have been <a href="https://www.djangoproject.com/foundation/minutes/2023/jan/12/dsf-board-monthly-meeting/">approved as an individual member of the Django Software Foundation</a>. I&rsquo;m honored and hope that this will be another way where I can give something back. It also feels good to be seen.</p> <p>I didn&rsquo;t even know that the Foundation&rsquo;s <a href="https://www.djangoproject.com/foundation/minutes/">meeting minutes</a> were public. I still use RSS and can wholeheartedly recommend <a href="https://theoldreader.com/">The Old Reader</a> (no affiliation) and so I thought I&rsquo;d try adding a RSS feed of meeting minutes to the website. <a href="https://github.com/django/djangoproject.com/pull/1316">The pull request is still being reviewed.</a> at the time of writing.</p> <h2>feincms3-forms documentation</h2> <p>I worked on the <a href="https://github.com/matthiask/feincms3-forms">feincms3-forms</a> README a bit. The README hopefully clarifies the purpose and the components of the app a bit.</p> <blockquote> <p>This is an extremely flexible forms builder for the Django admin interface.</p> </blockquote> <h2>Migrating from Bitbucket to GitHub</h2> <p>I have been a long time Bitbucket user since GitHub used a pricing where many private repositories were very expensive and Bitbucket only charged a fee per user. This has changed long ago though. I wanted to consolidate the tools used at $work and have therefore taken the time to move all private repositores to GitHub. Determining the list of repositories in a Bitbucket Workspace was surprisingly annoying but I got there in the end with an app-specific password and curl.</p> <p>Next, I converted the JSON into a list of Git/SSH cloning URLs:</p> <div class="chl"><pre><span></span><code><span class="kn">import</span> <span class="nn">json</span><span class="o">,</span> <span class="nn">sys</span> <span class="n">d</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">read</span><span class="p">())</span> <span class="nb">print</span><span class="p">(</span> <span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span> <span class="nb">next</span><span class="p">(</span><span class="n">link</span><span class="p">[</span><span class="s2">&quot;href&quot;</span><span class="p">]</span> <span class="k">for</span> <span class="n">link</span> <span class="ow">in</span> <span class="n">rec</span><span class="p">[</span><span class="s2">&quot;links&quot;</span><span class="p">][</span><span class="s2">&quot;clone&quot;</span><span class="p">]</span> <span class="k">if</span> <span class="n">link</span><span class="p">[</span><span class="s2">&quot;name&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;ssh&quot;</span><span class="p">)</span> <span class="k">for</span> <span class="n">rec</span> <span class="ow">in</span> <span class="n">d</span><span class="p">[</span><span class="s2">&quot;values&quot;</span><span class="p">]</span> <span class="p">)</span> <span class="p">)</span> </code></pre></div> <p>I used it as (I had to repeat the first command for every 100 repos):</p> <div class="chl"><pre><span></span><code><span class="nv">python3</span><span class="w"> </span><span class="nv">filter</span>.<span class="nv">py</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nv">repos</span>.<span class="nv">json</span><span class="w"> </span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="nv">clone</span>.<span class="nv">txt</span> <span class="k">for</span><span class="w"> </span><span class="nv">url</span><span class="w"> </span><span class="nv">in</span><span class="w"> </span>$<span class="ss">(</span><span class="nv">cat</span><span class="w"> </span><span class="nv">clone</span>.<span class="nv">txt</span><span class="ss">)</span><span class="c1">; do (cd mirror; git clone --mirror $url) ; done</span> </code></pre></div> <p>Then, I wrote another Python script to create the repositores and upload them all:</p> <div class="chl"><pre><span></span><code><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">from</span> <span class="nn">subprocess</span> <span class="kn">import</span> <span class="n">run</span> <span class="k">for</span> <span class="n">repo</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:]:</span> <span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;gh repo create --private feinheit-archive/</span><span class="si">{</span><span class="n">repo</span><span class="p">[:</span><span class="o">-</span><span class="mi">4</span><span class="p">]</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> <span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;cd </span><span class="si">{</span><span class="n">repo</span><span class="si">}</span><span class="s2"> &amp;&amp; git push git@github.com:feinheit-archive/</span><span class="si">{</span><span class="n">repo</span><span class="p">[:</span><span class="o">-</span><span class="mi">4</span><span class="p">]</span><span class="si">}</span><span class="s2">.git --all&quot;</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> <span class="n">run</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;rm -rf </span><span class="si">{</span><span class="n">repo</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> </code></pre></div> <p>And ran ist as:</p> <div class="chl"><pre><span></span><code><span class="n">cd</span><span class="w"> </span><span class="n">mirror</span> <span class="n">python3</span><span class="w"> </span><span class="o">../</span><span class="n">upload</span><span class="o">.</span><span class="n">py</span><span class="w"> </span><span class="o">*</span> </code></pre></div> <p>Funnily enough I learned that GitHub ratelimits repository creation. So I had to wait an hour after several dozen repos and start the command again afterwards. Of course the code wasn&rsquo;t written defensively at first and deleted repos which weren&rsquo;t uploaded yet so I had to work out the set of repos I have to re-download from Bitbucket&hellip; I think it&rsquo;s a bit surprising that <code>subprocess.run</code> doesn&rsquo;t raise an exception by default when a command fails. <code>check=True</code> should definitely be the default.</p> <p>I&rsquo;m not posting the code because I&rsquo;m proud but because I&rsquo;m not. Throwing such stuff together with Python feels great, and using <code>subprocess.run(..., shell=True)</code> is more fun than writing bash scripts with <code>sed</code> etc.</p> <h2>Use Frontend frameworks to kickstart your site and to drag you down later</h2> <p>A longer blog post is in the making as I&rsquo;m trying to formulate my thoughts on this.</p> <p>Frontend frameworks are a great help in getting a product out of the door quickly. But tools and needs evolve. Backwards incompatible versions<sup id="fnref:semver"><a class="footnote-ref" href="#fn:semver">1</a></sup> of those Frameworks are published. And then it happens: Too many things break at once and you have to pick up the pieces. What has been an advantage in the beginning drags you down, because now you have to maintain not just your own code but also everything you used from $framework.</p> <p>I wish I had been (even 😂) more cautious when selecting tools for potentially long running projects.</p> <div class="footnote"> <hr /> <ol> <li id="fn:semver"> <p>Sometimes stuff breaks even when updating patch versions.&#160;<a class="footnote-backref" href="#fnref:semver" title="Jump back to footnote 1 in the text">&#8617;</a></p> </li> </ol> </div>