Matthias Kestenholz: Posts about Djangohttps://406.ch/writing/category-django/2025-06-04T12:00:00ZMatthias KestenholzPreserving 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>How I'm bundling frontend assets using Django and rspack these dayshttps://406.ch/writing/how-i-m-bundling-frontend-assets-using-django-and-rspack-these-days/2025-05-26T12:00:00Z2025-05-26T12:00:00Z<h1 id="how-im-bundling-frontend-assets-using-django-and-rspack-these-days"><a class="toclink" href="#how-im-bundling-frontend-assets-using-django-and-rspack-these-days">How I&rsquo;m bundling frontend assets using Django and rspack these days</a></h1> <p>I last wrote about configuring Django with bundlers in 2018: <a href="https://406.ch/writing/our-approach-to-configuring-django-webpack-and-manifeststaticfilesstorage/">Our approach to configuring Django, Webpack and ManifestStaticFilesStorage</a>. An update has been a long time coming. I wanted to write this down for a while already, but each time I started explaining how configuring rspack is actually nice I look at the files we&rsquo;re using and switch to writing about something else. This time I managed to get through &ndash; it&rsquo;s not that bad, I promise.</p> <p>This is quite a long post. A project where all of this can be seen in action is <a href="https://github.com/matthiask/traduire/">Traduire</a>, a platform for translating gettext catalogs. I announced it on the <a href="https://forum.djangoproject.com/t/traduire-a-platform-for-editing-gettext-translations-on-the-web/32687">Django forum</a>.</p> <h2 id="our-requirements"><a class="toclink" href="#our-requirements">Our requirements</a></h2> <p>The requirements were still basically the same:</p> <ul> <li>Hot module reloading during development</li> <li>A process which produces hashed filenames depending on their content so that we can use far-future expiry headers to cache assets in browsers</li> <li>While running Node.js in development is fine we do not want Node.js on the server (in the general case)</li> <li>We still want transpiling and bundling for now</li> </ul> <p>We have old projects using SASS. These days we&rsquo;re only using PostCSS (especially <a href="https://github.com/postcss/autoprefixer">autoprefixer</a> and maybe <a href="https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting">postcss-nesting</a>. Rewriting everything is out of the question, so we needed a tool which handled all that as well.</p> <p>People in the frontend space seem to like tools like Vite or Next.js a lot. I have also looked at Parcel, esbuild, rsbuild and others. Either they didn&rsquo;t support our old projects, were too limited in scope (e.g. no HMR), too opinionated or I hit bugs or had questions about their maintenance. I&rsquo;m sure all of them are great for some people, and I don&rsquo;t intend to talk badly about any of them!</p> <p>In the end, the flexibility, speed and trustworthiness of <a href="https://rspack.dev/">rspack</a> won me over even though I have a love-hate relationship with the Webpack/rspack configuration. We already had a reusable library of configuration snippets for webpack though and moving that library over to rspack was straightforward.</p> <p>That being said, configuring rspack from scratch is no joke, that&rsquo;s why tools such as <a href="https://rsbuild.dev/">rsbuild</a> exist. If you already know Webpack well or really need the flexibility, going low level can be good.</p> <h2 id="high-level-project-structure"><a class="toclink" href="#high-level-project-structure">High-level project structure</a></h2> <p>The high-level overview is:</p> <ul> <li>Frontend assets live in their own folder, <code>frontend/</code>.</li> <li>We&rsquo;re using <a href="https://www.fabfile.org/">fabric</a> and <a href="https://rspack.dev/">rspack</a>, their configuration resides in the root folder of the project as does Django&rsquo;s <code>manage.py</code>.</li> <li>The frontend is transpiled and bundled directly into <code>static/</code> for production and into <code>tmp/</code> during development.</li> <li>We use the HTML plugin of rspack to emit snippets containing <code>&lt;link&gt;</code> and <code>&lt;script&gt;</code> tags. The HTML snippet can be included as-is, without any postprocessing.</li> <li><code>frontend/</code> or <code>frontend/static</code> is optionally added to <code>STATICFILES_DIRS</code> so that some of the files from the frontend can easily be referenced in <code>{% static %}</code> tags.</li> </ul> <p>During development:</p> <ul> <li>We use the dev server of rspack/node to handle <code>127.0.0.1:8000</code>. This server handles requests for frontend assets and the websocket for hot module reloading and proxies everything else to the Django backend running on a different random port.</li> </ul> <p>During deployment:</p> <ul> <li>The assets are compiled to <code>static/</code> and either rsynced to the server or added to the container separately from the standard <code>./manage.py collectstatic --noinput</code>.</li> </ul> <p>In production:</p> <ul> <li>Separate cache busting filenames from <code>ManifestStaticFilesStorage</code> and rspack allow us to set far-future expiry headers on all static assets.</li> <li>I&rsquo;m serving static assets from the same origin as the website itself. (rspack can be configured for different requirements!)</li> <li>I don&rsquo;t worry anymore about duplicating assets which are both referenced from frontend code and backend code. This doesn&rsquo;t affect many assets after all.</li> <li>The HTML snippet is loaded once only.</li> </ul> <h2 id="example-configuration"><a class="toclink" href="#example-configuration">Example configuration</a></h2> <p>Here&rsquo;s an example configuration which works well for us. What follows is the rspack configuration itself, building on our snippet library <code>rspack.library.js</code>. We mostly do not change anything in here except for the list of PostCSS plugins:</p> <p>rspack.config.js:</p> <div class="chl"><pre><span></span><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">env</span><span class="p">,</span><span class="w"> </span><span class="nx">argv</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">base</span><span class="p">,</span><span class="w"> </span><span class="nx">devServer</span><span class="p">,</span><span class="w"> </span><span class="nx">assetRule</span><span class="p">,</span><span class="w"> </span><span class="nx">postcssRule</span><span class="p">,</span><span class="w"> </span><span class="nx">swcWithPreactRule</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span> <span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s2">&quot;./rspack.library.js&quot;</span><span class="p">)(</span><span class="nx">argv</span><span class="p">.</span><span class="nx">mode</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s2">&quot;production&quot;</span><span class="p">)</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="p">...</span><span class="nx">base</span><span class="p">,</span> <span class="w"> </span><span class="nx">devServer</span><span class="o">:</span><span class="w"> </span><span class="nx">devServer</span><span class="p">({</span><span class="w"> </span><span class="nx">backendPort</span><span class="o">:</span><span class="w"> </span><span class="nx">env</span><span class="p">.</span><span class="nx">backend</span><span class="w"> </span><span class="p">}),</span> <span class="w"> </span><span class="nx">module</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">rules</span><span class="o">:</span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="nx">assetRule</span><span class="p">(),</span> <span class="w"> </span><span class="nx">postcssRule</span><span class="p">({</span> <span class="w"> </span><span class="nx">plugins</span><span class="o">:</span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="s2">&quot;postcss-nesting&quot;</span><span class="p">,</span> <span class="w"> </span><span class="s2">&quot;autoprefixer&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">],</span> <span class="w"> </span><span class="p">}),</span> <span class="w"> </span><span class="nx">swcWithPreactRule</span><span class="p">(),</span> <span class="w"> </span><span class="p">],</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">}</span> <span class="p">}</span> </code></pre></div> <p>The default entry point is <code>main</code> and loads <code>frontend/main.js</code>. The rest of the JavaScript and styles are loaded from there.</p> <p>The HTML snippet loader works by adding <code>WEBPACK_ASSETS = BASE_DIR / "static"</code> to the Django settings and adding the following tags to the <code>&lt;head&gt;</code> of the website, most often in <code>base.html</code>:</p> <div class="chl"><pre><span></span><code><span class="cp">{%</span> <span class="k">load</span> <span class="nv">webpack_assets</span> <span class="cp">%}</span> <span class="cp">{%</span> <span class="k">webpack_assets</span> <span class="s1">&#39;main&#39;</span> <span class="cp">%}</span> </code></pre></div> <p>The corresponding template tag in <code>webpack_assets.py</code> follows:</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">cache</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django</span><span class="w"> </span><span class="kn">import</span> <span class="n">template</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django.conf</span><span class="w"> </span><span class="kn">import</span> <span class="n">settings</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django.utils.html</span><span class="w"> </span><span class="kn">import</span> <span class="n">mark_safe</span> <span class="n">register</span> <span class="o">=</span> <span class="n">template</span><span class="o">.</span><span class="n">Library</span><span class="p">()</span> <span class="k">def</span><span class="w"> </span><span class="nf">webpack_assets</span><span class="p">(</span><span class="n">entry</span><span class="p">):</span> <span class="n">path</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">BASE_DIR</span> <span class="o">/</span> <span class="p">(</span><span class="s2">&quot;tmp&quot;</span> <span class="k">if</span> <span class="n">settings</span><span class="o">.</span><span class="n">DEBUG</span> <span class="k">else</span> <span class="s2">&quot;static&quot;</span><span class="p">)</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">entry</span><span class="si">}</span><span class="s2">.html&quot;</span> <span class="k">return</span> <span class="n">mark_safe</span><span class="p">(</span><span class="n">path</span><span class="o">.</span><span class="n">read_text</span><span class="p">())</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">settings</span><span class="o">.</span><span class="n">DEBUG</span><span class="p">:</span> <span class="n">webpack_assets</span> <span class="o">=</span> <span class="n">cache</span><span class="p">(</span><span class="n">webpack_assets</span><span class="p">)</span> <span class="n">register</span><span class="o">.</span><span class="n">simple_tag</span><span class="p">(</span><span class="n">webpack_assets</span><span class="p">)</span> </code></pre></div> <p>Last but not least, the fabfile contains the following task definition:</p> <div class="chl"><pre><span></span><code><span class="nd">@task</span> <span class="k">def</span><span class="w"> </span><span class="nf">dev</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s2">&quot;127.0.0.1&quot;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">8000</span><span class="p">):</span> <span class="n">backend</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">50000</span><span class="p">,</span> <span class="mi">60000</span><span class="p">)</span> <span class="n">jobs</span> <span class="o">=</span> <span class="p">[</span> <span class="sa">f</span><span class="s2">&quot;.venv/bin/python manage.py runserver </span><span class="si">{</span><span class="n">backend</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&quot;HOST=</span><span class="si">{</span><span class="n">host</span><span class="si">}</span><span class="s2"> PORT=</span><span class="si">{</span><span class="n">port</span><span class="si">}</span><span class="s2"> yarn run rspack serve --mode=development --env backend=</span><span class="si">{</span><span class="n">backend</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span> <span class="p">]</span> <span class="c1"># Run these two jobs at the same time:</span> <span class="n">_concurrently</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">jobs</span><span class="p">)</span> </code></pre></div> <p>The fh-fablib repository contains the <a href="https://github.com/feinheit/fh-fablib/blob/8109a76b63b37d3433356fabb4469263f8b18d66/fh_fablib/__init__.py#L194-L214"><code>_concurrently</code></a> implementation we&rsquo;re using at this time.</p> <h2 id="the-library-which-enables-the-nice-configuration-above"><a class="toclink" href="#the-library-which-enables-the-nice-configuration-above">The library which enables the nice configuration above</a></h2> <p>Of course, the whole library of snippets has to be somewhere. The fabfile automatically updates the library when we release a new version, and the library is the same in all the dozens of projects we&rsquo;re working on. Here&rsquo;s the current version of <code>rspack.library.js</code>:</p> <div class="chl"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s2">&quot;node:path&quot;</span><span class="p">)</span> <span class="kd">const</span><span class="w"> </span><span class="nx">HtmlWebpackPlugin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s2">&quot;html-webpack-plugin&quot;</span><span class="p">)</span> <span class="kd">const</span><span class="w"> </span><span class="nx">rspack</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s2">&quot;@rspack/core&quot;</span><span class="p">)</span> <span class="kd">const</span><span class="w"> </span><span class="nx">assert</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s2">&quot;node:assert/strict&quot;</span><span class="p">)</span> <span class="kd">const</span><span class="w"> </span><span class="nx">semver</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s2">&quot;semver&quot;</span><span class="p">)</span> <span class="nx">assert</span><span class="p">.</span><span class="nx">ok</span><span class="p">(</span><span class="nx">semver</span><span class="p">.</span><span class="nx">satisfies</span><span class="p">(</span><span class="nx">rspack</span><span class="p">.</span><span class="nx">rspackVersion</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;&gt;=1.1.3&quot;</span><span class="p">),</span><span class="w"> </span><span class="s2">&quot;rspack outdated&quot;</span><span class="p">)</span> <span class="kd">const</span><span class="w"> </span><span class="nx">truthy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(...</span><span class="nx">list</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">list</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">el</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="o">!!</span><span class="nx">el</span><span class="p">)</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">PRODUCTION</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">cwd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">process</span><span class="p">.</span><span class="nx">cwd</span><span class="p">()</span> <span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">swcWithPreactRule</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">test</span><span class="o">:</span><span class="w"> </span><span class="sr">/\.(j|t)sx?$/</span><span class="p">,</span> <span class="w"> </span><span class="nx">loader</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;builtin:swc-loader&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">exclude</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="sr">/node_modules/</span><span class="p">],</span> <span class="w"> </span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">jsc</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">parser</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">syntax</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;ecmascript&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">jsx</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">transform</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">react</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">runtime</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;automatic&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">importSource</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;preact&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">externalHelpers</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;javascript/auto&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">swcWithReactRule</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">test</span><span class="o">:</span><span class="w"> </span><span class="sr">/\.(j|t)sx?$/</span><span class="p">,</span> <span class="w"> </span><span class="nx">loader</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;builtin:swc-loader&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">exclude</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="sr">/node_modules/</span><span class="p">],</span> <span class="w"> </span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">jsc</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">parser</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">syntax</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;ecmascript&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">jsx</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">transform</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">react</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">runtime</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;automatic&quot;</span><span class="p">,</span> <span class="w"> </span><span class="c1">// importSource: &quot;preact&quot;,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">externalHelpers</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;javascript/auto&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">htmlPlugin</span><span class="p">(</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{})</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">HtmlWebpackPlugin</span><span class="p">({</span> <span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="nx">name</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">name</span><span class="si">}</span><span class="sb">.html`</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;[name].html&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">inject</span><span class="o">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span> <span class="w"> </span><span class="nx">templateContent</span><span class="o">:</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">htmlWebpackPlugin</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span> <span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">htmlWebpackPlugin</span><span class="p">.</span><span class="nx">tags</span><span class="p">.</span><span class="nx">headTags</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="w"> </span><span class="p">...</span><span class="nx">config</span><span class="p">,</span> <span class="w"> </span><span class="p">})</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">htmlSingleChunkPlugin</span><span class="p">(</span><span class="nx">chunk</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&quot;&quot;</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">htmlPlugin</span><span class="p">(</span><span class="nx">chunk</span><span class="p">,</span><span class="w"> </span><span class="nx">chunk</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">chunks</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="nx">chunk</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="p">{})</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">postcssLoaders</span><span class="p">(</span><span class="nx">plugins</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">loader</span><span class="o">:</span><span class="w"> </span><span class="nx">rspack</span><span class="p">.</span><span class="nx">CssExtractRspackPlugin</span><span class="p">.</span><span class="nx">loader</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">loader</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;css-loader&quot;</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">loader</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;postcss-loader&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">postcssOptions</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">plugins</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">]</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">cssExtractPlugin</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">rspack</span><span class="p">.</span><span class="nx">CssExtractRspackPlugin</span><span class="p">({</span> <span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="nx">PRODUCTION</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s2">&quot;[name].[contenthash].css&quot;</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;[name].css&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">chunkFilename</span><span class="o">:</span><span class="w"> </span><span class="nx">PRODUCTION</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s2">&quot;[name].[contenthash].css&quot;</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;[name].css&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">})</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">truthy</span><span class="p">,</span> <span class="w"> </span><span class="nx">base</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">context</span><span class="o">:</span><span class="w"> </span><span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">cwd</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;frontend&quot;</span><span class="p">),</span> <span class="w"> </span><span class="nx">entry</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">main</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;./main.js&quot;</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">output</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">clean</span><span class="o">:</span><span class="w"> </span><span class="nx">PRODUCTION</span><span class="p">,</span> <span class="w"> </span><span class="nx">path</span><span class="o">:</span><span class="w"> </span><span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">cwd</span><span class="p">,</span><span class="w"> </span><span class="nx">PRODUCTION</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s2">&quot;static&quot;</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;tmp&quot;</span><span class="p">),</span> <span class="w"> </span><span class="nx">publicPath</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;/static/&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="nx">PRODUCTION</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="s2">&quot;[name].[contenthash].js&quot;</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;[name].js&quot;</span><span class="p">,</span> <span class="w"> </span><span class="c1">// Same as the default but prefixed with &quot;_/[name].&quot;</span> <span class="w"> </span><span class="nx">assetModuleFilename</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;_/[name].[hash][ext][query][fragment]&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">plugins</span><span class="o">:</span><span class="w"> </span><span class="nx">truthy</span><span class="p">(</span><span class="nx">cssExtractPlugin</span><span class="p">(),</span><span class="w"> </span><span class="nx">htmlSingleChunkPlugin</span><span class="p">()),</span> <span class="w"> </span><span class="nx">target</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;browserslist:defaults&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">devServer</span><span class="p">(</span><span class="nx">proxySettings</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">host</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;0.0.0.0&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">hot</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="nx">port</span><span class="o">:</span><span class="w"> </span><span class="nb">Number</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="mf">4000</span><span class="p">),</span> <span class="w"> </span><span class="nx">allowedHosts</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;all&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">client</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">overlay</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">errors</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="nx">warnings</span><span class="o">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span> <span class="w"> </span><span class="nx">runtimeErrors</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">devMiddleware</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">headers</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">&quot;Access-Control-Allow-Origin&quot;</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;*&quot;</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">index</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="nx">writeToDisk</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">path</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="o">/</span><span class="err">\</span><span class="p">.</span><span class="nx">html$</span><span class="o">/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">path</span><span class="p">),</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">proxy</span><span class="o">:</span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="nx">proxySettings</span> <span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">context</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span> <span class="w"> </span><span class="nx">target</span><span class="o">:</span><span class="w"> </span><span class="sb">`http://127.0.0.1:</span><span class="si">${</span><span class="nx">proxySettings</span><span class="p">.</span><span class="nx">backendPort</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="p">{},</span> <span class="w"> </span><span class="p">],</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">assetRule</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">test</span><span class="o">:</span><span class="w"> </span><span class="sr">/\.(png|webp|woff2?|svg|eot|ttf|otf|gif|jpe?g|mp3|wav)$/i</span><span class="p">,</span> <span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;asset&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">parser</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">dataUrlCondition</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">maxSize</span><span class="o">:</span><span class="w"> </span><span class="mf">512</span><span class="w"> </span><span class="cm">/* bytes */</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">postcssRule</span><span class="p">(</span><span class="nx">cfg</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">test</span><span class="o">:</span><span class="w"> </span><span class="sr">/\.css$/i</span><span class="p">,</span> <span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;javascript/auto&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">use</span><span class="o">:</span><span class="w"> </span><span class="nx">postcssLoaders</span><span class="p">(</span><span class="nx">cfg</span><span class="o">?</span><span class="p">.</span><span class="nx">plugins</span><span class="p">),</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">sassRule</span><span class="p">(</span><span class="nx">options</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{})</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">cssLoaders</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">options</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">cssLoaders</span><span class="p">)</span><span class="w"> </span><span class="nx">cssLoaders</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">postcssLoaders</span><span class="p">([</span><span class="s2">&quot;autoprefixer&quot;</span><span class="p">])</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">test</span><span class="o">:</span><span class="w"> </span><span class="sr">/\.scss$/i</span><span class="p">,</span> <span class="w"> </span><span class="nx">use</span><span class="o">:</span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="p">...</span><span class="nx">cssLoaders</span><span class="p">,</span> <span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">loader</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;sass-loader&quot;</span><span class="p">,</span> <span class="w"> </span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">sassOptions</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">includePaths</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">cwd</span><span class="p">,</span><span class="w"> </span><span class="s2">&quot;node_modules&quot;</span><span class="p">))],</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">],</span> <span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;javascript/auto&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">swcWithPreactRule</span><span class="p">,</span> <span class="w"> </span><span class="nx">swcWithReactRule</span><span class="p">,</span> <span class="w"> </span><span class="nx">resolvePreactAsReact</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">resolve</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">alias</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">react</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;preact/compat&quot;</span><span class="p">,</span> <span class="w"> </span><span class="s2">&quot;react-dom/test-utils&quot;</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;preact/test-utils&quot;</span><span class="p">,</span> <span class="w"> </span><span class="s2">&quot;react-dom&quot;</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;preact/compat&quot;</span><span class="p">,</span><span class="w"> </span><span class="c1">// Must be below test-utils</span> <span class="w"> </span><span class="s2">&quot;react/jsx-runtime&quot;</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;preact/jsx-runtime&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">htmlPlugin</span><span class="p">,</span> <span class="w"> </span><span class="nx">htmlSingleChunkPlugin</span><span class="p">,</span> <span class="w"> </span><span class="nx">postcssLoaders</span><span class="p">,</span> <span class="w"> </span><span class="nx">cssExtractPlugin</span><span class="p">,</span> <span class="w"> </span><span class="p">}</span> <span class="p">}</span> </code></pre></div> <h2 id="closing-thoughts"><a class="toclink" href="#closing-thoughts">Closing thoughts</a></h2> <p>Several utilities from this library aren&rsquo;t used in the example above, for example the <code>sassRule</code> or the HTML plugin utilities which are useful when you require several entry points on your website, e.g. an entry point for the public facing website and an entry point for a dashboard used by members of the staff.</p> <p>Most of the code in here is freely available in our <a href="https://github.com/feinheit/fh-fablib">fh-fablib</a> repo under an open source license. Anything in this blog post can also be used under the <a href="https://creativecommons.org/public-domain/cc0/">CC0</a> license, so feel free to steal everything. If you do, I&rsquo;d be happy to hear your thoughts about this post, and please share your experiences and suggestions for improvement &ndash; if you have any!</p>Django, JavaScript modules and importmapshttps://406.ch/writing/django-javascript-modules-and-importmaps/2025-05-22T12:00:00Z2025-05-22T12:00:00Z<h1 id="how-im-using-django-javascript-modules-and-importmaps-together"><a class="toclink" href="#how-im-using-django-javascript-modules-and-importmaps-together">How I&rsquo;m using Django, JavaScript modules and importmaps together</a></h1> <p>I have been spending a lot of time in the last few months working on <a href="https://github.com/matthiask/django-prose-editor/">django-prose-editor</a>. First I&rsquo;ve rebuilt the editor on top of <a href="https://406.ch/writing/rebuilding-django-prose-editor-from-the-ground-up/">Tiptap</a> because I wanted a framework for extending the underlying <a href="https://prosemirror.net/">ProseMirror</a> and didn&rsquo;t want to reinvent this particular wheel. While doing that work I noticed that using JavaScript modules in the browser would be really nice, but Django&rsquo;s <code>ManifestStaticFilesStorage</code> doesn&rsquo;t yet support rewriting <code>import</code> statement in modules out-of-the-box without opting into the experimental support accessible through subclassing the storage. A better way to use JavaScript modules with the cache busting offered by <code>ManifestStaticFilesStorage</code> would be <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap">importmaps</a>.</p> <h2 id="motivation"><a class="toclink" href="#motivation">Motivation</a></h2> <p>Developing Django applications that include JavaScript has always been challenging when it comes to properly distributing, loading, and versioning those assets. The traditional approach using Django&rsquo;s <code>forms.Media</code> works well for simple use cases, but falls short when dealing with modern JavaScript modules.</p> <p>The ability to ship reusable JavaScript utilities in third-party Django apps has been a pain point for years. Often developers resort to workarounds like bundling all JS into a single file, using jQuery-style global variables, or requiring complex build processes for consumers of their apps.</p> <p>Importmaps offer a cleaner solution that works with native browser modules, supports cache busting, and doesn&rsquo;t require complex bundling for simple use cases.</p> <h2 id="the-history"><a class="toclink" href="#the-history">The history</a></h2> <p>The conversation around better JavaScript handling in Django has been ongoing for years. Thibaud Colas&rsquo; <a href="https://github.com/django/deps/pull/84">DEP draft</a> come to mind as does the <a href="https://forum.djangoproject.com/t/rejuvenating-vs-deprecating-form-media/21285">discussion about whether to improve or deprecate <code>forms.Media</code></a>.</p> <p>A few packages exist which are offering solutions in this space:</p> <ul> <li><a href="https://github.com/codingjoe/django-esm">django-esm</a> provides a solution for using ES modules with Django without bundling.</li> <li><a href="https://github.com/matthiask/django-js-asset/">django-js-asset</a> provides helpers for delivering JavaScript modules, importmaps, JSON blobs etc. to the browser through Django&rsquo;s <code>forms.Media</code>. The blog post <a href="https://406.ch/writing/object-based-assets-for-django-s-forms-media/">Object-based assets for Django&rsquo;s forms.Media</a> explores this in more detail.</li> <li>The article on <a href="https://406.ch/writing/django-admin-apps-and-content-security-policy-compliance/">Content Security Policy compliance</a> explores better approaches to use JavaScript in the Django admin while avoiding inline JavaScript.</li> </ul> <p>django-js-asset came before Django <a href="https://github.com/django/django/commit/4c76ffc2d6c77">added official support for object-based media CSS and JS paths</a> but has since been changed to take advantage of that official support. It has enabled the removal of ugly hacks. In the meantime, Django has even added <a href="https://github.com/django/django/pull/18782">official support for object-based <code>Script</code> tags</a>.</p> <h2 id="my-dep-draft"><a class="toclink" href="#my-dep-draft">My DEP draft</a></h2> <p>Building on these efforts, I&rsquo;ve been thinking about <a href="https://github.com/django/deps/pull/101">submitting my own DEP draft for importmap support</a>. It hasn&rsquo;t yet come far though, and I&rsquo;m still more occupied with verifying and using my existing solution, especially learning if it has limitations which would make the implemented approach unworkable for official inclusion.</p> <h2 id="the-current-effort"><a class="toclink" href="#the-current-effort">The current effort</a></h2> <p>As alluded to above, I already have a working solution for using importmaps (in django-js-asset) and I&rsquo;m actively using it in django-prose-editor. Here&rsquo;s how it works:</p> <div class="chl"><pre><span></span><code><span class="n">importmap</span><span class="o">.</span><span class="n">update</span><span class="p">({</span> <span class="s2">&quot;imports&quot;</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&quot;django-prose-editor/editor&quot;</span><span class="p">:</span> <span class="n">static_lazy</span><span class="p">(</span><span class="s2">&quot;django_prose_editor/editor.js&quot;</span><span class="p">),</span> <span class="p">}</span> <span class="p">})</span> </code></pre></div> <p>A minimal editor implementation using this:</p> <div class="chl"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="c1">// Tiptap extensions</span> <span class="w"> </span><span class="nx">Document</span><span class="p">,</span><span class="w"> </span><span class="nx">Paragraph</span><span class="p">,</span><span class="w"> </span><span class="nx">HardBreak</span><span class="p">,</span><span class="w"> </span><span class="nx">Text</span><span class="p">,</span><span class="w"> </span><span class="nx">Bold</span><span class="p">,</span><span class="w"> </span><span class="nx">Italic</span><span class="p">,</span> <span class="w"> </span><span class="c1">// Prose editor utilities</span> <span class="w"> </span><span class="nx">Menu</span><span class="p">,</span><span class="w"> </span><span class="nx">createTextareaEditor</span><span class="p">,</span><span class="w"> </span><span class="nx">initializeEditors</span><span class="p">,</span> <span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">&quot;django-prose-editor/editor&quot;</span> <span class="kd">const</span><span class="w"> </span><span class="nx">extensions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="nx">Document</span><span class="p">,</span><span class="w"> </span><span class="nx">Paragraph</span><span class="p">,</span><span class="w"> </span><span class="nx">HardBreak</span><span class="p">,</span><span class="w"> </span><span class="nx">Text</span><span class="p">,</span><span class="w"> </span><span class="nx">Bold</span><span class="p">,</span><span class="w"> </span><span class="nx">Italic</span><span class="p">,</span><span class="w"> </span><span class="nx">Menu</span><span class="p">,</span> <span class="p">]</span> <span class="nx">initializeEditors</span><span class="p">((</span><span class="nx">textarea</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">createTextareaEditor</span><span class="p">(</span><span class="nx">textarea</span><span class="p">,</span><span class="w"> </span><span class="nx">extensions</span><span class="p">)</span> <span class="p">})</span> </code></pre></div> <p>The importmap looks as follows when using Django&rsquo;s <code>ManifestStaticFilesStorage</code> which produces filenames containing the hash of the file&rsquo;s contents for cache busting (edited for readability):</p> <div class="chl"><pre><span></span><code><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;importmap&quot;</span><span class="p">&gt;</span> <span class="p">{</span><span class="s2">&quot;imports&quot;</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="s2">&quot;django-prose-editor/editor&quot;</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;/static/django_prose_editor/editor.6e8dd4c12e2e.js&quot;</span> <span class="p">}}</span> <span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span> </code></pre></div> <p>This means that when your code has <code>import { ... } from "django-prose-editor/editor"</code>, the browser automatically loads the file from <code>/static/django_prose_editor/editor.6e8dd4c12e2e.js</code>. The hashed filename provides cache busting while the import statement remains clean and consistent.</p> <h2 id="problems-with-the-current-implementation"><a class="toclink" href="#problems-with-the-current-implementation">Problems with the current implementation</a></h2> <p>While this approach works, there are several issues to address:</p> <ul> <li> <p>I don&rsquo;t really like global variables but there doesn&rsquo;t seem to be a way around it. Browsers want to use a single importmap only (even though the algorithm for merging importmaps exists in the spec!) and the importmap has to be included above all ES modules.</p> </li> <li> <p>The fact that browsers only want a single importmap also means that when you use django-js-asset&rsquo;s importmap support you <strong>cannot</strong> use a different package offering its own solution for importmaps.</p> </li> <li> <p>The importmap may be added twice to the HTML when using a widget that works in both the admin and frontend contexts. Currently, if you want to avoid this problem or ugliness you have to determine in your Django form field if the code is requesting an admin widget or another widget, either by inspecting the callstack (very ugly) or by checking if the <code>widget</code> argument to the form field constructor is set to an admin-specific widget (also somewhat ugly, since widgets can be classes, instances, or not provided at all).</p> </li> <li> <p>It would be nice if we the installation of django-prose-editor didn&rsquo;t have more steps than what we have when installing any other Django widget integration. I&rsquo;d like a more elegant solution, but haven&rsquo;t found one yet that doesn&rsquo;t introduce too much magic.</p> </li> </ul> <h2 id="comparison-to-django-esm"><a class="toclink" href="#comparison-to-django-esm">Comparison to django-esm</a></h2> <p><a href="https://github.com/codingjoe/django-esm">django-esm</a> takes a different approach. It assumes you&rsquo;re using JavaScript modules everywhere and solves the problem of exposing the correct paths to those modules to the browser. It supports both private modules from your repository and modules installed in <code>node_modules</code>.</p> <p>However, it doesn&rsquo;t fully address the scenario where a third-party <strong>Django</strong> app (a Python package) ships JavaScript modules that need to be integrated into your application.</p> <p>I still use a bundler for most of my JavaScript from <code>node_modules</code>, so I don&rsquo;t need this specific functionality yet. That will probably change in the future.</p> <h2 id="using-bundlers"><a class="toclink" href="#using-bundlers">Using bundlers</a></h2> <p>If you&rsquo;re still using a bundler, <a href="https://rspack.dev/">as I do</a>, you want to ensure that the <code>import</code> isn&rsquo;t actually evaluated by the bundler but left as-is. The <a href="https://rspack.dev/">rspack</a> configuration I&rsquo;m using at the moment is also documented in the django-prose-editor README but I&rsquo;m duplicating it here for convenience:</p> <div class="chl"><pre><span></span><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="c1">// ...</span> <span class="w"> </span><span class="nx">experiments</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">outputModule</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">externals</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="s2">&quot;django-prose-editor/editor&quot;</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;module django-prose-editor/editor&quot;</span><span class="p">,</span> <span class="w"> </span><span class="c1">// Or the following, I&#39;m never sure.</span> <span class="w"> </span><span class="s2">&quot;django-prose-editor/editor&quot;</span><span class="o">:</span><span class="w"> </span><span class="s2">&quot;import django-prose-editor/editor&quot;</span><span class="p">,</span> <span class="w"> </span><span class="p">},</span> <span class="p">}</span> </code></pre></div> <p>This configuration marks the dependency as &ldquo;external&rdquo; (so it won&rsquo;t be bundled) and specifies that it should be loaded as a module using a static <code>import</code> statement.</p> <p>For browser compatibility, you can also include <a href="https://github.com/guybedford/es-module-shims">es-module-shims</a> to support browsers that don&rsquo;t yet handle importmaps natively (around 5% at the time of writing according to <a href="https://caniuse.com/import-maps">caniuse.com</a>).</p> <h2 id="using-django-compressor-or-similar-packages"><a class="toclink" href="#using-django-compressor-or-similar-packages">Using django-compressor or similar packages</a></h2> <p>Tools like django-compressor aren&rsquo;t well-suited for modern JavaScript modules as they typically produce old-style JavaScript files rather than ES modules. They&rsquo;re designed for a different era of web development and don&rsquo;t integrate well with the importmap approach.</p> <div class="admonition note"> <p class="admonition-title">Note</p> <p>The problem is that django-compressor at this time emits non-module script files. Using import statements in these files isn&rsquo;t possible, instead you have to use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import">dynamic imports</a>.</p> <div class="chl"><pre><span></span><code><span class="c1">// Instead of</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">Document</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">&quot;django-prose-editor/editor&quot;</span> <span class="c1">// you need</span> <span class="k">import</span><span class="p">(</span><span class="s2">&quot;django-prose-editor/editor&quot;</span><span class="p">).</span><span class="nx">then</span><span class="p">(({</span><span class="w"> </span><span class="nx">Document</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="p">})</span> </code></pre></div> <p>Both work fine. The bundle emitted by django-compressor will not contain the prose editor module itself though; including this module inside the bundle is not possible.</p> </div> <h2 id="conclusion"><a class="toclink" href="#conclusion">Conclusion</a></h2> <p>Using importmaps with Django provides a clean solution for managing JavaScript modules in Django applications, especially for third-party apps that need to ship their own JavaScript. While there are still some rough edges to smooth out, this approach works well and offers a path forward that aligns with modern web standards.</p> <p>Have you tried using importmaps with Django? I&rsquo;d be interested to hear about your experiences and approaches.</p>Weeknotes (2025 week 21)https://406.ch/writing/weeknotes-2025-week-21/2025-05-21T12:00:00Z2025-05-21T12:00:00Z<h1 id="weeknotes-2025-week-21"><a class="toclink" href="#weeknotes-2025-week-21">Weeknotes (2025 week 21)</a></h1> <p>I have missed two co-writing sessions and didn&rsquo;t manage to post much outside of that, but let&rsquo;s get things back on track.</p> <h2 id="django-prose-editor-012"><a class="toclink" href="#django-prose-editor-012">django-prose-editor 0.12</a></h2> <p>The <a href="https://406.ch/writing/weeknotes-2025-week-15/#progress-on-the-prose-editor">last weeknotes entry</a> contains more details about the work of really connecting Tiptap extensions with server-side sanitization. 0.12 includes many improvements and bugfixes which have been made during real-world use of the prose editor in customer-facing products.</p> <p>I&rsquo;m not completely happy about the way we&rsquo;re specifying the editor configuration and haven&rsquo;t been able to settle on either <code>extensions</code> or <code>config</code> as a keyword argument. The field supports both ways, at least for now. It&rsquo;s probably fine.</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/django-auto-admin-fieldsets/">django-auto-admin-fieldsets 0.2</a>: I wrote a blog post here: <a href="https://406.ch/writing/customizing-django-admin-fieldsets-without-fearing-forgotten-fields/">Customizing Django admin fieldsets without fearing forgotten fields</a></li> <li><a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar 5.2</a>: This release contains the second half of improvements from <a href="https://djangonaut.space/">Djangonaut Space</a> session four where I helped out as a Navigator. The toolbar properly supports code highlighting in dark mode, sanitizes request variables better, allows customizing redirects, supports projects using <a href="https://github.com/carltongibson/django-template-partials/">django-template-partials</a> and more!</li> <li><a href="https://pypi.org/project/FeinCMS/">FeinCMS 25.5.1</a>: The first FeinCMS release of 2025. We&rsquo;re still maintaining the project and fixing bugs!</li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.12</a>: See above.</li> <li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.4.1</a>: Fixes much too small checkboxes when used inside tables.</li> </ul>Customizing Django admin fieldsets without fearing forgotten fieldshttps://406.ch/writing/customizing-django-admin-fieldsets-without-fearing-forgotten-fields/2025-04-14T12:00:00Z2025-04-14T12:00:00Z<h1 id="customizing-django-admin-fieldsets-without-fearing-forgotten-fields"><a class="toclink" href="#customizing-django-admin-fieldsets-without-fearing-forgotten-fields">Customizing Django admin fieldsets without fearing forgotten fields</a></h1> <p>When defining <a href="https://docs.djangoproject.com/en/5.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.fieldsets">fieldsets on Django modeladmin classes</a> I always worry that I forget updating the fieldsets later when adding or removing new model fields, and not without reason: It has already happened to me several times. Forgetting to remove fields is mostly fine because system checks will complain about it, forgetting to add fields may be real bad. A recent example was a crashing website because a required field was missing from the admin and therefore was left empty when creating new instances!</p> <p>I have now published another Django package which solves this by adding support for specifying the special <code>"__remaining__"</code> field in a fieldsets definition. The <code>"__remaining__"</code> placeholder is automatically replaced by all model fields which haven&rsquo;t been explicitly added already or added to <code>exclude</code><sup id="fnref:1"><a class="footnote-ref" href="#fn:1">1</a></sup>.</p> <p>Here&rsquo;s a short example for a modeladmin definition using django-auto-admin-fieldsets:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django.contrib</span><span class="w"> </span><span class="kn">import</span> <span class="n">admin</span> <span class="kn">from</span><span class="w"> </span><span class="nn">django_auto_admin_fieldsets.admin</span><span class="w"> </span><span class="kn">import</span> <span class="n">AutoFieldsetsModelAdmin</span> <span class="kn">from</span><span class="w"> </span><span class="nn">app</span><span class="w"> </span><span class="kn">import</span> <span class="n">models</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">MyModel</span><span class="p">)</span> <span class="k">class</span><span class="w"> </span><span class="nc">MyModelAdmin</span><span class="p">(</span><span class="n">AutoFieldsetsModelAdmin</span><span class="p">):</span> <span class="c1"># Define fieldsets as usual with a placeholder</span> <span class="n">fieldsets</span> <span class="o">=</span> <span class="p">[</span> <span class="p">(</span><span class="s2">&quot;Basic Information&quot;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;fields&quot;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">,</span> <span class="s2">&quot;slug&quot;</span><span class="p">]}),</span> <span class="p">(</span><span class="s2">&quot;Content&quot;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;fields&quot;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&quot;__remaining__&quot;</span><span class="p">]}),</span> <span class="p">]</span> </code></pre></div> <p>I have used Claude Code a lot for the code and the package, and as always, I had to fix bugs and oversights. I hope it didn&rsquo;t regurgitate the code of an existing package &ndash; I searched for an existing solution first but didn&rsquo;t find any.</p> <p>The package is available on <a href="https://pypi.org/project/django-auto-admin-fieldsets/">PyPI</a> and is developed on <a href="https://github.com/matthiask/django-auto-admin-fieldsets">GitHub</a>, at least for the time being.</p> <div class="footnote"> <hr /> <ol> <li id="fn:1"> <p>Autocreated fields such as surrogate primary keys or fields which aren&rsquo;t editable are also excluded automatically of course.&#160;<a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text">&#8617;</a></p> </li> </ol> </div>Weeknotes (2025 week 15)https://406.ch/writing/weeknotes-2025-week-15/2025-04-09T12:00:00Z2025-04-09T12:00:00Z<h1 id="weeknotes-2025-week-15"><a class="toclink" href="#weeknotes-2025-week-15">Weeknotes (2025 week 15)</a></h1> <h2 id="djangonaut-space"><a class="toclink" href="#djangonaut-space">Djangonaut Space</a></h2> <p>We have already reached the final week of the <a href="https://djangonaut.space/">Djangonaut Space</a> session 4. I had a great time as a navigator and am looking forward to participate more, but for now I&rsquo;m also glad that I do not have the additional responsibility at least for the close future.</p> <p>We have done great work on the <a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar</a> in our group, more is to come.</p> <h2 id="progress-on-the-prose-editor"><a class="toclink" href="#progress-on-the-prose-editor">Progress on the prose editor</a></h2> <p>I have done much work on <a href="https://pypi.org/project/django-prose-editor/">django-prose-editor</a> in the last few weeks and after a large list of alphas and betas I&rsquo;m nearing a state which I want to release into the wild.</p> <p>The integration has been completely rethought (again) and now uses JavaScript modules and importmaps. The ground work to support all of that in Django has been laid in <a href="https://pypi.org/project/django-js-asset/">django-js-asset</a>.</p> <p>The nice thing about using JavaScript modules and importmaps is that we now have an easy way to combine the power of modern JavaScript customization with easy cache busting using Django&rsquo;s <code>ManifestStaticFilesStorage</code>. A longer post on this is brewing and I hope to have it ready soon-ish.</p> <p>As a sneak peek, here&rsquo;s the way it works:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django_prose_editor.fields</span><span class="w"> </span><span class="kn">import</span> <span class="n">ProseEditorField</span> <span class="n">content</span> <span class="o">=</span> <span class="n">ProseEditorField</span><span class="p">(</span> <span class="n">extensions</span><span class="o">=</span><span class="p">{</span> <span class="s2">&quot;Bold&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&quot;Italic&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&quot;BulletList&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&quot;Link&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="p">},</span> <span class="c1"># sanitize=True is the default when using extensions</span> <span class="p">)</span> </code></pre></div> <p>The nice thing about it is that the sanitization allowlist for <a href="https://github.com/messense/nh3">nh3</a> only includes tags and attributes which are enabled via the <code>extensions</code> dict. So, you don&rsquo;t have to do anything else to be safe from XSS etc.</p> <p>Check out the pre-releases on <a href="https://pypi.org/project/django-prose-editor/#history">PyPI</a> or have a look at the <a href="https://django-prose-editor.readthedocs.io/">documentation</a> to learn more about this project!</p> <h2 id="using-claude-code"><a class="toclink" href="#using-claude-code">Using Claude Code</a></h2> <p>I have been using Claude Code (without editor integrations, thank you very much) more and more. It&rsquo;s a good coding companion when it comes to throwing around ideas, drafting docs and writing unit tests including integration tests.</p> <p>Sometimes I&rsquo;m really surprised at how good it is. Other times&hellip; less so. The tool often finds a way to get tests passing, but when the editor integration tests directly manipulate <code>innerHTML</code> and then Claude proclaims that interacting with the editor is now shown to work I have to chuckle a bit. And when I insist on doing what I mean and not just finding broken workarounds it doesn&rsquo;t really change anything. After spinning more we&rsquo;re always back where we started.</p> <p>I am somewhat glad that this is where we&rsquo;re at now. I&rsquo;m not 100% sure if it&rsquo;s progress. At least it&rsquo;s surprisingly funny at times.</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <p>I haven&rsquo;t written a regular weeknotes entry since the end of January, so naturally the list here is longer than usual.</p> <ul> <li><a href="https://pypi.org/project/feincms3-forms/">feincms3-forms 0.5.1</a>: I inadvertently bumped the Django dependency without actually wanting that; this patch release reverts that (while adding official support for new Django and Python versions).</li> <li><a href="https://pypi.org/project/django-mptt/">django-mptt 0.17</a>: Mariusz has done all the hard work for supporting newer versions of Django. I just had to press the release button. That being said, four years after marking the package as unmaintained I&rsquo;m still maintaining it. At least I don&rsquo;t get complaints anymore&hellip;</li> <li><a href="https://pypi.org/project/django-json-schema-editor/">django-json-schema-editor 0.4</a>: Added a dependency on the pre-release of django-prose-editor and added a test suite including integration tests so that we actually now when stuff breaks the next time!</li> <li><a href="https://pypi.org/project/django-debug-toolbar/">django-debug-toolbar 5.1</a>: See above.</li> <li><a href="https://pypi.org/project/feincms3-data/">feincms3-data</a>: Added fixes to dump distinct objects. Spent more time than useful on the Django change which added a final newline to JSON-serialized data.</li> <li><a href="https://pypi.org/project/django-js-asset/">django-js-asset 3.1.2</a>: Importmaps support, added a <code>static_lazy</code> helper which is useful to define module-scoped static URLs. The later wouldn&rsquo;t work with the <code>ManifestStaticFilesStorage</code> because the manifest doesn&rsquo;t yet exist when <code>collectstatic</code> runs, so the actual evaluation of static URLs has to be postponed. The lazy version solves this nicely.</li> <li><a href="https://pypi.org/project/feincms3-sites/">feincms3-sites 0.21.1</a> and <a href="https://pypi.org/project/feincms3-language-sites/">feincms3-language-sites 0.4.1</a>: See the relevant <a href="https://406.ch/writing/til-tools-exist-which-do-not-lowercase-domain-names-when-requesting-websites-over-http-s/">TIL</a> blogpost.</li> </ul>TIL: Tools exist which do not lowercase domain names when requesting websites over HTTP(S)https://406.ch/writing/til-tools-exist-which-do-not-lowercase-domain-names-when-requesting-websites-over-http-s/2025-02-13T12:00:00Z2025-02-13T12:00:00Z<h1 id="til-tools-exist-which-do-not-lowercase-domain-names-when-requesting-websites-over-https"><a class="toclink" href="#til-tools-exist-which-do-not-lowercase-domain-names-when-requesting-websites-over-https">TIL: Tools exist which do not lowercase domain names when requesting websites over HTTP(S)</a></h1> <p>About a week ago I received error mails for a surprising behavior (to me!) where some tool requested an URL from one of our websites using <a href="https://github.com/feincms/feincms3-language-sites/">feincms3-language-sites</a> (a Django library for multilingual websites) with a domain name containing uppercase characters.</p> <p>I knew that the domain part of all sorts of URLs is case sensitive, but what surprised me was that our server actually got a request with such a domain name, I hadn&rsquo;t really seen that before.</p> <p>After researching a bit I learned that for example <a href="https://curl.se/">curl</a> intentionally preserves the casing of domain names, but browsers generally do lowercase domains because it&rsquo;s more consistent. It&rsquo;s interesting that the initial error was caused by a client with a proper Safari/macOS user agent, but further research showed that the request was probably sent by something called <code>go-social-activity-parser</code>, whatever that is.</p> <p>I fixed the bug in <a href="https://github.com/feincms/feincms3-language-sites/">feincms3-language-sites</a> and also in <a href="https://github.com/feincms/feincms3-sites/">feincms3-sites</a> by switching to case-insensitive matching of domain names. I have not yet added punycode or IDNA equivalence to the code because I haven&rsquo;t needed it yet and because I&rsquo;m not 100% sure how to do it without breaking anything. Even though I often work on websites in languages with lots of accents and umlauts we don&rsquo;t use such domain names too often, so it hasn&rsquo;t been a problem yet. I&rsquo;ll cross that bridge when I get there.</p>Weeknotes (2025 week 05)https://406.ch/writing/weeknotes-2025-week-05/2025-01-29T12:00:00Z2025-01-29T12:00:00Z<h1 id="weeknotes-2025-week-05"><a class="toclink" href="#weeknotes-2025-week-05">Weeknotes (2025 week 05)</a></h1> <h2 id="djangonaut-space"><a class="toclink" href="#djangonaut-space">Djangonaut Space</a></h2> <p>In December I wrote a few paragraphs about <a href="https://406.ch/writing/weeknotes-2024-week-49/">my decision to not run for the Django Steering Council</a>, mentioning that I want to contribute in different ways.</p> <p>I have offered to contribute to Djangonaut Space to do some mentoring. I&rsquo;m already a bit stressed, but that&rsquo;s normal and to be expected. I&rsquo;ll probably have more to share about that in the close future!</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://github.com/feincms/feincms3-cookiecontrol/commits/main/">feincms-cookiecontrol 1.6</a>: Removed the hardcoded dependency upon <a href="https://feincms3.readthedocs.io/">feincms3</a> and some additional code golfing. The cookie banner JavaScript is now back to &lt;4KiB.</li> <li><a href="https://pypi.org/project/django-curtains/">django-curtains 0.7</a>: Updated the CI job list and modernized the package somewhat, no code changes necessary. It&rsquo;s good to release updated versions though just to show that it&rsquo;s still actively maintained.</li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.10.3</a>: Small CSS fixes and mainly updated TipTap/ProseMirror.</li> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.22</a>: The updated version no longer autodeletes processed images; this wasn&rsquo;t really a problem before but I was a little bit fearful that images are still referenced elsewhere and this change let&rsquo;s me sleep better.</li> <li><a href="https://pypi.org/project/feincms-oembed/">feincms-oembed 2.0</a>: Oembed support for FeinCMS 1 without actually depending upon the FeinCMS package itself. Still works.</li> <li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.2</a>: The <code>Region</code> type is now hashable; this may be useful, or not.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 5.3.1</a>: I undeprecated the <code>TemplateMixin</code> because even though <code>PageTypeMixin</code> is nicer, sometimes all you need is a template selector.</li> </ul>Django admin tip: Adding links to related objects in change formshttps://406.ch/writing/django-admin-tip-adding-links-to-related-objects-in-change-forms/2025-01-17T12:00:00Z2025-01-17T12:00:00Z<h1 id="django-admin-tip-adding-links-to-related-objects-in-change-forms"><a class="toclink" href="#django-admin-tip-adding-links-to-related-objects-in-change-forms">Django admin tip: Adding links to related objects in change forms</a></h1> <p>Any issue which came up on the Django Forum and Discord is how to add links to other objects to the Django administration interface. It&rsquo;s something I&rsquo;m doing often and I want to share the pattern I&rsquo;m using.</p> <p>It&rsquo;s definitely not rocket science and there are probably better ways to do it, but this one works well for me.</p> <h2 id="method-1-override-the-change-form-template"><a class="toclink" href="#method-1-override-the-change-form-template">Method 1: Override the change form template</a></h2> <p>In one project users can be the editor of exactly one organization. The link between organizations and users is achieved using a <code>Editor</code> model with a <code>ForeignKey(Organization)</code> and a <code>OneToOneField(User)</code>.</p> <p>I wanted to add a link to the organization page at the bottom of the user form. An easy way to achieve this is to add a template at <code>templates/admin/auth/user/change_form.html</code> (or something similar if you&rsquo;re using a custom user model):</p> <div class="chl"><pre><span></span><code><span class="cp">{%</span> <span class="k">extends</span> <span class="s2">&quot;admin/change_form.html&quot;</span> <span class="cp">%}</span> <span class="cp">{%</span> <span class="k">block</span> <span class="nv">after_related_objects</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="cp">{%</span> <span class="k">if</span> <span class="nv">original.editor</span> <span class="cp">%}</span> <span class="p">&lt;</span><span class="nt">fieldset</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;module aligned&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span>Organization<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;form-row&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&quot;</span><span class="cp">{%</span> <span class="k">url</span> <span class="s1">&#39;admin:organizations_organization_change&#39;</span> <span class="nv">original.editor.organization.pk</span> <span class="cp">%}</span><span class="s">&quot;</span><span class="p">&gt;</span><span class="cp">{{</span> <span class="nv">original.editor.organization</span> <span class="cp">}}</span><span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">fieldset</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="nv">after_related_objects</span> <span class="cp">%}</span> </code></pre></div> <p>The <code>original</code> context variable contains the object being edited. The <code>editor</code> attribute is the reverse accessor for the <code>OneToOneField</code> mentioned above.</p> <h2 id="method-2-add-a-method-to-the-model-admin-class-returning-a-html-blob"><a class="toclink" href="#method-2-add-a-method-to-the-model-admin-class-returning-a-html-blob">Method 2: Add a method to the model admin class returning a HTML blob</a></h2> <p>A terrible but also nice way is to add a method to the <code>ModelAdmin</code> class which returns the HTML containing the links you want, and adding the name of the method to <code>readonly_fields</code>. This is even mentioned in <a href="https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.readonly_fields">the official <code>readonly_fields</code> documentation</a> but I discovered this by accident a few years back.</p> <p>The method name doesn&rsquo;t have to be added anywhere else, not to <code>fields</code> nor do you have to define <code>fieldsets</code> for this to work. Just adding it to <code>readonly_fields</code> appends it to the end of the form, before any eventual inlines you&rsquo;re using.</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django.template.loader</span><span class="w"> </span><span class="kn">import</span> <span class="n">render_to_string</span> <span class="kn">from</span><span class="w"> </span><span class="nn">app</span><span class="w"> </span><span class="kn">import</span> <span class="n">models</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">Class</span><span class="p">)</span> <span class="k">class</span><span class="w"> </span><span class="nc">ClassAdmin</span><span class="p">(</span><span class="n">admin</span><span class="o">.</span><span class="n">ModelAdmin</span><span class="p">):</span> <span class="n">list_display</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&quot;name&quot;</span><span class="p">,</span> <span class="s2">&quot;language_code&quot;</span><span class="p">]</span> <span class="n">readonly_fields</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&quot;admin_show_custom_districts&quot;</span><span class="p">]</span> <span class="nd">@admin</span><span class="o">.</span><span class="n">display</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="n">_</span><span class="p">(</span><span class="s2">&quot;districts&quot;</span><span class="p">)</span> <span class="k">def</span><span class="w"> </span><span class="nf">admin_show_custom_districts</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span> <span class="k">return</span> <span class="n">render_to_string</span><span class="p">(</span> <span class="s2">&quot;admin/admin_show_custom_districts.html&quot;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;custom_districts&quot;</span><span class="p">:</span> <span class="n">obj</span><span class="o">.</span><span class="n">customdistrict_set</span><span class="o">.</span><span class="n">all</span><span class="p">()},</span> <span class="p">)</span> </code></pre></div>Weeknotes (2025 week 03)https://406.ch/writing/weeknotes-2025-week-03/2025-01-15T12:00:00Z2025-01-15T12:00:00Z<h1 id="weeknotes-2025-week-03"><a class="toclink" href="#weeknotes-2025-week-03">Weeknotes (2025 week 03)</a></h1> <h2 id="claude-ai-helped-me-for-the-first-time"><a class="toclink" href="#claude-ai-helped-me-for-the-first-time">Claude AI helped me for the first time</a></h2> <p><a href="https://github.com/matthiask/django-imagefield">django-imagefield</a> prefers processing thumbnails, cropped images etc. directly when saving the model and not later on demand; it&rsquo;s faster and also you&rsquo;ll know it immediately when an image couldn&rsquo;t be processed for some reason instead of only later when people actually try browsing your site.</p> <p>A consequence is that if you change formats you have to remember that you have to reprocess the images. The Django app comes with a management command <code>./manage.py process_imagefields</code> to help with this. I have added parallel processing based on <code>concurrent.futures</code> to it some time ago so that the command completes faster when it is being run on a system with several cores.</p> <p>A work colleague is using macOS (many are, in fact), and he always got multiprocessing Python crashes. This is a well known issue and I remember reading about it a few years ago. I checked the docs and saw that the <a href="https://docs.python.org/3/library/concurrent.futures.html"><code>concurrent.futures</code></a> page doesn&rsquo;t mention macOS, but <a href="https://docs.python.org/3/library/multiprocessing.html"><code>multiprocessing</code></a> does. So, I hoped that a simple rewrite of the management command using <code>multiprocessing</code> might fix it.</p> <p>Because I was in a rush and really didn&rsquo;t want to do it I turned to an AI assistant for doing this boring work. To my surprise it immediately produced a version which I could easily fix by hand to produce a working version. Of course, the initial response was totally broken, removed code it wasn&rsquo;t supposed to, and even the syntax was invalid. I didn&rsquo;t expect more though, but what was surprising was that it actually felt like I had to do less work at this time.</p> <p>The assistant also helped adding a <code>--no-parallel</code> flag to the management command. The output was even more broken than the output of the change mentioned above, but again, I could easily fix it to achieve what I wanted.</p> <p>The fact that I know the code and <a href="https://git-scm.com/">git</a> well certainly helped, the assistant would really have helped without that knowledge.</p> <p>In the end, switching to <code>multiprocessing</code> didn&rsquo;t help, but adding the <code>--no-parallel</code> flag allowed them to run the processing themselves by not spawning any additional threads or processes.</p> <p>The energy use and the stealing of copyrighted material done by the AI companies is still really bad. It does feel somewhat OK to use an AI assistant in an area where I&rsquo;m proficient as well and where I probably also supplied training material (without being asked if I wanted this) though. It&rsquo;s making me slightly faster, and doesn&rsquo;t allow me to do things I really couldn&rsquo;t otherwise.</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/FeinCMS/">FeinCMS 24.12.3</a>: I have added a TinyMCE 7 integration to FeinCMS.</li> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.21.1</a>: See above.</li> </ul>Weeknotes (2024 week 51)https://406.ch/writing/weeknotes-2024-week-51/2024-12-20T12:00:00Z2024-12-20T12:00:00Z<h1 id="weeknotes-2024-week-51"><a class="toclink" href="#weeknotes-2024-week-51">Weeknotes (2024 week 51)</a></h1> <h2 id="building-forms-using-django"><a class="toclink" href="#building-forms-using-django">Building forms using Django</a></h2> <p>I last wrote about this topic <a href="https://406.ch/writing/building-forms-with-the-django-admin/">in April</a>. It has <a href="https://mastodon.social/@webology/113669270531953652">resurfaced on Mastodon this week</a>. I&rsquo;m thinking about writing a <a href="https://github.com/feincms/feincms3-forms">feincms3-forms</a> demo app, but I already have too much on my plate. I think composing a forms builder on top of <a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> is the way to go, instead of replacing the admin interface altogether &ndash; sure, you can always do that, but it&rsquo;s so much less composable&hellip;</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/blacknoise/">blacknoise 1.2</a>: No real changes, added support for Python 3.13 basically without changing anything. It&rsquo;s always nice when this happens.</li> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.21</a></li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.10</a>: I rebuilt django-prose-editor from the ground up <a href="https://406.ch/writing/rebuilding-django-prose-editor-from-the-ground-up/">and wrote about that two weeks ago</a>. The 0.10 release marks the final point of this particular rewrite.</li> <li><a href="https://pypi.org/project/django-js-asset/">django-js-asset 3.0</a>: See the blog post from <a href="https://406.ch/writing/object-based-assets-for-django-s-forms-media/">this week</a></li> </ul>Object-based assets for Django's forms.Mediahttps://406.ch/writing/object-based-assets-for-django-s-forms-media/2024-12-18T12:00:00Z2024-12-18T12:00:00Z<h1 id="object-based-assets-for-djangos-formsmedia"><a class="toclink" href="#object-based-assets-for-djangos-formsmedia">Object-based assets for Django&rsquo;s forms.Media</a></h1> <p>The pull request for adding <a href="https://github.com/django/django/pull/18782">object-based script media assets into Django</a> is in a good state and I hope it will be merged soon. I have been using object-based assets long before <a href="https://github.com/django/django/commit/4c76ffc2d6c77">Django actually added support for them in 4.1</a> (<a href="https://github.com/feincms/django-content-editor/commit/82ac91ea7af2409bb3672e11c18871002ddc9753">since 2016</a>, that&rsquo;s before Django 1.10!) by using a gross hack. Luckily I have been able to clean up the code when Django 4.1 landed.</p> <p>I have been asking myself at times why I haven&rsquo;t proposed the change to Django myself despite having been a user of something like this for such a long time. After all, I have been happily contributing issue reports, bug fixes and tests to Django. The process of adding new features sometimes is terribly frustrating though even when looking (and cheering) from the sidelines. It feels bad that adding another package to the <a href="https://pypi.org/user/matthiask/">list of packages I maintain</a> so clearly seems to be the better way to <strong>get things done</strong> compared to proposing a new feature for Django itself. I hope <a href="https://406.ch/writing/weeknotes-2024-week-49/">processes change somewhat</a>.</p> <p>But I digress.</p> <p>The <code>ProseEditorWidget</code> in <a href="https://github.com/matthiask/django-prose-editor/">django-prose-editor</a> wants to ship CSS, JavaScript and some JSON to the browser for the widget. So, of course I used object-based media assets for this instead of widget HTML templates. Media assets are deduplicated and sorted by Django. If different editor presets use differing lists of assets they are smartly merged by <code>forms.Media</code> using a topological sort. You get those niceties for free when using <code>forms.Media</code> and everything just works, so what&rsquo;s not to like?</p> <p>The only thing which isn&rsquo;t to like is that Django, at the time of writing, doesn&rsquo;t provide any classes helping with this. You can put strings into <code>forms.Media</code> or you can put objects with a <code>__html__()</code> method in there. The latter of course is all that&rsquo;s needed to support more advanced use cases &ndash; and that&rsquo;s exactly what <a href="https://pypi.org/project/django-js-asset/">django-js-asset</a> now provides, and what django-prose-editor uses.</p> <p><a href="https://pypi.org/project/django-js-asset/">django-js-asset</a> has long supported a <code>JS</code> class with support for additional attributes, for example:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">js_asset</span><span class="w"> </span><span class="kn">import</span> <span class="n">JS</span> <span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span> <span class="n">JS</span><span class="p">(</span><span class="s2">&quot;asset.js&quot;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&quot;id&quot;</span><span class="p">:</span> <span class="s2">&quot;asset-script&quot;</span><span class="p">,</span> <span class="s2">&quot;data-answer&quot;</span><span class="p">:</span> <span class="s2">&quot;42&quot;</span><span class="p">}),</span> <span class="p">])</span> </code></pre></div> <p>Since 3.0 the package also comes with a <code>CSS</code> and <code>JSON</code> class:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">js_asset</span><span class="w"> </span><span class="kn">import</span> <span class="n">CSS</span><span class="p">,</span> <span class="n">JS</span><span class="p">,</span> <span class="n">JSON</span> <span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span> <span class="n">JSON</span><span class="p">({</span><span class="s2">&quot;cfg&quot;</span><span class="p">:</span> <span class="mi">42</span><span class="p">},</span> <span class="nb">id</span><span class="o">=</span><span class="s2">&quot;widget-cfg&quot;</span><span class="p">),</span> <span class="n">CSS</span><span class="p">(</span><span class="s2">&quot;widget/style.css&quot;</span><span class="p">),</span> <span class="n">CSS</span><span class="p">(</span><span class="s2">&quot;p{color:red;}&quot;</span><span class="p">,</span> <span class="n">inline</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span> <span class="n">JS</span><span class="p">(</span><span class="s2">&quot;widget/script.js&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;module&quot;</span><span class="p">}),</span> <span class="p">])</span> </code></pre></div> <p>This produces the following HTML:</p> <div class="chl"><pre><span></span><code><span class="p">&lt;</span><span class="nt">script</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;widget-cfg&quot;</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;application/json&quot;</span><span class="p">&gt;{</span><span class="s2">&quot;cfg&quot;</span><span class="o">:</span><span class="w"> </span><span class="mf">42</span><span class="p">}&lt;/</span><span class="nt">script</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&quot;stylesheet&quot;</span> <span class="na">href</span><span class="o">=</span><span class="s">&quot;/static/widget/style.css&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span><span class="nt">p</span><span class="p">{</span><span class="k">color</span><span class="p">:</span><span class="kc">red</span><span class="p">;}&lt;/</span><span class="nt">style</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&quot;/static/widget/script.js&quot;</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;module&quot;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span> </code></pre></div> <p>The code which is proposed for Django supports the JavaScript use case but with a slightly different API:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django.forms</span><span class="w"> </span><span class="kn">import</span> <span class="n">Script</span> <span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span> <span class="n">Script</span><span class="p">(</span><span class="s2">&quot;widget/script.js&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="s2">&quot;module&quot;</span><span class="p">),</span> <span class="p">])</span> </code></pre></div> <p>This looks slightly nicer as long as you don&rsquo;t use e.g. data attributes, because then you have to do:</p> <div class="chl"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">django.forms</span><span class="w"> </span><span class="kn">import</span> <span class="n">Script</span> <span class="n">forms</span><span class="o">.</span><span class="n">Media</span><span class="p">(</span><span class="n">js</span><span class="o">=</span><span class="p">[</span> <span class="n">Script</span><span class="p">(</span><span class="s2">&quot;widget/script.js&quot;</span><span class="p">,</span> <span class="o">**</span><span class="p">{</span><span class="s2">&quot;data-cfg&quot;</span><span class="p">:</span> <span class="o">...</span><span class="p">}),</span> <span class="p">])</span> </code></pre></div> <p>I always forget that Python supports passing keyword arguments names which aren&rsquo;t valid Python identifiers (but only when using <code>**kwargs</code>). I personally don&rsquo;t care much either way, and when my packages can finally drop compatibility with Django versions which do not support all these functionalities yet I&rsquo;ll finally be able to retire <a href="https://pypi.org/project/django-js-asset/">django-js-asset</a>. That won&rsquo;t happen any time soon though, if only because I like supporting old versions of Django because I have so many Django-based websites running somewhere.</p>Weeknotes (2024 week 49)https://406.ch/writing/weeknotes-2024-week-49/2024-12-06T12:00:00Z2024-12-06T12:00:00Z<h1 id="weeknotes-2024-week-49"><a class="toclink" href="#weeknotes-2024-week-49">Weeknotes (2024 week 49)</a></h1> <h2 id="django-steering-council-elections"><a class="toclink" href="#django-steering-council-elections">Django Steering Council elections</a></h2> <p>I have been thinking long and hard about running for the Django Steering Council. I think there are a few things I could contribute since I&rsquo;ve been using Django for 16 or more years, and have been working on, maintaining and publishing third-party apps almost all this time. I have also contributed a few small features to Django core itself, and contributed my fair share of tests and bugfixes. The reason why I haven&rsquo;t been more involved was always that I feared the review process with what I perceive to be a too unrestrained perfectionism. Teaching people is good, but I fear that those who teach are self-selected survivors of the process, which come to appreciate the perfectionism a bit too much. It&rsquo;s somewhat the same as with the Swiss naturalization process &ndash; the hurdles are very high, and some of those who weather the process maybe are or grow to be too fond of it.</p> <p>An important point is that this has nothing to do with being nice (or not). Everybody has always been great, maybe with the exception of myself back when I didn&rsquo;t understand that the problem wasn&rsquo;t the individuals but the way everyone has agreed things should be done.</p> <p>I&rsquo;m not the only one who thinks that we <a href="https://knowyourmeme.com/memes/we-should-improve-society-somewhat">should improve the process somewhat</a>. So, I&rsquo;m definitely going to look out for candidates who think this is important.</p> <p>There are a few reasons why I&rsquo;m not running myself at this time. A somewhat important reason is that my candidacy wouldn&rsquo;t help diversity at all. This shouldn&rsquo;t discourage anyone else with the same background from running &ndash; we cannot change the world all at once. More importantly, I have more personal reasons for being hesitant to accept new commitments. That being said, I&rsquo;m looking forward to be more involved in the community in other ways. And also, it&rsquo;s not now or never.</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.5.4</a>: No functional changes, only code golfing. It&rsquo;s nice to have a working cookie banner with a solution for embedding third party content only when people consent in less than 4KiB of minified (not compressed!) JavaScript.</li> <li><a href="https://pypi.org/project/django-admin-ordering/">django-admin-ordering 0.20</a>: Objects can now be reordered using arrow buttons instead of drag drop or manually changing the ordering field&rsquo;s value. This should make the package more accessible. It&rsquo;s always a joy when people contribute such useful improvements.</li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.10a?</a>: See <a href="https://406.ch/writing/rebuilding-django-prose-editor-from-the-ground-up/">the recent blog post</a>.</li> </ul>Rebuilding django-prose-editor from the ground uphttps://406.ch/writing/rebuilding-django-prose-editor-from-the-ground-up/2024-12-04T12:00:00Z2024-12-04T12:00:00Z<h1 id="rebuilding-django-prose-editor-from-the-ground-up"><a class="toclink" href="#rebuilding-django-prose-editor-from-the-ground-up">Rebuilding django-prose-editor from the ground up</a></h1> <p>The <a href="https://pypi.org/project/django-prose-editor/">django-prose-editor</a> package provides a HTML editor based upon the <a href="https://prosemirror.net/">ProseMirror toolkit</a> for the Django administration interface and for the frontend.</p> <p>The package has been extracted from a customer project and open sourced so that it could be used in other projects as well. It followed a very restricted view of how rich text editors should work, which I have initially added to the <a href="https://github.com/feincms/feincms/commit/70cd7a1244438d2ba97852256f77daa2c870c345#diff-556c5559a716059d4fb714ad34de6a9845870e8d55bbd2cb9d77c732eb961388">FeinCMS repository when documenting the design decisions more than 15 years ago</a> <small>(Note that I didn&rsquo;t edit the paragraph, it&rsquo;s reproduced here as it was back then, with all the errors and heedlessness.)</small></p> <blockquote> <p>All of this convinced me that offering the user a rich text editor with too much capabilites 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 &ndash; 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 he&rsquo;s doing, I&rsquo;d still like to give him the power to shot his own foot).</p> </blockquote> <p>My personal views are unchanged. I have to recognize though that forcing this idea upon everyone isn&rsquo;t workable and that this would mean that I&rsquo;d have to find a different editor for most projects just because people really want or need more rope. Going back to an editor which allows everything was out of the question, so I had to look around for a way to allow project-specific extensions for the editor.</p> <p>Of course that&rsquo;s problematic, since Django packages and Python virtualenvs do not offer a good way of shipping CSS and JavaScript which should be available for a frontend bundler to process. The existing <a href="https://docs.djangoproject.com/en/5.1/ref/contrib/staticfiles/">Django staticfiles app</a> is great, works well, but it&rsquo;s not a bundler &ndash; and it shouldn&rsquo;t be.</p> <p>So, I started shopping around for ways to make ProseMirror extensible while keeping extensions clean and well localized. Instead of inventing another plugin ecosystem I settled on <a href="https://tiptap.dev/">Tiptap</a> which uses ProseMirror under the hood. The abstractions are pleasantly leaky &ndash; if you know how to work with ProseMirror&rsquo;s API, you can use Tiptap&rsquo;s API without any issues. That was important for me, since I already have a somewhat large selection of plugins which I do not want to reimplement from the ground up.</p> <p>I had already looked at Tiptap a few years back, but ultimately stayed with ProseMirror because I liked some behaviors better (such as not including trailing spaces in marks) and because I didn&rsquo;t need the extensibility which at the time only made the resulting bundle much bigger.</p> <p>Now, things have improved a lot, and I&rsquo;m really happy with Tiptap and the development version of django-prose-editor. <a href="https://github.com/matthiask/django-prose-editor/?tab=readme-ov-file#customization">Writing an editor extension in project code is great</a>, and my editor core stays nice. Also the list of readily available extensions is large, and most of the things just work.</p>Weeknotes (2024 week 47)https://406.ch/writing/weeknotes-2024-week-47/2024-11-20T12:00:00Z2024-11-20T12:00:00Z<h1 id="weeknotes-2024-week-47"><a class="toclink" href="#weeknotes-2024-week-47">Weeknotes (2024 week 47)</a></h1> <p>I missed a single co-writing session and of course that lead to four weeks of no posts at all to the blog. Oh well.</p> <h2 id="debugging"><a class="toclink" href="#debugging">Debugging</a></h2> <p>I want to share a few debugging stories from the last weeks.</p> <h3 id="pillow-11-and-djangos-get_image_dimensions"><a class="toclink" href="#pillow-11-and-djangos-get_image_dimensions">Pillow 11 and Django&rsquo;s <code>get_image_dimensions</code></a></h3> <p>The goal of <a href="https://github.com/matthiask/django-imagefield">django-imagefield</a> was to deeply verify that Django and Pillow are able to work with uploaded files; some files can be loaded, their dimensions can be inspected, but problems happen later when Pillow actually tries resizing or filtering files. Because of this django-imagefield does more work when images are added to the system instead of working around it later. (Django doesn&rsquo;t do this on purpose because doing all this work up-front could be considered a DoS factor.)</p> <p>In the last weeks I suddenly got recurring errors from saved files again, something which shouldn&rsquo;t happen, but obviously did.</p> <p>Django wants to read image dimensions when accessing or saving image files (by the way, always use <code>height_field</code> and <code>width_field</code>, otherwise Django will open and inspect image files even when you&rsquo;re only loading Django models from the database&hellip;!) and it uses a smart and wonderful<sup id="fnref:fn1"><a class="footnote-ref" href="#fn:fn1">1</a></sup> hack to do this: It reads a few hundred bytes from the image file, instructs Pillow to inspect the file and if an exception happens it reads more bytes and tries again. This process relies on the exact type of exceptions raised internally though, and the release of Pillow 11 changed the types&hellip; for some file types only. Fun times.</p> <p>The issue had already been reported as <a href="https://code.djangoproject.com/ticket/33240">#33240</a> and is now tracked as <a href="https://github.com/python-pillow/Pillow/issues/8530">#8530</a> on the Pillow issue tracker. Let&rsquo;s see what happens. For now, django-imagefield declares itself to be incompatible with Pillow 11.0.0 so that this error cannot happen.</p> <h3 id="rspack-and-lightningcss-shuffled-css-properties"><a class="toclink" href="#rspack-and-lightningcss-shuffled-css-properties">rspack and lightningcss shuffled CSS properties</a></h3> <p><a href="https://rspack.dev/">rspack</a> 1.0 started reordering CSS properties which of course lead to CSS properties overriding each other in the incorrect order. That was a fun one to debug. I tracked the issue down to the switch from the swc CSS minimizer to <a href="https://github.com/parcel-bundler/lightningcss">lightningcss</a> and submitted a reproduction to the <a href="https://github.com/parcel-bundler/lightningcss/issues/805#issuecomment-2358219597">issue tracker</a>. My rust knowledge wasn&rsquo;t up to the task of attempting to submit a fix myself. Luckily, it has been fixed in the meantime.</p> <h3 id="rspack-problems"><a class="toclink" href="#rspack-problems">rspack problems</a></h3> <p>I have another problem with rspack where I haven&rsquo;t yet tracked down the issue. rspack produces a broken bundle starting with <a href="https://github.com/web-infra-dev/rspack/releases/tag/v1.0.0-beta.2">1.0.0-beta.2</a> when compiling a particular project of mine. I have the suspicion that I have misconfigured some stuff related to import paths and yarn workspaces. I have no idea how anyone could have a complete understanding of these things&hellip;</p> <p><strong>Update:</strong> The problem was <a href="https://github.com/web-infra-dev/rspack/issues/8027">#8027</a>, <code>experiments.css</code> is quite broken for now.</p> <p>Bundlers are complex beasts, and I&rsquo;m happy that I mostly can just use them.</p> <h3 id="closing-thoughts"><a class="toclink" href="#closing-thoughts">Closing thoughts</a></h3> <p>Debugging is definitely a rewarding activity for me. I like tracking stuff down like this. Unfortunately, problems always tend to crop up when time is scarce already, but what can you do.</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <p>Quite a few releases, many of them verifying Python 3.13 and Django 5.1 support (if it hasn&rsquo;t been added already in previous releases). The nicest part: If I remember correctly I didn&rsquo;t have to change anything anywhere, everything just continues to work.</p> <ul> <li><a href="https://pypi.org/project/django-admin-ordering/">django-admin-ordering 0.19</a>: I added support for automatically renumbering objects on page load. This is mostly useful if you already have existing data which isn&rsquo;t ordered yet.</li> <li><a href="https://pypi.org/project/feincms3-data/">feincms3-data 0.7</a>: Made sure that objects are dumped in a deterministic order when dumping. I wanted to compare JSON dumps by hand before and after a big data migration in a customer project and differently ordered dumps made the comparison impossible.</li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.9</a>: I updated the ProseMirror packages, and put the editor into read-only mode for <code>&lt;textarea disabled&gt;</code> elements.</li> <li><a href="https://pypi.org/project/feincms3-language-sites/">feincms3-language-sites 0.4</a>: Finally released the update containing the necessary hook to validate page trees and their unique paths before moving produces integrity errors. Error messages are nicer than internal server errors.</li> <li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.17.2</a>: The value of the cookie which is used to save the URL where users should be redirected to after authentication wasn&rsquo;t checked for validity when setting it, only when reading it. This meant that attackers could produce invalid header errors in application servers. No real security problem here when using authlib&rsquo;s code.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 5.3</a>: Minor update which mostly removes support for outdated Python and Django versions.</li> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.20</a>: See above.</li> </ul> <div class="footnote"> <hr /> <ol> <li id="fn:fn1"> <p>wonderfully ugly&#160;<a class="footnote-backref" href="#fnref:fn1" title="Jump back to footnote 1 in the text">&#8617;</a></p> </li> </ol> </div>Weeknotes (2024 week 43)https://406.ch/writing/weeknotes-2024-week-43/2024-10-23T12:00:00Z2024-10-23T12:00:00Z<h1 id="weeknotes-2024-week-43"><a class="toclink" href="#weeknotes-2024-week-43">Weeknotes (2024 week 43)</a></h1> <p>I had some much needed time off, so this post isn&rsquo;t that long even though <a href="https://406.ch/writing/weeknotes-2024-week-39/">four weeks have passed since the last entry</a>.</p> <h2 id="from-webpack-to-rspack"><a class="toclink" href="#from-webpack-to-rspack">From webpack to rspack</a></h2> <p>I&rsquo;ve been really happy with <a href="https://rspack.dev/">rspack</a> lately. Converting webpack projects to rspack is straightforward since it mostly supports the same configuration, but it&rsquo;s much much faster since it&rsquo;s written in Rust. Rewriting things in Rust is a recurring theme, but in this case it really helps a lot. Building the frontend of a larger project of ours consisting of several admin tools and complete frontend implementations for different teaching materials only takes 10 seconds now instead of several minutes. That&rsquo;s a big and relevant difference.</p> <p>Newcomers should probably still either use <a href="https://rsbuild.dev/">rsbuild</a>, <a href="https://vite.dev/">Vite</a> or maybe no bundler at all. Vanilla JS and browser support for ES modules is great. That being said, I like cache busting, optimized bundling and far-future expiry headers in production and hot module reloading in development a lot, so learning to work with a frontend bundler is definitely still worth it.</p> <h2 id="dark-mode-and-light-mode"><a class="toclink" href="#dark-mode-and-light-mode">Dark mode and light mode</a></h2> <p>I have been switching themes in my preferred a few times per year in the past. The following ugly bit of vimscript helps switch me the theme each time the sun comes out when working outside:</p> <div class="chl"><pre><span></span><code><span class="nv">let</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span> <span class="nv">function</span><span class="o">!</span><span class="w"> </span><span class="nv">FiatLux</span><span class="ss">()</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span> <span class="w"> </span>:<span class="nv">set</span><span class="w"> </span><span class="nv">background</span><span class="o">=</span><span class="nv">light</span> <span class="w"> </span><span class="nv">let</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span> <span class="w"> </span><span class="k">else</span> <span class="w"> </span>:<span class="nv">set</span><span class="w"> </span><span class="nv">background</span><span class="o">=</span><span class="nv">dark</span> <span class="w"> </span><span class="nv">let</span><span class="w"> </span><span class="nv">t</span>:<span class="nv">light</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span> <span class="w"> </span><span class="k">endif</span> <span class="nv">endfunction</span> <span class="nv">nnoremap</span><span class="w"> </span><span class="o">&lt;</span><span class="nv">F12</span><span class="o">&gt;</span><span class="w"> </span>:<span class="k">call</span><span class="w"> </span><span class="nl">FiatLux</span><span class="ss">()</span><span class="o">&lt;</span><span class="nv">CR</span><span class="o">&gt;</span> </code></pre></div> <p>I&rsquo;m using the <a href="https://devsuite.app/ptyxis/">Ptyxis</a> terminal emulator currently, I haven&rsquo;t investigated yet if there&rsquo;s a shortcut to toggle dark and light mode for it as well. Using F10 to open the main menu works fine though, and using the mouse wouldn&rsquo;t be painful either.</p> <h2 id="helping-out-in-the-django-forum-and-the-discord"><a class="toclink" href="#helping-out-in-the-django-forum-and-the-discord">Helping out in the Django forum and the Discord</a></h2> <p>I have found some pleasure in helping out in the <a href="https://forum.djangoproject.com/">Django Forum</a> and in the official <a href="https://discord.gg/xcRH6mN4fa">Django Discord</a>. I sometimes wonder why more people aren&rsquo;t reading the Django source code when they hit something which looks like a bug or something which they do not understand. I find Django&rsquo;s source code very readable and I have found many nuggets within it. I&rsquo;d always recommend checking the documentation or maybe official help channels first, but the code is also out there and that fact should be taken advantage of.</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.1</a>: Fixed a bug where the ordering and region fields were handled incorrectly when they appear on one line in the fieldset. Also improved the presentation of inlines in unknown regions and clarified the meaning of the move to region dropdown. Also, released the improvements from previous patch releases as a new minor release because that&rsquo;s what I should have been doing all along.</li> <li><a href="https://pypi.org/project/form-designer/">form-designer 0.27</a>: A user has been bitten by <code>slugify</code> removing cyrillic characters because it only keeps ASCII characters around. Here&rsquo;s the wontfixed bug in the Django issue tracker: <a href="https://code.djangoproject.com/ticket/8391">#8391</a>. I fixed the issue by removing the slugification (is that even a word?) when generating choices.</li> </ul>Weeknotes (2024 week 39)https://406.ch/writing/weeknotes-2024-week-39/2024-09-25T12:00:00Z2024-09-25T12:00:00Z<h1 id="weeknotes-2024-week-39"><a class="toclink" href="#weeknotes-2024-week-39">Weeknotes (2024 week 39)</a></h1> <h2 id="css-for-django-forms"><a class="toclink" href="#css-for-django-forms">CSS for Django forms</a></h2> <p>Not much going on in OSS land. I have been somewhat active in the official Django forum, discussing ways to add Python-level hooks to allow adding CSS classes around form fields and their labels. The discussion on the <a href="https://forum.djangoproject.com/t/proposal-make-it-easy-to-add-css-classes-to-a-boundfield/32022">forum</a> and on the <a href="https://github.com/django/django/pull/18266">pull request</a> goes in the direction of allowing using custom <code>BoundField</code> classes per form or even per project (instead of only per field as is already possible today). This would allow overriding <code>css_classes</code>, e.g. to add a simple <code>class="field"</code>. Together with <code>:has()</code> this would probably allow me to skip using custom HTML templates in 99% of all cases.</p> <p>I have also been lurking in the Discord, but more to help and less to promote my packages and ideas :-)</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/django_user_messages/">django-user-messages 1.1</a>: Added Django 5.0, 5.1 to the CI, and fixed the migrations to no longer mention <code>index_together</code> at all. It seems that squashing the migrations wasn&rsquo;t sufficient, I also had to actually delete the old migrations.</li> <li><a href="https://pypi.org/project/blacknoise/">blacknoise 1.1</a>: <a href="https://www.starlette.io/">Starlette</a>&rsquo;s <code>FileResponse</code> has gained support for the HTTP Range header, allowing me to remove my homegrown implementation from the package. The blacknoise implementation is now half as long as it was in 1.0.</li> <li><a href="https://pypi.org/project/django_fhadmin/">django-fhadmin 2.3</a>: No new features, only tweaks to the styling and behavior prompted by updates to Django&rsquo;s admin interface.</li> <li><a href="https://pypi.org/project/django-cabinet/">django-cabinet 0.17</a>: I have pruned the CI matrix and accepted a pull request adding a ru translation. I feel conflicted about that since I strongly believe that everything is political, but I don&rsquo;t know if rejecting translations helps anyone.</li> </ul>django-content-editor now supports nested sectionshttps://406.ch/writing/django-content-editor-now-supports-nested-sections/2024-09-13T12:00:00Z2024-09-13T12:00:00Z<h1 id="django-content-editor-now-supports-nested-sections"><a class="toclink" href="#django-content-editor-now-supports-nested-sections">django-content-editor now supports nested sections</a></h1> <p><a href="https://django-content-editor.readthedocs.io/">django-content-editor</a> (and it&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> <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 (2024 week 37)https://406.ch/writing/weeknotes-2024-week-37/2024-09-11T12:00:00Z2024-09-11T12:00:00Z<h1 id="weeknotes-2024-week-37"><a class="toclink" href="#weeknotes-2024-week-37">Weeknotes (2024 week 37)</a></h1> <h2 id="django-debug-toolbar-alpha-with-async-support"><a class="toclink" href="#django-debug-toolbar-alpha-with-async-support">django-debug-toolbar alpha with async support!</a></h2> <p>I have helped mentoring Aman Pandey who has worked all summer to add async support to <a href="https://github.com/jazzband/django-debug-toolbar/">django-debug-toolbar</a>. <a href="https://github.com/tim-schilling">Tim</a> has released an alpha which contains all of the work up to a few days ago. Test it! Let&rsquo;s find the breakages before the final release.</p> <h2 id="dropping-python-39-from-my-projects"><a class="toclink" href="#dropping-python-39-from-my-projects">Dropping Python 3.9 from my projects</a></h2> <p>I have read <a href="https://noumenal.es/posts/the-only-green-python/yLw/">Carlton&rsquo;s post about the only green Python release</a> and have started dropping Python 3.9 support from many of the packages I maintain. This is such a good point:</p> <blockquote> <p>[&hellip;] I’m also thinking about it in terms of reducing the number of Python versions we support in CI. It feels like a lot of trees to support 5 full versions of Python for their entire life. 🌳</p> </blockquote> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/django-debug-toolbar/5.0.0a0/">django-debug-toolbar 5.0.0a0</a>: See above.</li> <li><a href="https://pypi.org/project/form-designer/">form-designer 0.26.2</a>: The values of choice fields are now returned as-is when sending mails or exporting form submissions instead of only returning the slugified version.</li> <li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.17.1</a>: The <a href="https://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/">role-based permissions backend</a> had a bug where it wouldn&rsquo;t return all available permissions in all circumstances, leading to empty navigation sidebars in the Django administration. This has been fixed.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 5.2.3</a>: Bugfix release, the page moving interface is no longer hidden by an expanded navigation sidebar. I almost always turn off the sidebar in my projects so I haven&rsquo;t noticed this.</li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.8.1</a>: Contains the most recent ProseMirror updates and bugfixes.</li> <li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.0.10</a>: Now supports sections. Separate blog post coming up!</li> </ul>Weeknotes (2024 week 35)https://406.ch/writing/weeknotes-2024-week-35/2024-08-28T12:00:00Z2024-08-28T12:00:00Z<h1 id="weeknotes-2024-week-35"><a class="toclink" href="#weeknotes-2024-week-35">Weeknotes (2024 week 35)</a></h1> <h2 id="getting-deep-into-htmx-and-django-template-partials"><a class="toclink" href="#getting-deep-into-htmx-and-django-template-partials">Getting deep into htmx and django-template-partials</a></h2> <p>I have been skeptical about <a href="https://htmx.org/">htmx</a> for some time because basically everything the library does is straightforward to do myself with a few lines of JavaScript. I am a convert now because, really, adding a few HTML attributes is nicer than copy pasting a few lines of JavaScript. Feels good.</p> <p>The combination of htmx with <a href="https://github.com/carltongibson/django-template-partials/">django-template-partials</a> is great as well. I didn&rsquo;t know I had been missing template partials until I started using them. Includes are still useful, but replacing some of them with partials makes working on the project much more enjoyable.</p> <p>I haven&rsquo;t yet had a use for <a href="https://django-htmx.readthedocs.io/">django-htmx</a> but I may yet surprise myself.</p> <h2 id="releases"><a class="toclink" href="#releases">Releases</a></h2> <ul> <li><a href="https://pypi.org/project/django-authlib/">django-authlib 0.17</a>: django-authlib bundles <code>authlib.little_auth</code> which offers an user model which uses the email address as the username. I have also introduced the concept of <a href="https://406.ch/writing/keep-content-managers-admin-access-up-to-date-with-role-based-permissions/">roles instead of permissions</a>; now I have reorganized the user admin fieldset to hide user permissions altogether. Group permissions are still available as are roles. I&rsquo;m personally convinced that user permissions were a mistake.</li> <li><a href="https://pypi.org/project/feincms3-forms/">feincms3-forms 0.5</a>: Allowed setting a maximum length for the bundled URL and email fields through the Django administration interface.</li> <li><a href="https://pypi.org/project/django-content-editor/">django-content-editor 7.0.7</a>: Fixed a bug where plugins with several fieldsets weren&rsquo;t collapsed completely.</li> <li><a href="https://pypi.org/project/django-imagefield/">django-imagefield 0.19.1</a>: Allowed deactivating autogeneration of thumbnails completely through the <code>IMAGEFIELD_AUTOGENERATE</code> setting. This is very useful for batch processing. Also, documented all available settings.</li> <li><a href="https://pypi.org/project/django-prose-editor/">django-prose-editor 0.8</a>: Added support for translating the interface elements and for restricting the available heading levels in the UI.</li> <li><a href="https://pypi.org/project/form-designer/">form-designer 0.25</a>: Fixed the type of the author field for the send-to-author processing action.</li> <li><a href="https://pypi.org/project/feincms3/">feincms3 5.2.2</a>: Added support for embedding <a href="https://www.srf.ch/play/tv">SRF play</a> external content. They do not support oEmbed unfortunately.</li> <li><a href="https://pypi.org/project/feincms3-cookiecontrol/">feincms3-cookiecontrol 1.5.3</a>: Added support for SRF play embeddings as well. The difference is that the feincms3-cookiecontrol embedding requires consent before embedding external content.</li> </ul>