Matthias Kestenholz: Posts about Djangohttps://406.ch/writing/category-django/2024-04-12T12:00:00ZMatthias KestenholzBuilding forms with the Django adminhttps://406.ch/writing/building-forms-with-the-django-admin/2024-04-12T12:00:00Z2024-04-12T12:00:00Z<h1>Building forms with the Django admin</h1> <p>The title of this post was shamelessly copied from <a href="https://mastodon.social/@webology/112235938469045649">Jeff Triplett&rsquo;s post on Mastodon</a>.</p> <h2>Why?</h2> <p>Many websites need a simple way of embedding forms, for example as a contact form or for simple surveys to collect some data or inputs from visitors. <a href="https://docs.djangoproject.com/en/5.0/topics/forms/">Django&rsquo;s forms library</a> makes building such forms straightforward but changing those forms requires programming skills and programmer time. Both of them may not be readily available. More importantly, sometimes it&rsquo;s just nice to give more tools to web publishers.</p> <p>The simple way to build something like this is to use a form builder such as Google Forms, Typeform, Paperform or anything of the sort. Those options work nicely. The downsides are that embedded forms using those services load slowly, look differently, cost a lot or collect a lot of data on users, or all of those options. Because of that there&rsquo;s still a place for building such functionality locally.</p> <p>If I wanted to use PHP and WordPress I could just use <a href="https://wpforms.com/">WPForms</a> and call it a day. Since I do not actually want that this blog post is a bit longer.</p> <h2>The early days: form-designer</h2> <p>One of the first Django-based third party apps I published was the <a href="https://github.com/feincms/form-designer">form-designer</a>. The first version was uploaded to PyPI in 2012 but it had already been used in production for more than two years at that point in time. I had used <a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules">Git submodules</a> for the deployment back then, before switching to <a href="https://virtualenv.pypa.io/">Python virtualenvs</a> some time later (and never looking back!)</p> <p>The form-designer is still maintained actively. Because of Django&rsquo;s stability and because of the fact that the app doesn&rsquo;t do all that much it doesn&rsquo;t require much development at all.</p> <p><img alt="A screenshot of the admin interface" src="https://406.ch/assets/20240410-form-designer.png" /></p> <p>The form designer supports a selection of standard HTML5 input fields out of the box and also has an optional <a href="https://github.com/django-recaptcha/django-recaptcha">django-recaptcha</a> integration. All fields support some basic configuration such as setting a title, a help text, marking the field as required etc. Submissions can be sent to a configurable email address and can be saved in the database and later exported as an XLSX file. It&rsquo;s also possible to define your own actions.</p> <h2>More flexibility needed: feincms3-forms</h2> <p>A few years back I mentioned <a href="https://406.ch/writing/weeknotes-2021-week-13-and-14/">feincms3-forms in a weeknotes entry</a>. The reasons why form-designer wasn&rsquo;t sufficient for a project back then are outlined in the blog post:</p> <blockquote> <h3>feincms3-forms – A new forms builder for the Django admin interface</h3> <p>For a current project <a href="https://feinheit.ch/">we</a> needed a forms builder with the following constraints:</p> <ul> <li>Simple fields (text, email, checkboxes, dropdowns etc.)</li> <li>Custom validation and processing logic</li> <li>It should be possible to add other content, e.g. headings and explanations between form fields</li> </ul> <p>The <a href="https://github.com/feincms/form_designer">form_designer</a> fulfilled a few of these requirements but not all. It still works well but I wanted a forms builder based on <a href="https://github.com/matthiask/django-content-editor">django-content-editor</a> for a long time already. Also, I really like the feincms3 pattern where the third party app only provides abstract models. Yeah, it is much more work to start with but the flexibility and configurability is worth it – especially since it&rsquo;s possible to write straightforward code to handle special cases[^2] instead of configuring even more settings.</p> <p>The humble beginnings are here in the <a href="https://github.com/matthiask/feincms3-forms/">feincms3-forms</a> repository. The <a href="https://github.com/matthiask/feincms3-forms/tree/main/tests/testapp">test suite already shows how things work together</a> but as of now no documentation exists and no release has been made yet. I hope it will be ready for a first beta release in the next few weeks 😄</p> </blockquote> <p>Since then I have used feincms3-forms more often than form-designer, for building simple forms and also to build multi-step form wizards with custom fields, custom validation, configurable steps etc. The <a href="https://github.com/feincms/feincms3-forms?tab=readme-ov-file#feincms3-forms">README</a> now actually explains why the project exists and how it could be used.</p> <p>It still doesn&rsquo;t come close to WPForms in terms of included functionality; a big feature which is missing is conditional logic because I haven&rsquo;t yet had a use for it.</p> <p><img alt="The feincms3-forms admin interface" src="https://406.ch/assets/20240410-feincms3-forms.png" /></p> <p>The feincms3-forms forms support all types of content between form fields (basically everything <a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> supports). Plugins for form fields are more flexible and can add as many input fields to the form as they want, you&rsquo;re not restricted to single values or single input fields.</p> <h2>Packages</h2> <ul> <li><a href="https://github.com/feincms/form-designer">form-designer</a></li> <li><a href="https://github.com/feincms/feincms3-forms">feincms3-forms</a></li> </ul>Weeknotes (2024 week 14)https://406.ch/writing/weeknotes-2024-week-14/2024-04-06T12:00:00Z2024-04-06T12:00:00Z<h1>Weeknotes (2024 week 14)</h1><p>I&rsquo;m having a bit of a slow week with the easter weekend and a wisdom tooth extraction. I&rsquo;m recovering quite quickly it seems and I&rsquo;m glad about it.</p> <p>This weeknotes entry is short and quick. I&rsquo;m trying to get back into the habit of writing them after a mediocre start this year.</p> <h2>20th Anniversary Celebration of Young Greens Switzerland</h2> <p>I have attended the celebration of Young Greens Switzerland. I have been a founding member and have been active for close to ten years. A lot of time has passed since then. It has been great to reminisce about old times with friends and, more importantly, to see how the torch is carried on.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/blacknoise/">blacknoise 0.0.5</a>: blacknoise is an ASGI app for static file serving inspired by <a href="https://whitenoise.readthedocs.io/en/latest/">whitenoise</a>. It only supports a very limited subset of whitenoise&rsquo;s functionality, but it supports async.</li> <li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.4.1</a>: The lxml library moved the HTML cleaner into its own package, <a href="https://pypi.org/project/lxml-html-clean/">lxml-html-clean</a>; this release adds support for that. I didn&rsquo;t know that the HTML cleaner is viewed as being problematic by the lxml maintainers. I&rsquo;m having another look at <a href="https://github.com/messense/nh3">nh3</a> and will maybe switch html-sanitizer&rsquo;s guts from lxml to nh3 in the future.</li> <li><a href="https://pypi.org/project/django-tree-queries/">django-tree-queries 0.18</a>: django-tree-queries now supports ordering siblings by multiple fields and even allows descending orderings.</li> <li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.14.2</a>: This release fixes the CKEditor 4 filebrowser popup when using Django 5 or better.</li> </ul>The Django admin is a CMShttps://406.ch/writing/the-django-admin-is-a-cms/2024-03-27T12:00:00Z2024-03-27T12:00:00Z<h1>The Django admin is a CMS</h1><p>The post <a href="https://www.coderedcorp.com/blog/why-is-the-django-admin-ugly/">Why is the Django Admin “Ugly”?</a> and the <a href="https://hachyderm.io/@paulox@fosstodon.org/111298440425647176">discussion on Mastodon</a> around it finally motivated me to write down my thoughts regarding the recurring theme in Django land that the <a href="https://docs.djangoproject.com/en/5.0/ref/contrib/admin/">Django administration interface</a> isn&rsquo;t a CMS (Content Management System).</p> <p>I think that this is misguided and needlessly limits the discourse around what the admin&rsquo;s current functionality is and the ideas what it could be and already is.</p> <p><a href="https://en.wikipedia.org/wiki/Web_content_management_system">A web content management system</a> is about website authoring for users who do not need to be web programming experts in their own rights. <a href="https://en.wikipedia.org/wiki/Django_(web_framework)">Django was created at the Lawrence Journal-World newspaper</a>. The admin itself was created to allow quickly spinning up new websites, where the admin interface was used by content managers to fill in the content while programmers finalized the rest of the website. So obviously the admin interface was a system used to manage content<sup id="fnref:words"><a class="footnote-ref" href="#fn:words">1</a></sup> from the beginning.</p> <p>Sure, the <a href="https://docs.djangoproject.com/en/5.0/ref/contrib/admin/">Django admin site documentation</a> states:</p> <blockquote> <p>One of the most powerful parts of Django is the automatic admin interface. It reads metadata from your models to provide a quick, model-centric interface where trusted users can manage content on your site. The admin’s recommended use is limited to an organization’s internal management tool. <strong>It’s not intended for building your entire front end around.</strong> [emphasis added]</p> </blockquote> <p>In other words, the Django documentation also points out that the admin is powerful and that it allows trusted users to manage content<sup id="fnref2:words"><a class="footnote-ref" href="#fn:words">1</a></sup>.</p> <p>Yes, it will be very painful if you try to do everything on top of the Django admin site. The warnings against using the Django admin for more than it was designed to are necessary and I totally support them. As soon as you&rsquo;re getting into workflows, into complex permission scenarios (sad noises) or similar things the admin definitely isn&rsquo;t for you. But, the admin nicely solves 90% of the problems with 10% of the effort. And it&rsquo;s very good at that.</p> <p>And sure, if you try building your own frontend on top of the Django admin you&rsquo;re in for a bumpy ride, but that much should be obvious.</p> <p>Many third party apps for Django actually target the Django admin interface itself, and not one of the (excellent!) Django-based CMS such as <a href="https://wagtail.org/">Wagtail</a>. This means that by building on the Django admin instead of one of the CMS you&rsquo;re <a href="https://406.ch/writing/run-less-code-in-production-or-youll-end-up-paying-the-price-later/">running less code</a>, by using more libraries instead of frameworks (on top of frameworks) you&rsquo;re <a href="https://406.ch/writing/low-maintenance-software/">keeping maintenance lower</a>, and you&rsquo;re a part of a larger community<sup id="fnref:community"><a class="footnote-ref" href="#fn:community">2</a></sup>, which brings the potential benefit of being able to profit more from the general Django packages ecosystem.</p> <p>Since you&rsquo;re depending on smaller pieces of additional software it will generally be possible to upgrade to new Django versions quicker. This isn&rsquo;t true for all packages of course, and I&rsquo;m a reluctant maintainer of some of them. Anecdotes aren&rsquo;t data, but I see that some larger CMS systems are definitely having a hard time keeping up with Django&rsquo;s release schedule.</p> <p>I&rsquo;m not trying to say that the Django admin is a better CMS than other Django-based CMS, or any other CMS. I&rsquo;m saying it&rsquo;s a trade off and you should be mindful of the downsides of choosing a larger system. And I&rsquo;m saying that the people who tell you that you shouldn&rsquo;t be using the Django admin interface are wrong in the first approximation.</p> <p>The fact that it&rsquo;s so easy to spin up an additional site and with minimal effort and still be able to work with clean database schemas and all the great tools Django (and Python) offers is important for those of us who are working on many different projects with limited financial resources, because the website often is for example just a small part of a campaign.</p> <div class="footnote"> <hr /> <ol> <li id="fn:words"> <p>Sorry-not-sorry for my choice of words.&#160;<a class="footnote-backref" href="#fnref:words" title="Jump back to footnote 1 in the text">&#8617;</a><a class="footnote-backref" href="#fnref2:words" title="Jump back to footnote 1 in the text">&#8617;</a></p> </li> <li id="fn:community"> <p>The assumption that the communities of these Django-based CMS projects are a subset of the Django community itself shouldn&rsquo;t be too controversial.&#160;<a class="footnote-backref" href="#fnref:community" title="Jump back to footnote 2 in the text">&#8617;</a></p> </li> </ol> </div>blacknoise – ASGI app for static file servinghttps://406.ch/writing/blacknoise-asgi-app-for-static-file-serving/2024-03-20T12:00:00Z2024-03-20T12:00:00Z<h1>blacknoise – ASGI app for static file serving</h1><div class="admonition note"> <p class="admonition-title">Note</p> <p>This blog post consists of the <a href="https://github.com/matthiask/blacknoise">blacknoise README</a> at the time of publishing.</p> </div> <p>blacknoise is an <a href="https://asgi.readthedocs.io/en/latest/">ASGI</a> app for static file serving inspired by <a href="https://github.com/evansd/whitenoise/">whitenoise</a> and following the principles of <a href="https://406.ch/writing/low-maintenance-software/">low maintenance software</a>.</p> <p><strong>This is pre-alpha software and everything is subject to change. I&rsquo;m not even sure if blacknoise should exist at all or if the energy wouldn&rsquo;t be better spent improving whitenoise or other tools. Feedback and contributions are very welcome though!</strong></p> <h2>Using blacknoise with Django to serve static files</h2> <p>Install blacknoise into your Python environment:</p> <div class="chl"><pre><span></span><code><span class="go">pip install blacknoise</span> </code></pre></div> <p>Wrap your ASGI application with the <code>BlackNoise</code> app:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">blacknoise</span> <span class="kn">import</span> <span class="n">BlackNoise</span> <span class="kn">from</span> <span class="nn">django.core.asgi</span> <span class="kn">import</span> <span class="n">get_asgi_application</span> <span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span> <span class="n">BASE_DIR</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="n">application</span> <span class="o">=</span> <span class="n">BlackNoise</span><span class="p">(</span><span class="n">get_asgi_application</span><span class="p">())</span> <span class="n">application</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">BASE_DIR</span> <span class="o">/</span> <span class="s2">&quot;static&quot;</span><span class="p">,</span> <span class="s2">&quot;/static&quot;</span><span class="p">)</span> </code></pre></div> <p><code>BlackNoise</code> will automatically handle all paths below the prefixes added, and either return the files or return 404 errors if files do not exist. The files are added on server startup, which also means that <code>BlackNoise</code> only knows about files which existed at that particular point in time.</p> <h2>Improving performance</h2> <p><code>BlackNoise</code> has worse performance than when using an optimized webserver such as nginx and others. Sometimes it doesn&rsquo;t matter much if the app is behind a caching reverse proxy or behind a content delivery network anyway. To further support this use case <code>BlackNoise</code> can be configured to serve media files with far-future expiry headers and has support for serving compressed assets.</p> <p>Compressing is possible by running:</p> <div class="chl"><pre><span></span><code><span class="go">python -m blacknoise.compress static/</span> </code></pre></div> <p><code>BlackNoise</code> will try compress non-binary files using gzip or brotli (if the <a href="ttps://pypi.org/project/Brotli/">Brotli</a> library is available), and will serve the compressed version if the compression actually results in (significantly) smaller files and if the client also supports it.</p> <p>Far-future expiry headers can be enabled by passing the <code>immutable_file_test</code> callable to the <code>BlackNoise</code> constructor:</p> <div class="chl"><pre><span></span><code><span class="k">def</span> <span class="nf">immutable_file_test</span><span class="p">(</span><span class="n">path</span><span class="p">):</span> <span class="k">return</span> <span class="kc">True</span> <span class="c1"># Enable far-future expiry headers for all files</span> <span class="n">application</span> <span class="o">=</span> <span class="n">BlackNoise</span><span class="p">(</span> <span class="n">get_asgi_application</span><span class="p">(),</span> <span class="n">immutable_file_test</span><span class="o">=</span><span class="n">immutable_file_test</span><span class="p">,</span> <span class="p">)</span> </code></pre></div> <p>Maybe you want to add some other logic, for example check if the path contains a hash based upon the contents of the static file. Such hashes can be added by Django&rsquo;s <code>ManifestStaticFilesStorage</code> or by appropriately configuring bundlers such as <code>webpack</code> and others.</p> <h2>License</h2> <p><code>blacknoise</code> is distributed under the terms of the <a href="https://spdx.org/licenses/MIT.html">MIT</a> license.</p>Podcasts I like listening tohttps://406.ch/writing/podcasts-i-like-listening-to/2024-03-18T12:00:00Z2024-03-18T12:00:00Z<h1>Podcasts I like listening to</h1><p>I discovered listening to podcasts about one year ago. Previously, I never knew why anyone would want to listen to people talk when they could listen to music or nothing, but that has changed a bit.</p> <p>So, here&rsquo;s a list of podcasts I&rsquo;m currently listening to on a regular basis.</p> <h2><a href="https://www.techwontsave.us/">Tech Won&rsquo;t Save Us</a></h2> <p>I have recently stumbled over Tech Won&rsquo;t Save Us, a Podcast which is critical of the technological &ldquo;progress&rdquo; offered by Silicon Valley elites. It&rsquo;s a great antidote for the generative AI hype.</p> <p>I still have some stupid hope that AGI will solve our problems because maybe people will trust a computer more than scientists that something has to be done about the combined crisis we&rsquo;re facing as humans. Who knows. I do not want to be too negative about it though, there are positive news if you&rsquo;re looking in the right places. I myself do not write about those bigger issues <a href="https://406.ch/writing/category-climate/">as much</a> <a href="https://406.ch/writing/category-politik/">as I used to</a>. I support those more important issues elsewhere.</p> <p>Generative AI is certainly not helping <em>me</em> in my work. I do not want a computer to generate code which I have to review and maintain when I still enjoy writing code myself. Maybe that will change at some point in the future. When that happens I&rsquo;ll probably retire and become a gardener or something.</p> <h2><a href="ttps://www.nytimes.com/column/ezra-klein-podcast">The Ezra Klein Show</a></h2> <p>This show seems to be a favorite of many people. I joined for the (relatively) critical perspectives on AI and stayed for the insights into politics and into the near east conflict. I think it&rsquo;s great that people from different sides get a voice on the show, even if Ezra doesn&rsquo;t agree with them.</p> <p>I sometimes wish the questions they ask on the show were a little bit more critical.</p> <h2><a href="https://www.nytimes.com/column/hard-fork">Hard Fork</a></h2> <p>I like the perspectives and the bantering on this podcast. It&rsquo;s a good way for me to stay informed about what&rsquo;s going on in AI/ML land, among other things. It&rsquo;s a lighthearted listen I often look forward to.</p> <h2><a href="https://djangochat.com/">Django Chat</a></h2> <p>I work with Django all the time and Django Chat is a great way to learn more about Django, about the people involved and also about the surrounding ecosystem. A wholehearted recommendation!</p> <h2><a href="https://talkpython.fm/">Talk Python To Me</a></h2> <p>Good interviews, interesting guests and topics. Just a great way to learn about libraries, tools etc. which I cannot wait to use in my own projects.</p> <h2><a href="https://podcast.datenschutzpartner.ch/">Datenschutzplaudereien</a></h2> <p>Required listening for people working with customer data in Switzerland. Swiss german knowledge required.</p> <h2>Others</h2> <p>A few other podcasts I&rsquo;m listening to more or less regularly:</p> <ul> <li><a href="https://www.publiceye.ch/de/wir-muessen-reden-public-eye-spricht-klartext">Wir müssen reden. Public Eye spricht Klartext</a>: The podcast from public eye, an organization which uncovers human right violations perpetrated by Swiss companies around the globe. I&rsquo;m not yet sure if I like the podcast, but I like and support the organization very much.</li> <li><a href="https://hanselminutes.com/">Hanselminutes</a>: The more polifor the wide and diverse array of guests.tical podcasts have displaced Hanselminutes in my listening schedule, but I&rsquo;d definitely recommend it to anyone who wants to learn more about tech. Recommended because of the diverse array of guests, among many other reasons.</li> <li><a href="https://syntax.fm/">Syntax</a>: At times very interesting, at times not much new for me. I still recommend it.</li> <li><a href="https://www.weirdstudies.com/">Weird Studies</a>: Really weird. I liked the discussion of Hellraiser and The Thing, and have listened to some of the other episodes. Religiosity and spiritualism was always around me when I was a child, and it&rsquo;s interesting to revisit some of the ideas about reality from a different angle after years and years of not engaging with these questions at all. (Writing this down does sound a bit like a midlife crisis on the horizon, but I don&rsquo;t think that&rsquo;s it. I have studied a little bit of philosophy and ethics and have stayed interested in these topics ever since.)</li> <li>I don&rsquo;t think the german &ldquo;Welt&rdquo; is a good newspaper at all. I do like the «Aha! History» and «Aha! Zehn Minuten Alltagswissen» podcasts because they are short and often examine topics I don&rsquo;t know much about.</li> <li><a href="https://www.srf.ch/play/tv/sendung/sternstunde-philosophie?id=b7705a5d-4b68-4cb1-9404-03932cd8d569">Sternstude Philosophie</a>: It&rsquo;s a bit slow and definitely more geared towards viewers than listeners, but definitely interesting.</li> </ul>Weeknotes (2024 week 11)https://406.ch/writing/weeknotes-2024-week-11/2024-03-16T12:00:00Z2024-03-16T12:00:00Z<h1>Weeknotes (2024 week 11)</h1><h2>Estimates</h2> <p><a href="https://jacobian.org/2024/mar/11/breaking-down-tasks/">Jacob wrote an excellent post on breaking down tasks</a>. I did like the post a lot. Maybe I&rsquo;ll write a longer reply later, but for now just this. <a href="https://hachyderm.io/@jacob@jacobian.org/112081126379604868">There definitely are good reasons for the pushback against estimation</a>, and it&rsquo;s really not just that some people lack professionalism.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.14.1</a>: Mini release containing a Turkish translation. It&rsquo;s always nice if software is used.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 4.6</a>: Fixed a bug where the move form wouldn&rsquo;t use a potentially overridden <code>ModelAdmin.get_queryset</code> method.</li> <li><a href="https://pypi.org/project/form-designer/">form-designer 0.24</a>: Updated the package for django-recaptcha 4.0.</li> <li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.3.1</a>: Fixed an edge case sanitization bug (luckily without security implications).</li> <li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 6.4.2</a>: django-content-editor now again supports transitioning plugin fieldsets when opening <em>and</em> closing thanks to CSS grid&rsquo;s ability to animate the maximum height of an element. Also, the initialization in 6.4 was badly broken.</li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.2</a>: <a href="https://406.ch/writing/django-prose-editor-prose-editing-component-for-the-django-admin/">See the announcement blog post from Wednesday</a>.</li> </ul>django-prose-editor – Prose-editing component for the Django adminhttps://406.ch/writing/django-prose-editor-prose-editing-component-for-the-django-admin/2024-03-13T12:00:00Z2024-03-13T12:00:00Z<h1>django-prose-editor – Prose-editing component for the Django admin</h1><p>During the last few days I have been working on a prose-editing component for the Django admin which will replace the basically dead <a href="https://406.ch/writing/django-ckeditor/">django-ckeditor</a> in all of my projects. It is based on <a href="https://prosemirror.net/">ProseMirror</a>, in my opinion the greatest toolkit for building prose editors for the web.</p> <p>Here&rsquo;s a screenshot:</p> <p><img alt="django-prose-editor screenshot" src="https://406.ch/assets/20240313-prose-editor.png" /></p> <p>django-prose-editor is in active development; it&rsquo;s <a href="https://pypi.org/project/django-prose-editor/">available on PyPI</a> and is developed in the open <a href="https://github.com/matthiask/django-prose-editor/">here on GitHub</a>. The version at the time of writing is 0.2, and it&rsquo;s not yet used in production environments, only in staging/preview environments. That will soon change though.</p> <h2>Researching alternatives to django-ckeditor</h2> <p>I have spent a lot of time evaluating alternatives. All of them are great choices, and I don&rsquo;t want to bash any of them. But, I didn&rsquo;t feel good betting on any of the choices from the <a href="https://djangopackages.org/grids/g/wysiwyg/">WYSIWYG editors grid on Django Packages</a>.</p> <p>A few packages have potential licensing issues. CKEditor 5&rsquo;s change to the GPL is what basically killed django-ckeditor. The upcoming TinyMCE 7 version will also change to the GPL according to the <a href="https://github.com/tinymce/tinymce/">TinyMCE GitHub repository</a>. Froala only has a free trial. The <a href="https://github.com/summernote/django-summernote/">django-summernote app</a> doesn&rsquo;t have a dedicated maintainer, so I wouldn&rsquo;t bet on it. <a href="https://github.com/withlogicco/django-prose">django-prose</a> uses Trix; there are various reasons why I didn&rsquo;t want to bet on Trix, among them my personal experience that it sometimes gets into a buggy state where only a full page reload unfreezes the editor.</p> <p>Other packages are basically <a href="https://en.wikipedia.org/wiki/Markdown">Markdown</a> editors. I like Markdown and use it for my blog. I don&rsquo;t think Markdown is a good choice for a CMS which is used by people of many different skill levels. I don&rsquo;t want to teach people to use the Markdown link syntax or the heading syntax even if the editor helps out a bit.</p> <p>Some of the remaining choices are using JavaScript widgets which haven&rsquo;t been updated for a really long time or they are really code editors, not WYSIWYG editors, also a no go.</p> <h2>Deciding on going with ProseMirror</h2> <p>I have worked with <a href="https://prosemirror.net/">ProseMirror</a> on and off since October 2015, soon after the crowdfunding ended. It is used in a project where people can write their own book with a standardized pipeline and process, where the technical side of the project is implemented using Django, ProseMirror and LaTeX. A hacked <a href="https://github.com/ProseMirror/prosemirror-example-setup/">prosemirror-example-setup</a> still is good enough for this project.</p> <p>The ProseMirror deep dive came much later, only a few years back, when implementing an editor with more custom functionality such as annotations, different ways of marking up text and even interactive elements within the text, for example to use it as a cloze in teaching materials.</p> <p>The learning curve is steep. I haven&rsquo;t worked with another library which was so hard to get started with. It is my conviction that the reason for this is that rich-text editing is actually a hard problem. The ProseMirror architecture and implementation definitely makes sense when it finally clicks.</p> <p>As additional bonuses the ProseMirror community is nice and ProseMirror is used in a few large software projects which make me believe that the software has a non-zero probability of being maintained in the long run.</p> <h2>Current plans</h2> <p>The only thing which is configurable right now is the server-side sanitizing of submitted HTML content. I plan on allowing some configurability, for example to disable links when the content will only be used inside a card teaser which itself is already a link. Too much configurability isn&rsquo;t a good thing though in my mind, so I&rsquo;ll probably be slow to add features and rather keep complexity low.</p> <p>I&rsquo;m definitely interested to hear from people who want to use the package but cannot do so right now or who would want some additional features. Issues and pull requests are welcome!</p>Weeknotes (2024 week 07)https://406.ch/writing/weeknotes-2024-week-07/2024-02-16T12:00:00Z2024-02-16T12:00:00Z<h1>Weeknotes (2024 week 07)</h1><p>This is a short weeknotes entry which mainly contains a large list of releases. The reason for the large list is that I haven&rsquo;t published a weeknotes entry in weeks.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/form-designer/">form-designer 0.23</a>: Only small changes, mainly updated the package for current Django and Python versions.</li> <li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.4.6</a>: A minor change: Swallow exceptions which happen during startup when clobbering the scripts data fails. As an aside: I find it funny that I have discovered the <code>.f3cc</code> class in some cookie banner blocklists. It feels good to be recognized even if this maybe isn&rsquo;t the nicest way, but it works for me since I actually do not like cookie banners either. At least feincms3-cookiecontrol doesn&rsquo;t inject anything without users&rsquo; consent, and doesn&rsquo;t require a third party service to run.</li> <li><a href="https://pypi.org/project/django_simple_redirects/">django-simple-redirects 2.2.0</a>: Minor release which adds a search field to the admin changelist. django-simple-redirects is a repackaged version of <code>django.contrib.redirects</code> without the <code>django.contrib.sites</code> dependency.</li> <li><a href="https://pypi.org/project/speckenv/">speckenv 6.2</a>: <code>django_cache_url</code> now supports parsing redis configuration for a leader-replica redis installation with a read-write leader host and read-only replica hosts. I use the same configuration format as <a href="https://github.com/epicserve/django-cache-url">django-cache-url</a> does.</li> <li><a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar 4.3</a>: I haven&rsquo;t done much here, just some reviewing here and there. I enjoy the Djangonaut Space contributions a lot.</li> <li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.14</a>: I have removed the constraint which enforces unique names for subfolders. Enforcing the uniqueness does make sense, but it also makes bulk-updating the media library using serialized data more painful than it should be. It&rsquo;s a clear case of worse is better for me. If people want to confuse themselves I&rsquo;m not going to stop them (anymore, in this case) but it makes the rest of the code so much easier to write that it&rsquo;s not even funny.</li> <li><a href="https://pypi.org/project/html-sanitizer/">html-sanitizer 2.3</a>: This release contains a nice contribution which removes some whitespace which has been added by the sanitizer when merging adjacent tags of the same type, e.g. <code>&lt;strong&gt;abc&lt;/strong&gt;&lt;strong&gt;def&lt;/strong&gt;</code>.</li> <li><a href="https://pypi.org/project/django-ckeditor/">django-ckeditor 6.7.1</a>: See above.</li> <li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.0.11</a>: Fixed a crash which happened when not providing the optional (!) configuration. Shit happens. I should really have a test suite for this package.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 4.5.2</a>: Disables the CKEditor version check.</li> <li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 6.4</a>: The first release since December 2022! Very stable software. The editor now restores the collapsed state of inlines and the scroll position when using &ldquo;Save and continue editing&rdquo;. This is especially useful if editing an object with many content blocks.</li> </ul>django-ckeditorhttps://406.ch/writing/django-ckeditor/2024-02-14T12:00:00Z2024-02-14T12:00:00Z<h1>django-ckeditor</h1> <p>It has finally happened. The open source version of CKEditor 4 does not contain fixes for known problems, see <a href="https://ckeditor.com/cke4/release/CKEditor-4.24.0-LTS">the CKEditor 4.24.0 LTS announcement</a>.</p> <p>I totally get why the CKEditor developers did this and can only thank them for all the work that went into the editor.</p> <p>I wish I didn&rsquo;t have to do the migration work to move basically everything to a different editor. The CKEditor 4 LTS version is only expected to be supported until the end of 2026 and I have a few projects which will be around far longer than this (or at least I hope so). Therefore, buying the LTS package would only delay the inevitable. CKEditor 5 is a completely different editor and uses the GPL license, so that&rsquo;s not really an option either. TinyMCE is well known and I have been using it much earlier in my career, but reimplementing plugins isn&rsquo;t fun to do.</p> <p>I would prefer moving everything to ProseMirror or some other structured editor, but we have so much legacy content contained in HTML blobs which do not use any schema at all that this isn&rsquo;t workable unfortunately.</p> <p>Stay tuned for updates &ndash; they will come since I unfortunately cannot just ignore this problem.</p> <p>Which brings me to <a href="https://github.com/django-ckeditor/django-ckeditor">django-ckeditor</a>. CKEditor shows a very annoying popup to users when it detects a newer version with security fixes, see <a href="https://github.com/django-ckeditor/django-ckeditor/issues/761">the GitHub issue</a>. It&rsquo;s not a bad idea but users cannot do much about it, so I opted to disable the version check (and warning) in django-ckeditor and replaced it with a Django system check which annoys developers instead.</p> <p>The release containing these changes is available as <a href="https://pypi.org/project/django-ckeditor/">django-ckeditor 6.7.1</a> on PyPI.</p>Weeknotes (2024 week 03)https://406.ch/writing/weeknotes-2024-week-03/2024-01-17T12:00:00Z2024-01-17T12:00:00Z<h1>Weeknotes (2024 week 03)</h1><h2>Djangonaut Space</h2> <p>I wish all participants a good time and much success. I do not have anything to do with it really but I enjoy the idea a lot and maybe there will be a pull request or two to review.</p> <h2>Kubernetes</h2> <p>After years and years of hosting all sites on VPS I have finally reached the point where the old setup is more annoying to work with than switching to a new one. I have searched long for a solution which wasn&rsquo;t as limited as some PaaS and as complex as going full Kubernetes, and where I can still delegate the responsibility of actually keeping things up and running to other people. In the end I have now accepted that such a thing doesn&rsquo;t exist; either you have the limitations of a ready made solution, the limitation of having to open many many support tickets or the problem of having to learn Kubernetes (or something similar) with its extremely steep learning curve.</p> <p>After spending days with it I&rsquo;m slowly getting to the point where setting up local development environments and deploying changes is fun again. I&rsquo;m using the GitOps paradigm; while I&rsquo;m still building and uploading Docker (podman) images from the local development environment everything else is automated and goes through a Git based process. That&rsquo;s much nicer than clicking around in some interface or copy pasting obscure commands into the console.</p> <p>The biggest problem I encountered was (perhaps unsurprisingly) managing secrets as a team. It seems to me that while <a href="https://github.com/bitnami-labs/sealed-secrets/">sealed secrets</a> work great as an individual developer they don&rsquo;t really offer straightforward solutions to avoid different people overwriting and resetting each others secrets when updating them. I&rsquo;m a happy user of the external secrets operator and using some cloud service to actually store those secrets.</p> <p>I have started using <a href="https://github.com/emmett-framework/granian/">granian</a> in production. I like the idea of a Rust-based ASGI/WSGI server. Nothing mission critical yet. My idea is to build confidence in the software stack.</p> <h2>Compulsory social measures</h2> <p>The more I learn about how Switzerland treats its citizens the more I wonder about the ways in which humans can mistreat other humans in a so called civilized and peaceful society.</p> <p>It&rsquo;s not exactly a new topic for me, but working on platforms which help remember and which help introducing people to the history certainly causes a heightened awareness for issues such as these.</p> <p><a href="https://www.bj.admin.ch/bj/en/home/gesellschaft/fszm.html">More on this</a>.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.20.2</a>: It previously wasn&rsquo;t possible to filter the list of sites to only show those sites which do <em>not</em> have a default language. This has been fixed.</li> <li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.0.10</a>: It&rsquo;s now actually possible to use the JSON editor outside inlines! That was a fun bug&hellip; not. Apart from the mentioned bug this new release mostly contains fixes to the styles. We&rsquo;re slowly getting there.</li> <li><a href="https://pypi.org/project/django-mptt/">django-mptt 0.16</a>: I didn&rsquo;t do anything except for the changelog and the release. That&rsquo;s alright. The release contains a few minor fixes.</li> </ul>Weeknotes (2024 week 01)https://406.ch/writing/weeknotes-2024-week-01/2024-01-03T12:00:00Z2024-01-03T12:00:00Z<h1>Weeknotes (2024 week 01)</h1><p>First weeknotes post for 2024! Happy new year!</p> <h2>Looking back on 2023</h2> <h3>Writing</h3> <p>I have published almost 40 posts last year. That&rsquo;s almost as many posts as I published in the time period from 2014 to 2023. <a href="https://jacobian.org/2021/mar/9/coworking-to-write-more/">Coworking to write more</a> does work.</p> <p>I already had a quite active blog from 2005 to 2008 with a few posts after that; everything before 2014 was in german and mainly concerned with green politics and climate change. I&rsquo;m still very interested in these topics but I don&rsquo;t feel as if I have much to add to the conversation, even though it&rsquo;s the more important issue.</p> <h3>Open Source</h3> <p>Not much changed here. I enjoy basically everything I do in open source land, and co-maintaining the Django Debug Toolbar with Tim is a joy.</p> <p>I still wish that some of my projects had more impact in Django land, especially those who augment the Django administration interface to be a lightweight CMS which requires very little maintenance and work in the long run. I think it&rsquo;s great that one of the core components, <a href="https://pypi.org/project/django-content-editor/#history">django-content-editor</a>, hasn&rsquo;t required a release in more than one year. It doesn&rsquo;t have to be expanded because it just works. It would be great if there was a good way to <a href="https://406.ch/writing/managing-complexity-and-technical-debt-by-releasing-open-source-software/">distinguish</a> between software which basically doesn&rsquo;t require any updates and software which is abandoned.</p> <p>The only project which doesn&rsquo;t bring me much joy is django-mptt. Most people accept that there are no guarantees, but some people are just rude in the way in which they expect others to do the work. The first reaction was to return the blow and I&rsquo;m glad that I didn&rsquo;t give in to the temptation to do that. It&rsquo;s basically never worth it to do that in writing.</p> <h3>Family</h3> <p>We had a good 2023 together and I&rsquo;m very much looking forward to a just as good 2024.</p> <h2>Plans for 2024</h2> <p>I have bought a ticket for the <a href="https://2024.djangocon.eu/">DjangoCon Europe 2024</a> in Spain and I&rsquo;m very much looking forward to that.</p> <p>Maybe we&rsquo;ll visit a music festival again this summer. After listening to a lot of metal music in the last ten or more years I rediscovered the dark side of D&rsquo;n&rsquo;B. Good times.</p> <h2>Advent of Code</h2> <p>I have finished the Advent of Code with some help from the Subreddit. Almost all of the puzzles were fun to think about. I didn&rsquo;t have fun solving each and everyone of them, especially not the second part of a few of the later days. I feel good checking out solutions from other people in the subreddit, and maybe adding newly gained ideas to my code or even running someone else&rsquo;s code 1:1 on my data after studying the algorithms used and hopefully learning something.</p> <p>I had a good time and enjoyed shutting down the computer after that.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/FeinCMS/">FeinCMS 23.12</a>: A few minor changes to the thumbnailing code. It&rsquo;s always nice to hear from people who are still using this project.</li> </ul>Weeknotes (2023 week 50)https://406.ch/writing/weeknotes-2023-week-50/2023-12-15T12:00:00Z2023-12-15T12:00:00Z<h1>Weeknotes (2023 week 50)</h1><h2>django-imagefield</h2> <p>The path building scheme used by <a href="https://pypi.org/project/django-imagefield/">django-imagefield</a> has proven problematic: It&rsquo;s too likely that processed images will have the same path.</p> <p>I have changed the strategy used for generating paths to use more data from the source; it&rsquo;s now possible (and recommended!) to set <code>IMAGEFIELD_BIN_DEPTH</code> to a value greater than 1; 2 or 3 should be sufficient. The default value is 1 which corresponds to the old default so that the change won&rsquo;t be backwards incompatible. However, you&rsquo;ll always get a deprecation warning if you don&rsquo;t set a bigger value yourself. The default will probably change in the future.</p> <h2>Advent of Code</h2> <p>I have always felt a bit as an imposter because I do not have any formal CS education; not so much in the last few years but certainly earlier in my career. I have enjoyed participating in the <a href="https://adventofcode.com/">Advent of Code 2022</a> a lot and I have definitely learned to know when to use and how to use a few algorithms I didn&rsquo;t even know before. I&rsquo;m again working through the puzzles in my own pace and have managed to solve almost all of them up to today this year. There still are some puzzles where I don&rsquo;t even know how to start the second part 😅.</p> <h2>Hosting</h2> <p>We&rsquo;re still hosting most sites on virtualized servers, without any containers or any of the new stuff. I&rsquo;m finally reaching the point where the downsides of this approach start to drag new projects down and the workarounds start looking worse than maybe switching to containers or even Kubernetes. Wish me luck, I&rsquo;m more confused than I&rsquo;ve been in years.</p> <h2>Health</h2> <p>To absolutely nobody&rsquo;s surprise the family and myself have continued to be sick in the last two weeks. Nothing really bad happened, so we&rsquo;re still lucky.</p> <p>There&rsquo;s unfortunately no way to solve a societal problem individually, so that will probably continue to be our life for now.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.18</a>: See above.</li> <li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.20.1</a>: Added additional validation (cleaning) checks. Showing error messages is preferrable to crashing with <code>IntegrityError</code> exceptions after all.</li> <li><a href="https://pypi.org/project/django-js-asset/">django-js-asset 2.2.0</a>: Hatchling seems to dislike it if the project name and the Python module name do not match. I actually like <code>django-js-asset</code>&rsquo;s Python module to be <code>js_asset</code> but I&rsquo;m beginning to rethink this decision.</li> <li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.0.4</a>: See <a href="https://406.ch/writing/django-json-schema-editor/">the post from this week</a>.</li> </ul>django-json-schema-editorhttps://406.ch/writing/django-json-schema-editor/2023-12-13T12:00:00Z2023-12-13T12:00:00Z<h1>django-json-schema-editor</h1><p>I have extracted a JSON editing component based on <a href="https://www.npmjs.com/package/@json-editor/json-editor">@json-editor/json-editor</a> from a client&rsquo;s project and released it as open source. It isn&rsquo;t the first JSON editing component by far but I like it a lot for the following reasons:</p> <ul> <li>It works really well.</li> <li>It supports editing arrays of objects using a tabular presentation. Tabular isn&rsquo;t always better, but stacked definitely isn&rsquo;t always better as well.</li> <li>The data structure is defined as <a href="https://json-schema.org/">JSON schema</a>,the data which is being entered is validated on the server using the <a href="https://pypi.org/project/fastjsonschema/">fastjsonschema</a> library. Having a schema and schema-based validation fixes most problems I have with less structured data than when using only Django model fields (without JSON).</li> </ul> <p>Here&rsquo;s a screenshot of the editing component used as a <a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> plugin:</p> <p><img alt="django-json-schema-editor screenshot" src="/assets/20231313-json-schema-editor.png" /></p> <p>Within the first few days of having released the package it has already proven useful in several other projects. A pleasant (but not totally unexpected) surprise.</p> <h2>Links:</h2> <ul> <li><a href="https://pypi.org/project/django-json-schema-editor/">PyPI</a></li> <li><a href="https://github.com/matthiask/django-json-schema-editor">GitHub</a></li> </ul>Weeknotes (2023 week 48)https://406.ch/writing/weeknotes-2023-week-48/2023-11-30T12:00:00Z2023-11-30T12:00:00Z<h1>Weeknotes (2023 week 48)</h1><p>A few weeks have passed since the last update. The whole family was repeatedly sick with different viruses etc&hellip; I hope that the worst is over now. Who knows.</p> <h2>12-factor Django storage configuration</h2> <p>I should maybe write a longer and separate post about this, but <a href="https://pypi.org/project/speckenv/">speckenv</a> has gained support for the Django <code>STORAGES</code> setting. No documentation yet, but it supports two storage backends for now, the file system storage and <a href="https://github.com/etianen/django-s3-storage/">django-s3-storage</a>, my go-to library for S3-compatible services.</p> <p>Using it looks something like this:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">speckenv</span> <span class="kn">import</span> <span class="n">env</span> <span class="kn">from</span> <span class="nn">speckenv_django</span> <span class="kn">import</span> <span class="n">django_storage_url</span> <span class="n">STORAGES</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&quot;default&quot;</span><span class="p">:</span> <span class="n">django_storage_url</span><span class="p">(</span> <span class="n">env</span><span class="p">(</span> <span class="s2">&quot;STORAGE_URL&quot;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s2">&quot;file:./media/?base_url=/media/&quot;</span><span class="p">,</span> <span class="n">warn</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="p">),</span> <span class="n">base_dir</span><span class="o">=</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="p">),</span> <span class="s2">&quot;staticfiles&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&quot;BACKEND&quot;</span><span class="p">:</span> <span class="s2">&quot;django.contrib.staticfiles.storage.ManifestStaticFilesStorage&quot;</span><span class="p">,</span> <span class="p">},</span> <span class="p">}</span> </code></pre></div> <p>Then, if you want to use S3 you can put something like this in your <code>.env</code> file:</p> <div class="chl"><pre><span></span><code><span class="n">STORAGE_URL</span><span class="o">=</span><span class="nl">s3</span><span class="p">:</span><span class="o">//</span><span class="n">access</span><span class="o">-</span><span class="k">key</span><span class="err">:</span><span class="n">secret</span><span class="nv">@bucket</span><span class="p">.</span><span class="n">name</span><span class="p">.</span><span class="n">s3</span><span class="p">.</span><span class="n">eu</span><span class="o">-</span><span class="n">central</span><span class="o">-</span><span class="mf">1.</span><span class="n">amazonaws</span><span class="p">.</span><span class="n">com</span><span class="o">/</span><span class="n">media</span><span class="o">/</span> </code></pre></div> <p>Or maybe something like this, if you want to serve media files without authentication:</p> <div class="chl"><pre><span></span><code><span class="n">STORAGE_URL</span><span class="o">=</span><span class="nl">s3</span><span class="p">:</span><span class="o">//</span><span class="n">access</span><span class="o">-</span><span class="k">key</span><span class="err">:</span><span class="n">secret</span><span class="nv">@bucket</span><span class="p">.</span><span class="n">name</span><span class="p">.</span><span class="n">s3</span><span class="p">.</span><span class="n">eu</span><span class="o">-</span><span class="n">central</span><span class="o">-</span><span class="mf">1.</span><span class="n">amazonaws</span><span class="p">.</span><span class="n">com</span><span class="o">/</span><span class="n">media</span><span class="o">/</span><span class="vm">?</span><span class="n">aws_s3_public_auth</span><span class="o">=</span><span class="k">False</span><span class="o">&amp;</span><span class="n">aws_s3_max_age_seconds</span><span class="o">=</span><span class="mi">31536000</span> </code></pre></div> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/speckenv/">speckenv 6.1.1</a>: See above.</li> <li><a href="https://pypi.org/project/feincms3-meta/">feincms3-meta 4.6</a>: York has contributed support for emitting structured data records. Looks nice. No documentation yet.</li> <li><a href="https://pypi.org/project/django-tree-queries/">django-tree-queries 0.16.1</a>: <code>.values()</code> and <code>.values_list()</code> queries are now handled better and more consistently than before.</li> </ul>Weeknotes (2023 week 44)https://406.ch/writing/weeknotes-2023-week-44/2023-11-02T12:00:00Z2023-11-02T12:00:00Z<h1>Weeknotes (2023 week 44)</h1><h2>Unmaintained but maintained packages</h2> <p>There&rsquo;s a discussion going on in the <a href="https://github.com/django-mptt/django-mptt/issues/833">django-mptt issue tracker</a> about the maintenance state of django-mptt. <a href="https://github.com/django-mptt/django-mptt/commit/6f6c1c485f3adc1d579f8d22e0279ce1d52334f6">I have marked the project as unmaintained in March 2021</a> and haven&rsquo;t regretted this decision at all. I haven&rsquo;t had to fix <a href="https://github.com/django-mptt/django-mptt/labels/Broken%20Tree">inconsistencies in the tree structure</a> once since switching to <a href="https://406.ch/writing/django-tree-queries/">django-tree-queries</a>. And if that wasn&rsquo;t enough, I get little but only warm and thankful feedback for the latter, so that&rsquo;s extra nice.</p> <p>Despite marking django-mptt as unmaintained I seem to be doing a little bit of maintenance still. I&rsquo;m still using it in old paid projects and so the things I do to make the package work for me is paid work. I&rsquo;m not personally invested in the package anymore, so I&rsquo;m able to tell people that there are absolutely no guarantees about the maintenance, and that feels good.</p> <h2>Read the Docs</h2> <p>I do understand why the <code>.readthedocs.yaml</code> file is now necessary. I wish that I wouldn&rsquo;t have to do all the busywork of adding one to projects. I have just resubscribed to the Read the Docs Gold Membership which probably has expired at some point in the past. Read the Docs is excellent and everybody who can should support them.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/feincms3/">feincms3 4.5</a>, <a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.20</a> and <a href="https://github.com/feincms/feincms3-language-sites">feincms3-language-sites 0.3</a>: Fixed the check which only allows adding an application through the CMS to the page tree (yes, that&rsquo;s right) once; feincms3 worked fine, feincms3-language-sites by accident but feincms3-sites didn&rsquo;t.</li> <li><a href="https://pypi.org/project/towel/">towel 0.31</a>: Towel is one of my oldest packages which is still being used in real-world projects. Towel is a tool for building CRUD-type applications and is designed to keep you DRY while doing that. The project has been heavily inspired by a Django-based agency software I built many years back. The package even has <a href="https://towel.readthedocs.io/en/latest/">docs</a>! I&rsquo;m still quite proud of the mostly transparent support for multitenancy, but apart from that I haven&rsquo;t used it in many new projects.</li> </ul>Customize the Django admin to differentiate environmentshttps://406.ch/writing/customize-the-django-admin-to-differentiate-environments/2023-10-19T12:00:00Z2023-10-19T12:00:00Z<h1>Customize the Django admin to differentiate environments</h1> <p><img alt="Four different themes" src="https://user-images.githubusercontent.com/2627/276531977-6787c55e-4e8c-448c-8ed4-c71cd98c9750.png" /></p> <p>We often have the same website running in different configurations:</p> <ul> <li>Once as a production site.</li> <li>Once as a place where editors update and preview the content. The content is later automatically (and maybe <a href="https://406.ch/writing/moving-data-including-deletions-between-the-same-django-app-running-in-different-environments/">partially</a>) transferred from this environment to the production environment.</li> <li>Once as a stage environment to stabilize the code.</li> <li>And maybe additional environments for local development.</li> </ul> <p>The Django admin panel mainly uses CSS variables for styling since <a href="https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#admin-theming">theming support was introduced in Django 3.2</a> (by yours truly with a lot of help from others). This makes it simple and fun to customize the colors of all interface elements in a straightforward way without having to write loads of CSS.</p> <p>If you have a <code>ENVIRONMENT</code> context variable available (as we do) you could add the following template as <code>admin/base.html</code> to your project, giving you a red color scheme for the production environment (to discourage people from updating content) and a nice scheme for the <code>preproduction</code> environment which clearly deviates from the standard color scheme used everywhere else:</p> <div class="chl"><pre><span></span><code><span class="cp">{%</span> <span class="k">block</span> <span class="nv">extrahead</span> <span class="cp">%}</span> <span class="cp">{{</span> <span class="nb">block</span><span class="nv">.super</span> <span class="cp">}}</span> <span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span> <span class="p">#</span><span class="nn">site-name</span><span class="p">::</span><span class="nd">after</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">content</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot; (</span><span class="cp">{{</span> <span class="nv">ENVIRONMENT</span> <span class="cp">}}</span><span class="s2">)&quot;</span><span class="p">;</span> <span class="w"> </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mi">60</span><span class="kt">%</span><span class="p">;</span> <span class="p">}</span> <span class="w"> </span><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span> <span class="cp">{%</span> <span class="k">if</span> <span class="nv">ENVIRONMENT</span> <span class="o">==</span> <span class="s1">&#39;production&#39;</span> <span class="cp">%}</span> <span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span> <span class="p">:</span><span class="nd">root</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nv">--primary</span><span class="p">:</span><span class="w"> </span><span class="mh">#aa0000</span><span class="p">;</span> <span class="w"> </span><span class="nv">--secondary</span><span class="p">:</span><span class="w"> </span><span class="mh">#810000</span><span class="p">;</span> <span class="w"> </span><span class="nv">--accent</span><span class="p">:</span><span class="w"> </span><span class="kc">yellow</span><span class="p">;</span> <span class="p">}</span> <span class="w"> </span><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span> <span class="cp">{%</span> <span class="k">elif</span> <span class="nv">ENVIRONMENT</span> <span class="o">==</span> <span class="s1">&#39;preproduction&#39;</span> <span class="cp">%}</span> <span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span> <span class="p">:</span><span class="nd">root</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nv">--primary</span><span class="p">:</span><span class="w"> </span><span class="mh">#30b181</span><span class="p">;</span> <span class="w"> </span><span class="nv">--secondary</span><span class="p">:</span><span class="w"> </span><span class="mh">#1f7957</span><span class="p">;</span> <span class="w"> </span><span class="nv">--accent</span><span class="p">:</span><span class="w"> </span><span class="mh">#cdffea</span><span class="p">;</span> <span class="p">}</span> <span class="w"> </span><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span> <span class="cp">{%</span> <span class="k">endif</span> <span class="cp">%}</span> <span class="cp">{%</span> <span class="k">endblock</span> <span class="cp">%}</span> </code></pre></div>Weeknotes (2023 week 42)https://406.ch/writing/weeknotes-2023-week-42/2023-10-18T12:00:00Z2023-10-18T12:00:00Z<h1>Weeknotes (2023 week 42)</h1><h2>Vacation in Italy</h2> <p>We have spent a wonderful family week in Italy. The voyage by train was very comfortable and we had a great time there. I have lived close to lakes all my life but the sea is always something else. Now I enjoy the cold temperatures of fall.</p> <h2>Going back (forward) to GitJournal</h2> <p>I have tried several note taking apps but I&rsquo;m now back using <a href="https://gitjournal.io/">GitJournal</a> with a Git repository filled with Markdown notes. It works well enough. I just wish that there was a way to make notes more distinguishable and I wish that the editor was more forgiving when encountering badly formatted checklists.</p> <h2>Analog blogging</h2> <p>I have long wanted to write about <a href="https://406.ch/writing/why-we-switched-from-slack-to-discord-at-work/">our switch from Slack to Discord</a>. I have started to write this post with pen and paper. I find that I think better when using pen and paper than when using the computer keyboard. One factor is certainly that the computer offers more distractions, but I suspect that another, more important factor is that as a fast typist the fingers and the thinking are always getting out of step, and this happens less when using a slower method of writing. This actually isn&rsquo;t an idea I had myself, but I don&rsquo;t remember where I got it from.</p> <h2>Zero-based versioning: Good or bad?</h2> <p>I discovered <a href="https://0ver.org/">ZeroVer</a> sometime in the last few days. I have many many Django packages with zero-based versions. Some of them have been used in production for years now. I sometimes wonder if staying with <code>0.</code> is unprofessional and I should just release 1.0 and be done with it or if it doesn&rsquo;t really matter at all.</p> <p>If I evaluate software packages more often than not I don&rsquo;t look at the version number or the version numbering scheme (except when a package is still using <code>0.0.</code>) when deciding whether to rely on it or not. The documentation and the code itself are much more important to me.</p> <h2>Releases</h2> <p>I haven&rsquo;t uploaded any releases in the last 14 days. That&rsquo;s good: I&rsquo;m one of those people who have made their passion their job (which is great) but that sometimes makes it hard to not work at all since I can always tell myself that I&rsquo;m not working, that it&rsquo;s just a hobby.</p>Weeknotes (2023 week 40)https://406.ch/writing/weeknotes-2023-week-40/2023-10-04T12:00:00Z2023-10-04T12:00:00Z<h1>Weeknotes (2023 week 40)</h1><h2>More work on hosting several websites from a single Django application server using feincms3-sites</h2> <p>I have mentioned feincms3-sites last week in my last weeknotes entry; I have again given this package a lot of attention in the last days, so another update is in order.</p> <p>It is now possible to override the list of languages available on each site. That&rsquo;s especially useful for an upcoming campaign site where the umbrella group&rsquo;s site is available in three languages, but (most?) individual group sites (hosted on subdomains) will only have a subset of languages. Since I live in a country with four national languages (english isn&rsquo;t one of them, but is spoken by many!) supporting more than one language, or even many languages is totally commonplace. It&rsquo;s great that Django has good support for internationalization. For the sake of an example, I have the following sites:</p> <ul> <li><code>example.com</code>: The default. The host has to match exactly.</li> <li><code>subdomain.example.com</code>: One individual group&rsquo;s site. The host has to match the regex <code>^subdomain\.</code> (sorry, I actually do like regexes).</li> </ul> <h3>Overriding configured hosts for local development</h3> <p>One thing which always annoyed me when using <code>django.contrib.sites</code> was that &ldquo;just&rdquo; pulling the database from production to the local development environment always produced links pointing back to the remote host instead of working locally (when producing absolute URLs). This problem was shared by feincms3-sites as well. I have now found a very ugly but perfectly workable solution: Overwrite <code>Site.get_host()</code> locally:</p> <div class="chl"><pre><span></span><code><span class="k">if</span> <span class="n">DEBUG</span><span class="p">:</span> <span class="n">domain</span> <span class="o">=</span> <span class="s2">&quot;example.com&quot;</span> <span class="c1"># Or whatever</span> <span class="n">_get_host</span> <span class="o">=</span> <span class="k">lambda</span> <span class="n">site</span><span class="p">:</span> <span class="n">site</span><span class="o">.</span><span class="n">host</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">domain</span><span class="p">,</span> <span class="s2">&quot;localhost:8000&quot;</span><span class="p">)</span> <span class="n">FEINCMS3_SITES_SITE_GET_HOST</span> <span class="o">=</span> <span class="n">_get_host</span> </code></pre></div> <p>This works especially well when using <code>example.com</code> and maybe subdomains of <code>example.com</code>: All absolute links will point to <code>localhost:8000</code> or <code>subdomain.localhost:8000</code>. Since <code>*.localhost</code> always resolves to the local IP the browser knows where it should connect to, and since <code>subdomain.localhost:8000</code> also matches the <code>^subdomain\.</code> regex mentioned above, the site selection logic works as well.</p> <p>Of course if you have more domains, not just subdomains, you could adapt the <code>get_host</code> override and the relevant regexes to those use cases.</p> <h3>Closing words</h3> <p>We&rsquo;re at 100% code coverage now when running the test suite. That&rsquo;s really nice.</p> <h2>Logging into the Django admin using your Google account</h2> <p>This functionality has long been provided by <a href="https://pypi.org/project/django-authlib/">django-admin-sso</a>; however, as mentioned a long time ago this package still uses a deprecated OAuth2 library. <a href="https://github.com/matthiask/django-authlib/">django-authlib</a> supports using a Google account to authenticate with the Django admin since 2017. I have now fixed a small problem with it: If you are logged into a single Google account, and this account&rsquo;s email address doesn&rsquo;t match the configured admin login rule, you were out of luck: There was no way to add another account at that time because the library didn&rsquo;t request the account selection. That has changed now, if the first login attempt doesn&rsquo;t work, it now explicitly tells Google to let the user select their Google account. A small quality of life improvement for those using more than one Google account (voluntarily or not).</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/feincms3/">feincms3 4.4.3</a>: Polished the CKEditor integration a little bit. Re-enabled the source button now that we&rsquo;re back to using the classic iframe-based editor again.</li> <li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.19.3</a>: See above.</li> <li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.16.4</a>: See above.</li> </ul>Weeknotes (2023 week 39)https://406.ch/writing/weeknotes-2023-week-39/2023-09-28T12:00:00Z2023-09-28T12:00:00Z<h1>Weeknotes (2023 week 39)</h1><p>Again a few weeks have passed since the last weeknotes entry :-)</p> <h2>Moving feincms3 repositories into the feincms organization</h2> <p>The <a href="https://github.com/feincms">feincms</a> GitHub organization has seen more active days when FeinCMS 1.x was still actively developed. Since my interest has moved to feincms3 some years ago I haven&rsquo;t kept the organization up to date. That has changed this week, and I have moved most feincms3-related repositories into the organization.</p> <p>This move doesn&rsquo;t change much though, but it certainly feels more official now.</p> <h2>Adding scheduled tests</h2> <p>I have started using the cronjob schedule feature of GitHub actions to ensure that tests run at least once a month in a few important projects. I want to get notified of changes in Django@main affecting my packages not only when actively working on them. I try to keep up with Django@main in all packages I maintain.</p> <h2>Releases</h2> <ul> <li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.18.2</a>: Many releases in the last weeks. Stopped using permanent redirects in DEBUG mode. Avoid migrations when Django adds more languages. Added utilities which allow restricting model relations to objects in the same site (trickier than it sounds). Added utilities for building full URLs to other sites without taxing the database as much.</li> <li><a href="https://pypi.org/project/feincms3-language-sites/">feincms3-language-sites 0.2.0</a>: No biggie. No permanent redirects in DEBUG mode anymore.</li> <li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.4.5</a>: Reduced the byte size of the CSS and JavaScript some more. Added spanish translations.</li> <li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.16.3</a>: I have published a post last week describing the new <a href="https://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/">role-based permissions feature</a>.</li> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.17</a>: The <code>process_imagefields</code> management command now allows specifying globs. If you wanted to prerender all imagefields in the pages app you can use <code>./manage.py process_imagefields pages.*</code> now instead of listing all image fields&rsquo; labels explicitly.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 4.4.1</a>: I&rsquo;m enormously unhappy but I had to go back to the classic CKEditor instead of using the inline editor. The latter looked much nicer but overriding the Django admin CSS was very very painful. Also, I can totally understand why CKEditor 5 is completely different and why CKEditor 4 is only maintained in a paid LTS plan. It still is making me look for alternatives.</li> <li><a href="https://pypi.org/project/django-mptt/">django-mptt 0.15</a>: I unfortunately am still using this despite the fact that I have marked it as officially unmaintained since march 2021. I did a mediocre job of making the library run on Django@main again. Parts of the library do not work, but since I&rsquo;m not using them I don&rsquo;t care too much. I&rsquo;m still wondering if someone wants to take over maintenance of the library since it still seems to be actively used in projects of others as well. When I don&rsquo;t have to use django-mptt I&rsquo;m still really happy with <a href="https://406.ch/writing/django-tree-queries/">django-tree-queries</a>.</li> <li><a href="https://pypi.org/project/form-designer/">form-designer 0.22</a>: This is probably my oldest actively developed project these days. 13 years! (Except for django-content-editor of course.) I have modernized the package, switched to hatchling and put out a new release.</li> </ul>Keep content managers' admin access up-to-date with role-based permissionshttps://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/2023-09-20T12:00:00Z2023-09-20T12:00:00Z<h1>Keep content managers&rsquo; Django admin access up-to-date with role-based permissions</h1> <p><a href="https://docs.djangoproject.com/en/4.2/topics/auth/default/#permissions-and-authorization">Django&rsquo;s built-in permissions system</a> is great if you want fine-grained control of the permissions content managers should have. The allowlist-based approach where users have no permissions by default and must be granted each permission individually makes a lot of sense to me and is easy to understand.</p> <p>When we build a CMS at <a href="https://feinheit.ch/">Feinheit</a> we often use the Django administration panel as a CMS. Unfortunately, Django doesn&rsquo;t provide a way to specify that content managers should have all permissions in the <code>pages</code> and <code>articles</code> app (just as an example). Adding all current permissions in a particular app is straightforward when using the <a href="https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.filter_horizontal"><code>filter_horizontal</code></a> interface but keeping the list up-to-date later isn&rsquo;t. When we add an additional <a href="https://406.ch/writing/my-reaction-to-the-block-driven-cms-blog-post/">content block plugin</a> we always have to remember to also update the permissions after deploying the change &ndash; and often, deployment happens some time after the code has been written, e.g. because clients want to approve the change first. What happens all too often is that the manual step of updating permissions gets forgotten.</p> <p>This has annoyed me (intermittently) for a long time and my preferred solution has always been to give superuser permissions to everyone and trust them to not make changes which they aren&rsquo;t supposed to according to the <em>Trusted Users Editing Structured Content</em> principle which was mentioned in a Django book I read early in my Django journey.</p> <h2>The basic ideas of my role-based permissions implementation</h2> <p>A recent project has resurfaced this annoyance and I did finally bite the bullet and implement a solution for this in the form of a <a href="https://github.com/matthiask/django-authlib/">django-authlib</a> extension. The basic ideas are:</p> <p><strong>All users are assigned a single role</strong>: Single roles sound inflexible, but is good enough for my default use case. Examples for roles could be <em>default</em> (no additional permissions granted), <em>content managers</em> (grant access to the pages and articles apps) or maybe <em>deny auth</em> (deny access to users, groups and permissions).</p> <p><strong>The permission check is implemented using a single callable</strong>: A custom backend is provided whose only job is to call the correct callable for the user&rsquo;s current role.</p> <p><strong>The callable either returns a boolean or raises <code>PermissionDenied</code> to prevent other backends from granting access</strong>: No new ideas here, it&rsquo;s exactly what <a href="https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#handling-authorization-in-custom-backends">Django&rsquo;s authentication backends are supposed to do</a>.</p> <p><strong>Permission checkers for the most common scenarios are bundled</strong>: django-authlib only ships one permission checker right now, <code>allow_deny_globs</code>, which allows specifying a list of permission name globs to allow and to deny. Deny overrides allow as is probably expected.</p> <h2>Using roles in your own project</h2> <p>Specify the available roles in your settings and add the authentication backend:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">partial</span> <span class="kn">from</span> <span class="nn">authlib.roles</span> <span class="kn">import</span> <span class="n">allow_deny_globs</span> <span class="kn">from</span> <span class="nn">django.utils.translation</span> <span class="kn">import</span> <span class="n">gettext_lazy</span> <span class="k">as</span> <span class="n">_</span> <span class="n">AUTHLIB_ROLES</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&quot;default&quot;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="n">_</span><span class="p">(</span><span class="s2">&quot;default&quot;</span><span class="p">)},</span> <span class="s2">&quot;staff&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&quot;title&quot;</span><span class="p">:</span> <span class="n">_</span><span class="p">(</span><span class="s2">&quot;editorial staff&quot;</span><span class="p">),</span> <span class="s2">&quot;callback&quot;</span><span class="p">:</span> <span class="n">partial</span><span class="p">(</span> <span class="n">allow_deny_globs</span><span class="p">,</span> <span class="n">allow</span><span class="o">=</span><span class="p">{</span> <span class="s2">&quot;pages.*&quot;</span><span class="p">,</span> <span class="s2">&quot;articles.*&quot;</span><span class="p">,</span> <span class="p">},</span> <span class="p">),</span> <span class="p">},</span> <span class="p">}</span> <span class="n">AUTHENTICATION_BACKENDS</span> <span class="o">=</span> <span class="p">(</span> <span class="c1"># This is the necessary additional backend</span> <span class="s2">&quot;authlib.backends.PermissionsBackend&quot;</span><span class="p">,</span> <span class="c1"># Maybe you want to use authlib&#39;s email authentication ...</span> <span class="s2">&quot;authlib.backends.EmailBackend&quot;</span><span class="p">,</span> <span class="c1"># ... or the standard username &amp; password combination:</span> <span class="s2">&quot;django.contrib.auth.backends.ModelBackend&quot;</span><span class="p">,</span> <span class="p">)</span> </code></pre></div> <p>You have to extend your user model (you have to use <a href="https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#specifying-custom-user-model">a custom user model</a> if you&rsquo;re not using django-authlib&rsquo;s <code>little_user.User</code>):</p> <div class="chl"><pre><span></span><code><span class="kn">from</span> <span class="nn">authlib.roles</span> <span class="kn">import</span> <span class="n">RoleField</span> <span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">AbstractUser</span><span class="p">):</span> <span class="c1"># ...</span> <span class="n">role</span> <span class="o">=</span> <span class="n">RoleField</span><span class="p">()</span> </code></pre></div> <p>And that&rsquo;s basically it.</p> <p>Of course the globbing is flexible, you could also allow users to view all objects:</p> <div class="chl"><pre><span></span><code><span class="n">partial</span><span class="p">(</span><span class="n">allow_deny_globs</span><span class="p">,</span> <span class="n">allow</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;*.view_*&quot;</span><span class="p">})</span> </code></pre></div> <p>Or you could block users from deleting anything:</p> <div class="chl"><pre><span></span><code><span class="n">partial</span><span class="p">(</span><span class="n">allow_deny_globs</span><span class="p">,</span> <span class="n">deny</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;*.delete_*&quot;</span><span class="p">})</span> </code></pre></div> <p>And as mentioned above, you can also combine <code>allow</code> and <code>deny</code> (<code>deny</code> wins over <code>allow</code>) or even provide your own callables. If you provide your own callable it must accept <code>user</code>, <code>perm</code> and <code>obj</code> (which may be <code>None</code>) as keyword arguments. Implementing such a callable is probably less work than implementing an authentication backend yourself; I had to do more work than initially expected because only implementing <code>.has_perm</code> isn&rsquo;t sufficient if you want to see any apps and models in the admin index page. The current <code>allow_deny_globs</code> implementation is nice and short:</p> <div class="chl"><pre><span></span><code><span class="k">def</span> <span class="nf">allow_deny_globs</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">perm</span><span class="p">,</span> <span class="n">obj</span><span class="p">,</span> <span class="n">allow</span><span class="o">=</span><span class="p">(),</span> <span class="n">deny</span><span class="o">=</span><span class="p">()):</span> <span class="k">for</span> <span class="n">rule</span> <span class="ow">in</span> <span class="n">deny</span><span class="p">:</span> <span class="k">if</span> <span class="n">fnmatch</span><span class="p">(</span><span class="n">perm</span><span class="p">,</span> <span class="n">rule</span><span class="p">):</span> <span class="k">raise</span> <span class="n">PermissionDenied</span> <span class="k">return</span> <span class="nb">any</span><span class="p">(</span><span class="n">fnmatch</span><span class="p">(</span><span class="n">perm</span><span class="p">,</span> <span class="n">rule</span><span class="p">)</span> <span class="k">for</span> <span class="n">rule</span> <span class="ow">in</span> <span class="n">allow</span><span class="p">)</span> </code></pre></div>