Matthias Kestenholz: Posts about feincmshttps://406.ch/writing/category-feincms/2025-12-17T12:00:00ZMatthias KestenholzRich text editors: How restrictive can we be?https://406.ch/writing/rich-text-editors-how-restrictive-can-we-be/2025-12-17T12:00:00Z2025-12-17T12:00:00Z<h1 id="rich-text-editors-how-restrictive-can-we-be"><a class="toclink" href="#rich-text-editors-how-restrictive-can-we-be">Rich text editors: How restrictive can we be?</a></h1> <p>How restrictive should a rich text editor be? It&rsquo;s a question I keep coming back to as I work on FeinCMS and Django-based content management systems.</p> <p>I published the last blog post on <a href="https://github.com/feincms/django-prose-editor">django-prose-editor</a> specifically in August 2025, <a href="https://406.ch/writing/menu-improvements-in-django-prose-editor/">Menu improvements in django-prose-editor</a>. The most interesting part of the blog post was the short mention of the <code>TextClass</code> extension at the bottom which allows adding a predefined list of CSS classes to arbitrary spans of text.</p> <p>In the meantime, I have spent a lot of time working on extensions that try to answer this question: the <a href="https://django-prose-editor.readthedocs.io/en/latest/textclass.html"><code>TextClass</code> extension</a> for adding CSS classes to inline text, and more recently the <a href="https://django-prose-editor.readthedocs.io/en/latest/nodeclass.html"><code>NodeClass</code> extension</a> for adding classes to nodes and marks. It&rsquo;s high time to write a post about it.</p> <h2 id="rich-text-editing-philosophy"><a class="toclink" href="#rich-text-editing-philosophy">Rich Text editing philosophy</a></h2> <blockquote> <p>All of this convinced me that offering the user a rich text editor with too much capabilities is a really bad idea. The rich text editor in FeinCMS only has bold, italic, bullets, link and headlines activated (and the HTML code button, because that&rsquo;s sort of inevitable – sometimes the rich text editor messes up and you cannot fix it other than going directly into the HTML code. Plus, if someone really knows what they are doing, I&rsquo;d still like to give them the power to shoot their own foot).</p> </blockquote> <p>&ndash; <a href="https://github.com/feincms/feincms/commit/70cd7a1244438d2ba97852256f77daa2c870c345#diff-556c5559a716059d4fb714ad34de6a9845870e8d55bbd2cb9d77c732eb961388">Commit in the FeinCMS repository, August 2009</a>, current version from <a href="https://django-content-editor.readthedocs.io/en/latest/design-decisions.html">django-content-editor design decisions</a></p> <h2 id="should-we-let-users-shoot-themselves-in-the-foot"><a class="toclink" href="#should-we-let-users-shoot-themselves-in-the-foot">Should we let users shoot themselves in the foot?</a></h2> <p>Giving power users an HTML code button would have been somewhat fine if only the editors themselves were affected. Unfortunately, that was not the case.</p> <p>As a team we have spent more time than we ever wanted debugging strange problems only to find out that the culprit was a blob of CSS or JavaScript inserted directly into an unsanitized rich text editor field. We saw everything from a few reasonable and well scoped lines of CSS to hundreds of KiBs of hotlinked JavaScript code that broke layouts, caused performance issues, and possibly even created security vulnerabilities.</p> <p>We have one more case of <a href="https://en.wikipedia.org/wiki/Betteridge%27s_law_of_headlines">Betteridge&rsquo;s law of headlines</a> here.</p> <h2 id="the-pendulum-swings"><a class="toclink" href="#the-pendulum-swings">The pendulum swings</a></h2> <p>The first version of django-prose-editor which replaced the venerable CKEditor 4 in our project was much more strict and reduced &ndash; no attributes, no classes, just a very short list of allowlisted HTML tags in the schema.</p> <p>We quickly hit some snags. When users needed similar headings with different styles, we worked around it by using H2 and H3 — not semantic at all. I wasn&rsquo;t exactly involved in this decision; I just didn&rsquo;t want to rock the boat too much, since I was so happy that we were even able to use the more restricted editor at all in this project.</p> <p>Everything was good for a while, but more and more use cases crept up until it was clear that something had to be done about it. First, the <a href="https://django-prose-editor.readthedocs.io/en/latest/textclass.html"><code>TextClass</code> extension</a> was introduced to allow adding classes to inline text, and later also the <a href="https://django-prose-editor.readthedocs.io/en/latest/nodeclass.html"><code>NodeClass</code> extension</a> mentioned above. This was a compromise: The customer wanted inline styles, we wanted as little customizability as possible without getting in the way.</p> <p>That said, we obviously had to move a bit. After all, going back to a less strict editor or even offering a HTML blob injection would be worse. If we try to be too restrictive we will probably have to go back to allowing everything some way or the other, after all:</p> <blockquote> <p>The more you tighten your grip, Tarkin, the more star systems will slip through your fingers.</p> </blockquote> <p>&ndash; Princess Leia</p> <h2 id="combining-css-classes"><a class="toclink" href="#combining-css-classes">Combining CSS classes</a></h2> <p>The last words are definitely not spoken just yet. As <a href="https://hachyderm.io/@matthiask/115650714479718340">teased on Mastodon</a> at the beginning of this month I am working on an even more flexible extension which unifies the <code>NodeClass</code> and <code>TextClass</code> extensions into a single <code>ClassLoom</code> extension.</p> <p>The code is getting real world use now, but I&rsquo;m not ready to integrate it yet into the official repository. However, you can use it if you want, it&rsquo;s 1:1 the version from a project repository. <a href="https://gist.github.com/matthiask/64ea64b539d63d45ff71467752c2f307">Get the <code>ClassLoom</code> extension here</a>.</p> <p>This extension also allows combining classes on a single element. If you have 5 colors and 3 text styles, you&rsquo;d have to add 15 combinations if you were only able to apply a single class. Allowing combinations brings the number of classes down to manageable levels.</p> <h2 id="conclusion"><a class="toclink" href="#conclusion">Conclusion</a></h2> <p>So, back to the original question: How restrictive can we be?</p> <p>The journey from CKEditor 4&rsquo;s permissiveness through django-prose-editor&rsquo;s initial strictness to today&rsquo;s <code>ClassLoom</code> extension has been one of finding that balance. Each extension — <code>TextClass</code>, <code>NodeClass</code>, and now <code>ClassLoom</code> — represents a step toward controlled flexibility: giving content editors the styling options they need while keeping the content structured, maintainable, and safe.</p>Thoughts about Django-based content management systemshttps://406.ch/writing/thoughts-about-django-based-content-management-systems/2025-11-05T12:00:00Z2025-11-05T12:00:00Z<h1 id="thoughts-about-django-based-content-management-systems"><a class="toclink" href="#thoughts-about-django-based-content-management-systems">Thoughts about Django-based content management systems</a></h1> <p>I have almost exclusively used Django for implementing content management systems (and other backends) since 2008.</p> <p>In this time, content management systems have come and gone. The big three systems many years back were <a href="https://www.django-cms.org/">django CMS</a>, <a href="https://github.com/stephenmcd/mezzanine">Mezzanine</a> and <a href="https://feinheit.ch">our</a> own <a href="https://406.ch/writing/the-future-of-feincms/">FeinCMS</a>.</p> <p>During all this time I have always kept an eye open for other CMS than our own but have steadily continued working in my small corner of the Django space. I think it&rsquo;s time to write down why I have been doing this all this time, for myself and possibly also for other interested parties.</p> <h2 id="why-not-use-wagtail-django-cms-or-any-of-those-alternatives"><a class="toclink" href="#why-not-use-wagtail-django-cms-or-any-of-those-alternatives">Why not use Wagtail, django CMS or any of those alternatives?</a></h2> <p>Let&rsquo;s start with the big one. Why not use Wagtail?</p> <p>The Django administration interface is actually great. Even though some people say that it should be treated as a tool for developers only, recent improvements to the accessibility and the general usability suggest otherwise. I have written more about my views on this in <a href="https://406.ch/writing/the-django-admin-is-a-cms/">The Django admin is a CMS</a>. Using and building on top of the Django admin is a great way to immediately profit from all current and future improvements without having to reimplement anything.</p> <p>I don&rsquo;t want to have to reimplement Django&rsquo;s features, I want to add what I need on top.</p> <h2 id="faster-updates"><a class="toclink" href="#faster-updates">Faster updates</a></h2> <p>Everyone implementing and maintaining other CMS is doing a great job and I don&rsquo;t want to throw any shade. I still feel that it&rsquo;s important to point out that systems can make it hard to adopt new Django versions on release day:</p> <ul> <li>The update cycle of many large apps using Django lag behind Django. Wagtail declares an <a href="https://github.com/wagtail/wagtail/discussions/12574">upper version boundary for Django</a> which makes it hard to adopt Django versions faster than Wagtail releases updates.</li> <li>Some django CMS components such as <a href="https://github.com/django-cms/django-filer">django-filer</a> have lagged behind in the past. Looking at the project&rsquo;s CI matrix and activity suggests that this is not the case anymore. That said, a <a href="https://406.ch/writing/django-cabinet-a-media-library-for-django/">simpler alternative exists</a>.</li> </ul> <p>These larger systems have many more (very talented) people working on them. I&rsquo;m not saying I&rsquo;m doing a better job. I&rsquo;m only pointing out that I&rsquo;m following a different philosophy where I&rsquo;m <a href="https://406.ch/writing/run-less-code-in-production-or-youll-end-up-paying-the-price-later/">conservative about running code in production</a> and I&rsquo;d rather <a href="https://406.ch/writing/low-maintenance-software/">have less features when the price is a lot of maintenance later</a>. I&rsquo;m always thinking about long term maintenance. I really don&rsquo;t want to maintain some of these larger projects, or even parts of them. So I&rsquo;d rather not adopt them for projects which hopefully will be developed and maintained for a long time to come. By the way: This experience has been earned the hard way.</p> <h2 id="the-rule-of-least-power"><a class="toclink" href="#the-rule-of-least-power">The rule of least power</a></h2> <p>From <a href="https://en.wikipedia.org/wiki/Rule_of_least_power">Wikipedia</a>:</p> <blockquote> <p>In programming, the rule of least power is a design principle that &ldquo;suggests choosing the least powerful [computer] language suitable for a given purpose&rdquo;. Stated alternatively, given a choice among computer languages, classes of which range from descriptive (or declarative) to procedural, the less procedural, more descriptive the language one chooses, the more one can do with the data stored in that language.</p> </blockquote> <p>Django itself already provides lots and lots of power. I&rsquo;d argue that a very powerful platform on top of Django may be too much of a good thing. I&rsquo;d rather keep it simple and stupid.</p> <h2 id="editing-heterogenous-collections-of-content"><a class="toclink" href="#editing-heterogenous-collections-of-content">Editing heterogenous collections of content</a></h2> <p>Django admin&rsquo;s inlines are great, but they are not sufficient for building a CMS. You need something to manage different types. django-content-editor does that and has done that since 2009.</p> <p><a href="https://torchbox.com/blog/rich-text-fields-and-faster-horses/">When Wagtail introduced the StreamField in 2015</a> it was definitely a great update to an already great CMS but it wasn&rsquo;t a new idea generally and not a new thing in Django land. They didn&rsquo;t say it was and <a href="https://406.ch/writing/i-just-learned-about-wagtail-s-streamfield/">welcomed the fact that they also started using a better way to structure content</a>.</p> <p>Structured content is great. Putting everything into one large rich text area isn&rsquo;t what I want. Django&rsquo;s ORM and admin interface are great for actually modelling the data in a reusable way. And when you need more flexibility than what&rsquo;s offered by Django&rsquo;s forms, the community offers many projects extending the admin. These days, I really like working with the <a href="https://406.ch/writing/django-json-schema-editor/">django-json-schema-editor</a> component; I even reference other model instances in the database and let the JSON editor handle the referential integrity transparently for me (so that referenced model instances do not silently disappear).</p> <h2 id="more-reading"><a class="toclink" href="#more-reading">More reading</a></h2> <p><a href="https://406.ch/writing/the-future-of-feincms/">The future of FeinCMS</a> and the <a href="https://406.ch/writing/category-feincms/">feincms category</a> may be interesting. Also, I&rsquo;d love to talk about these thoughts, either by email or on Mastodon.</p>Preserving referential integrity with JSON fields and Djangohttps://406.ch/writing/preserving-referential-integrity-with-json-fields-and-django/2025-06-04T12:00:00Z2025-06-04T12:00:00Z<h1 id="preserving-referential-integrity-with-json-fields-and-django"><a class="toclink" href="#preserving-referential-integrity-with-json-fields-and-django">Preserving referential integrity with JSON fields and Django</a></h1> <h2 id="motivation"><a class="toclink" href="#motivation">Motivation</a></h2> <p>The great thing about using <a href="https://feincms3.readthedocs.io/">feincms3</a> and <a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> is that CMS plugins are Django models &ndash; if using them you immediately have access to the power of Django&rsquo;s ORM and Django&rsquo;s administration interface.</p> <p>However, using one model per content type can be limiting on larger sites. Because of this <a href="https://feinheit.ch/">we</a> like using JSON plugins with schemas for more fringe use cases or for places where we have richer data but do not want to write a separate Django app for it. This works well as long as you only work with text, numbers etc. but gets a bit ugly once you start referencing Django models because you never know if those objects are still around when actually using the data stored in those JSON fields.</p> <p>Django has a nice <a href="https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.ForeignKey.on_delete"><code>on_delete=models.PROTECT</code></a> feature, but that of course only works when using real models. So, let&rsquo;s bridge this gap and allow using foreign key protection with data stored in JSON fields!</p> <h2 id="models"><a class="toclink" href="#models">Models</a></h2> <p>First, you have to start using the <a href="https://github.com/matthiask/django-json-schema-editor">django-json-schema-editor</a> and specifically its <code>JSONField</code> instead of the standard Django <code>JSONField</code>. The most important difference between those two is that the schema editor&rsquo;s field wants a JSON schema. So, for the sake of an example, let&rsquo;s assume that we have a model with images and a model with galleries. Note that we&rsquo;re omitting many of the fields actually making the interface nice such as titles etc.</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django.db</span><span class="w"> </span><span class="kn">import</span> <span class="n">models</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django_json_schema_editor.fields</span><span class="w"> </span><span class="kn">import</span> <span class="n">JSONField</span> <span class="k">class</span><span class="w"> </span><span class="nc">Image</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span> <span class="n">image</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ImageField</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="n">gallery_schema</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;object&quot;</span><span class="p">,</span> <span class="s2">&quot;properties&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&quot;caption&quot;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;string&quot;</span><span class="p">},</span> <span class="s2">&quot;images&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;array&quot;</span><span class="p">,</span> <span class="s2">&quot;format&quot;</span><span class="p">:</span> <span class="s2">&quot;table&quot;</span><span class="p">,</span> <span class="s2">&quot;minItems&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">&quot;items&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;string&quot;</span><span class="p">,</span> <span class="s2">&quot;format&quot;</span><span class="p">:</span> <span class="s2">&quot;foreign_key&quot;</span><span class="p">,</span> <span class="s2">&quot;options&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="c1"># raw_id_fields URL:</span> <span class="s2">&quot;url&quot;</span><span class="p">:</span> <span class="s2">&quot;/admin/myapp/image/?_popup=1&amp;_to_field=id&quot;</span><span class="p">,</span> <span class="p">},</span> <span class="p">},</span> <span class="p">},</span> <span class="p">},</span> <span class="p">}</span> <span class="k">class</span><span class="w"> </span><span class="nc">Gallery</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span> <span class="n">data</span> <span class="o">=</span> <span class="n">JSONField</span><span class="p">(</span><span class="n">schema</span><span class="o">=</span><span class="n">gallery_schema</span><span class="p">)</span> </code></pre></div> <p>Now, if we were to do it by hand, we&rsquo;d define a <code>through</code> model for a <code>ManyToManyField</code> linking galleries to images, and adding a <code>on_delete=models.PROTECT</code> foreign key to this through model&rsquo;s <code>image</code> foreign key and we would be updating this many to many table when the <code>Gallery</code> object changes. Since that&rsquo;s somewhat <a href="https://github.com/matthiask/django-json-schema-editor/blob/4bc1ab0cf44eda4c0e824f96f2bd08cd94832c1c/django_json_schema_editor/fields.py#L9-L47">boring but also tricky code</a> I have already written it (including unit tests of course) and all that&rsquo;s left to do is define the linking:</p> <div class="chl"><pre><span></span><code><span class="n">Gallery</span><span class="o">.</span><span class="n">register_data_reference</span><span class="p">(</span> <span class="c1"># The model we&#39;re referencing:</span> <span class="n">Image</span><span class="p">,</span> <span class="c1"># The name of the ManyToManyField:</span> <span class="n">name</span><span class="o">=</span><span class="s2">&quot;images&quot;</span><span class="p">,</span> <span class="c1"># The getter which returns a list of stringified primary key values or nothing:</span> <span class="n">getter</span><span class="o">=</span><span class="k">lambda</span> <span class="n">obj</span><span class="p">:</span> <span class="n">obj</span><span class="o">.</span><span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;images&quot;</span><span class="p">),</span> <span class="p">)</span> </code></pre></div> <p>Now, attempting to delete an image which is still used in a gallery somewhere will raise <a href="https://docs.djangoproject.com/en/5.2/ref/exceptions/#django.db.models.ProtectedError">ProtectedError</a> exceptions. That&rsquo;s what we wanted to achieve.</p> <h2 id="using-a-gallery-instance"><a class="toclink" href="#using-a-gallery-instance">Using a gallery instance</a></h2> <p>When you have a gallery instance you can now use the <code>images</code> field to fetch all images and use the order from the JSON data:</p> <div class="chl"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">gallery_context</span><span class="p">(</span><span class="n">gallery</span><span class="p">):</span> <span class="n">images</span> <span class="o">=</span> <span class="p">{</span><span class="nb">str</span><span class="p">(</span><span class="n">image</span><span class="o">.</span><span class="n">pk</span><span class="p">):</span> <span class="n">image</span> <span class="k">for</span> <span class="n">image</span> <span class="ow">in</span> <span class="n">gallery</span><span class="o">.</span><span class="n">images</span><span class="o">.</span><span class="n">all</span><span class="p">()}</span> <span class="k">return</span> <span class="p">{</span> <span class="s2">&quot;caption&quot;</span><span class="p">:</span> <span class="n">gallery</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">&quot;caption&quot;</span><span class="p">],</span> <span class="s2">&quot;images&quot;</span><span class="p">:</span> <span class="p">[</span><span class="n">images</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span> <span class="k">for</span> <span class="n">pk</span> <span class="ow">in</span> <span class="n">gallery</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">&quot;images&quot;</span><span class="p">]],</span> <span class="p">}</span> </code></pre></div> <h2 id="jsonpluginbase-and-jsonplugininline"><a class="toclink" href="#jsonpluginbase-and-jsonplugininline">JSONPluginBase and JSONPluginInline</a></h2> <p>I would generally do the instantiation of models slightly differently and use <code>django-json-schema-editor</code>&rsquo;s <code>JSONPluginBase</code> and <code>JSONPluginInline</code> which offer additional niceties such as streamlined JSON models with only one backing database table (using <a href="https://docs.djangoproject.com/en/5.2/topics/db/models/#proxy-models">proxy models</a>) and supporting not just showing the primary key of referenced model instances but also their <code>__str__</code> value.</p> <p>The example above would have to be changed to look more like this:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django_json_schema_editor</span><span class="w"> </span><span class="kn">import</span> <span class="n">JSONPluginBase</span> <span class="k">class</span><span class="w"> </span><span class="nc">JSONPlugin</span><span class="p">(</span><span class="n">JSONPluginBase</span><span class="p">,</span> <span class="o">...</span><span class="p">):</span> <span class="k">pass</span> <span class="n">JSONPlugin</span><span class="o">.</span><span class="n">register_data_reference</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="n">Gallery</span> <span class="o">=</span> <span class="n">JSONPlugin</span><span class="o">.</span><span class="n">proxy</span><span class="p">(</span><span class="s2">&quot;gallery&quot;</span><span class="p">,</span> <span class="n">schema</span><span class="o">=</span><span class="n">gallery_schema</span><span class="p">)</span> </code></pre></div> <p>However, that&rsquo;s not documented yet so for now you unfortunately have to read the <a href="https://github.com/matthiask/django-json-schema-editor">code and the test suite</a>, sorry for that. It&rsquo;s used heavily in production though so if you start using it it won&rsquo;t suddenly start breaking in the future.</p>django-content-editor now supports nested sectionshttps://406.ch/writing/django-content-editor-now-supports-nested-sections/2024-09-13T12:00:00Z2024-09-13T12:00:00Z<h1 id="django-content-editor-now-supports-nested-sections"><a class="toclink" href="#django-content-editor-now-supports-nested-sections">django-content-editor now supports nested sections</a></h1> <p><a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> (and it&rsquo;s ancestor FeinCMS) has been the Django admin extension for editing content consisting of reusable blocks since 2009. In the last years we have more and more often started <a href="https://feincms3.readthedocs.io/en/latest/guides/rendering.html#grouping-plugins-into-subregions">automatically grouping related items</a>, e.g. for rendering a sequence of images as a gallery. But, sometimes it&rsquo;s nice to give editors more control. This has been possible by using blocks which open a subsection and blocks which close a subsection for a long time, but it hasn&rsquo;t been friendly to content managers, especially when using nested sections.</p> <p>The content editor now has first-class support for such nested sections. Here&rsquo;s a screenshot showing the nesting:</p> <p><img alt="django-content-editor with sections" src="https://406.ch/assets/20240911-content-editor-sections.png" /></p> <p>Finally it&rsquo;s possible to visually group blocks into sections, collapse those sections as once and drag and drop whole sections into their place instead of having to select the involved blocks individually.</p> <p>The best part about it is that the content editor still supports all Django admin widgets, as long as those widgets have support for the Django administration interface&rsquo;s <a href="https://docs.djangoproject.com/en/latest/ref/contrib/admin/javascript/">inline form events</a>! Moving DOM nodes around breaks attached JavaScript behaviors, but we do not actually move DOM nodes around after the initialization &ndash; instead, we use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Ordering_flex_items">Flexbox ordering</a> to visually reorder blocks. It&rsquo;s a bit more work than using a ready-made sortable plugin, but &ndash; as mentioned &ndash; the prize is that we don&rsquo;t break any other Django admin extensions.</p> <h2 id="simple-patterns"><a class="toclink" href="#simple-patterns">Simple patterns</a></h2> <p>I previously already reacted to a blog post by Lincoln Loop here in my post <a href="https://406.ch/writing/my-reaction-to-the-block-driven-cms-blog-post/">My reaction to the block-driven CMS blog post</a>.</p> <p>The latest blog post, <a href="https://lincolnloop.com/insights/simple-block-pattern-wagtail-cms/">Solving the Messy Middle: a Simple Block Pattern for Wagtail CMS</a> was interesting as well. It dives into the configuration of a <a href="https://wagtail.org/">Wagtail</a> stream field which allows composing content out of reusable blocks of content (<a href="https://406.ch/writing/i-just-learned-about-wagtail-s-streamfield/">sounds familiar!</a>). The result is saved in a JSON blob in the database with all the advantages and disadvantages that entails.</p> <p>Now, django-content-editor is a worthy competitor when you do not want to add another interface to your website besides the user-facing frontend and the Django administration interface.</p> <p>The example from the Lincoln Loop blog post can be replicated quite closely with django-content-editor by using sections. I&rsquo;m using the <a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor</a> package for the section plugin since it easily allows adding more fields if some section type needs it.</p> <p>Here&rsquo;s an example model definition:</p> <div class="chl"><pre><span></span><code><span class="c1"># Models</span> <span class="kn">from</span><span class="w"> </span><span class="nn">content_editor.models</span><span class="w"> </span><span class="kn">import</span> <span class="n">Region</span><span class="p">,</span> <span class="n">create_plugin_base</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django_json_schema_editor.plugins</span><span class="w"> </span><span class="kn">import</span> <span class="n">JSONPluginBase</span> <span class="kn">from</span><span class="w"> </span><span class="nn">feincms3</span><span class="w"> </span><span class="kn">import</span> <span class="n">plugins</span> <span class="k">class</span><span class="w"> </span><span class="nc">Page</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span> <span class="c1"># You have to define regions; each region gets a tab in the admin interface</span> <span class="n">regions</span> <span class="o">=</span> <span class="p">[</span><span class="n">Region</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="s2">&quot;content&quot;</span><span class="p">,</span> <span class="n">title</span><span class="o">=</span><span class="s2">&quot;Content&quot;</span><span class="p">)]</span> <span class="c1"># Additional fields for the page...</span> <span class="n">PagePlugin</span> <span class="o">=</span> <span class="n">create_plugin_base</span><span class="p">(</span><span class="n">Page</span><span class="p">)</span> <span class="k">class</span><span class="w"> </span><span class="nc">RichText</span><span class="p">(</span><span class="n">plugins</span><span class="o">.</span><span class="n">richtext</span><span class="o">.</span><span class="n">RichText</span><span class="p">,</span> <span class="n">PagePlugin</span><span class="p">):</span> <span class="k">pass</span> <span class="k">class</span><span class="w"> </span><span class="nc">Image</span><span class="p">(</span><span class="n">plugins</span><span class="o">.</span><span class="n">image</span><span class="o">.</span><span class="n">Image</span><span class="p">,</span> <span class="n">PagePlugin</span><span class="p">):</span> <span class="k">pass</span> <span class="k">class</span><span class="w"> </span><span class="nc">Section</span><span class="p">(</span><span class="n">JSONPluginBase</span><span class="p">,</span> <span class="n">PagePlugin</span><span class="p">):</span> <span class="k">pass</span> <span class="n">AccordionSection</span> <span class="o">=</span> <span class="n">Section</span><span class="o">.</span><span class="n">proxy</span><span class="p">(</span> <span class="s2">&quot;accordion&quot;</span><span class="p">,</span> <span class="n">schema</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;object&quot;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;properties&quot;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;string&quot;</span><span class="p">}}}},</span> <span class="p">)</span> <span class="n">CloseSection</span> <span class="o">=</span> <span class="n">Section</span><span class="o">.</span><span class="n">proxy</span><span class="p">(</span> <span class="s2">&quot;close&quot;</span><span class="p">,</span> <span class="n">schema</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;object&quot;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;properties&quot;</span><span class="p">:</span> <span class="p">{}}},</span> <span class="p">)</span> </code></pre></div> <p>Here&rsquo;s the corresponding admin definition:</p> <div class="chl"><pre><span></span><code><span class="c1"># Admin</span> <span class="kn">from</span><span class="w"> </span><span class="nn">content_editor.admin</span><span class="w"> </span><span class="kn">import</span> <span class="n">ContentEditor</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django_json_schema_editor.plugins</span><span class="w"> </span><span class="kn">import</span> <span class="n">JSONPluginInline</span> <span class="kn">from</span><span class="w"> </span><span class="nn">feincms3</span><span class="w"> </span><span class="kn">import</span> <span class="n">plugins</span> <span class="nd">@admin</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Page</span><span class="p">)</span> <span class="k">class</span><span class="w"> </span><span class="nc">PageAdmin</span><span class="p">(</span><span class="n">ContentEditor</span><span class="p">):</span> <span class="n">inlines</span> <span class="o">=</span> <span class="p">[</span> <span class="n">plugins</span><span class="o">.</span><span class="n">richtext</span><span class="o">.</span><span class="n">RichTextInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">RichText</span><span class="p">),</span> <span class="n">plugins</span><span class="o">.</span><span class="n">image</span><span class="o">.</span><span class="n">ImageInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Image</span><span class="p">),</span> <span class="n">JSONPluginInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">AccordionSection</span><span class="p">,</span> <span class="n">sections</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span> <span class="n">JSONPluginInline</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">CloseSection</span><span class="p">,</span> <span class="n">sections</span><span class="o">=-</span><span class="mi">1</span><span class="p">),</span> <span class="p">]</span> </code></pre></div> <p>The somewhat cryptic <code>sections=</code> argument says how many levels of sections the individual blocks open or close.</p> <p>To render the content including accordions I&rsquo;d probably use a <a href="https://feincms3.readthedocs.io/en/latest/guides/rendering.html#using-marks">feincms3 renderer</a>. At the time of writing the renderer definition for sections is a bit tricky.</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">feincms3.renderer</span><span class="w"> </span><span class="kn">import</span> <span class="n">RegionRenderer</span><span class="p">,</span> <span class="n">render_in_context</span><span class="p">,</span> <span class="n">template_renderer</span> <span class="k">class</span><span class="w"> </span><span class="nc">PageRenderer</span><span class="p">(</span><span class="n">RegionRenderer</span><span class="p">):</span> <span class="k">def</span><span class="w"> </span><span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span> <span class="n">plugins</span> <span class="o">=</span> <span class="n">deque</span><span class="p">(</span><span class="n">plugins</span><span class="p">)</span> <span class="k">yield from</span> <span class="bp">self</span><span class="o">.</span><span class="n">_handle</span><span class="p">(</span><span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span> <span class="k">def</span><span class="w"> </span><span class="nf">_handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">in_section</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span> <span class="k">while</span> <span class="n">plugins</span><span class="p">:</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">plugins</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">models</span><span class="o">.</span><span class="n">Section</span><span class="p">):</span> <span class="n">section</span> <span class="o">=</span> <span class="n">plugins</span><span class="o">.</span><span class="n">popleft</span><span class="p">()</span> <span class="k">if</span> <span class="n">section</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">&quot;close&quot;</span><span class="p">:</span> <span class="k">if</span> <span class="n">in_section</span><span class="p">:</span> <span class="k">return</span> <span class="c1"># Ignore close section plugins when not inside section</span> <span class="k">continue</span> <span class="k">if</span> <span class="n">section</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">&quot;accordion&quot;</span><span class="p">:</span> <span class="k">yield</span> <span class="n">render_in_context</span><span class="p">(</span><span class="s2">&quot;accordion.html&quot;</span><span class="p">,</span> <span class="p">{</span> <span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="n">accordion</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">],</span> <span class="s2">&quot;content&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_handle</span><span class="p">(</span><span class="n">plugins</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="n">in_section</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span> <span class="p">})</span> <span class="k">else</span><span class="p">:</span> <span class="k">yield</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_plugin</span><span class="p">(</span><span class="n">plugin</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span> <span class="n">renderer</span> <span class="o">=</span> <span class="n">PageRenderer</span><span class="p">()</span> <span class="n">renderer</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">RichText</span><span class="p">,</span> <span class="n">template_renderer</span><span class="p">(</span><span class="s2">&quot;plugins/richtext.html&quot;</span><span class="p">))</span> <span class="n">renderer</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Image</span><span class="p">,</span> <span class="n">template_renderer</span><span class="p">(</span><span class="s2">&quot;plugins/image.html&quot;</span><span class="p">))</span> <span class="n">renderer</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Section</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span> </code></pre></div> <div class="admonition note"> <p class="admonition-title">Note</p> <p>A better way to to this is documented in the API docs, specifically <a href="https://feincms3.readthedocs.io/en/latest/ref/renderer.html#feincms3.renderer.RegionRenderer.render_section_plugins"><code>RegionRenderer.render_section_plugins</code></a>.</p> <p>(Note added in Oct 2025.)</p> </div> <h2 id="closing-thoughts"><a class="toclink" href="#closing-thoughts">Closing thoughts</a></h2> <p>Sometimes, I think to myself, I&rsquo;ll &ldquo;just&rdquo; write a &ldquo;simple&rdquo; blog post. I get what I deserve when using those forbidden words. This blog post is neither short or simple. That being said, the rendering code is a bit tricky, the rest is quite straightforward. The amount of code in django-content-editor and feincms3 is reasonable as well. Even though it may look like a lot you&rsquo;ll still be <a href="https://406.ch/writing/run-less-code-in-production-or-youll-end-up-paying-the-price-later/">running less code in production</a> than when using comparable solutions built using Django.</p>Weeknotes (2023 week 42)https://406.ch/writing/weeknotes-2023-week-42/2023-10-18T12:00:00Z2023-10-18T12:00:00Z<h1 id="weeknotes-2023-week-42"><a class="toclink" href="#weeknotes-2023-week-42">Weeknotes (2023 week 42)</a></h1> <h2 id="vacation-in-italy"><a class="toclink" href="#vacation-in-italy">Vacation in Italy</a></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 id="going-back-forward-to-gitjournal"><a class="toclink" href="#going-back-forward-to-gitjournal">Going back (forward) to GitJournal</a></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 id="analog-blogging"><a class="toclink" href="#analog-blogging">Analog blogging</a></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 id="zero-based-versioning-good-or-bad"><a class="toclink" href="#zero-based-versioning-good-or-bad">Zero-based versioning: Good or bad?</a></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 id="releases"><a class="toclink" href="#releases">Releases</a></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 id="weeknotes-2023-week-40"><a class="toclink" href="#weeknotes-2023-week-40">Weeknotes (2023 week 40)</a></h1> <h2 id="more-work-on-hosting-several-websites-from-a-single-django-application-server-using-feincms3-sites"><a class="toclink" href="#more-work-on-hosting-several-websites-from-a-single-django-application-server-using-feincms3-sites">More work on hosting several websites from a single Django application server using feincms3-sites</a></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 id="overriding-configured-hosts-for-local-development"><a class="toclink" href="#overriding-configured-hosts-for-local-development">Overriding configured hosts for local development</a></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 id="closing-words"><a class="toclink" href="#closing-words">Closing words</a></h3> <p>We&rsquo;re at 100% code coverage now when running the test suite. That&rsquo;s really nice.</p> <h2 id="logging-into-the-django-admin-using-your-google-account"><a class="toclink" href="#logging-into-the-django-admin-using-your-google-account">Logging into the Django admin using your Google account</a></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 id="releases"><a class="toclink" href="#releases">Releases</a></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 id="weeknotes-2023-week-39"><a class="toclink" href="#weeknotes-2023-week-39">Weeknotes (2023 week 39)</a></h1> <p>Again a few weeks have passed since the last weeknotes entry :-)</p> <h2 id="moving-feincms3-repositories-into-the-feincms-organization"><a class="toclink" href="#moving-feincms3-repositories-into-the-feincms-organization">Moving feincms3 repositories into the feincms organization</a></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 id="adding-scheduled-tests"><a class="toclink" href="#adding-scheduled-tests">Adding scheduled tests</a></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 id="releases"><a class="toclink" href="#releases">Releases</a></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 id="keep-content-managers-django-admin-access-up-to-date-with-role-based-permissions"><a class="toclink" href="#keep-content-managers-django-admin-access-up-to-date-with-role-based-permissions">Keep content managers&rsquo; Django admin access up-to-date with role-based permissions</a></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 id="the-basic-ideas-of-my-role-based-permissions-implementation"><a class="toclink" href="#the-basic-ideas-of-my-role-based-permissions-implementation">The basic ideas of my role-based permissions implementation</a></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 id="using-roles-in-your-own-project"><a class="toclink" href="#using-roles-in-your-own-project">Using roles in your own project</a></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="w"> </span><span class="nn">functools</span><span class="w"> </span><span class="kn">import</span> <span class="n">partial</span> <span class="kn">from</span><span class="w"> </span><span class="nn">authlib.roles</span><span class="w"> </span><span class="kn">import</span> <span class="n">allow_deny_globs</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django.utils.translation</span><span class="w"> </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="w"> </span><span class="nn">authlib.roles</span><span class="w"> </span><span class="kn">import</span> <span class="n">RoleField</span> <span class="k">class</span><span class="w"> </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="w"> </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 id="my-reaction-to-the-block-driven-cms-blog-post"><a class="toclink" href="#my-reaction-to-the-block-driven-cms-blog-post">My reaction to the block-driven CMS blog post</a></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 id="weeknotes-2023-week-33"><a class="toclink" href="#weeknotes-2023-week-33">Weeknotes (2023 week 33)</a></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 id="py_modules-using-hatchling"><a class="toclink" href="#py_modules-using-hatchling"><code>py_modules</code> using hatchling</a></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 id="django-debug-toolbar-and-tracing-the-cause-of-db-queries-in-an-async-world"><a class="toclink" href="#django-debug-toolbar-and-tracing-the-cause-of-db-queries-in-an-async-world">django-debug-toolbar and tracing the cause of DB queries in an async world</a></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 id="releases"><a class="toclink" href="#releases">Releases</a></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 id="composition-over-inheritance-the-case-for-function-based-views"><a class="toclink" href="#composition-over-inheritance-the-case-for-function-based-views">Composition over inheritance: The case for function-based views</a></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 id="the-early-days"><a class="toclink" href="#the-early-days">The early days</a></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 id="the-introduction-of-class-based-views"><a class="toclink" href="#the-introduction-of-class-based-views">The introduction of class-based views</a></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="w"> </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="w"> </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 id="generic-views-could-be-simple"><a class="toclink" href="#generic-views-could-be-simple">Generic views could be simple</a></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 id="listview-and-detailview"><a class="toclink" href="#listview-and-detailview">ListView and DetailView</a></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="w"> </span><span class="nn">django.shortcuts</span><span class="w"> </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="w"> </span><span class="nn">feincms3.shortcuts</span><span class="w"> </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="w"> </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="w"> </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="w"> </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="w"> </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="w"> </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 id="detail-view-with-additional-behavior"><a class="toclink" href="#detail-view-with-additional-behavior">Detail view with additional behavior</a></h2> <div class="chl"><pre><span></span><code><span class="k">def</span><span class="w"> </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 id="form-views"><a class="toclink" href="#form-views">Form views</a></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="w"> </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="w"> </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="w"> </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="w"> </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="w"> </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="w"> </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="w"> </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="w"> </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="w"> </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 id="date-based-generic-views"><a class="toclink" href="#date-based-generic-views">Date-based generic views</a></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 id="wrapping-up"><a class="toclink" href="#wrapping-up">Wrapping up</a></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 id="closing-words"><a class="toclink" href="#closing-words">Closing words</a></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 id="weeknotes"><a class="toclink" href="#weeknotes">Weeknotes</a></h1> <h2 id="async-django"><a class="toclink" href="#async-django">Async Django</a></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 id="releases"><a class="toclink" href="#releases">Releases</a></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 id="weeknotes-2023-week-28"><a class="toclink" href="#weeknotes-2023-week-28">Weeknotes (2023 week 28)</a></h1> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></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 id="pipx-inject"><a class="toclink" href="#pipx-inject">pipx inject</a></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 id="hatchling-and-data-files"><a class="toclink" href="#hatchling-and-data-files">hatchling and data files</a></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 id="payment-providers-that-must-not-be-named"><a class="toclink" href="#payment-providers-that-must-not-be-named">Payment providers that must not be named</a></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 id="weeknotes-2023-week-26"><a class="toclink" href="#weeknotes-2023-week-26">Weeknotes (2023 week 26)</a></h1> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></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 id="github-projects"><a class="toclink" href="#github-projects">GitHub projects</a></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 id="to-cloud-or-not"><a class="toclink" href="#to-cloud-or-not">To cloud or not</a></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 id="scheduled-publishing"><a class="toclink" href="#scheduled-publishing">Scheduled publishing</a></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 id="i-like-programming-more-than-writing-even-though-i-like-writing"><a class="toclink" href="#i-like-programming-more-than-writing-even-though-i-like-writing">I like programming more than writing (even though I like writing)</a></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 id="feincms-is-a-dead-end-but-feincms3-is-not"><a class="toclink" href="#feincms-is-a-dead-end-but-feincms3-is-not">FeinCMS is a dead end (but feincms3 is not)</a></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 id="weeknotes-2023-week-24"><a class="toclink" href="#weeknotes-2023-week-24">Weeknotes (2023 week 24)</a></h1> <p>Life happened and I missed a month of weeknotes. Oh well.</p> <h2 id="django-debug-toolbar-41"><a class="toclink" href="#django-debug-toolbar-41">django-debug-toolbar 4.1</a></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 id="going-all-in-on-hatch-and-hatchling"><a class="toclink" href="#going-all-in-on-hatch-and-hatchling">Going all in on hatch and hatchling</a></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 id="ckeditor-5s-new-license-and-django-ckeditor"><a class="toclink" href="#ckeditor-5s-new-license-and-django-ckeditor">CKEditor 5&rsquo;s new license and django-ckeditor</a></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 id="mountain-biking"><a class="toclink" href="#mountain-biking">Mountain biking.</a></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 id="using-css-variables-to-ship-customizable-css-in-django-apps"><a class="toclink" href="#using-css-variables-to-ship-customizable-css-in-django-apps">Using CSS variables<sup id="fnref:variables"><a class="footnote-ref" href="#fn:variables">1</a></sup> to ship customizable CSS in Django apps</a></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 id="patterns-for-overrideable-values"><a class="toclink" href="#patterns-for-overrideable-values">Patterns for overrideable values</a></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 id="less-repetition-but-trouble-awaits"><a class="toclink" href="#less-repetition-but-trouble-awaits">Less repetition (but trouble awaits)</a></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 id="a-better-way"><a class="toclink" href="#a-better-way">A better way</a></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 id="weeknotes-2023-week-18-and-19"><a class="toclink" href="#weeknotes-2023-week-18-and-19">Weeknotes (2023 week 18 and 19)</a></h1> <p>Not much programming this week :-(</p> <h2 id="oembed"><a class="toclink" href="#oembed">oEmbed</a></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 id="vacation"><a class="toclink" href="#vacation">Vacation</a></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 id="weeknotes-2023-week-17"><a class="toclink" href="#weeknotes-2023-week-17">Weeknotes (2023 week 17)</a></h1> <h2 id="birthday"><a class="toclink" href="#birthday">Birthday</a></h2> <p>Another year achieved. Feels the same as last year. I&rsquo;m glad.</p> <h2 id="feincms3-cookiecontrol"><a class="toclink" href="#feincms3-cookiecontrol">feincms3-cookiecontrol</a></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 id="oembed"><a class="toclink" href="#oembed">oEmbed</a></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 id="weeknotes-2023-week-15"><a class="toclink" href="#weeknotes-2023-week-15">Weeknotes (2023 week 15)</a></h1> <h2 id="romansh-translations-for-feincms3-cookiecontrol-and-django-fineforms"><a class="toclink" href="#romansh-translations-for-feincms3-cookiecontrol-and-django-fineforms">Romansh translations for feincms3-cookiecontrol and django-fineforms</a></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 id="django-ckeditor"><a class="toclink" href="#django-ckeditor">django-ckeditor</a></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 id="django-debug-toolbar"><a class="toclink" href="#django-debug-toolbar">django-debug-toolbar</a></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 id="meta"><a class="toclink" href="#meta">Meta</a></h2> <p>Blogging with vim is fun.</p>