<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blog</title><link>https://overtag.dk/v2/blog/</link><description/><atom:link href="https://example.com/_blog/blog/rss/" rel="self"/><language>en-us</language><lastBuildDate>Sun, 05 Apr 2026 21:03:18 +0000</lastBuildDate><item><title>Software development without LLMs: A methodology and a reasoning</title><link>https://overtag.dk/v2/blog/llm-free-methodology/</link><description>&lt;p&gt;As experiences gather up and technology improves, there are more and more evaluations and strategies coming in - all about when and why to use this data-center-driven LLM technology. A lot of decision-makers have already jumped aboard the AI/LLM train. Maybe you're one of them - and I kind of hope so, because this blog post is for you!&lt;/p&gt;
&lt;p&gt;My departing point for everything here is this: For the sake of not burning down the planet, it seems pretty decent to try to argue in favor of NOT needing LLM-assistance for producing software projects, whether sloppy ones or state-of-the-art.&lt;/p&gt;
&lt;p&gt;I find it more interesting to ask questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How can we improve software development without resorting to cloud-based LLM?&lt;/li&gt;
&lt;li&gt;How can we make software development have &lt;em&gt;less&lt;/em&gt; direct impact on the climate?&lt;/li&gt;
&lt;li&gt;How can we maintain software over a longer period of time?&lt;/li&gt;
&lt;li&gt;How can we write more effective test cases?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The answers to such questions are LONG, often branching deep into methodology / architecture / programming language.&lt;/p&gt;
&lt;p&gt;I.e. I'm not asking "how can we write more code?" or "how can people without knowledge of software development be empowered to write software?" - to me they are bizarre questions. Half the planet's population can drive a car, so if self-driving cars are the answer, then what exactly was the question?&lt;/p&gt;
&lt;p&gt;And I keep returning to this main finding: We didn't have any real problems with producing software, so the AI hype is trying to solve a non-problem. If you think about it for a moment, things were going pretty well in the software industry and people were mostly concerned with things like: Monopolies and vendor lock-in, privacy/data issues, e-waste, infosec etc. For software development itself, we had nice conferences, nice colleagues, we knew how to do our work pretty well.&lt;/p&gt;
&lt;p&gt;In cases like infosec, then we have LLM technology solving issues that it's producing itself. Got problems caused by AI? Use AI to solve them!&lt;/p&gt;
&lt;p&gt;Let's first break down some major areas of applying LLM to software, and then finally define a methodology that doesn't actually use LLMs but instead focuses on the benefits that arise from NOT using LLMs (aside from the omnipresent benefits like not burning down the planet).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LLM reviews&lt;/li&gt;
&lt;li&gt;LLM security scanning&lt;/li&gt;
&lt;li&gt;LLM research&lt;/li&gt;
&lt;li&gt;LLM code generation&lt;/li&gt;
&lt;/ul&gt;</description><guid>https://overtag.dk/v2/blog/llm-free-methodology/</guid></item><item><title>Making decisions in teams</title><link>https://overtag.dk/v2/blog/collaborating-as-equals/</link><description>&lt;p&gt;It's easy to feel joy of building and crafting your own software. Seeing the results, knowing that "I did it". Maybe someone else will notice, too, and tell you about the great work that you did.&lt;/p&gt;
&lt;p&gt;We all have agency and motivation - and we're all equals and equally different. Sure, some are more experienced than others and will say something correct more often. Someone will have more power, and others will listen to them more than others. That's not exactly equal, so what do I mean with everyone being equals? Well what I mean is that everyone should at least be offered the same chances and resources to achieve the same goals. That goes for software teams as well.&lt;/p&gt;
&lt;p&gt;Everyone should be allowed to fail and make amends, say something incorrect and change their mind.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/collaborating-as-equals/</guid></item><item><title>A primer on Django app design</title><link>https://overtag.dk/v2/blog/django-app-design/</link><description>&lt;p&gt;In this follow-up to &lt;a href="./django-project-structure"&gt;A primer on Django project structure&lt;/a&gt;, we're gonna look at the layout of individual Django apps.&lt;/p&gt;
&lt;p&gt;In the former topic, I went as far as to define a basic, universal project layout and also intended to fuel a discussion about templates, tutorials and the recent &lt;code&gt;django new&lt;/code&gt; command. But this blog post is more an introduction to operating your Django project once you start filling it in with Django apps, Python modules and Python packages.&lt;/p&gt;
&lt;h2&gt;When things go wrong 🙀&lt;/h2&gt;
&lt;p&gt;Let's look at some examples of a layout with symptoms of needing a refactor...&lt;/p&gt;
&lt;h3&gt;Circular imports&lt;/h3&gt;
&lt;p&gt;Have you ever seen local imports (below example) run wild? The local imports are there to avoid the top-level imports becoming circular. But they often just keep increasing in numbers, as they're a sign that the dependency coupling between Django apps or the modules inside the app is wrong. When you see a single local import in an entire project, it's &lt;em&gt;maybe&lt;/em&gt; just fine. But once you see the first one, it might also be a sign that the code isn't optimally structured. A so-called &lt;em&gt;smell&lt;/em&gt;.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;def do_something(...)
    # Use local import to avoid circularity
    # (because other_app.models depends on this module)
    from other_app.models import MyModel
    ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Confusion about where to add new stuff&lt;/h3&gt;
&lt;p&gt;Is having some simple answers to what goes where is becoming problematic? It's not nice to be in situations where modules overlap in scope/domain and don't provide clear guidance.&lt;/p&gt;
&lt;p&gt;In doubt about where your functionality should go? Add a new app / module! 😖 🙃&lt;/p&gt;
&lt;h3&gt;Lack of motivation&lt;/h3&gt;
&lt;p&gt;If you keep throwing functionalities into a messy Django project, it will eventually become a mess -- and that might quickly get worse when people have a perception that it needs to be rewritten, anyways. "Someone in the future will have to deal with this" is the anti-thesis of well-structured and documented code because that "someone" is usually just yourself 6-12 months from now.&lt;/p&gt;
&lt;h3&gt;Memory feels fuzzy&lt;/h3&gt;
&lt;p&gt;Likely the hardest symptom to grasp is when you start feeling that you're having a hard time quickly remembering which file to open in your project. We have IDE auto-completion for most things that can be imported, but even that can become difficult if naming conventions differ across your project.&lt;/p&gt;
&lt;h2&gt;App and module domains: Planning and definition&lt;/h2&gt;
&lt;p&gt;There's lots of truth in the &lt;a href="https://en.wikipedia.org/wiki/Unix_philosophy"&gt;Unix Philosophy&lt;/a&gt;: Do one thing and do it well. But we need to somehow translate this into how we organize our Apps and their modules inside our Django Project.&lt;/p&gt;
&lt;p&gt;In order to dive into architecture, you can watch Paul Wolf's &lt;a href="https://www.youtube.com/watch?v=l5AtMQbAsAk"&gt;Clean Architecture with Django Rethinking basic assumptions&lt;/a&gt; from DjangoCon Europe 2021.&lt;/p&gt;
&lt;p&gt;Let's consider the point that the &lt;em&gt;volatile parts&lt;/em&gt; of your code should depend on the more &lt;em&gt;stable parts&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;admin.py: should be rapidly adapting to your day-to-day needs. Consider these definitions volatile.&lt;/li&gt;
&lt;li&gt;views.py: with all their UX logic: Very volatile!&lt;/li&gt;
&lt;li&gt;urls.py: is almost like a kind of configuration: You can change a URL and everything else should just work because we have &lt;code&gt;urls.reverse()&lt;/code&gt; etc - of course that's not to say that we don't have permalinks and external references.. so we probably wanna consider it a more stable area.&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;models.py: Requires migrations for many changes and can be considered one of your app's most "stable" interfaces. Once you've added a method to a model, you'll start to depend on it in other modules, and so these changes should be seen as "stable".&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So all-in-all, the order of your module dependencies here reflect that general idea.&lt;/p&gt;
&lt;h2&gt;Recipe: Layout of each app&lt;/h2&gt;
&lt;p&gt;Let's try to answer these questions about stability, volatility and dependencies. We'll use common conventions (that already took this into account!),, and we end up with a blueprint of modules that a Django app contains and what they're for.&lt;/p&gt;
&lt;p&gt;Here's the example recipe of a Django app, with some inevitable conventions for the module names:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
    my_app/
      admin.py
      forms.py
      models.py
      shortcuts.py
      tasks.py
      views.py
      urls.py
      utils.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We'll step through these choices - it might sound like defining abilities of chess pieces, but it's different since each module is strictly higher in ranking than its successors in the list.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;urls.py&lt;/code&gt; sits on top of everything else - it MUST define &lt;code&gt;url_patterns&lt;/code&gt; and isn't imported by much else than Django's URL resolver, but that's also by design, because actually this is a very volatile piece of code: You want to be able to constantly add and refactor URLs and what they point to.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;views.py&lt;/code&gt; comes next, the only module that imports from views is: &lt;code&gt;urls&lt;/code&gt;. But &lt;code&gt;views&lt;/code&gt; import pretty much everything else.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin.py&lt;/code&gt; can use tasks, shortcuts and models - this module sits almost at the top of the "food chain" so to say. But I would recommend to avoid depending on &lt;code&gt;views&lt;/code&gt; and keep all admin views local to the &lt;code&gt;admin&lt;/code&gt; module, and even better: As methods of your ModelAdmins.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shortcuts.py&lt;/code&gt; can import from all other modules, except your views which normally consume the shortcuts. Don't confuse shortcuts with a service layer, but use it to avoid having one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forms.py&lt;/code&gt; contains for instance all the ModelForms used by your views and depends on your models. It will also often spawn tasks since it processes form data.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tasks.py&lt;/code&gt; comes next - it contains all your async tasks and can depend on models and acts as a kind of controller/service layer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;models.py&lt;/code&gt; is second-to-last: If you have more modules, make sure to make them depend on models, not the other way around.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;utils.py&lt;/code&gt; is a utility library that ALL OTHER modules can depend on. If &lt;code&gt;my_app.utils&lt;/code&gt; depends on other things in &lt;code&gt;my_app&lt;/code&gt;, then the utilities themselves have utilities inside &lt;code&gt;my_app&lt;/code&gt;.. that doesn't work! Under no circumstances let your app's utility layer end up depending on your app's models etc.: What you're looking for is model methods, managers etc. Utilities are for instance functions that call Django itself, convert a text string, localizes a number etc. A good rule of thumb is to never access the database from utils.py.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thinking behind what &lt;code&gt;utils&lt;/code&gt; and &lt;code&gt;shortcuts&lt;/code&gt; should scope to and depend on is from Django &lt;em&gt;itself&lt;/em&gt;, i.e. in Django, the utils module has close to zero dependencies to other Django modules.&lt;/p&gt;
&lt;h3&gt;Conventions for templates and static media&lt;/h3&gt;
&lt;p&gt;I also just wanna mention this, since it's overlooked once too often: Remember to use sub-folders as namespaces for your templates and static files since they are subject to various kinds of "overrides" in how templates and static media is resolved and collected.&lt;/p&gt;
&lt;p&gt;So let's say you have an app &lt;code&gt;foobar&lt;/code&gt;, then it should have this structure:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
      foobar/
          templates/
              foobar/  # &amp;lt;- that&amp;#39;s the important sub-folder!
                  base.html
            static/
              foobar/  # &amp;lt;- and this!
                  menu.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Nice discussions / exercise&lt;/h3&gt;
&lt;p&gt;Here is some homework/discussion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can you guess where widgets.py would fit in?&lt;/li&gt;
&lt;li&gt;Can you guess where exceptions.py would fit in? And would you want one?&lt;/li&gt;
&lt;li&gt;Should we have a fields.py module?&lt;/li&gt;
&lt;li&gt;What code should execute at load time?&lt;/li&gt;
&lt;li&gt;How does tests reflect on stable vs. volatile?&lt;/li&gt;
&lt;li&gt;Are Django template files stable or volatile pieces of code?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;h3&gt;Test-ability wins&lt;/h3&gt;
&lt;p&gt;If you think about writing a unit test, then it's always easier to write if your methods and functions are defined by simple input-output logic.&lt;/p&gt;
&lt;p&gt;This also wins in the longer run: If you are servicing a large project with lots of data, you can for instance need to manage data directly in the shell. When things are broken and new exceptions show up in your monitoring, you might need to improvise patching up your data. Did the order processing break and you want to call &lt;code&gt;order.complete()&lt;/code&gt;? That works nicely if you've defined it as a method on a model, but not if the order logic is nested deep within a view that relies on an HttpRequest object. That same logic is true for unit testing. And it'll make everything more readable.&lt;/p&gt;
&lt;p&gt;Refactoring code into smaller routines isn't "premature abstraction": This happens when you invent functionality and data structures while refactoring into smaller routines. See the &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#AHA"&gt;AHA principle&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Skip &lt;code&gt;managers.py&lt;/code&gt; - put query logic in the models&lt;/h3&gt;
&lt;p&gt;A Django model should declare its managers, it's great to have custom managers and business logic reusable and manifested in managers. And it's great for unit testing! But the managers usually depend on the models. So at this stage, they're mutually dependent and should live in the same module.&lt;/p&gt;
&lt;p&gt;You can also use nested classes, since a manager is rarely reused outside of its model. It looks like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;class MyModel(models.Model):
    class DefaultQuerySet(models.QuerySet):
        def active(self):
            return self.filter(is_active=True)

    objects = DefaultQuerySet.as_manager()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But then models.py will become very long? YES! And that's not really a problem since IDEs help you navigate code structures. Having convoluted import patters is much worse, and declaring custom QuerySets inside the model is very easy to regard as Locality of Behavior.&lt;/p&gt;
&lt;h3&gt;Do have a custom user model&lt;/h3&gt;
&lt;p&gt;It's awful to change from using the built-in &lt;code&gt;User&lt;/code&gt; model from &lt;code&gt;django.contrib.auth&lt;/code&gt;, so I recommend to always bootstrap your project with a &lt;code&gt;users&lt;/code&gt; app. See &lt;a href="https://docs.djangoproject.com/en/stable/topics/auth/customizing/#specifying-custom-user-model"&gt;Django Docs about customizing the User model&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Avoid the Service layer&lt;/h3&gt;
&lt;p&gt;You could probably put a service layer somewhere next to &lt;code&gt;shortcuts&lt;/code&gt;. I would recommend to avoid this, though, since most of the time it will simply do things that should otherwise have been encoded in views, tasks, model methods and manager/queryset methods.&lt;/p&gt;
&lt;p&gt;If you think services are the answer to encapsulating business logic (as they're usually lauded for), you can read this guide:&lt;/p&gt;
&lt;p&gt;https://github.com/HackSoftware/Django-Styleguide?tab=readme-ov-file#services&lt;/p&gt;
&lt;p&gt;My own short take is that business logic should be implemented in APIs, so if you're doing API-driven stuff, put your business logic in API views, serializers and model methods/managers/querysets. If you're doing a full-stack Django project, then the business logic resides in views and model methods/managers/querysets. This will make your life simpler and your applications will contain a better structure and better tests.&lt;/p&gt;
&lt;p&gt;BUT if your service layer is actually going to be very big and it needs to live its own life, you should probably read the HackSoft guide and take their word:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sometimes, the service layer is not always looking like a single file with a bunch of functions, but rather like an entire Python module with well-defined interface, so we can encapsulate something complex &amp;amp; isolate it from the rest of the app.&lt;/p&gt;
&lt;p&gt;We’ve even gone further creating a layer of types, between the ORM &amp;amp; the business logic (this is an overkill for most cases, but there are cases where it helps)&lt;/p&gt;
&lt;p&gt;The general idea - separation of concerns - is key.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;https://forum.djangoproject.com/t/structuring-large-complex-django-projects-and-using-a-services-layer-in-django-projects/1487/22&lt;/p&gt;
&lt;p&gt;If you want to know why service layers aren't such a good idea, read:&lt;/p&gt;
&lt;p&gt;https://www.b-list.org/weblog/2020/mar/16/no-service/&lt;/p&gt;
&lt;h3&gt;Do use a global &lt;code&gt;tests/&lt;/code&gt; for all apps&lt;/h3&gt;
&lt;p&gt;Do not put tests inside your app - tests observe their subjects from outside and aren't deployed to production. This is also discussed in &lt;a href="./django-project-structure"&gt;A primer on Django project structure&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Big modules are usually preferable over packages&lt;/h3&gt;
&lt;p&gt;We can feel tempted to turn &lt;code&gt;models&lt;/code&gt; or &lt;code&gt;tasks&lt;/code&gt; into a package simply because there are many tasks or models. But having a big module isn't necessarily bad in itself.&lt;/p&gt;
&lt;p&gt;Django will usually need to load your entire project, anyways. So there's no performance arguments.&lt;/p&gt;
&lt;p&gt;The harmful part comes when we start to re-invent the whole domain/scoping problem inside &lt;code&gt;foobar.models&lt;/code&gt; - if we already defined the scope of foobar, we now want to define a sub-scope? Is that even possible? Or will we simply end up with circular imports inside &lt;code&gt;foobar.models&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Sometimes, it's easy or trivial to do. For instance, we could have:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;foobar/
  tasks/
      emails.py
    sms.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That looks good, right? ✅ In one module, we have tasks that send emails, and in the other one, the tasks send out SMS. If we need to do both, the caller will combine them.&lt;/p&gt;
&lt;p&gt;But what if we would divide our models?&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;shop/
  models/
      messages.py
    purchases.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You see, we would pretty easily end up in a situation where &lt;code&gt;models.messages&lt;/code&gt; depend on &lt;code&gt;models.purchases&lt;/code&gt; and vice-versa! ⚠️ Or we could have models that would fit in both places.&lt;/p&gt;
&lt;h3&gt;Navigate Locality of Behavior vs DRY&lt;/h3&gt;
&lt;p&gt;DRY is a great principle because it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gives us less code to test, review etc.&lt;/li&gt;
&lt;li&gt;Makes it easier to avoid bugs when introducing new features&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But it also gives us abstraction, which means that we end up designing functionality only for the sake of abstractions. You probably want to avoid that, unless you're writing a reusable library for external code bases.&lt;/p&gt;
&lt;p&gt;And on the contrary, Locality of Behavior can result in more code and more to test and review + larger risk of bugs since there's more code to "remember" to change when refactoring. "Oh oops, I was updating the thing here, but forgot to update the same thing &lt;em&gt;there&lt;/em&gt;".&lt;/p&gt;
&lt;p&gt;But Locality of Behavior is great because it &lt;strong&gt;makes life easier&lt;/strong&gt; when we make choices about where to put code &lt;strong&gt;inside&lt;/strong&gt; a Django app.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/django-app-design/</guid></item><item><title>Running Pre-commit in Docker</title><link>https://overtag.dk/v2/blog/pre-commit-in-docker/</link><description>&lt;p&gt;&lt;strong&gt;Make your "pre-commit" command run in a Docker container and reduce attack surface on your development system!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You probably know &lt;a href="https://pre-commit.com/"&gt;pre-commit&lt;/a&gt; which is a super-popular way to automatically install and run the required linters of a Git repository. It'll fetch all the required software and run it from an isolated environment, such as a virtual environment or a node environment.&lt;/p&gt;
&lt;p&gt;Only problem: It's not really that isolated. So if any of these pre-commit hooks would suffer a vulnerability, they could have access to the secret files in the repository, your SSH keys or your various tokens for PyPi, GitHub, AWS, NPM etc.&lt;/p&gt;
&lt;p&gt;As you might have noticed, supply-chain attacks are becoming more and more common. So the chance that a pre-commit hook falls victim seems evermore increasing. Especially given that exploiting a pre-commit hook is very attractive, given that it would gain the attacker access to developer's setups... the types of setups that usually also have access to lots of other exciting things.&lt;/p&gt;
&lt;p&gt;I'm not trying to tip off attackers, I'm sure they already know this.&lt;/p&gt;
&lt;h2&gt;What can we do?&lt;/h2&gt;
&lt;p&gt;Obviously, if all the hooks could be packaged from trusted sources that would follow certain standards (such as Debian packages), we could start running linters in a more standardized way etc. But that seems a bit like wishful thinking right now. Pre-commit hasn't been trying to make it easy to install standardized packages, but more the opposite: To speed up the distribution of small, convenient automatic script-like features.&lt;/p&gt;
&lt;p&gt;That does seem like something you'd want to run in a less trusted environment.&lt;/p&gt;
&lt;p&gt;So the proposition here is to see how far we can get with Docker.&lt;/p&gt;
&lt;h2&gt;Try it out&lt;/h2&gt;
&lt;p&gt;I've written a little Docker image with some instructions here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/benjaoming/pre-commit-docker-alias"&gt;https://codeberg.org/benjaoming/pre-commit-docker-alias&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What you get from that is basically a global alias for &lt;code&gt;pre-commit&lt;/code&gt; that will work for all your projects. It contains a PyPi and NPM environment, so it's ready to handle these types of hooks.&lt;/p&gt;
&lt;p&gt;More can be added, and we can figure out the basic day-to-day needs.&lt;/p&gt;
&lt;h2&gt;What are the catches?&lt;/h2&gt;
&lt;p&gt;I found only a small &lt;strong&gt;performance&lt;/strong&gt; catch of like a 0.4s startup overhead on my laptop. This comes from spinning up the Docker container. It's nothing that you'll notice.&lt;/p&gt;
&lt;p&gt;We should also be aware that the &lt;strong&gt;Git repository is mounted into the Docker container&lt;/strong&gt;, which means you still have to secure any secrets that are exposed through the repository. I imagine we should stop storing secrets in .env files.&lt;/p&gt;
&lt;p&gt;The other catches relate to &lt;strong&gt;current limitations to what you can do in the Docker image&lt;/strong&gt;: It doesn't support Docker itself and some other language environments. But the images can be customized for this.&lt;/p&gt;
&lt;p&gt;I encourage that you just fork my project and make it your own. It's not like the global image needs to do everything, we can manage different images for different projects and come up with a way to manage that,.&lt;/p&gt;</description><pubDate>Sun, 05 Apr 2026 21:03:18 +0000</pubDate><guid>https://overtag.dk/v2/blog/pre-commit-in-docker/</guid></item><item><title>A primer on Django project structure</title><link>https://overtag.dk/v2/blog/django-project-structure/</link><description>&lt;p&gt;&lt;strong&gt;Here's an example recipe for a Django project layout:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
    users/
  project/
    settings/
    templates/
    asgi.py, urls.py etc..
  manage.py
tests/
pyproject.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's the basic layout we're going to discuss now. But if you want to get started quickly, try running the prototype  &lt;code&gt;startproject.sh&lt;/code&gt; command that will bootstrap the above, with your project name as the parameter (source: &lt;a href="https://codeberg.org/benjaoming/django-project-example"&gt;Codeberg repo&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;And food recipe blogs, notice how we STARTED with the recipe... make note that you CAN actually serve the recipe BEFORE the word soup&lt;/em&gt; 😎 🥗&lt;/p&gt;
&lt;h2&gt;The pitch&lt;/h2&gt;
&lt;p&gt;Whether your project is a quick'n'dirty one-off project or you're starting something with a longer trajectory, try to avoid stalling at universal decisions and instead use that time to design project-specific structures or simply start building. If your project is small or has low complexity, you can be happy with a simple structure like the above. If you expand to more complex needs, this is a good start that doesn't impose technical debt from the inception of a new project.&lt;/p&gt;
&lt;p&gt;In this post, we'll discuss more principles for adding additional structure (UPDATE: and a blog post that's &lt;em&gt;just about to be published here&lt;/em&gt; will dig even further into the architecture of Django projects and apps).&lt;/p&gt;
&lt;p&gt;If you're new to Django and you don't know what exactly these various components in the structure are, there'll be some additional pointers in the below discussion of choices. Even if you're not new to Django, I encourage that you revisit these decisions and wonder "wouldn't it be nice to always bootstrap a project this way?".&lt;/p&gt;
&lt;p&gt;At the time of writing this, &lt;a href="https://github.com/django/deps/pull/100"&gt;DEP-14&lt;/a&gt; and &lt;a href="https://github.com/django/deps/pull/98"&gt;DEP-15&lt;/a&gt; were accepted. These are both proposals to change things in Django: The former proposes a new &lt;code&gt;django&lt;/code&gt; command for everything, and the latter introduces &lt;code&gt;django new&lt;/code&gt; sub-command that will prompt the user about which starter template to use for a new Django project. So let's say the trajectory of this blog post is &lt;em&gt;also&lt;/em&gt; to open the floor on how such a template could look and behave 🌠.&lt;/p&gt;
&lt;p&gt;Here is what the &lt;a href="https://codeberg.org/benjaoming/django-project-example"&gt;startproject.sh&lt;/a&gt; offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your project is a Python package from the beginning&lt;/li&gt;
&lt;li&gt;You can bootstrap a Django project without installing Django firstly&lt;/li&gt;
&lt;li&gt;It contains opinions on the Django project layout (detailed below)&lt;/li&gt;
&lt;li&gt;uv, pre-commit and Ruff from the beginning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;NB! (to be updated later) At the time of writing this, the owners of uv have sold themselves to OpenAI - this blog post is not an endorsement of future uv/Ruff&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ultimately (moonshot part 🚀🌕), we should let &lt;code&gt;django new&lt;/code&gt; break &lt;em&gt;new&lt;/em&gt; ground by combining its nice simple interface with a new tutorial that makes it easier for people to use and understand the combination of Django project &lt;em&gt;and&lt;/em&gt; Python package.&lt;/p&gt;
&lt;p&gt;🗯️ Comments, questions, thoughts? Please feel free to respond to &lt;a href="https://social.data.coop/@benjaoming/116272310697436779"&gt;the Fediverse announcement of this post&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Folders&lt;/h2&gt;
&lt;p&gt;This was intentionally made as a kind of baseline for a Django project. We will have a section for additional common problem and their solutions. But here are some explanations of the initial choices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/&lt;/code&gt;: We will put all our Python source code in a distinct folder, because your project WILL have other files and folders that aren't Python source code. You may or may not want to distribute a Wheel, but this is just a tidy and very simple rule. Polluting the root various packages is not what you want, trust me.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/apps/&lt;/code&gt;: We have &lt;em&gt;apps&lt;/em&gt; and &lt;em&gt;projects&lt;/em&gt; in Django, those are very important distinctions that should trigger a Django developer to engage in long philosophical pondering. So in order to reflect this understanding in the folder layout, let's avoid the conflation of apps and projects (or actually "project" in singular).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/project/&lt;/code&gt;: Almost the same thing, except see what I did here? I made the main project simply be called &lt;code&gt;project&lt;/code&gt;, in singular. Yes, it could have been &lt;code&gt;django_project&lt;/code&gt; or &lt;code&gt;site&lt;/code&gt; or &lt;code&gt;&amp;lt;organization_name&amp;gt;&lt;/code&gt;. But ultimately there is just one project and it's the &lt;em&gt;Django&lt;/em&gt; project ☺ By simply calling it "project", we maintain the terminology of what makes a project be different from all your app domains, and no one needs to wonder "is this an app?" - no, it's a project!&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/project/settings/&lt;/code&gt;: You will of course need a settings module, otherwise your Django project cannot run. But from the inception of your project, make it a package so you can have a variety of settings for each of your fundamental environments (often that's development and production, maybe test and staging).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/project/templates/&lt;/code&gt;: You should always anticipate at least on project-wide templates folder because there are templates that form the base of all your apps. Otherwise, you will end up storing project-related templates in your apps.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/manage.py&lt;/code&gt;: This is the main entrypoint of it all, it could be in the root folder, but let's treat it as a part of the main src that should be installed on the target. This makes it possible to ultimately create a Python package and point to manage.py with an entrypoint. Do you wanna run &lt;code&gt;acme runserver&lt;/code&gt; inside your VPS or AWS task or Docker Container? Sure thing, just configure the pyproject.yaml with these aliases.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests/&lt;/code&gt;: Here is a very very important simple step: Always keep tests separate. We will get into how we can organize tests later. But do not mix them with the actual project, tests should be observing their subject (the application/project/software) from outside.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pyproject.yaml&lt;/code&gt;: Python in 2025 pretty much makes it possible to manifest every configurable aspect of your project in this toml file, so let's stick to that. This also means we can operate &lt;code&gt;uv&lt;/code&gt; from the beginning to do things quickly, like managing your environment and adding more dependencies to your &lt;em&gt;Python&lt;/em&gt; project.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Non-Django ingredients&lt;/h2&gt;
&lt;p&gt;It's possible that other tools such as environment and project managers like uv (or pip, hatch , etc - and combinations) and pre-commit (or the recent prek) will also fit very well when bootstrapping a project. Some people would even say that Docker Compose is a fixed ingredient in all their project recipies. Aside from that, there are commonly used Django dependencies like psycopg(3) and django-debug-toolbar. It means that we might have A LOT of considerations to present in a template or a tutorial. But these are more like variables, unlike the basic structuring of the Django project itself. Django itself can have more opinions about how to get people started, which is what the new &lt;code&gt;django new&lt;/code&gt; command will offer.&lt;/p&gt;
&lt;p&gt;This very adjacent to the idea of "batteries included": Get people started with developing their web project, including pytest, pre-commit, ruff and uv.&lt;/p&gt;
&lt;h2&gt;Distinction between apps and project&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;src/apps/ vs. src/project/&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The following sentence is not very intuitive: We will stop thinking that a Django &lt;em&gt;project&lt;/em&gt; consists of apps - the apps actually live &lt;em&gt;outside&lt;/em&gt; of the project scope itself. That means we need to consider what a project really is, and what it means if the apps are not included? Well, a Django project is a central, single unit that combines and configures a set of apps. Those apps might be very targeted to work in the project, but ultimately, they are standalone Python packages - and even if their usage outside of project is unthinkable, we still maintain that position! The unintuitive part here will serve to help us define and design each app.&lt;/p&gt;
&lt;p&gt;So you have these layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Python project: Has everything, and is defined by pyproject.toml&lt;/li&gt;
&lt;li&gt;The apps: Are Python packages inside the Python project, designed to behave as Django apps&lt;/li&gt;
&lt;li&gt;The Django project: A Python package inside the Python project designed to be... a Django project!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We're not gonna mix these layers, so now we can stop worrying about Django apps somehow being inside a Django project. Because that is confusing! (Sorry to the people advocating the single-folder django app &amp;amp; project combination)&lt;/p&gt;
&lt;p&gt;Remember also that outside of these layers, we also have "the IDE project", "the Git project", "the Docker Compose project" etc. So people are more or less familiar with the ambiguity of the word "project" - or let's just call it an inevitable condition.&lt;/p&gt;
&lt;h3&gt;App domains&lt;/h3&gt;
&lt;p&gt;Apps each have a domain: This is taken from good old domain-driven design. This should avoid the apps from overlapping, making it easy to decide what goes where, and avoiding circular dependencies. Apps may depend on each other one-directionally, but you should always try to make it so that dependencies aren't two-way (this might be impossible in some rare occasions). The &lt;code&gt;user&lt;/code&gt; app is there from the inception as a good practice: You should probably always have your custom user model and avoid having anything in this app's domain intersect or depend on other apps. If you have a &lt;code&gt;shop&lt;/code&gt; app, it will depend on the &lt;code&gt;user&lt;/code&gt; app, but the &lt;code&gt;user&lt;/code&gt; app should not depend on &lt;code&gt;shop&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;Discussing app domains is a &lt;strong&gt;positive&lt;/strong&gt; problem. It helps you and your team understand your problem better. If your app domains are hard to define, it's very likely that the solution needs a bit more work - and hopefully if this is &lt;em&gt;hard&lt;/em&gt;, it's also a fun challenge!&lt;/p&gt;
&lt;h2&gt;src/apps/api/&lt;/h2&gt;
&lt;p&gt;We all want APIs now and then, but do we create one API per app? Or a common API for all our custom apps?&lt;/p&gt;
&lt;p&gt;Let's think for a moment: We have the Python project containing the Django project and a set of stand-alone Python packages. Do we have a simple API for each app? If that makes sense to you, you should definitely define APIs inside each Django app - maybe inside a sub-package like &lt;code&gt;blog.api&lt;/code&gt; with nested views and serializers (if you're using Django Rest Framework).&lt;/p&gt;
&lt;p&gt;But if this does not immediately make sense to you, my recommendation is to start differently:&lt;/p&gt;
&lt;p&gt;Create a Django app simply called "api".&lt;/p&gt;
&lt;p&gt;So you get:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
    api/  # &amp;lt;- A unified app depending on all the other apps included in API views
    blog/
  project/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;From this starting point, you have both the option to try to keep your APIs separate for each app domain or allow the intersection. But even better, you will have a mirrored solution in your tests where you can target all your API tests on this particular app. That can help to structure your project in general, not least since most APIs share a common codebase around authentication, rate limiting, caching etc.&lt;/p&gt;
&lt;h2&gt;Templates&lt;/h2&gt;
&lt;p&gt;If you're using your Django project as a fullstack project with templates, then template structures are very important. You don't want templates and templatetags to be inheriting across apps. The templating language has less support from IDE and linting, so it's important to try to stick to something that doesn't become complex.&lt;/p&gt;
&lt;p&gt;The suggestion here is to ALWAYS have a templates directory for your project and to create a "base.html" here that each app will inherit from. Even if you're not targeting a distributed, reusable model, you should still organize these things in a neat, intuitive way. This makes it easy for other developers to guess your structure's logic.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
  blog/
    templates/
      blog/
      base.html  # extends from &amp;quot;base.html&amp;quot;
  project/
  templates/
    base.html  # your whole site&amp;#39;s base template
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is quite similar for static files.&lt;/p&gt;
&lt;h2&gt;Settings&lt;/h2&gt;
&lt;p&gt;As a baseline, we should always use environment variables, but there are a few things that aren't well-suited. But instead of unlimited settings permutations, we assume that there are a very few selected, eligible settings.&lt;/p&gt;
&lt;p&gt;We ALWAYS want a separate development settings module. This will not only include DEBUG=True but also configure things like django-debug-toolbar, maybe a different database configuration, maybe eager triggering of Celery tasks - who knows. The point here is to AVOID following the 12-factor model strictly and end up with a jungle of environment variables. You may argue that it's also 12-factor to specify DJANGO_SETTINGS_MODULE in your deployment.&lt;/p&gt;
&lt;p&gt;The main caution here is to avoid drifting. This can lead to developers to make wrong assumptions about the production environment because their development environment is too different.&lt;/p&gt;
&lt;p&gt;We are intentionally NOT including the otherwise populare "settings/local.py" because this is often a source of developer bugs. A developer may have different settings from the rest of the team, and suddenly there are two sources that have to be debugged: The environment variables AND a local.py.&lt;/p&gt;
&lt;h2&gt;Celery/django tasks&lt;/h2&gt;
&lt;p&gt;Like views or models, tasks are a very specific kind of object in Django. It's a function that receives a set of serializable paramenters and perfoms its functionality from only that context. It happens outside of the request-response loop. It's important to scope tasks so they are easy to test and maintain. The conclusion here is to keep your tasks bound to each app's domain and have for instance:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
    blog/
      tasks.py  # &amp;lt;- all tasks within the blog domain
  project/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Again, if you have dependencies between apps, make sure they only depend in one direction. A new &lt;code&gt;shop&lt;/code&gt; app can depend on a &lt;code&gt;users&lt;/code&gt; but not the other way. So you can have &lt;code&gt;shop.tasks&lt;/code&gt; doing shop-related operations with users, but then you should not put shop-related things in &lt;code&gt;users.tasks&lt;/code&gt;! Whenever app domains and their dependencies become tricky or complex, it hints towards defining new apps.&lt;/p&gt;
&lt;h2&gt;Tests&lt;/h2&gt;
&lt;p&gt;If you read this far: You might be inclined to skip tests in the beginning of bootstrapping your project, but you will need them soon. And the worst kind of test setups are the ones that don't distinguish between their various genres: e2e, integration and unit testing.&lt;/p&gt;
&lt;p&gt;A good goal for your layout is to eventually create one test package per app. And you might as well also start out like that.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
    blog/
  project/
tests/
  conftest.py  # &amp;lt;- Here we have global test fixtures
  blog/
    conftest.py
    e2e_tests.py  # &amp;lt;- test your views here and let everything execute. Few but expensive.
    unit_tests.py  # &amp;lt;- test individual functions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is really just an example: You might also want to create view_tests, model_tests etc. But still make sure that you can grasp the different genres of tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;e2e&lt;/strong&gt;: Everything runs, your database layer, views, Celery tasks, external (mocked response) calls etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;integration tests&lt;/strong&gt;: Rarely used in Django projects, but you might fancy this to allow certain layers to be called with internal functionality executed. This is especially useful if you decide that your unit tests should not access the database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unit tests&lt;/strong&gt;: A simple function is tested, external calls are mocked&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The idea here is still to ensure that you design every test case with some understanding of its scope: You should not test a simple model method by calling a view, and you should not write loads of assertions on the internals of a model method if you're actually testing the view.&lt;/p&gt;
&lt;p&gt;This all boils down to finding the value and motivation for writing tests. Try to find yourself doing test-driven development as soon as possible, but if it doesn't make sense, don't do it... yet... but still, at least squeeze in some e2e tests from the beginning, they usually provide a payoff quite quickly&lt;/p&gt;
&lt;p&gt;Enough about testing, it's a whole category of its own 🤪&lt;/p&gt;
&lt;h2&gt;Q&amp;amp;A&lt;/h2&gt;
&lt;h3&gt;Should I start with a &lt;code&gt;users&lt;/code&gt; app and &lt;code&gt;AUTH_USER_MODEL&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;Django provides a simple &lt;code&gt;User&lt;/code&gt; model from &lt;code&gt;django.contrib.auth&lt;/code&gt;. However, once you have started using it, migrating to your own custom model becomes tricky and most people resort to having a relation to some kind of &lt;code&gt;UserProfile&lt;/code&gt; model.&lt;/p&gt;
&lt;p&gt;That's why I recommend to start all projects with your own &lt;code&gt;users&lt;/code&gt; app and define &lt;code&gt;settings.AUTH_USER_MODEL&lt;/code&gt; to point to that model. You can read more about this in the Django docs: https://docs.djangoproject.com/en/stable/topics/auth/customizing/#specifying-custom-user-model&lt;/p&gt;
&lt;p&gt;In order to reduce how opinionated this project layout is, I kept it out, and I think these sorts of choices are more appropriate for a tool like cookiecutter.&lt;/p&gt;
&lt;h3&gt;Should I create packages for views and models?&lt;/h3&gt;
&lt;p&gt;I've seen many examples where a Django application have for instance a whole package &lt;code&gt;app.models&lt;/code&gt; full of nested models, each with their own domain. Having domains inside the app domain is easily a sign that you need a new separate app, but it can also be a sensible and intuitive way to split up models and views. Maybe you have &lt;code&gt;models.abstract&lt;/code&gt; or &lt;code&gt;views.api&lt;/code&gt;, &lt;code&gt;views.public&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;I'd prefer my models.py to be lengthy, rather than having to wrestle around with &lt;code&gt;__init__.py&lt;/code&gt; imports and &lt;code&gt;from .models.submodule import ModelName&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Service layers?&lt;/h3&gt;
&lt;p&gt;It's outside of the scope here to discuss the idea of service layers. I might discourage it, and I certainly don't feel like something like an &lt;code&gt;apps/blog/service.py&lt;/code&gt; would be appropriate in a template.&lt;/p&gt;
&lt;h3&gt;Shared libraries and abstract models&lt;/h3&gt;
&lt;p&gt;It makes sense to have a &lt;code&gt;common&lt;/code&gt; or &lt;code&gt;shared&lt;/code&gt; app, but seeing that it's not likely to have any concrete models, it's best to place it outside of your apps (so it's not a Django app) and project (since it's not really part of the Django project). That could look like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src/
  apps/
  project/
  shared/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Monorepos?&lt;/h3&gt;
&lt;p&gt;Not far from this discussion maybe: it's true that backend API projects could discuss whether they live alongside the frontend etc. I &lt;/p&gt;
&lt;h3&gt;Why "project" and not "config" or "site"?&lt;/h3&gt;
&lt;p&gt;There are some drawbacks to using these other names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;config&lt;/code&gt; or &lt;code&gt;conf&lt;/code&gt;: I think that &lt;code&gt;config/templates&lt;/code&gt; is wrong, if we allow an HTML file to be considered "configuration", then anything is configuration or we have to invent a NEW location for project-wide templates, static data, backends etc. It's not a terrible convention, definitely better than nothing. It's used by the project &lt;a href="https://github.com/adamghill/django-new"&gt;django-new&lt;/a&gt; for bootstrapping Django projects and apps.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;site&lt;/code&gt; could also work but I think &lt;code&gt;project&lt;/code&gt; works much better because we already have a concept of a "Django project", whereas a "Django site" would be something completely new and without clear advantages.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why is &lt;code&gt;tests/&lt;/code&gt; separate from &lt;code&gt;src/&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;In this article, we knowingly recommend to maintain all test-related code in its own &lt;code&gt;tests/&lt;/code&gt; folder/Python package.&lt;/p&gt;
&lt;p&gt;Maybe you've seen layouts where Django applications each have their own &lt;code&gt;app/tests/&lt;/code&gt; folder. This is a bad idea because your tests will be built into your application. If you have test fixtures, they will be loaded into your deployed application image. If you do a mistake, the entire test stack might be inspected/imported.&lt;/p&gt;
&lt;p&gt;A common "lazy" mistake of maintaining test code together with the application, is to start mixing test requirements with application requirements, too. This could ultimately result in pytest or playright ends up in production images.&lt;/p&gt;
&lt;h2&gt;Overriding Django middleware, db routers etc&lt;/h2&gt;
&lt;p&gt;I would propose that anything you extend and override from inside Django is placed in your src/project/ folder. We can see it as a kind of configuration. In certain cases (such as authentication) you might have an app's domain fit perfectly, but in most cases, you need a one-off customization that fits well in src/project/.&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;p&gt;Using template partials to avoid lots of files:&lt;br&gt;
&lt;a href="https://indieweb.social/@adamghill/115329417459835209"&gt;https://indieweb.social/@adamghill/115329417459835209&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Against service layers in Django &lt;br&gt;
&lt;a href="https://www.b-list.org/weblog/2020/mar/16/no-service/"&gt;https://www.b-list.org/weblog/2020/mar/16/no-service/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Why do we need apps? &lt;br&gt;
&lt;a href="https://forum.djangoproject.com/t/why-do-we-need-apps/827/28"&gt;https://forum.djangoproject.com/t/why-do-we-need-apps/827/28&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;HackSoft's Django Style Guide&lt;br&gt;
&lt;a href="https://github.com/HackSoftware/Django-Styleguide"&gt;https://github.com/HackSoftware/Django-Styleguide&lt;/a&gt;&lt;/p&gt;</description><pubDate>Sat, 18 Oct 2025 20:58:19 +0000</pubDate><guid>https://overtag.dk/v2/blog/django-project-structure/</guid></item><item><title>Deploying Django on a VPS</title><link>https://overtag.dk/v2/blog/django-vps-deployment/</link><description>&lt;p&gt;Containerizing applications comes with its own set of issues, but in a world where application and cloud platforms are pretty much the norm, it feels like we have to argue the case of using a VPS (virtual private server) in terms of &lt;em&gt;its&lt;/em&gt; advantages. The tables have turned, so rather than argue &lt;em&gt;why&lt;/em&gt; to containerize our applications, it seems like we need a little reminder about &lt;em&gt;the advantages of a VPS&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Share monitoring of several small projects and services&lt;/li&gt;
&lt;li&gt;Keep important services updated using a central package manager&lt;/li&gt;
&lt;li&gt;No Docker debugging&lt;/li&gt;
&lt;li&gt;Webhook automation for fast, downtime-free deployments without rebuilding containers&lt;/li&gt;
&lt;li&gt;SSH access "live in production"&lt;/li&gt;
&lt;li&gt;Easy cron jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How-to and ingredient list&lt;/h2&gt;
&lt;p&gt;As an example, here's a tiny how-to where I'll go through some steps. What we need is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;(optional) if you are firm with Bash, write your own &lt;em&gt;idempotent&lt;/em&gt; bootstrapping script&lt;/li&gt;
&lt;li&gt;An Ubuntu or Debian-based VPS with systemd&lt;/li&gt;
&lt;li&gt;Git + other essential dependencies&lt;/li&gt;
&lt;li&gt;Nginx and certbot&lt;/li&gt;
&lt;li&gt;Postgres (assuming you want a database)&lt;/li&gt;
&lt;li&gt;Gunicorn&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/adnanh/webhook"&gt;Webhook&lt;/a&gt;  for continuous deployment&lt;/li&gt;
&lt;li&gt;Other nice things&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. Optional: Write everything in a script&lt;/h3&gt;
&lt;p&gt;I highly recommend that you develop your own private script for bootstrapping a Django project on your VPS. This will help you to document what you do, and to reproduce it in the future.&lt;/p&gt;
&lt;p&gt;Also: Write the script to be "idempotent". This means that the script can always &lt;strong&gt;run again&lt;/strong&gt; and will to some degree simply update an existing setup. The main advantage is that you can continue to develop the script while using it to bootstrap a new project.&lt;/p&gt;
&lt;p&gt;If it's a Bash script (recommended), then make sure it has &lt;code&gt;set -e&lt;/code&gt; so it will crash when a step fails.&lt;/p&gt;
&lt;h3&gt;2. A Debian-based VPS with systemd&lt;/h3&gt;
&lt;p&gt;In this blog post, it's assumed that you have something with Debian on. Here are some things you should do to harden the VPS in general - even if it was to run Docker, you should still do this. There's basically now exception:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use unattended security upgrades: &lt;code&gt;sudo dpkg-reconfigure unattended-upgrades&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set your timezone: &lt;code&gt;sudo dpkg-reconfigure tzdata&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run SSH on a non-default port&lt;/li&gt;
&lt;li&gt;Add your own user to the &lt;code&gt;sudo&lt;/code&gt; group and disable the root user&lt;/li&gt;
&lt;li&gt;Don't allow password-based logins via SSH (use SSH keys)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;locale-gen en_US.UTF-8&lt;/code&gt; - often this is missing. If you have other locales, make sure to generate them, too.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxblog.io/remove-snap-ubuntu-22-04-lts/"&gt;Remove snapd&lt;/a&gt; (if you use an Ubuntu image)&lt;/li&gt;
&lt;li&gt;Consider &lt;a href="https://github.com/fail2ban/fail2ban"&gt;fail2ban&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Git + other essential dependencies&lt;/h3&gt;
&lt;p&gt;When bootstrapping the new VPS, there are some pretty common dependencies that you will need later. I'll just leave this single &lt;code&gt;apt install&lt;/code&gt; command, but I highly encourage that you document your own needs and write them in a script:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt install git python3-pip nginx certbot python3-certbot-nginx gettext postgresql postgresql-server-dev-all
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you want to use &lt;code&gt;pip&lt;/code&gt; instead of &lt;code&gt;uv&lt;/code&gt;, I recommend using the system package &lt;code&gt;python3-virtualenvwrapper&lt;/code&gt; so you can issue commands like &lt;code&gt;mkvirtualenv&lt;/code&gt; directly in your VPS. This how-to assumes you want to use &lt;code&gt;uv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This list has gotten shorter over time. Back in the days, Pillow (or Python-PIL) had a bunch of system requirements in order to compile things.&lt;/p&gt;
&lt;h3&gt;4. Nginx and certbot&lt;/h3&gt;
&lt;p&gt;Now we need to setup Nginx! We're gonna set it up to use SSL and you need to update DNS settings to point to your VPS before getting started.&lt;/p&gt;
&lt;p&gt;Nginx has a brilliant structure for VPS hosting many sites side-by-side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/nginx/sites-available&lt;/code&gt; contains ALL your configurations&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/nginx/sites-enabled&lt;/code&gt; contains symbolic links to the enabled configuration files from &lt;code&gt;../sites-available&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We use certbot and LetsEncrypt to fetch a certificate. I recommend setting this up in a way where you have a default server to listen on port 80 and thus bootstrap your first SSL certificate for a server on port 443.&lt;/p&gt;
&lt;p&gt;Do a quick &lt;code&gt;mkdir /var/www/certbot&lt;/code&gt;, then edit &lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt; to say the following:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # Allow certbot ACME root
    location /.well-known/acme-challenge {
        root /var/www/certbot;
    }

    # Redirect all traffic to https://
    location / {
       return 301 https://$host$request_uri;
    }

}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now run &lt;code&gt;systemctl reload nginx&lt;/code&gt; and let's issue our first certificate, supposing the DNS setup is correct:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;certbot certonly --cert-name mysite.com -d mysite.com  --webroot-path /var/www/certbot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now let's add some general SSL settings in &lt;code&gt;/etc/nginx/includes/ssl.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Listen on 443
listen 443 ssl;

# https://wiki.mozilla.org/Security/Server_Side_TLS
# http://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/

# Consider even removing TLSv1.2
ssl_protocols TLSv1.2 TLSv1.3;

# We will prefer a compatible client-chosen certificate
ssl_prefer_server_ciphers off;

# Recommended
ssl_ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM;

# Enable the below on Nginx 1.19.4 and later, disable if Nginx fails to start
# ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If this goes well, you're now ready to add the fist SSL configuration and then we can expand that later with our Django application. But for now, you can add the following in &lt;code&gt;/etc/nginx/sites-available/mysite.com&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Redirect traffic on www.mysite.com to mysite.com, still host acme challenge
server {
    server_name www.mysite.com;
    ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
    include &amp;quot;includes/ssl.conf&amp;quot;;

    location /.well-known/acme-challenge {
        root /var/www/certbot;
    }

    location / {
        return 301 https://mysite.com$request_uri;
    }
}

# Here is your actual mysite.com configuration
server {
    server_name mysite.com;

    include &amp;quot;includes/ssl.conf&amp;quot;;

    # Check if this certificate is really served for this server_name
    # http:/serverfault.com/questions/578648/properly-setting-up-a-default-nginx-server-for-https
    if ($host !~* &amp;quot;(www.)?mysite.com&amp;quot;) {
        return 400 &amp;quot;this is an invalid request&amp;quot;;
    }

    ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;

    # Adjust the allowed upload size
    client_max_body_size 50m;

    # location /favicon.ico {
    #     alias /var/sites/mysite/git/staticfiles/img/favicon.ico;
    # }

    # location /robots.txt {
    #     alias /var/sites/mysite/git/staticfiles/robots.txt;
    # }

    # location /media  {
    #     alias /var/sites/mysite/git/media;
    # }

    # location /static {
    #     alias /var/sites/mysite/git/staticfiles;
    # }

    # Webhook reverse proxy
    # location /hooks/ {
    #     proxy_pass http:/127.0.0.1:9000/hooks/;
    # }

    location / {
        try_files $uri @proxy;
    }

    location @proxy {
        # TODO: Add gunicorn reverse proxy
    }

}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Cron job for certbot&lt;/h4&gt;
&lt;p&gt;What's nice about a VPS is that you have a cron daemon builtin. So let's add a rule that automatically renews our certificate.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Try running &lt;code&gt;certbot renew --post-hook 'service nginx reload'&lt;/code&gt; to ensure that it works. Adjust as needed.&lt;/li&gt;
&lt;li&gt;As root, run &lt;code&gt;crontab -e&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make sure the following line appears (example renews at 1 AM everyday and adds &lt;code&gt;--quiet&lt;/code&gt; to the command): &lt;code&gt;0 1 * * * certbot renew --quiet --post-hook 'service nginx reload'&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5. Postgres&lt;/h3&gt;
&lt;p&gt;If you need Postgres, here are some nifty commands to run either manually or add to your script:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Assuming you fetch the username from command line arg $1
db_username=$1
# Assuming dbname is the same
postgresdb=$db_username

su postgres -c &amp;quot;createuser -P $db_username&amp;quot;
su postgres -c &amp;quot;createdb --owner $db_username $postgresdb&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;6. gunicorn&lt;/h3&gt;
&lt;h4&gt;Some comparisons between gunicorn and uWSGI on a VPS&lt;/h4&gt;
&lt;p&gt;As you might have heard, uWSGI is in maintenance mode. It means you should consider to use a different option for future projects. As Gunicorn through uvicorn also provides a nice path towards support of ASGI, you can consider this a good bet for the future.&lt;/p&gt;
&lt;p&gt;However, because uWSGI has such a lot of history, it also has a lot of things we should pay attention to, especially on a VPS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uWSGI centralizes configuration which makes things easier to manage and reuse. We should manage our gunicorn configurations in a way that's easy and so we don't lose track.&lt;/li&gt;
&lt;li&gt;uWSGI's debian package and Nginx plugin comes with bundled in Nginx parameters in /etc/nginx/uwsgi_params - we should also try to centralize our gunicorn parameters.&lt;/li&gt;
&lt;li&gt;Some reverse proxy headers are better defined out-of-the-box with uWSGI. We need to pay attention, especially when migrating existing projects.&lt;/li&gt;
&lt;li&gt;uWSGI is part of the operating system and integrates with systemd - we need to create and enable our own systemd scripts for each of our gunicorn services.&lt;/li&gt;
&lt;li&gt;Your application does not provide the application manager? Does that make better sense?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Adding a Django middleware&lt;/h4&gt;
&lt;p&gt;Because gunicorn is a reverse proxy, having Nginx in front isn't trivial. Many things are addressed in the Nginx settings and work well, but you have to pay attention to especially the fact that your HTTP request meta data have passed through one layer before it's handed to your web application. I recommend that you add an additional middleware in Django to handle the missing &lt;code&gt;request.META["REMOTE_ADDRESS"]&lt;/code&gt; (and any other rules you might add), which is expected to contain the client's IP:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;def reverse_proxy(get_response):
    def process_request(request):
        if &amp;quot;x-forwarded-for&amp;quot; in request.headers:
            request.META[&amp;quot;REMOTE_ADDR&amp;quot;] = request.headers[&amp;quot;x-forwarded-for&amp;quot;]
        return get_response(request)

    return process_request
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;7. Webhook for continuous deployment&lt;/h3&gt;
&lt;h3&gt;8. Other nice things&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Forward system mail to your actual mail account! Setup your email daemon to receive notifications, for instance when cron jobs fail.&lt;/li&gt;
&lt;li&gt;fail2ban&lt;/li&gt;
&lt;li&gt;Log rotation&lt;/li&gt;
&lt;li&gt;goaccess&lt;/li&gt;
&lt;/ol&gt;</description><guid>https://overtag.dk/v2/blog/django-vps-deployment/</guid></item><item><title>A Venn diagram of Open Source governance</title><link>https://overtag.dk/v2/blog/maintenance-venn/</link><description/><pubDate>Tue, 02 Jul 2024 15:05:03 +0000</pubDate><guid>https://overtag.dk/v2/blog/maintenance-venn/</guid></item><item><title>Duplicate form submissions and how to handle them in Django</title><link>https://overtag.dk/v2/blog/duplicate-form-submissions-and-how-to-handle-them-in-django/</link><description>&lt;p&gt;Let's have a look at &lt;strong&gt;duplicate form submissions&lt;/strong&gt;, understand the problem and fix it with a nice server-side solution. It's a common problem that almost always needs to be addressed - whether you are building HTML-over-the wire or a backend/frontend architecture, you will have the same issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User clicks the same button twice.&lt;/li&gt;
&lt;li&gt;User navigates back and re-submits a form that we already processed.&lt;/li&gt;
&lt;li&gt;User reloads the response page and resubmits same POST/GET form data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Typically, this is because of some disturbance in the network or your own server having hiccups that prompt the user to retry. The duplicate form submission can then trigger:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Duplicate database objects&lt;/li&gt;
&lt;li&gt;Duplicate user notifications&lt;/li&gt;
&lt;li&gt;Duplicate &lt;code&gt;anything_really&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;False-positive UI validation errors because of uniqueness constraints (first submission was valid!)&lt;/li&gt;
&lt;li&gt;Further unnecessary server-side errors in case of unhandled uniqueness errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Likely, the &lt;em&gt;worst&lt;/em&gt; part of this problem is already fixed with the "POST-response-redirect" pattern, where the post target responds with an HTTP 302 and sends the user to a "thank you" page. Since it's built into Django's generic form handling views, you've probably already done that part.&lt;/p&gt;
&lt;h2&gt;Desired properties&lt;/h2&gt;
&lt;p&gt;You can skip this if you don't care about what the solution aims to provide, but I'd love to hear back about anything that's been missed here!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MUST process duplicate form submissions exactly once.&lt;/li&gt;
&lt;li&gt;MUST allow user to perform duplicate form submission and succeed journey.&lt;/li&gt;
&lt;li&gt;MUST process non-duplicate form submissions (defining what "duplicate" means is the solution to this requirement).&lt;/li&gt;
&lt;li&gt;MUST allow duplicate form data from different user sessions (same as above, but a good illustration).&lt;/li&gt;
&lt;li&gt;MUST function across multiple application workers / containers / threads.&lt;/li&gt;
&lt;li&gt;SHOULD give user a meaningful response when submitting duplicate form data.&lt;/li&gt;
&lt;li&gt;SHOULD be generic and easy to reuse.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In terms of multiple application workers and containers, there are race-conditions in which I actually haven't solved the above 100%. UPDATE: See final discussion. &lt;/p&gt;
&lt;h2&gt;Client-side disabling the submit button &lt;em&gt;onsubmit&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;Here is a really simple snippet that resembles a common solution. You can add to avoid users accidentally or deliberately clicking the submit button multiple times:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// When a form is submitted, disable all its submit buttons&lt;/span&gt;
&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;form&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:submit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;disabled&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But &lt;strong&gt;STOP&lt;/strong&gt;: What could possibly go wrong?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The user never gets a second chance to submit the form, in case they had connection issues. So we'd need a timeout or some error handling to restore it to enabled state.&lt;/li&gt;
&lt;li&gt;If the user &lt;em&gt;does&lt;/em&gt; submit but receives an error in the browser (network error, Wi-Fi login etc) and they press "Back" in the browser, the button is still disabled!&lt;/li&gt;
&lt;li&gt;If you use &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt; properties to know what button the user clicked, this will be disabled before submitting the form.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because of all these issues, my opinion is that it's unfeasible to solve this with JavaScript, so let's just make the server understand if the form was submitted twice and give a meaningful response. It's a bit the same as when over-investing efforts in client-side validation - sure, it's nice (if implemented correctly), but you need to do it on the server anyways.&lt;/p&gt;
&lt;h2&gt;Database uniqueness constraints&lt;/h2&gt;
&lt;p&gt;Another preventive measure can be to add uniqueness constraints to your database. If you can do this, you should. And then you should handle these errors gracefully.&lt;/p&gt;
&lt;p&gt;Here's some code that we &lt;em&gt;might&lt;/em&gt; use to handle duplicate form submissions:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

     &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;form_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="c1"&gt;# Try to save the form, assume that IntegrityErrors from the database&lt;/span&gt;
         &lt;span class="c1"&gt;# are because we already saved the same form. &lt;/span&gt;
         &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
             &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;IntegrityError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
             &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This form was probably already saved.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;thank-you&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice though, that we don't actually know if it was the case that the form failed because it was a double-submission? We might also have form validation that kicks in before we try saving the object. In any case, handling the constraint error at this level, we risk informing the user that their form was saved - when it actually wasn't! We need to &lt;em&gt;know&lt;/em&gt; that it was saved.&lt;/p&gt;
&lt;p&gt;We can also do this while validating the form. In this case, we block the form and the user receives a validation error in the UI. But then the validation error might shadow the initial successful submission of the form! Remember that we are dealing with the problem of &lt;strong&gt;double-submissions&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;So what &lt;em&gt;can&lt;/em&gt; you do?&lt;/h2&gt;
&lt;p&gt;Let's get started with an approach for rejecting form POST data that we've already processed (it's not quite perfect though):&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyCreateView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;form_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

        &lt;span class="c1"&gt;# Unique key to store session for this view&lt;/span&gt;
        &lt;span class="n"&gt;session_form_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;form-submission+&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__class__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="c1"&gt;# Unique key for storing the pk when the object is created&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_last_saved_pk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session_form_hash&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-pk&amp;quot;&lt;/span&gt;

        &lt;span class="c1"&gt;# Calculate hash of the POST data&lt;/span&gt;
        &lt;span class="n"&gt;excluded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;csrfmiddlewaretoken&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;this_post_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;excluded&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Previously calculated hash&lt;/span&gt;
        &lt;span class="n"&gt;previous_post_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_form_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Form has already been processed!&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;this_post_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;previous_post_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Fetch the previous object and return a helpful message to the user&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_object_or_404&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_last_saved_pk&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;This form was submitted several times. It has been processed just once.&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponseRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_success_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_form_hash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;this_post_hash&lt;/span&gt;
            &lt;span class="c1"&gt;# self.object is defined when using ModelFormMixin&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_last_saved_pk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We're almost there:&lt;/p&gt;
&lt;p&gt;Before the form is saved, we start by hashing its POST data (excluding the &lt;code&gt;csrfmiddlewaretoken&lt;/code&gt; which can change). We're sorting the keys before hashing, so we can say that the hashing method is consistent.&lt;/p&gt;
&lt;p&gt;We store the hash with a session key that's unique for this particular view. This means that we can detect duplicates that are unique for the current user's session and we can also share it across application workers/threads/containers.&lt;/p&gt;
&lt;p&gt;Finally, we check if any existing form POST data for this particular form and user was hashed. If not, we proceed to saving the form and marking it saved. If it &lt;em&gt;was&lt;/em&gt; already saved, we inform the user that the form was saved on the thank-you page and add a message that we registered duplicate submissions.&lt;/p&gt;
&lt;p&gt;But we only do this in the &lt;code&gt;form_valid&lt;/code&gt; view. If that's good enough, you can stop here. But if you have constraints and validation rules that would make the second form submission invalid, it will trigger the final form submission to come back as invalid. The user won't know that the first form was correctly saved.&lt;/p&gt;
&lt;h2&gt;Icing on the cake 🍰️&lt;/h2&gt;
&lt;p&gt;We're gonna continue to hash and save our new object's PK in the &lt;code&gt;form_valid()&lt;/code&gt; method, but we are going to move the duplicate check to the &lt;code&gt;post()&lt;/code&gt; method. That way, we can avoid issues with invalid form data when it's due to uniqueness constraints kicking in. In fact, we just skip processing and validating the form all-together when we've already seen it.&lt;/p&gt;
&lt;p&gt;Win-win! 💯️&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyCreateView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

        &lt;span class="c1"&gt;# Unique key to store session for this view&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_form_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;form-submission+&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__class__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="c1"&gt;# Unique key for storing the pk when the object is created&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_last_saved_pk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session_form_hash&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-pk&amp;quot;&lt;/span&gt;

        &lt;span class="c1"&gt;# Calculate hash of the POST data&lt;/span&gt;
        &lt;span class="n"&gt;excluded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;csrfmiddlewaretoken&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;excluded&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Previously calculated hash&lt;/span&gt;
        &lt;span class="n"&gt;previous_post_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_form_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Form has already been processed!&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;previous_post_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Fetch the previous object and return a helpful message to the user&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_object_or_404&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;MyModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_last_saved_pk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;This form was submitted several times. It hans been processed just once.&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponseRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_success_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;form_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_form_hash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;this_post_hash&lt;/span&gt;
        &lt;span class="c1"&gt;# self.object is defined when using ModelFormMixin&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_last_saved_pk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By storing the POST hash &lt;em&gt;after&lt;/em&gt; the fact of validating and saving the form, we are guaranteed against wrongly blocking a form submission. And we'll know that it was previously validated and saved already before validating the next form submission, catching any duplicate form submissions.&lt;/p&gt;
&lt;h2&gt;What's next?&lt;/h2&gt;
&lt;p&gt;Because I think that any project would need to adjust this to their own definition of what a "double submission" is, I'm going to leave this here for now. But we might be able to brew together a nice view decorator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; There are a couple of edge-cases with respect to high load and concurrency that you might consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Race-conditions: Processing the form in the database can take time, leaving a gap for double submissions to enter while another submission is being saved in the database by another worker/thread/container.&lt;/li&gt;
&lt;li&gt;Session and cache invalidation: We use a session to store form hashes. If you have a LOT of sessions, consider that the timeout for a session is much higher than what is needed to guard against duplicate form submissions. So you might use a cache item directly and base it off the current session ID, but setting a timeout to some amount of minutes.&lt;/li&gt;
&lt;li&gt;In any case, remember to always run &lt;a href="https://docs.djangoproject.com/en/5.0/ref/django-admin/#django-admin-clearsessions"&gt;clearsessions&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I didn't have any large-scale or high load/concurrency use cases on my radar, but if you have any inputs, &lt;a href="https://social.data.coop/@benjaoming/111839493753905259"&gt;join the conversation in the Fediverse&lt;/a&gt;.&lt;/p&gt;</description><pubDate>Sun, 28 Jan 2024 14:10:58 +0000</pubDate><guid>https://overtag.dk/v2/blog/duplicate-form-submissions-and-how-to-handle-them-in-django/</guid></item><item><title>Code globally, meet locally</title><link>https://overtag.dk/v2/blog/code-globally-meet-locally/</link><description>&lt;p&gt;I recently shared initial thoughts around &lt;a href="https://overtag.dk/v2/blog/local-conferences-big-potential/"&gt;Local conferences: Big potential!&lt;/a&gt; In this follow-up blog post, I want to share practical information on the specific format that we've been organizing &lt;a href="https://2023.djangoday.dk/about/"&gt;Django Day Copenhagen&lt;/a&gt; around -- aaaand finish with some good old motivation 🥕️&lt;/p&gt;
&lt;p&gt;With a practical focus, I hope that the blog post can make people consider &lt;em&gt;for real&lt;/em&gt; that doing a small local conference is easy, fun and worth-while!&lt;/p&gt;
&lt;p&gt;In October, we hosted a little event called Django Day Copenhagen. It was the 4th time that we hosted the event, which attracts around 50-60 people to the venue and an additional ~100 live streamers. After the event, the live stream is watched many times more (2022's live stream had 1.3k views), and each of the talk recordings already have some 100-500 views.&lt;/p&gt;
&lt;p&gt;More importantly for this blog post, we can compare with our experience from a large international event since we hosted the &lt;a href="https://2019.djangocon.eu/"&gt;DjangoCon Europe 2019&lt;/a&gt; in Copenhagen, a 3-day event for 400 people. So we know a lot about larger events.&lt;/p&gt;
&lt;p&gt;Having local conferences isn't meant as an alternative to all larger conferences. It's meant as a reminder to communities that &lt;em&gt;unnecessarily&lt;/em&gt; converge on &lt;em&gt;only&lt;/em&gt; large conferences: There are alternatives for community and educational events that we are missing out on. I'll get back to this at the end of the blog post.&lt;/p&gt;
&lt;h2&gt;What is the concept?&lt;/h2&gt;
&lt;p&gt;Let's start with a definition of the format of our small local conference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 1-day single-track event, for which you probably need 6-10 talks to fill the day.&lt;/li&gt;
&lt;li&gt;A smaller venue with room for 50-150 people.&lt;/li&gt;
&lt;li&gt;We want to ultimately attract a local audience.&lt;/li&gt;
&lt;li&gt;...but our speakers might travel from abroad, and so will people with an admirable motivation.&lt;/li&gt;
&lt;li&gt;We keep down costs, for instance we consider local socio-economic enterprises for catering and municipal/cultural spaces as venues.&lt;/li&gt;
&lt;li&gt;We use online pre-sales for tickets.&lt;/li&gt;
&lt;li&gt;Corporate supporter tickets (=no sponsorship program!)&lt;/li&gt;
&lt;li&gt;No conference booths.&lt;/li&gt;
&lt;li&gt;Breakfast and lunch included at the venue.&lt;/li&gt;
&lt;li&gt;Volunteer-driven (except recording and streaming).&lt;/li&gt;
&lt;li&gt;Online streaming&lt;/li&gt;
&lt;li&gt;Small budget for speaker transport or accommodation.&lt;/li&gt;
&lt;li&gt;Tiny budget and margins for error, meaning that we don't run big financial risks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...and with that said, I think everyone should adjust this to their needs and their venue.&lt;/p&gt;
&lt;h2&gt;Some specific goals&lt;/h2&gt;
&lt;p&gt;Along with the conference format, it's always good to keep in mind why you're interested in doing a conference, but more importantly a local one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It's fun, you meet a lot of people, and you can stress less with a local event.&lt;/li&gt;
&lt;li&gt;Reduce the climate impact of conferences: Most emissions caused by conferences are from attendee travel. Large conferences are unsustainable for our planet.&lt;/li&gt;
&lt;li&gt;Improve economic inclusion: You might already be affected by the global economic depression, but many people were already excluded from conference events before recent events have caused a decline in conference budgets.&lt;/li&gt;
&lt;li&gt;Strengthen socializing and local organizing: The Open Source movement as a whole will benefit from more local initiatives, as in the early days of "user groups" and later days of Meetups.&lt;/li&gt;
&lt;li&gt;And all the usual goals of a conference: Strengthen the community and interests in your favorite technology, for the benefit of a new generation, renewed commercial interest, innovation etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Steps to get started&lt;/h2&gt;
&lt;p&gt;Let's go through some practical ideas about how organizing a small local conference could look. This doesn't mean copy this "1:1", there's guaranteed to be parts that you can do better or differently or not at all.&lt;/p&gt;
&lt;h3&gt;Financial body&lt;/h3&gt;
&lt;p&gt;Let's start with the most simple aspect of the setup: In order to sell tickets and pay for things, you should probably consider running this through a real legal organization (as opposed to a private person). Typically that means a member-based association or non-profit.&lt;/p&gt;
&lt;p&gt;Note: You might be able to skip all this, if you can identify an existing financial body to run the event! Maybe there's an existing Open Source organization in your area that you can ask to host this?&lt;/p&gt;
&lt;p&gt;If you are just getting started, then set aside time to register the organization and its bank account. But don't let it stop you, just ensure you have enough time for the bureaucratic procedures that are necessary before you can sell your first ticket.&lt;/p&gt;
&lt;p&gt;By working as an association, we have become eligible for cultural and municipal venues, which are exclusively for non-profit activities. This is a big advantage. At the same time, an association can act as a company and sign up for various banking and online services that we need.&lt;/p&gt;
&lt;p&gt;If you want to do this as a private person, it's possible but not very recommendable. It will severely limit your reach. It will be hard to sell tickets online, and many attendees prefer to have an invoice. You'll have to be creative on a level that's going to take away focus, and it's probably more suitable to consider doing a "meetup" format where all costs are handled through donations or contributions from local companies.&lt;/p&gt;
&lt;h3&gt;Corporate Supporters: Skip the sponsorship program&lt;/h3&gt;
&lt;p&gt;Having a real sponsorship program implies a lot of tasks. You need to define exactly what the sponsor gets in terms of visibility and implement special ways to care for this.&lt;/p&gt;
&lt;p&gt;We simplify this work by offering a simple one-size-fits-all "Corporate Supporter" ticket. The ticket gives a standard experience for all supporters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 sizes: 1 ticket, 3 tickets and 5 tickets&lt;/li&gt;
&lt;li&gt;Logo on website&lt;/li&gt;
&lt;li&gt;Gratitude during the event&lt;/li&gt;
&lt;li&gt;Logo on sticker sheet (for the largest level)&lt;/li&gt;
&lt;li&gt;Access to the chat to post job offers&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Party&lt;/h3&gt;
&lt;p&gt;We finish Django Day Copenhagen with a party. Everyone gets some time after the event to go and get dinner somewhere and recharge, then we meet again after a 2 hour break for drinks and pub quiz.&lt;/p&gt;
&lt;p&gt;We have done this in 2 flavors: Either we have a venue that can also accommodate a party where we sell our own drinks in the bar, or we book a big pub somewhere close to the venue.&lt;/p&gt;
&lt;p&gt;It's a big advantage to host the party: It's fun to be in the the bar for volunteers, and the profits from hosting a party are enough to ensure that the event finishes off with a little extra financial boost.&lt;/p&gt;
&lt;p&gt;The disadvantage of hosting the party is that you'll have to shop everything for the bar. For all our events, we shopped all drinks and everything that wasn't sold at the party has been sold off afterwards. So we never had any issues with unsold drinks. To generalize this: Running a support-party is fun and very risk-free.&lt;/p&gt;
&lt;h3&gt;Thoughts on improving at Django Day Copenhagen&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Outreach: We didn't do any direct outreach for the event, despite our knowledge of several companies using Django that do not attend.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Posters were too much effort: We printed posters but people weren't interested. We conclude that probably it's not worth the money and effort to add physical merchandise on this level.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We also put up posters at 2 local universities but no one attended the event because of this. Advertising the event at universities requires a presence there, for instance by visiting relevant lectures and inviting students. We consider making it extremely cheap for students to attend, since the generational overlap is critical.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The evening before the event is for watching TV: As an organizer, I fell into the trap of doing stuff the evening before the event. I simply don't have the extra deposits of energy currently to pull through a full-day event like this without getting an insane headache. So I would say that anything that isn't done before 19:00 on the evening before will not get done.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The ticket sales should close no more than 3 days before the event, as many people seem to forget to buy a ticket. We introduced "late bird" tickets which tackles the issue of people buying too late by offering them an option without food. The ticket doesn't include any discount, so there's no reward for buying too late.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;First steps?&lt;/h3&gt;
&lt;p&gt;To do all this, you need to assemble a team, and you need to organize all tasks on a practical level. Here are some example columns for a project Kanban board:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pre-launch tasks: Organization and venue&lt;/li&gt;
&lt;li&gt;Launch: announcements, ticket sales&lt;/li&gt;
&lt;li&gt;Before event&lt;/li&gt;
&lt;li&gt;Outreach&lt;/li&gt;
&lt;li&gt;Content and talks&lt;/li&gt;
&lt;li&gt;Venue &lt;/li&gt;
&lt;li&gt;Day before&lt;/li&gt;
&lt;li&gt;During the day&lt;/li&gt;
&lt;li&gt;After the event&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Daniele Procida has previously written a very nice &lt;a href="https://conference-handbook.readthedocs.io/"&gt;Community conference organisers’s handbook&lt;/a&gt;, however this guide is written for large events and will probably make organizing a conference look overwhelming to most people looking to organize a small 1-day event.&lt;/p&gt;
&lt;p&gt;I would like to offer to do a simple practical handbook for getting started with Local conferences. If you're interested in seeing a more practical step-by-step handbook, please get in touch and motivate me!&lt;/p&gt;
&lt;h2&gt;Vision: Less large international conferences? ✈️ 🏨️ ➡️ 👣️ 🛖️&lt;/h2&gt;
&lt;p&gt;So, to finish off this long list of practical stuff, let's just talk about the need for large conferences vs. smaller local events.&lt;/p&gt;
&lt;p&gt;Let's start by thinking about our technology as if it was a cultural event. Think about all these big open source projects like Django, Ruby on Rails or Laravel. Or if you like, their language ancestors Python, Rust, Go, JavaScript etc. Or even the bigger topics like Security, DevOps - or the more common "Web". Conferences happen on big stages so important speakers can reach large audiences with their important subjects. Afterwards, there's maybe some time for questions and the speakers can mingle a bit.&lt;/p&gt;
&lt;p&gt;The audience and the speakers all travel to one location somewhere on the globe or on their continent.&lt;/p&gt;
&lt;p&gt;Would this work in a cultural space? No, not really. Artists travel to their audiences, although as mentioned in &lt;a href="https://overtag.dk/v2/blog/local-conferences-big-potential/"&gt;my previoues blog post&lt;/a&gt;, a recent episode of BBC's &lt;a href="https://www.bbc.co.uk/sounds/play/w3ct5bkf"&gt;The Climate Question&lt;/a&gt; concluded that even though it's the case that artists tour around, then the most significant climate impact of music festivals remains: audience travel. So that's what we need to focus on.&lt;/p&gt;
&lt;p&gt;Adding calls for &lt;em&gt;more&lt;/em&gt; diversity, &lt;em&gt;more&lt;/em&gt; first-time speakers etc., I propose a hypothesis like this:&lt;/p&gt;
&lt;p&gt;If our tech communities grow more local communities, more speakers will appear. If we have more speakers, we can afford a conference model with more speakers available to travel to local conferences, and audiences can converge on local events rather than big international events.&lt;/p&gt;
&lt;p&gt;This is how cultural events work: Musicians travel, stand-up comedians travel, circuses travel etc. The show comes to your local community, and so should tech events.&lt;/p&gt;
&lt;p&gt;By growing a local tech scene, we can adapt the whole system to depend less on expensive and climate-intensive shit like airplanes and hotels, and we can build more sustainable communities, both in social terms and in terms of the environment and the future economy of tech.&lt;/p&gt;</description><pubDate>Tue, 19 Dec 2023 16:24:06 +0000</pubDate><guid>https://overtag.dk/v2/blog/code-globally-meet-locally/</guid></item><item><title>A settings pattern for reusable Django apps</title><link>https://overtag.dk/v2/blog/a-settings-pattern-for-reusable-django-apps/</link><description/><pubDate>Sun, 10 Dec 2023 19:40:06 +0000</pubDate><guid>https://overtag.dk/v2/blog/a-settings-pattern-for-reusable-django-apps/</guid></item><item><title>Local conferences: Big potential!</title><link>https://overtag.dk/v2/blog/local-conferences-big-potential/</link><description/><pubDate>Thu, 05 Oct 2023 19:46:48 +0000</pubDate><guid>https://overtag.dk/v2/blog/local-conferences-big-potential/</guid></item><item><title>Incoming blog posts!</title><link>https://overtag.dk/v2/blog/incoming-blog-posts/</link><description/><pubDate>Tue, 26 Sep 2023 09:47:49 +0000</pubDate><guid>https://overtag.dk/v2/blog/incoming-blog-posts/</guid></item><item><title>Why I worked in the Danish public sector and stopped</title><link>https://overtag.dk/v2/blog/why-i-worked-in-the-danish-public-sector-and-stopped/</link><description/><guid>https://overtag.dk/v2/blog/why-i-worked-in-the-danish-public-sector-and-stopped/</guid></item><item><title>How to actually install Node-js on Debian/Ubuntu</title><link>https://overtag.dk/v2/blog/how-actually-install-node-js-debianubuntu/</link><description>&lt;p&gt;This post is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A rant and&lt;/li&gt;
&lt;li&gt;The simple instructions&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The tl;dr rant&lt;/h2&gt;
&lt;p&gt;In order to install Node.js, you should execute a 330 line shell script directly from their servers. It's done this way such that the end user doesn't have to understand what's going on, and such that strange edge cases from old systems can be automatically fixed.&lt;/p&gt;
&lt;p&gt;This is part of a larger trend of piping unread/unverified contents from the HTTP to a sudo-privileged shell. The hypothesis seems to be that users of all levels don't need to understand what is actually going on.&lt;/p&gt;
&lt;p&gt;In order to for instance build an efficient Docker image that contains Node.js, you have to reverse engineer the 330 lines of code to understand the essential parts. Which is actually just 2 things (getting to that after the rant).&lt;/p&gt;
&lt;h3&gt;The security concerns:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Responses on forums and Stackoverflow channels, where an attacker can manipulate official instructions from &lt;code&gt;curl -sL https://install.hype.io | sudo -E bash -&lt;/code&gt; to &lt;code&gt;curl -sL https://install.malicious.io | sudo -E bash -&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Domain renewal/hijacking? Nodejs is a large organization that probably will be sure to renew &lt;code&gt;nodesource.com&lt;/code&gt;, but what about the overloaded BDFL maintainer that wants to be with the cool kids, loses interest and has instructions all over the internet with &lt;code&gt;curl -Sl install.bdfl-project.io | sudo -E bash -&lt;/code&gt; ? Next thing we know, having a Wordpress on a website that used to contain documentation (for instance, pipenv.org got squatted) will be that the "official" installation script is replaced by malware.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The scaled up inefficiency:&lt;/h3&gt;
&lt;p&gt;Moreover, they can't seem to decide if you have wget or curl in your system.. so it automatically finds out for you. Facepalm.&lt;/p&gt;
&lt;p&gt;On a clean Ubuntu/Debian system, all that will happen is this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Trust their PGP signing key
wget -qO- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
# Add the binary release repository
echo &amp;#39;deb https://deb.nodesource.com/${NODEREPO} ${DISTRO} main&amp;#39; &amp;gt; /etc/apt/sources.list.d/nodesource.list
# For some reason, add the source repo, too, even though 99% of people won&amp;#39;t use that
echo &amp;#39;deb-src https://deb.nodesource.com/${NODEREPO} ${DISTRO} main&amp;#39; &amp;gt;&amp;gt; /etc/apt/sources.list.d/nodesource.list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Show me the code&lt;/h2&gt;
&lt;p&gt;Node.js on Ubuntu, adjust as you like.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# Trust their PGP signing key
sudo su -c &amp;quot;wget -qO- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -&amp;quot;
# Add the latest 13.x binary release repository for Ubuntu bionic (18.04)
sudo su -c &amp;quot;echo &amp;#39;deb https://deb.nodesource.com/node_13.x bionic main&amp;#39; &amp;gt; /etc/apt/sources.list.d/nodesource.list&amp;quot;
# Update apt
sudo apt update
# Install nodejs
sudo apt install nodejs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description><guid>https://overtag.dk/v2/blog/how-actually-install-node-js-debianubuntu/</guid></item><item><title>Taking back Firefox from the Internet</title><link>https://overtag.dk/v2/blog/taking-back-firefox-internet/</link><description>&lt;p&gt;Firefox wants to connect to quite a lot of services. And it's highly recommended to read the article &lt;a href="https://support.mozilla.org/en-US/kb/how-stop-firefox-making-automatic-connections"&gt;How to stop Firefox from making automatic connections&lt;/a&gt;, in which Mozilla explains and discloses a lot of these.&lt;/p&gt;
&lt;p&gt;The article gives an impression of how contemporary software developers perceive their authority or privilege to access the user's Internet connection and whatever environmental footprint follows from these pingbacks, updates etc.&lt;/p&gt;
&lt;p&gt;I'd like to point out the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;These decisions take the user's bandwidth for granted by default&lt;/li&gt;
&lt;li&gt;Connection retries, polling intervals etc. can be arbitrary and non-configurable.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Taking a user's bandwidth for granted&lt;/h2&gt;
&lt;p&gt;Recall that Firefox isn't the only software installed on a device or computer. Nowadays, software developers increasingly see their projects as outside or above an ecosystem and want them distributed in entire sandboxes (Flatpak, Snap etc). The decision of Firefox surely isn't unique.&lt;/p&gt;
&lt;p&gt;And one of the peculiar things about this decision is that most systems already have for instance the network hotspot detection built-in. I have seen it on all major platforms: Mac, Windows and Ubuntu. &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1363651"&gt;Firefox still floods "detectportal.firefox.com" with requests&lt;/a&gt;.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/taking-back-firefox-internet/</guid></item><item><title>Why I work for Learning Equality</title><link>https://overtag.dk/v2/blog/why-i-work-learning-equality/</link><description>&lt;p&gt;&lt;i&gt;Opinions and motivations of a tech-skeptical software developer @ an NGO working in education&lt;/i&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;i&gt;Building a future with equal access to learning opportunities (via technology!) is a long and winding road: We need to consider sustainable scenarios for our technology and face up to true challenges — not be tempted by hype or simple fixes.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;&lt;h2&gt;Anecdotes from when things were easier&lt;/h2&gt;&lt;p&gt;About a decade ago, when bandwidth was a sparsity in rich and poor countries alike, designing and developing offline-friendly technologies was prioritized. Software ran on computers without internet access, things got mirrored on local networks etc. Not least, we were used to sharing documents and media with CDRs and USB flash, a.k.a. The &lt;a href="https://en.wikipedia.org/wiki/Sneakernet"&gt;Sneakernet&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Almost any computer or laptop could be opened with the same screw driver.&lt;/p&gt;&lt;p&gt;From my experience of working with schools in Malawi since 2011, offline education technology support has been an increasing challenge. Open access contents, educational resources and information databases have become more and more dependent on online platforms. One example is &lt;a href="https://www.khanacademy.org/"&gt;Khan Academy&lt;/a&gt;. It’s a highly complex and interactive website (with a simple UI!), only available to people with internet access. But back in 2011, it was a static website that could easily be downloaded and made portable on a hard drive and shared on a local network. Khan Academy provides great content, and they allow other actors such as &lt;a href="https://learningequality.org/"&gt;Learning Equality&lt;/a&gt; to extend this content into the offline domain through a permissive license. That’s how &lt;a href="http://learningequality.org/ka-lite/"&gt;KA Lite&lt;/a&gt; was started (and eventually how I came to know about Learning Equality and join the team to help build platforms for sharing educational resources offline).&lt;/p&gt;&lt;p&gt;&lt;embed alt="Technicians from Mzuni" embedtype="image" format="fullwidth" id="4"/&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;Technicians (in Mzuzu, Malawi) installing software, replacing faulty hardware, instructing teachers: they are truly enablers, because they create the necessary environment for all the changes that advocates of a digital education would like to see. We can talk about technological enablers, but it’s actually people who maintain it, explain it, and configure it, who are key. (Photo: Benjamin Bach, &lt;a href="https://creativecommons.org/licenses/by/2.0/"&gt;CC BY 2.0&lt;/a&gt;)&lt;/i&gt;&lt;/p&gt;&lt;p&gt;Back in 2011, I was working with &lt;a href="http://www.fairdanmark.dk/"&gt;FAIR Denmark&lt;/a&gt;, and we left 4 refurbished rack servers running real Wikipedia (not just a static copy) on Ubuntu+Apache+Mediawiki+MySQL. They were installed in secondary school centres, serving approximately 50 computers through a cabled Ethernet. It had taken months to download and make Wikipedia work, with its 600+ extensions and ridiculous amount of image files. Finally, people could see photos in their original resolution and complete a free-text search in less than 10 seconds. It was actually pretty cool, because it was the same experience as having a real Internet connection: The slow search was compensated by the fast local bandwidth. We thought it was fine, but lo we forgot to calculate how fast Wikipedia was growing. 2 years later, it was evident that images could no longer fit on a hard drive, and the database was too big for text searches on a simple desktop server.&lt;/p&gt;&lt;p&gt;In 2016, I visited a centre that had originally had its server &lt;a href="https://www.fairdanmark.dk/da/gallery/15/131/"&gt;installed in 2011&lt;/a&gt;. Mponela and Malawi in general are dusty and warm places, and the hard drive with the database and all the images was an external USB 2.0 drive, consumer-grade. Surprisingly after 5 years, the rack server still worked, and the Wikipedia contents from 2011 were still showing (full disclosure: after 6 years, it went to lunch). Classroom computers that hadn’t developed hardware faults were still booting Edubuntu 10.04. Other places had maintenance and hardware swaps, but this particular server was the real deal. Stable software and stable hardware ❤.&lt;/p&gt;&lt;p&gt;The consideration here is not the encounter of an abnormally well-functional set of hardware and software, but real evidence of something that’s possible: Imagine what can be done educationally when an ICT centre Just Works™! Imagine a software and hardware platform that just spews out educational contents, and does so for years and years without any need for software updates, adapting to new functions or user interfaces. Just uninterrupted and simple delivery of educational contents.&lt;/p&gt;&lt;p&gt;&lt;embed alt="Ben and Andrew" embedtype="image" format="fullwidth" id="5"/&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;2011: Refurbished rack servers were available in abundance, and there was a low complexity of distributing online through local networks. So we did that, but online resources have gotten more complicated since then. Also: Almost any laptop and computer could be opened only with one type of screw driver. (Photo: Benjamin Bach, &lt;a href="https://creativecommons.org/licenses/by/2.0/"&gt;CC BY 2.0&lt;/a&gt;)&lt;/i&gt;&lt;/p&gt;&lt;p&gt;Lots has changed since then: It’s not possible to scrape huge websites like Wikipedia without having &lt;a href="http://kiwix.org/"&gt;a customized platform&lt;/a&gt;. Ubuntu no longer fits on a 700 MB CDR. Application memory footprints are increasing (you need at the very least 1 GB to run a modern desktop), and user interfaces are changing faster than educational books are published. Developers are shifting focus from the desktop applications over to web platforms, so for instance &lt;a href="https://lists.ubuntu.com/archives/ubuntu-devel/2016-March/039281.html"&gt;we no longer have Edubuntu&lt;/a&gt;, the education-flavored version of Ubuntu.&lt;/p&gt;&lt;p&gt;Most experiences and software setups deployed some 6 years ago are now obsolete. But I have to say: Back then, things were more stable, changed less frequently, and were less complex in their nature.&lt;/p&gt;&lt;p&gt;The world back then &lt;em&gt;kind of&lt;/em&gt; didn’t need Learning Equality, but now it certainly does!&lt;/p&gt;&lt;h2&gt;Complexity on the rise&lt;/h2&gt;&lt;p&gt;I’m skeptic about most things that relate to technology, but working for Learning Equality is not a compromise. I’ve seen many dusty computers providing little or zero value for recipients in development projects, and I have also seen digital content sitting stale on servers because of a lack of training or understanding of relevance (see also &lt;a href="https://blog.learningequality.org/pig-sickness-and-hummingbird-hopes-in-mexico-city-e4c7f58b9624?gi=64281f80bc29"&gt;Shivi’s post about Too-Muchness&lt;/a&gt;). I know that KA Lite is not some magic quick-fix for education: Amongst many decisive factors, it needs teachers, and those teachers need training; It needs computers, and those computers need electricity.&lt;/p&gt;&lt;p&gt;By allowing ourselves to be honest about these challenges and listening to experience, we can navigate our way to address the root causes of poverty and inequality, by removing barriers to quality education caused by dependency on broadband internet, intellectual property and technological disruption.&lt;/p&gt;&lt;p&gt;Recognizing the threat of complexity and disruption, is especially important for technologists who design and develop software that should have a long-term scope, the life-cycle of which does not include daily automated updates.&lt;/p&gt;&lt;h2&gt;It’s ideology!&lt;/h2&gt;&lt;p&gt;January 11th marked 5 years since we lost &lt;a href="https://en.wikipedia.org/wiki/Aaron_Swartz"&gt;Aaron Swartz&lt;/a&gt;, an activist and leader of making information available to those who need it the most. To some, he broke the law, but to me, he sacrificed everything he had for a better world. Sharing information with someone who needs it is a moral imperative, as he put it in the &lt;a href="https://archive.org/stream/GuerillaOpenAccessManifesto/Goamjuly2008_djvu.txt"&gt;Open Access Manifesto&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I always loved open source as an obvious choice for humanity: Why can’t everyone just share their software inventions freely? If competition is about everyone sitting in their own corner, re-inventing their own wheels, I’m out.&lt;/p&gt;&lt;p&gt;As a side-effect, I caught an allergy for acts of redundant inventing/innovation/disruption hype. I think by now, half my work is about support, community, integrating and helping other people’s contributions, and staying in touch with other people in the field — the other half of my work is software development. And that’s a good thing, because imagine all the &lt;a href="http://www.dailymail.co.uk/sciencetech/article-1381528/Knight-Ridder-tablet-looks-just-like-iPad-17-YEARS-OLD.html"&gt;re-invented wheels&lt;/a&gt;, if we didn’t coordinate our work and sharing our visions, plans and technology across teams and organizations.&lt;/p&gt;&lt;p&gt;Making true open source is an attitude of collaboration: communication and integration with the outside world, not just some legal license.&lt;/p&gt;&lt;p&gt;In order to create true value, we need to highlight &lt;a href="https://thenewinquiry.com/blog/my-working-will-be-the-work-maintenance-art-and-technologies-of-change/"&gt;the labor of maintenance&lt;/a&gt;, and to not get obsessed with innovation. It’s about long-term commitment to keep software functional for the coming generation of computers, and long enough for schools and teachers to learn about the platform, collect experience and integrate it; repeat. These processes are not likely to benefit from technological disruption.&lt;/p&gt;&lt;p&gt;Seeing open source as a principle in aid and education can help to reduce or even eliminate the &lt;a href="https://en.wikipedia.org/wiki/Colonialism"&gt;old power divides between rich and poor countries&lt;/a&gt;. Not wanting something back and not retaining ownership is critical to allow for the freedom of a society and its educational institutions to develop.&lt;/p&gt;&lt;p&gt;My work could be said to be in the scope of &lt;a href="https://en.wikipedia.org/wiki/Information_and_communication_technologies_for_development"&gt;ICT4D&lt;/a&gt;, Information and Communication Technology for Development, a very vibrant and intersecting field, both in academia, amongst NGOs and in the commercial aid sector. I would prefer to argue against this as a constructive term, although it may unify a lot of organizations, people, and projects! Technology is never neutral, and neither is the perception of development. They both quickly become terms that are too broad and overloaded, justifying criticism of manipulation, alternative interests, not addressing fundamental structures that uphold the global inequality, poverty divides etc. Those which we ultimately seek to eliminate as a consequence of our actions of global solidarity and compassion. I prefer to work with education, teacher training, tech support, maintenance and sustainability: The lesser hyped, but classical terms. Nonetheless accepting those true tools in our long struggle towards eliminating poverty and creating equal opportunities for our fellow humans.&lt;/p&gt;&lt;h2&gt;Meanwhile . . . at Learning Equality . . .&lt;/h2&gt;&lt;p&gt;We develop and share open source, and we contribute to other related open source projects. We are a balanced team of developers, designers, documentation authors, and education specialists.&lt;/p&gt;&lt;p&gt;Technology cannot solve this alone. But creating technology that helps to give access to education and information to those that need it the most is certainly addressing those root causes.&lt;/p&gt;&lt;p&gt;We are not building a prototype for this. We take all types of devices into account and work with a broad scope of education and deployment specialists, and we encourage anyone with an idea or a contribution to participate.&lt;/p&gt;&lt;p&gt;To grow our strength and potential, we are building our own unique organization, making lots of room for discussion, and applying horizontal organizational principles that joins everyone in strategy and visionary level. And I love this.&lt;/p&gt;&lt;h2&gt;It’s not about »our« technology&lt;/h2&gt;&lt;p&gt;At this point, the Learning Equality development team has started to widen its focus by recognizing our ability to contribute upstream. This is especially true in the Python community, where we interact with lots of other projects, thanks to the affordance of Github.&lt;/p&gt;&lt;p&gt;We’re working towards integrating the offline platform Kolibri (our flagship project) into a range of projects focused on the delivery of educational resources offline (e.g., &lt;a href="http://iiab.io/"&gt;IIAB&lt;/a&gt;, &lt;a href="https://ideas-box.org/"&gt;IdeasBox&lt;/a&gt;, &lt;a href="https://worldpossible.org/rachel/"&gt;RACHEL&lt;/a&gt;, &lt;a href="http://kubo.global/"&gt;Kubo&lt;/a&gt;, &lt;a href="http://edulution.org/"&gt;Edulution&lt;/a&gt;). Those projects contribute back and help shape the platform in ways that we cannot internally anticipate and design for.&lt;/p&gt;&lt;p&gt;Not least, we are not the sole group of people or organizations: other great platforms such as &lt;a href="http://kiwix.org/"&gt;Kiwix&lt;/a&gt; are also addressing offline access and distribution. By participating in their hackathons, we have built many new relations and expanded our horizon, as well as replacing the isolated instinct to compete with an love for collaboration. Together with these teams, we are coordinating protocols, data formats and interoperability.&lt;/p&gt;&lt;p&gt;For the future of Kolibri, I dream that we will manage to polish and engineer our technology all the way until it aligns with the very strict and highly regarded &lt;a href="https://www.debian.org/doc/debian-policy/"&gt;Debian policy&lt;/a&gt; and becomes an official open source package for Debian/Ubuntu/Raspbian/Mint etc. Not least, all the other Linux distributions. This way, we can adapt our technology to a finely polished and long-standing line of software that was &amp;amp; is sustainable. State-of-the-art.&lt;/p&gt;&lt;p&gt;We have hundreds of ideas, bugs and rough designs to sift through and prioritize in the coming months, and that makes me think about how this platform, which is open source, does not belong to Learning Equality, but belongs to everyone. We cannot realize all of the ideas alone. That’s why we built a plugin API, that’s why we write documentation, and that’s why we try to make everything as straight-forward and inviting for community to contribute.&lt;/p&gt;&lt;p&gt;If you need me, I’ll be &lt;a href="http://github.com/benjaoming/"&gt;on Github&lt;/a&gt;! Thanks for reading, feedback, anecdotes and likewise are most welcome in the comments section :)&lt;/p&gt;&lt;p&gt;&lt;i&gt;&lt;a href="https://blog.learningequality.org/why-i-work-for-learning-equality-914e5ffe7776"&gt;First posted on Medium&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;</description><pubDate>Sat, 17 Feb 2018 00:14:46 +0000</pubDate><guid>https://overtag.dk/v2/blog/why-i-work-learning-equality/</guid></item><item><title>Blogging in Django: Zinnia or Wagtail-Blog?</title><link>https://overtag.dk/v2/blog/blogging-django-zinnia-or-wagtail-blog/</link><description>&lt;p&gt;Simple answer: If you're already running Wagtail or planning to run it on your site, you'll probably benefit from an integrated blog like &lt;a href="https://github.com/thelabnyc/wagtail_blog"&gt;wagtail_blog&lt;/a&gt;. It may not be full of features in itself, but since Wagtail is, you can easily go where you want. I'm adding support for &lt;a href="https://github.com/thelabnyc/wagtail_blog/pull/42"&gt;custom models to wagtail_blog&lt;/a&gt;, and after that, the blog system is IMO flexible enough for most subsequent scenarios.  &lt;/p&gt;
&lt;p&gt;I'm also in the business of migrating an existing Zinnia installation to wagtail_blog, and here's a management command for that: &lt;a href="https://gist.github.com/benjaoming/016971944bbe8c41e4bd7f656fec540c"&gt;zinnia_to_wagtail.py&lt;/a&gt; (Gist).&lt;/p&gt;
&lt;h2&gt;What about django-blog-zinnia?&lt;/h2&gt;
&lt;p&gt;This project has been maintained wonderfully for so many years! The codebase is really good (seriously, look at it for inspiration), and it doesn't lack features.&lt;/p&gt;
&lt;p&gt;It has so many features! See: &lt;a href="https://django-blog-zinnia.com/"&gt;django-blog-zinnia.com&lt;/a&gt;  &lt;/p&gt;
&lt;p&gt;It may take a little while to customize its templates and you might want a better visual editor (which is basically what Wagtail+wagtail_blog gives you). But you have all the same flexibility and more with Zinnia. These options may especially come in handy once you start to wanna build out your blog with &lt;em&gt;all those little things&lt;/em&gt;. For instance Gravatar support, diversified feeds, pingback, spam checkers, code highlighting etc.&lt;/p&gt;
&lt;p&gt;Zinnia also integrates well with django-cms.&lt;/p&gt;
&lt;h2&gt;Final compromise?&lt;/h2&gt;
&lt;p&gt;I haven't heard about anyone using custom models in Zinnia to integrate it with Wagtail. But in theory, this is very possible! Without any extensions, just inherit from &lt;code&gt;wagtailcore.models.Page&lt;/code&gt; + &lt;code&gt;zinnia.models_bases.entry.AbstractEntry&lt;/code&gt; and see where it takes you...&lt;/p&gt;
&lt;p&gt;Oh, and btw. this blog is made with wagtail_blog.. it used to be a Wordpress blog.  &lt;/p&gt;</description><pubDate>Tue, 21 Mar 2017 14:32:26 +0000</pubDate><guid>https://overtag.dk/v2/blog/blogging-django-zinnia-or-wagtail-blog/</guid></item><item><title>Suspend using your own custom timing - unlike screen savers</title><link>https://overtag.dk/v2/blog/suspend-using-your-own-custom-timing-unlike-screen-savers/</link><description>&lt;p&gt;I fail to use automatic standby because:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;I often switch off automatic power saving and screen savers and forget to switch them back on&lt;/li&gt;&lt;li&gt;In many cases, it's disturbing unattended processes like downloading&lt;br/&gt;&lt;/li&gt;&lt;li&gt;Often defaults to not being active while in docking station or charger&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;So you might fall asleep in front of a movie. Or you might go out and forget your laptop.&lt;/p&gt;&lt;p&gt;Basically, if I for instance don't respond at 2 AM, I will assume that the computer can suspend because I'm asleep at that time.&lt;br/&gt;&lt;/p&gt;&lt;p&gt;&lt;embed alt="Are you awake" embedtype="image" format="fullwidth" id="3"/&gt;&lt;/p&gt;&lt;p&gt;Basically, add this script to your desktop user's crontab:&lt;/p&gt;&lt;p&gt;https://gist.github.com/benjaoming/49015c0265f80249da080c41f79ceebc&lt;br/&gt;&lt;/p&gt;&lt;h2&gt;Skipping it once in a while&lt;/h2&gt;&lt;p&gt;You can add your own custom logic in crontab or in the Python script.&lt;/p&gt;&lt;p&gt;A suggestion would be to only allow yourself to switch it off once per run.&lt;br/&gt;&lt;/p&gt;&lt;p&gt;For instance, if you want to switch it off in case &lt;code&gt;/tmp/suspend_skip&lt;/code&gt; exists: &lt;code&gt;if test -e /tmp/suspend_skip; then rm /tmp/suspend_skip; else python3 awake_or_suspend.py; fi&lt;/code&gt;&lt;br/&gt;&lt;/p&gt;</description><pubDate>Mon, 13 Mar 2017 07:28:32 +0000</pubDate><guid>https://overtag.dk/v2/blog/suspend-using-your-own-custom-timing-unlike-screen-savers/</guid></item><item><title>Debricking OpenWRT on TP Link AC1750 Archer C7 v2.0 via TFTP</title><link>https://overtag.dk/v2/blog/debricking-openwrt-tp-link-ac1750-archer-c7-v20-tftp/</link><description>&lt;p&gt;This is a little guide for people using Ubuntu who've bricked a router like mine.&lt;/p&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://wiki.openwrt.org/toh/tp-link/archer-c5-c7-wdr7500"&gt;OpenWRT wiki: TP-Link Archer C5 AC1200 / TP-Link Archer C7 AC1750 / TP-Link TL-WDR7500&lt;/a&gt;  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="http://fabianlee.org/2016/09/18/openwrt-installing-a-tftp-server-on-ubuntu-for-openwrt-firmware-updates/"&gt;Installing a TFTP server for OpenWRT on Ubuntu&lt;/a&gt;  &lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prerequisites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A workstation running Ubuntu Trusty+ (the tftp server is ancient, so no worries about the Ubuntu version)&lt;/li&gt;
&lt;li&gt;A router&lt;/li&gt;
&lt;li&gt;Wireshark for debugging  &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flashing the router via TFTP&lt;/h2&gt;
&lt;p&gt;As described in different ways on different sites, but these are the initial steps that will get you started:  &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Turn off the router  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Connect the router's ethernet LAN port 1 to your workstation's ethernet port&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shut down network manager in Ubuntu: &lt;code&gt;sudo service network-manager stop&lt;/code&gt;  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open up a command line&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Fetch your ethernet card's name by running &lt;code&gt;ifconfig&lt;/code&gt;, it's most likely the first on the list, called something like &lt;strong&gt;enp0s25&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Configure the card to have a special static IP address that C7 uses to fetch the image: &lt;code&gt;sudo ifconfig enp0s25 192.168.0.66&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now the networking is setup, but there is still no TFTP server.  &lt;/p&gt;
&lt;h2&gt;Inspect router behavior with Wireshark&lt;/h2&gt;
&lt;p&gt;Before starting a TFTP server, let's instead look at the communication between the router and your workstation.&lt;/p&gt;
&lt;p&gt;From command line, fire up Wireshark with root permissions so it can read all the network traffic.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo wireshark&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Select your network interface by double-clicking it in the Wireshark splash screen menu. Wireshark starts to capture network traffic as soon as this happens, but nothing will happen until we power on the router.&lt;/p&gt;
&lt;h2&gt;TFTP flash process during router start up&lt;/h2&gt;
&lt;p&gt;Now that you have Wireshark listening, try turning on the router:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hold down the "WPS" button next to the ethernet plugs and press the power button. Keep holding it for some seconds until the LEDs stop flashing and only the POWER LED and Ethernet is on.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Look at the Wireshark output. You should see that the router has tried to fetch a TFTP image from 192.168.0.66.  &lt;/p&gt;
&lt;h2&gt;What went wrong tl;dr&lt;/h2&gt;
&lt;p&gt;Don't feel ashamed if you've bricked a router during your first steps with OpenWRT. It's a lesson. Both for you, and for the sake of improving documentation.&lt;/p&gt;
&lt;p&gt;But remember also to blame the companies that manufacture their products to be as difficult as possible to install open source software on.&lt;/p&gt;
&lt;p&gt;Let's ask some questions regarding the highly popular TP-Link AC1750 Archer C7 (just nicked &lt;strong&gt;&lt;em&gt;C7&lt;/em&gt;&lt;/strong&gt; ).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why do they use "v2.0" and yet without documentation in other than serial number prefixing indicate that they've changed the firmware chip? Yes, that's how mine got bricked. One router works fine, the other doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;No external serial port, you have to break warranty to have a serial connection  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;No plug or indication on circuit board regarding where the serial port is&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;While TP-Link distributes their products with a paper copy of the GPLv2 license and you can download source code on their website, the company doesn't seem to provide any official embrace of the much superior open source alternative. This would really be in the interest of the consumer and ultimately in the interest of the company itself.&lt;/p&gt;
&lt;p&gt;Anyways, I've had it with TP-Link after 3 routers in a row, thinking they were the better choice - up next is Linksys - &lt;a href="http://www.linksys.com/us/wireless-routers/c/wrt-wireless-routers/"&gt;who do officially embrace open source&lt;/a&gt;!  &lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/debricking-openwrt-tp-link-ac1750-archer-c7-v20-tftp/</guid></item><item><title>Versioning your Python project for Debian</title><link>https://overtag.dk/v2/blog/versioning-your-python-project/</link><description>&lt;p&gt;Version numbers matter! Using a consistent version numbering throughout your project's life matters, too. Changing project name is a high price to pay for cleaning up version errors.&lt;/p&gt;
&lt;p&gt;So there's some good news! The Python community's &lt;a href="https://www.python.org/dev/peps/pep-0440/"&gt;PEP-440&lt;/a&gt; specifies how to use versions.&lt;/p&gt;
&lt;p&gt;Here are some examples of versions that abide PEP-440, &lt;em&gt;they are in order&lt;/em&gt;!!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0.1.dev1&lt;/code&gt; - Our first release, just playing around. We'll delete it from PyPi.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.1a1&lt;/code&gt; - After playing around, we decide to release an Alpha 1 for our 0.1 series. We don't know if people might depend on the version, but the Alpha status suggests that the package will be removed from PyPi.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.1b1&lt;/code&gt; - Then we release a Beta version.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.1&lt;/code&gt; - Our &lt;em&gt;final release&lt;/em&gt;. But of course, more will come.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.1.post1&lt;/code&gt; - just an hour later, we realize that we forgot a file in MANIFEST.in and we need to release a new version. PyPi does NOT allow us to overwrite versions so we need to bump the number.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.1.1&lt;/code&gt; - Following a series of issues or fixes, we release a new update.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0.1.2a1&lt;/code&gt; - The next release contains a couple of fixes that we wanna triage first, so we release an alpha.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...you probably get the picture.&lt;/p&gt;
&lt;p&gt;If you are in doubt, you can play around with &lt;code&gt;pkg_resource.parse_version&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from pkg_resources import parse_version  
&amp;gt;&amp;gt;&amp;gt; parse_version(&amp;quot;0.1a1&amp;quot;) &amp;gt; parse_version(&amp;quot;0.1b1&amp;quot;)  
False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Debian compatibility&lt;/h2&gt;
&lt;p&gt;Debian does not understand "special" characters or words. It just orders by ordinals, letters coming before numbers. You can play around with the rules using this code:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ if dpkg --compare-versions 0.1a1 lt 0.1; then echo true; else echo false ; fi  
false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice that? Debian does not understand the PEP-440 convention. That's because Debian compares &lt;code&gt;0.1a1&lt;/code&gt; and &lt;code&gt;0.1&lt;/code&gt; like this: &lt;code&gt;0=0, 1=1, a&amp;gt;(nothing), 1&amp;gt;(nothing)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In Debian, we should have called it:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ if dpkg --compare-versions 0.1~a1 lt 0.1; then echo true; else echo false ; fi  
true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is because &lt;code&gt;~&lt;/code&gt; orders higher than "nothing".&lt;/p&gt;
&lt;h3&gt;Maintaining a correct upstream version in Debian?&lt;/h3&gt;
&lt;p&gt;It's unfortunately not possible to maintain &lt;code&gt;upstream.version~ubuntu1&lt;/code&gt; in case you want to release &lt;code&gt;0.1a1&lt;/code&gt; followed by&lt;code&gt;0.1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Debian has introduced "epochs" for fixing wrong or changed versions. However, it will not work to fix our systematic problems. For instance, would you like this?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0:0.1a1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1:0.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1:0.2a1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2:0.2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Probably not a good idea.&lt;/p&gt;
&lt;h3&gt;The solution&lt;/h3&gt;
&lt;p&gt;As stated in the &lt;a href="https://www.debian.org/doc/manuals/maint-guide/first.en.html#namever"&gt;Debian Packaging Policy&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Although this simple approach works most of the times, you may need to adjust package name and upstream version by renaming the upstream source to follow Debian Policy and existing convention. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So with the state of things being that PEP-440 enforces a strict version syntax that's incompatible with a strict Debian version syntax, you should rename your &lt;code&gt;package_VERSION.orig.tar.gz&lt;/code&gt; and make sure that &lt;code&gt;VERSION&lt;/code&gt; is always translated from your original project's version to Debian's way.&lt;/p&gt;
&lt;p&gt;A work flow could look like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bump version number to 0.1a2, release on PyPi&lt;/li&gt;
&lt;li&gt;Go to the old debian package source and run &lt;code&gt;uupdate -v 0.1~a2 /path/to/project-0.1a2.tar.gz&lt;/code&gt;  &lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;dch&lt;/code&gt;, &lt;code&gt;dpkg-buildpackage -S&lt;/code&gt; etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So we won't have "pristine upstream version numbers", but we'll get pretty close.&lt;/p&gt;</description><pubDate>Sat, 31 Mar 2018 17:43:13 +0000</pubDate><guid>https://overtag.dk/v2/blog/versioning-your-python-project/</guid></item><item><title>Debating Internet.org</title><link>https://overtag.dk/v2/blog/debating-internet-org/</link><description>&lt;p&gt;[also brought &lt;a href="https://www.fairdanmark.dk/en/blog/2015/05/20/debating-internetorg/"&gt;on FAIR Denmark&lt;/a&gt;]&lt;/p&gt;
&lt;p&gt;Facebook has launched Internet.org, maybe a solution that will provide internet access to lots of people who need it? In this post, I'll explain what I think about it, because certainly it doesn't seem like a project we can just ignore.&lt;/p&gt;
&lt;p&gt;Others are also discussing &lt;em&gt;the issue&lt;/em&gt; , and already a group of 65 international organizations have signed a &lt;a href="https://www.facebook.com/notes/accessnoworg/open-letter-to-mark-zuckerberg-regarding-internetorg-net-neutrality-privacy-and-/935857379791271"&gt;letter&lt;/a&gt; with lots of criticism of Facebook's new project. Having just launched Internet.org in Malawi, Lecturer of Blantyre Polytechnic University and a dear contributor to FAIR's projects in Malawi, Bennett Kankuzi, has also &lt;a href="http://bkankuzi.blogspot.com/2015/05/why-facebooks-internetorg-initiative-is.html"&gt;joined the growing number of critics&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Common to the criticism, Internet.org is being questioned because it doesn't actually give internet access, and concerns are widespread about the fundamental break-off from net neutrality.&lt;/p&gt;
&lt;p&gt;Here's what I found important...  &lt;/p&gt;
&lt;h2&gt;The good&lt;/h2&gt;
&lt;p&gt;It's with seemingly good reasons and intentions that Internet.org wants to invest in technological transformations to increase internet access, this becomes evident from &lt;a href="https://www.internet.org/about"&gt;watching Zuckerberg's 3 minute pitch on the website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the video, Zuckerberg doesn't share many details about when or how things will be done, but on the other hand, we get to see a very kind and calm Zuckerberg addressing the needy world. The story wants us to believe that creating Internet.org is easy, just as when he built Facebook in a basement.&lt;/p&gt;
&lt;p&gt;Facebook/Internet.org also put out &lt;a href="https://www.internet.org/press/offline-and-falling-behind"&gt;a serious statement&lt;/a&gt; on the problems of 4.4 billion people, a very critical aspect of the digital divide. We agree.&lt;/p&gt;
&lt;p&gt;As mentioned earlier, a lot of the criticism towards Internet.org has been that it doesn't provide free and unlimited internet access, but rather the opposite: An application for mobile phones giving access to Facebook and partner services. &lt;strong&gt;But yet&lt;/strong&gt; , what we can say is that these services &lt;em&gt;also&lt;/em&gt; need a communication infrastructure that's connected to the internet. So regardless of whether or not the end users have real internet access, the project relies on an infrastructure that does.&lt;/p&gt;
&lt;p&gt;So far, so good.&lt;/p&gt;
&lt;p&gt;And while we're at it, when Facebook/Internet.org talks about open source, they should have credit where credit is due for the Open Compute project. It has actually produced some results, like detailed specs for construction of data centres. Facebook has also succeeded in making prominent players like Apple and Microsoft join their venture.  &lt;/p&gt;
&lt;h2&gt;The bad&lt;/h2&gt;
&lt;p&gt;Zuckerberg says that we are going to get a &lt;em&gt;100x more affordable internet&lt;/em&gt; by creating a &lt;em&gt;10x speed boost&lt;/em&gt; and making the internet &lt;em&gt;10x cheaper&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Firstly, there's no credible details supplied for the 10x speed boost. Zuckerberg speaks of compression, caching, and cleaning up airborn signals. Can caching and compression really achieve this and why wouldn't anybody have done that already!? The world works like this: Over-use of data costs money and makes apps run slower. Nobody wants that! It's as if he forgets that bandwidth is also sparse in rich countries and has been even more so in the past! The incentive has always been there and has always been acted upon in both the commercial and academic world. So even if we allow him to have a naive goal, I would say that he's going to go absolutely nowhere with this. Everything that's worth implementing about caching and compression is trivial and will be done where needed.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cleaning up the airwaves&lt;/em&gt; in developing countries? It's hard to believe that this is a serious issue in countries that &lt;em&gt;lack&lt;/em&gt; connectivity. I call bullshit here.&lt;/p&gt;
&lt;p&gt;But we should also wonder why he never says anything real about the &lt;em&gt;10x cheaper&lt;/em&gt; factor. What does it actually mean? Does it for instance mean that Internet.org will provide a connection that's &lt;em&gt;worth&lt;/em&gt; 1/10 of the comparative Western internet connection? As for phones, we know that Chinese manufacturers have already disregarded IP (Intellectual Property) laws and are selling dirt cheap smart phones that are going to be impossible to out-do -- after all, they're made by underpaid Chinese labour, have a minimum of quality and violates all the patents that an open source model was supposed to have delivered a cost-saving on. Again, it seems that we're being fed bullshit.&lt;/p&gt;
&lt;p&gt;And why does Zuckerberg talk of making the internet cheaper instead of making people in the "beneficiary" countries richer? Why should they have cheap, low quality products if we could pay everyone on the global market a decent price so everyone could afford quality? &lt;a href="http://www.fairphone.com/"&gt;Fairphone&lt;/a&gt; is a better answer to these problems than trying to make phones for developing countries even cheaper than they already are.&lt;/p&gt;
&lt;p&gt;Then Zuckerberg goes to say that making the internet 10x faster and 10x cheaper will make it &lt;em&gt;100x more affordable&lt;/em&gt;. Not only is it hard to understand how this logic works, but ultimately what we get is an internet that's: Less responsive, run on slower devices, and only open up the platform to Facebook and whoever they let through the gate. This is not &lt;em&gt;100x more affordable&lt;/em&gt;. This is something else.&lt;/p&gt;
&lt;p&gt;There are also other issues about Internet.org:  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mainly, it's an app and a business model. We should be talking about it in this context.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It isn't open source, yet promotes open source and open content as the solutions to global problems.. how inconsistent is that!?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Even though Facebook said, they'd open up the platform, you have to be approved to get content included, and maybe just maybe they'll let you?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It doesn't support end-to-end encryption, so it's possible for Facebook to read all communication.. again, Facebook and privacy seem to be very distant.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is &lt;a href="https://www.accessnow.org/blog/2015/05/06/facebooks-fix-for-internet.org-still-harms-users-rights"&gt;far from Net Neutrality&lt;/a&gt;, so we have to wonder why &lt;a href="http://www.theguardian.com/technology/2014/may/08/google-facebook-and-amazon-sign-letter-criticising-fcc-net-neutrality-plan"&gt;Facebook wants this policy in the US&lt;/a&gt; and not in their new markets.&lt;/p&gt;
&lt;p&gt;And what about open source hardware? Does Nokia and Eriksson really want to offer prime hardware designs for poor people? Or is this just an empty pledge? Certainly the illustration of a soldering irons and mobile phone disassembly followed up by Zuckerberg saying " (...) building low cost open source hardware and phones" would have us believe that Internet.org is going to open source mobile phones.  &lt;/p&gt;
&lt;h2&gt;The ugly&lt;/h2&gt;
&lt;p&gt;Facebook launched this initiative with the name "Internet.org" as if it compares to other open internet initiatives (hence the .org part). But nowhere can we see that this is an open, democratic initiative. How can you influence it? Where's the source code?&lt;/p&gt;
&lt;p&gt;What's also very ugly: If Facebook wanted to use their money to do good, why not join existing initiatives? There's already open source mobile phones, open source operating systems, mobile operators, NGOs addressing the digital divide, business and government initiatives to increase internet access etc.&lt;/p&gt;
&lt;p&gt;But this initiative is not about joining the community or unifying civic society, governments, NGOs, and companies... this is about ownership and control of emerging markets and new communication infrastructures.. and one of the methods to get there is this a lobbying / marketing scheme called "Internet.org".&lt;/p&gt;
&lt;p&gt;Lastly, let's consider what they say versus what they do in general: We have to compare their good efforts to their actual capacity and reach. For instance, with the Open Compute project with Apple and Microsoft joining: How does the project scale to the full production of the world's top ICT companies? This resembles when NestlÃ© launched a Fairtrade product, a KitKat. Did they take the non-Fairtrade KitKat off the shelves? No. Are they suddenly good guys because of this Fairtrade KitKat, a micro share of their total revenue!? They certainly would want you to think so.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/debating-internet-org/</guid></item><item><title>Regenerating Shotwell thumbnails</title><link>https://overtag.dk/v2/blog/regenerating-shotwell-thumbnails/</link><description>&lt;p&gt;When reinstalling, upgrading or moving settings, thumbnails may be missing in Shotwell. The problem can look like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="shotwell_missing_thumbs" src="/static/wordpress-uploads/shotwell_missing_thumbs.png"&gt;&lt;/p&gt;
&lt;p&gt;Each time you import photos, Shotwell will generate thumbnails in two different sizes. If you loose them, you have the problem that they are regenerated on-demand, everytime you scroll by an image. That stinks.&lt;/p&gt;
&lt;h2&gt;Requirements&lt;/h2&gt;
&lt;p&gt;You need to do &lt;code&gt;apt-get install sqlite3 imagemagick&lt;/code&gt; to fetch requirements.&lt;/p&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;Here's a script that will regenerate everything, and it's tested on Shotwell 0.18.0. &lt;a href="https://gist.github.com/benjaoming/097f09557c077c09497b"&gt;View / fork Gist souce&lt;/a&gt;.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;table class="codehilitetable"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;#!/bin/bash  
# Based on http://gagor.pl/2014/01/regenerate-thumbnails-in-shotwell-for-last-month/  
THUMB_ROOT=~/.cache/shotwell/thumbs  
# Remove &amp;quot; &amp;gt; date(&amp;#39;now&amp;#39;,&amp;#39;start of month&amp;#39;,&amp;#39;-1 month&amp;#39;)&amp;quot; if you want to re-generate everything  
sqlite3 ~/.local/share/shotwell/data/photo.db \  
  &amp;quot;select id||&amp;#39; &amp;#39;||filename from PhotoTable where date(timestamp,&amp;#39;unixepoch&amp;#39;,&amp;#39;localtime&amp;#39;) &amp;gt; date(&amp;#39;now&amp;#39;,&amp;#39;start of month&amp;#39;,&amp;#39;-1 month&amp;#39;) order by timestamp desc&amp;quot; |  
while read id filename; do  
  tf1=$(printf $THUMB_ROOT/thumbs128/thumb%016x.jpg $id);  
  tf2=$(printf $THUMB_ROOT/thumbs360/thumb%016x.jpg $id);  
  if [ -e &amp;quot;$tf1&amp;quot; ]  
  then  
    echo &amp;quot;Skipping $filename&amp;quot;  
  else  
    echo -n &amp;quot;Generating thumb for $filename&amp;quot;;  
    #echo $tf1  
    convert &amp;quot;$filename&amp;quot; -quality 60 -auto-orient -thumbnail 128x128 $tf1  
    convert &amp;quot;$filename&amp;quot; -quality 60 -auto-orient -thumbnail 360x360 $tf2  
    echo  
  fi  
done
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;</description><guid>https://overtag.dk/v2/blog/regenerating-shotwell-thumbnails/</guid></item><item><title>Nokia 301 on Wammu / Gammu</title><link>https://overtag.dk/v2/blog/nokia-301-on-wammu-gammu/</link><description>&lt;p&gt;I had an older Nokia and got a new one which I needed to upload my phone book to. In order to connect to Nokia 301 from Gammu, plug the phone in with a USB cable and select the "Modem" option on the phone. The others won't work.&lt;/p&gt;
&lt;p&gt;Wammu is a GTK frontend to Gammu. You can use for various tasks, but I found that restoring my backup through Wammu failed (even though the file was created with Wammu), while using the commandline tool Gammu directly worked.&lt;/p&gt;
&lt;p&gt;Firstly, use the command "gammu-detect" and find the line that reads something with "Nokia", most likely the first line. It will contain something like:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[gammu]  
device = /dev/ttyACM3  
name = Nokia Nokia_301_Dual_SIM  
connection = at
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Put this info in your file &lt;code&gt;~/.gammurc&lt;/code&gt;. You then use this command to backup a file created from either Wammu or Gammu:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gammu -s 0 restore ~/Desktop/nokia.backup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;0&lt;/code&gt; denotes that we are using the 0th section of the configuration file, so if you don't have any other configurations, simply leave the 0.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/nokia-301-on-wammu-gammu/</guid></item><item><title>Ubuntu 14.04 Trusty with DNS server and NetworkManager (disabling dnsmasq)</title><link>https://overtag.dk/v2/blog/ubuntu-14-04-trusty-with-dns-server-and-networkmanager-disabling-dnsmasq/</link><description>&lt;p&gt;I've badly been looking for a way to both run an internal network server on &lt;code&gt;eth0&lt;/code&gt; and connecting to any kind of internet device, be it on &lt;code&gt;wlan0&lt;/code&gt; or a USB dongle.&lt;/p&gt;
&lt;p&gt;That way, I can be online and browsing documentation, downloading new stuff etc. on the server but not depend on a static configuration but still use Network Manager for its intended purpose.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;/etc/NetworkManager/NetworkManager.conf&lt;/strong&gt; , uncomment the dnsmasq option like so, because Network Manager's dnsmasq blocks listening to these ports if you want to run your own DNS server:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# dns=dnsmasq
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But that's not all! We need &lt;strong&gt;/etc/resolv.conf&lt;/strong&gt; to be updated with the external DNS providers that Network Manager discovers. This can be achieved by removing resolvconf which automatically alters /etc/resolv.conf.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-get remove resolvconf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There, done! Now add static configurations in /etc/networks/interfaces.d/ or /etc/networks/interfaces and let Network Manager handle your WLAN interfaces.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/ubuntu-14-04-trusty-with-dns-server-and-networkmanager-disabling-dnsmasq/</guid></item><item><title>Ubuntu, Kickstart, preseeding, and Wireless (WLAN)</title><link>https://overtag.dk/v2/blog/ubuntu-kickstart-preseeding-and-wireless-wlan/</link><description>&lt;p&gt;I have been trying to get an automated network install running. The starting point is a Kickstart (Kickseed) setup that works fine on LAN. Problem is: Computers do not do PXE netbooting from WLAN, and secondly that the debian installer was not configuring the network correctly. But the steps are very simple.  &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Get &lt;a href="http://archive.ubuntu.com/ubuntu/dists/precise/main/installer-i386/current/images/netboot/mini.iso"&gt;mini.iso&lt;/a&gt;, basically the same kernel and initrd that the netbooter is running&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Put it on a usb drive to boot the computer (laptop) from: &lt;code&gt;sudo dd if=/path/to/mini.iso of=/dev/[usb device] bs=4096&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make sure that your wireless access point is not protected, just make an open one, we call it UBUNTU-INSTALL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test that you can connect to it and that the repository is working&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now, boot from the USB drive and press TAB to add boot options: &lt;code&gt;ks=http://1.2.3.4/ks.cfg ksdevice=wlan0 netcfg/wireless_essid=UBUNTU-INSTALL&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In order to add your changes to mini.iso, you need to unpack the whole filesystem and edit &lt;code&gt;txt.cfg&lt;/code&gt; which is a file that syslinux is using to display the menu.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Changing mini.iso&lt;/h2&gt;
&lt;p&gt;Tools like UCK do not work for the mini.iso file because it's not using stuff like Casper. So you need to create a custom boot image the hard way:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ mkdir iso  
$ sudo mount -o loop -t iso9660 mini.iso iso  
$ cp -R iso custom  
$ sudo chmod 600 custom/txt.cfg  
$ sudo nano custom/txt.cfg  
$ sudo mkisofs -r -V &amp;quot;Custom Ubuntu Netboot image&amp;quot; -cache-inodes -J -l -b isolinux.bin -c boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -o custom.iso custom-iso  
$ sudo umount /media/USB-DRIVE # Remember to unmount before writing the usb  
$ sudo dd if=custom.iso of=/dev/[YOU USB DRIVE] bs=4096
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description><guid>https://overtag.dk/v2/blog/ubuntu-kickstart-preseeding-and-wireless-wlan/</guid></item><item><title>Limiting choices in a ModelAdmin list_filter</title><link>https://overtag.dk/v2/blog/limiting-choices-in-a-modeladmin-list_filter/</link><description>&lt;p&gt;So, you need to limit choices, because the list of related choices is too long? Then &lt;strong&gt;do NOT&lt;/strong&gt; use &lt;code&gt;limit_choices_to&lt;/code&gt; parameter on the ForeignKey! Why? Because you risk having options missing in your forms, deleting relations unknowingly.&lt;/p&gt;
&lt;p&gt;For instance, consider that you have the following case, expressed in pseudo code:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ModelClass.related_instance = models.ForeignKey(MyRelatedModel, limit_choices_to = {&amp;#39;active&amp;#39;: True}, blank=True, null=True)  
object = &amp;lt;ModelClass instance&amp;gt;  
object.related_instance = &amp;lt;MyRelatedModel instance&amp;gt;  
object.related_instance.active = False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you edit &lt;code&gt;object&lt;/code&gt;, you will see an admin form where &lt;code&gt;&amp;lt;MyRelatedModel instance&amp;gt;&lt;/code&gt; is not selectable! Hence, you will risk editing and loosing data without knowing it.&lt;/p&gt;
&lt;p&gt;So your best bet is to stop using &lt;code&gt;limit_choices_to&lt;/code&gt; and instead define a filter. Luckily, it's quite easy. It's not beautiful, though, since &lt;code&gt;admin.filters.RelatedFieldListFilter&lt;/code&gt; does not offer a method to overwrite.&lt;/p&gt;
&lt;p&gt;Here is an example:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;class RelatedFilter(admin.filters.RelatedFieldListFilter):  
    def __init__(self, *args, **kwargs):  
        super(RelatedFilter, self).__init__(*args, **kwargs)  
        self.lookup_choices = [  
            (x.id, x) for x in  
                models.RelatedModel.objects.filter(status=&amp;#39;active&amp;#39;)  
        ]





class MyModelAdmin(admin.ModelAdmin):





    list_filter = ((&amp;#39;related_field&amp;#39;, RelatedFilter), &amp;#39;other_related_field&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description><guid>https://overtag.dk/v2/blog/limiting-choices-in-a-modeladmin-list_filter/</guid></item><item><title>Testing and tracking down Warnings in Django</title><link>https://overtag.dk/v2/blog/tracking-down-warnings-in-django/</link><description>&lt;p&gt;Warnings are often suppressed through many layers of control mechanisms in Python and Django. However, you should really be aware of these and clean them up once in a while!&lt;/p&gt;
&lt;p&gt;In other cases, the Warnings reflect an inconsistent state, such as if you are mixing naive and timezone-aware variables. It can be a real pain to track down &lt;code&gt;RuntimeWarning&lt;/code&gt; because it doesn't leave a nice stack trace. The scenario could be that you have assigned a naive &lt;code&gt;DateTime&lt;/code&gt; to a field, and the db layer is complaining at runtime.&lt;/p&gt;
&lt;p&gt;However, you can run python in a mode where Warning-types raise exceptions with full stack traces. Use the following syntax:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python -W error:&amp;quot;&amp;quot;:RuntimeWarning:django.db.models.fields:0 manage.py runserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;'error' means that python should raise an exception - this is what we want!&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;'RuntimeWarning' means to trigger when such is raised - you can also simply put 'Warning' to trigger for all Warning types.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;'django.db.models.fields' means activate for this module (you need the full path). Notice that simply putting 'django' does &lt;em&gt;not&lt;/em&gt; activate for all of django, you need to put the specific module or simply '""' to enable for all of Python (which might render some interesting Warning goodies that you have never seen!).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;'0' means any line (you probably don't need)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can read more in Python Documentation chapter: &lt;a href="http://docs.python.org/2/library/warnings.html"&gt;Warning Control&lt;/a&gt;.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/tracking-down-warnings-in-django/</guid></item><item><title>Django: Log and block brute force attempts</title><link>https://overtag.dk/v2/blog/django-log-and-block-brute-force-attempts/</link><description>&lt;p&gt;Here is a very simple mechanism for wrapping a decorator around your views to protect them against brute force attempts. For instance, if you have a secret file download available only with the right secret (/view/id/secret-hash/), you expose your view to simple brute force attempts.&lt;/p&gt;
&lt;p&gt;Simply put, this decorator will log a 404 response object or Http404 exception, count pr. IP and return &lt;code&gt;status=400&lt;/code&gt; and send you an email whenever a new block is put into place.&lt;/p&gt;
&lt;h2&gt;Model&lt;/h2&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;class IllegalLookup(LogModifications):  
    &amp;quot;&amp;quot;&amp;quot;Log and block illegal lookups&amp;quot;&amp;quot;&amp;quot;  
    created = models.DateTimeField(  
        verbose_name=_(u&amp;#39;created&amp;#39;),  
        auto_now_add = True,  
    )  
    modified = models.DateTimeField(  
        verbose_name=_(u&amp;#39;created&amp;#39;),  
        auto_now = True,  
    )  
    ip_address = models.CharField(  
        max_length=16,  
        null=True,  
        blank=True,  
        verbose_name=_(u&amp;#39;IP address&amp;#39;),  
    )  
    path = models.CharField(  
        max_length=255,  
        null=True,  
        blank=True,  
        verbose_name=_(u&amp;#39;path&amp;#39;),  
        help_text=_(u&amp;#39;First attempted path is always logged&amp;#39;),  
    )  
    count = models.PositiveIntegerField(  
        default=1,  
    )

    @classmethod  
    def log_lookup(cls, ip_address, path):  
        try:  
            now = timezone.now()  
            expired = now - timedelta(minutes=settings.BLOCK_EXPIRY)  
            lookup = cls.objects.get(ip_address=ip_address,  
                modified__gte=expired)  
            lookup.count += 1  
            lookup.save()  
        except cls.DoesNotExist:  
            # Delete old entries first  
            cls.objects.filter(ip_address=ip_address).delete()  
            lookup = cls.objects.create(ip_address=ip_address,  
                path=path)  
            lookup.save()

    @classmethod  
    def is_blocked(cls, ip_address):  
        try:  
            now = timezone.now()  
            expired = now - timedelta(minutes=settings.BLOCK_EXPIRY)  
            lookup = cls.objects.get(ip_address=ip_address,  
                modified__gte=expired)  
            if lookup.count == settings.BLOCK_ATTEMPTS:  
                mail_admins(&amp;quot;IP blocked&amp;quot;, &amp;quot;{0} is now blocked, IllegalLookup id: {1}&amp;quot;.format(ip_address, lookup.id))  
            if lookup.count &amp;gt; settings.BLOCK_ATTEMPTS:  
                return True  
        except cls.DoesNotExist:  
            pass  
        return False
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Decorator&lt;/h2&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;def log_and_block(func):

    def _log_and_block(request, *args, **kwargs):  
        remote_ip = request.META.get(&amp;#39;REMOTE_ADDR&amp;#39;, None)  
        if IllegalLookup.is_blocked(remote_ip):  
            return HttpResponse(&amp;#39;%s is blocked&amp;#39; % remote_ip,  
                status=400)

        is_404 = False  
        is_exception = False  
        try:  
            return_object = func(request, *args, **kwargs)  
            if return_object.status_code == 404:  
                is_404 = True  
        except Http404:  
            is_404 = True  
            is_exception = True

        if is_404:  
            if remote_ip:  
                IllegalLookup.log_lookup(remote_ip,  
                    request.META.get(&amp;#39;PATH_INFO&amp;#39;, &amp;#39;&amp;#39;))

            if is_exception:  
                raise





        return return_object

    return _log_and_block
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;Now, simply wrap the decorator around your view:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;@log_and_block  
def my_view(request, id, secret_hash):  
    object = get_object_or_404(models.MyModel, id=id, secret_hash=secret_hash)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description><guid>https://overtag.dk/v2/blog/django-log-and-block-brute-force-attempts/</guid></item><item><title>Share an internet connection: A nice little script for quickly getting the task done.</title><link>https://overtag.dk/v2/blog/share-an-internet-connection-a-nice-little-script-for-quickly-getting-the-task-done/</link><description>&lt;p&gt;This script uses iptable forwarding and dnsmasq to share an internet connection with full relay of remote DNS servers and a local DHCP server. Before trying the script, here is the over all steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You connect to the internet in your normal fashion. For instance with a 3G dongle and your network manager applet.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make sure that nothing is running on port 53, run &lt;code&gt;netstat -tlnp&lt;/code&gt; to debug&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;READ THE SCRIPT before starting and know which interfaces you are using. &lt;code&gt;EXTERNAL&lt;/code&gt; is the one connected to the internet and &lt;code&gt;INTERNAL&lt;/code&gt; is the one you are sharing the connection via.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Do not let network-manager mange the INTERNAL interface!&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You might have to adjust the script, especially the iwconfig part, as different interfaces may have different ways of configuring the WEP key&lt;/p&gt;
&lt;h1&gt;!/bin/bash&lt;/h1&gt;
&lt;p&gt;if [ ! &lt;code&gt;whoami&lt;/code&gt; = "root" ]&lt;br&gt;
then&lt;br&gt;
  echo "Only root"&lt;br&gt;
  exit&lt;br&gt;
fi&lt;/p&gt;
&lt;p&gt;DHCP='yes'&lt;br&gt;
EXTERNAL=ppp0&lt;br&gt;
INTERNAL=wlan0&lt;br&gt;
INTERNAL_HOST_IP='192.168.10.1'&lt;br&gt;
SHARE_TO_WLAN='yes'&lt;br&gt;
WEPKEY=abe1234567&lt;br&gt;
SSID='mynetwork'&lt;br&gt;
DNSMASQ_CONFIG='dhcp-range=192.168.10.20,192.168.10.255,12h'&lt;br&gt;
DNSMASQ_CONFIG_FILE='/etc/dnsmasq.d/shareconnection'&lt;/p&gt;
&lt;p&gt;if [[ $DHCP -eq 'yes' ]]&lt;br&gt;
then  &lt;/p&gt;
&lt;h1&gt;DHCP SERVER (enable below lines)&lt;/h1&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-get install dnsmasq  
echo $DNSMASQ_CONFIG &amp;gt; $DNSMASQ_CONFIG_FILE  
    echo &amp;quot;listen-address=192.168.10.1&amp;quot; &amp;gt;&amp;gt; $DNSMASQ_CONFIG_FILE  
    echo interface=$INTERNAL &amp;gt;&amp;gt; $DNSMASQ_CONFIG_FILE  
/etc/init.d/dnsmasq stop
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;fi&lt;/p&gt;
&lt;p&gt;echo 1 &amp;gt; /proc/sys/net/ipv4/ip_forward&lt;br&gt;
/sbin/iptables -t nat -A POSTROUTING -o $EXTERNAL -j MASQUERADE&lt;br&gt;
/sbin/iptables -A FORWARD -i $EXTERNAL -o $INTERNAL -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br&gt;
/sbin/iptables -A FORWARD -i $INTERNAL -o $EXTERNAL -j ACCEPT&lt;/p&gt;
&lt;p&gt;ifconfig $INTERNAL down&lt;br&gt;
ifconfig $INTERNAL $INTERNAL_HOST_IP&lt;br&gt;
iwconfig wlan0 mode ad-hoc&lt;br&gt;
iwconfig wlan0 essid $SSID&lt;br&gt;
iwconfig wlan0 key $WEPKEY&lt;br&gt;
ifconfig $INTERNAL up&lt;/p&gt;
&lt;p&gt;if [[ $DHCP -eq 'yes' ]]&lt;br&gt;
then&lt;br&gt;
    /etc/init.d/dnsmasq start&lt;br&gt;
fi  &lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description><guid>https://overtag.dk/v2/blog/share-an-internet-connection-a-nice-little-script-for-quickly-getting-the-task-done/</guid></item><item><title>To Save Everything, Click Here: The Folly of Technological Solutionism</title><link>https://overtag.dk/v2/blog/to-save-everything-click-here/</link><description>&lt;p&gt;There is a new book coming up with a very remarkable abstract, to be released on March 5, 2013:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the very near future, â€œsmartâ€ technologies and â€œbig dataâ€ will allow us to make large-scale and sophisticated interventions in politics, culture, and everyday life. Technology will allow us to solve problems in highly original ways and create new incentives to get more people to do the right thing. But how will such â€œsolutionismâ€ affect our society, once deeply political, moral, and irresolvable dilemmas are recast as uncontroversial and easily manageable matters of technological efficiency? What if some such problems are simply vices in disguise? What if some friction in communication is productive and some hypocrisy in politics necessary? The temptation of the digital age is to fix everythingâ€”from crime to corruption to pollution to obesityâ€”by digitally quantifying, tracking, or gamifying behavior. But when we change the motivations for our moral, ethical, and civic behavior we may also change the very nature of that behavior. Technology, Evgeny Morozov proposes, can be a force for improvementâ€”but only if we keep solutionism in check and learn to appreciate the imperfections of liberal democracy. Some of those imperfections are not accidental but by design.&lt;/p&gt;
&lt;p&gt;Arguing that we badly need a new, post-Internet way to debate the moral consequences of digital technologies, To Save Everything, Click Here warns against a world of seamless efficiency, where everyone is forced to wear Silicon Valleyâ€™s digital straitjacket.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;New Republic has a wonderful chapter from it, &lt;strong&gt;&lt;a href="http://www.newrepublic.com/article/112189/social-media-doesnt-always-help-social-movements"&gt;Why Social Movements Should Ignore Social Media&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;</description><guid>https://overtag.dk/v2/blog/to-save-everything-click-here/</guid></item></channel></rss>