<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Victor's Blog]]></title><description><![CDATA[an engineer and avid pythonista who is passionate about technology and automation]]></description><link>https://blog.victor.co.zm</link><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 01:09:47 GMT</lastBuildDate><atom:link href="https://blog.victor.co.zm/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Hello Neovim]]></title><description><![CDATA[Intro
Every programmer needs a way to write their code, whether using a general-purpose text editor like the one that comes bundled with your Operating System, a source code editor like VS Code or an Integrated Development Environment (IDE) like one ...]]></description><link>https://blog.victor.co.zm/hello-neovim</link><guid isPermaLink="true">https://blog.victor.co.zm/hello-neovim</guid><category><![CDATA[vim]]></category><category><![CDATA[neovim]]></category><category><![CDATA[editors]]></category><category><![CDATA[tools]]></category><category><![CDATA[learning]]></category><category><![CDATA[programming]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Wed, 11 Feb 2026 22:19:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770847632054/061ffbf2-288a-4038-bd41-d2d9fdc76b5a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-intro">Intro</h2>
<p>Every programmer needs a way to write their code, whether using a general-purpose text editor like the one that comes bundled with your Operating System, a <a target="_blank" href="https://en.wikipedia.org/wiki/Source-code_editor">source code editor</a> like <a target="_blank" href="https://code.visualstudio.com/">VS Code</a> or an Integrated Development Environment (IDE) like one of those from <a target="_blank" href="https://www.jetbrains.com/">JetBrains</a>. The one thing in common with all these editors I’ve mentioned is that they have a <a target="_blank" href="https://en.wikipedia.org/wiki/Graphical_user_interface">graphical user interface</a>. However, there’s another family of editors that are terminal-based, for example, <a target="_blank" href="https://www.vim.org/">Vim</a>, <a target="_blank" href="https://www.gnu.org/software/emacs/">GNU Emacs</a> and others like <a target="_blank" href="https://www.nano-editor.org/">GNU nano</a>.</p>
<p>I think my first exposure to these terminal-based editors was when I needed to edit files while working from the terminal, probably in my <a target="_blank" href="https://blog.victor.co.zm/my-journey-into-gnulinux">early Linux days</a>, and most likely when setting up Linux VPSes and editing config files. Most Ubuntu VPS setup tutorials typically mention nano, as I understand it’s available by default without requiring additional installation.</p>
<p>I can’t remember how I found myself using Vim, but I do know that I preferred it to GNU nano! To this day, I always install Vim on any machine I’m using — not because I use it as my primary editor, but because that’s what I wanna use when I need to edit files while working in the terminal.</p>
<p>I know the Vim basics, having gone through <code>vimtutor</code> a while ago. I have always wanted to know Vim better, and be able to comfortably and productively use it for day-to-day work. It requires putting in a bit of work, and constant practice in order to build muscle memory. I just never quite got to doing this.</p>
<h2 id="heading-so-many-editors">So many editors</h2>
<p>My first programming language was <a target="_blank" href="https://www.java.com/en/">Java</a>, and I used <a target="_blank" href="https://netbeans.apache.org/front/main/index.html">Netbeans IDE</a> for it, though I also experimented with <a target="_blank" href="https://eclipseide.org/">Eclipse</a>. When I got into building WordPress websites and tinkering with PHP, I used <a target="_blank" href="https://notepad-plus-plus.org/">Notepad++</a>, being on a Windows machine at the time. However, I later switched to <a target="_blank" href="https://atom-editor.cc/">Atom</a>, and also experimented with <a target="_blank" href="https://brackets.io/">Brackets</a>. By the time I was getting into python programming, I liked <a target="_blank" href="https://www.sublimetext.com/">Sublime Text</a>, so I used it for a while. Then I discovered VS Code, and decided to switch to it! I was used to the Sublime Text key bindings, so the <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.sublime-keybindings">Sublime Text Keymap and Settings Importer</a> extension was always handy. I still kept Sublime Text because I felt that it was more lightweight and faster, so I’d use it when I needed to make one-off edits, or just as a general purpose editor.</p>
<p>So VS Code has remained my main code editor, even though I'd have more lightweight editors lying around — I have since replaced Sublime Text with <a target="_blank" href="https://zed.dev/">Zed</a>, which I use when I'm not working on a specific project, e.g when I want to scribble something down or edit a file quickly.</p>
<h2 id="heading-talk-is-cheap">Talk is cheap …</h2>
<p>Alright, back to the Vim discussion! As I mentioned earlier, it's always been at the back of my mind to dig deeper into it and get more comfortable with it, as part of my own personal development as a programmer.</p>
<p>The thing is, I enjoy working from the terminal, but there are lots of important things I don't know well that would make me more productive in this environment, where I interact with files in one way or the other. Being proficient with Vim is one of those things.</p>
<p>Now, couple of days before writing this post, I started looking into resources I could use to up my Vim game. I bookmarked the following, with a plan to go through them and practice daily:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/iggredible/Learn-Vim">https://github.com/iggredible/Learn-Vim</a></p>
</li>
<li><p><a target="_blank" href="https://swedishembedded.com/developers/vim-in-minutes">https://swedishembedded.com/developers/vim-in-minutes</a></p>
</li>
<li><p><a target="_blank" href="https://coffeeaddict.dev/why-nvim/">https://coffeeaddict.dev/why-nvim/</a></p>
</li>
</ul>
<p>Alongside this, I also bookmarked some <a target="_blank" href="https://github.com/tmux/tmux/">tmux</a> resources, because I think it’s an equally important tool</p>
<ul>
<li><p><a target="_blank" href="https://thoughtbot.com/upcase/tmux">https://thoughtbot.com/upcase/tmux</a></p>
</li>
<li><p><a target="_blank" href="https://www.redhat.com/en/blog/introduction-tmux-linux">https://www.redhat.com/en/blog/introduction-tmux-linux</a></p>
</li>
<li><p><a target="_blank" href="https://hamvocke.com/blog/a-quick-and-easy-guide-to-tmux/">https://hamvocke.com/blog/a-quick-and-easy-guide-to-tmux/</a></p>
</li>
</ul>
<p>However, before I could dive deep into these resources, <a target="_blank" href="https://github.com/SuperninjaX2">another programmer</a>, in a local WhatsApp group for programmers, suggested trying out <a target="_blank" href="https://neovim.io/">Neovim</a>, specifically the <a target="_blank" href="https://astronvim.com/">AstroNvim</a> distribution. At this point, I'd already come across Neovim, and seen lots of folks on YouTube using it, but never really got to check it out. This time, I decided to look into Neovim.</p>
<p>I checked out the AstroNvim website, and I was amazed! Before diving in though, I decided to find out more on YouTube, so I came across this video, which compared various Neovim distributions:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/bbHtl0Pxzj8">https://youtu.be/bbHtl0Pxzj8</a></div>
<p> </p>
<p>Now I was presented with even more options, all of which looked awesome! Throughout the video, one thing that I particularly liked was <a target="_blank" href="https://github.com/folke/which-key.nvim">which-key</a>, a plugin that shows you available keybindings when you pause mid-command. This really blew my mind! Anyway, the YouTube algorithm then brought up the following video, where ThePrimeagen goes through an article “<a target="_blank" href="https://dev.to/ratiu5/beginners-should-use-a-preconfigured-neovim-distribution-253p">Beginners should use a preconfigured Neovim distribution</a>“, and gives his take.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=6qSzFWRz6Ck">https://www.youtube.com/watch?v=6qSzFWRz6Ck</a></div>
<p> </p>
<p>ThePrimeagen strongly advocates for <a target="_blank" href="https://github.com/nvim-lua/kickstart.nvim"><strong>Kickstart</strong></a> as the ideal starting point for new Neovim users, preferring it over heavier, pre-configured distributions like <a target="_blank" href="https://www.lunarvim.org/">LunarVim</a> or AstroNvim.</p>
<p>His advice can be broken down into the following key points:</p>
<ul>
<li><p><strong>The Best "First Step":</strong> ThePrimeagen believes Kickstart is a "really great first step" because it provides exactly what a user needs without being overwhelming. He contrasts this with full distributions, where "diving in deep" can be bewildering due to the sheer amount of configuration and abstraction involved.</p>
</li>
<li><p><strong>Minimal and Educational:</strong> He praises Kickstart for being contained within a <strong>single file</strong> and acting as a "constrained version" of a setup. This simplicity makes it a "Launchpad" for users to build their own unique configuration, rather than getting stuck in someone else's ecosystem.</p>
</li>
<li><p><strong>Avoids Complexity:</strong> ThePrimeagen argues that with heavy distributions, understanding how to make edits or fix issues requires a "huge leap" in knowledge regarding project structure, Lua, and plugin management (e.g., understanding <code>init</code>, <code>after</code>, <code>plugin</code>, etc.). Kickstart avoids this barrier to entry.</p>
</li>
<li><p><strong>Kickstart vs. Distros:</strong> ThePrimeagen holds the "opposite take" to the idea that beginners <em>must</em> use complex distros. He suggests that pre-configured distributions (like LazyVim) are better suited for users who already "know it all," are tired of configuring their own editor, and just want a working environment. For those actually starting their journey, he insists: "start off with Kickstart".</p>
</li>
</ul>
<p>I resonated with ThePrimeagen’s arguments, because my current Vim setup is very minimal, and I have good control over it, compared to the time when I used the <a target="_blank" href="https://github.com/carlhuda/janus">Janus</a> Vim distribution, which seems to no longer be maintained. I was convinced to go with the <a target="_blank" href="https://github.com/nvim-lua/kickstart.nvim">Kickstart</a> approach, as it felt like the right balance between "works immediately" and "I understand what's happening."</p>
<h2 id="heading-here-we-go">Here we go</h2>
<p>I found that I didn’t actually have to uninstall vim in order to use Neovim, because Neovim has its own configuration, and you launch it via <code>nvim</code>, so it can coexist with <code>vim</code>.</p>
<p>On Fedora, installation was straightforward:</p>
<pre><code class="lang-bash">sudo dnf install -y neovim python3-neovim
</code></pre>
<p>Setting up Kickstart was simple:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Clone kickstart</span>
git <span class="hljs-built_in">clone</span> https://github.com/nvim-lua/kickstart.nvim.git ~/.config/nvim

<span class="hljs-comment"># Start neovim - plugins auto-install on first run</span>
nvim
</code></pre>
<h2 id="heading-first-customizations">First Customizations</h2>
<h3 id="heading-theme-catppuccin-mocha">Theme: Catppuccin Mocha</h3>
<p>I love <a target="_blank" href="https://catppuccin.com/">Catppuccin</a>, I use it wherever it’s supported (which is almost everywhere!), so I wanted to keep my Catppuccin Mocha theme from my Vim setup. In Kickstart, this meant replacing the default tokyonight theme. I found the colorscheme plugin section in <code>init.lua</code> and swapped it out:</p>
<pre><code class="lang-lua">{
  'catppuccin/nvim',
  name = 'catppuccin',
  priority = 1000,
  config = function()
    require('catppuccin').setup {
      flavour = 'mocha', -- latte, frappe, macchiato, mocha
      transparent_background = false,
      integrations = {
        blink_cmp = true,
        gitsigns = true,
        nvimtree = true,
        treesitter = true,
        telescope = true,
        mason = true,
        which_key = true,
      },
    }
    vim.cmd.colorscheme 'catppuccin'
  end,
},
</code></pre>
<h3 id="heading-enabling-nerd-font-support">Enabling Nerd Font Support</h3>
<p>My <a target="_blank" href="https://github.com/engineervix/fedora-setup">Fedora setup script(s)</a> already installed JetBrains Mono Nerd Font and Fantasque Sans Mono Nerd Font. To enable the fancy icons in Neovim, I just needed to flip one setting:</p>
<pre><code class="lang-lua">vim.g.have_nerd_font = true  -- Changed from false
</code></pre>
<h3 id="heading-adding-neo-tree-file-explorer">Adding Neo-tree File Explorer</h3>
<p>Kickstart is minimal by design—it doesn't even include a file explorer by default! It expects you to use Telescope for navigation (<code>Space + s + f</code>), which is actually faster once you get used to it.</p>
<p>But coming from VSCode, I wanted a traditional tree view available. Kickstart includes an optional <a target="_blank" href="https://github.com/nvim-neo-tree/neo-tree.nvim">Neo-tree</a> plugin that's commented out. I just uncommented this line:</p>
<pre><code class="lang-lua">require 'kickstart.plugins.neo-tree',
</code></pre>
<p>Now I can toggle the file tree with <code>\</code> (backslash).</p>
<h3 id="heading-fixing-language-server-names">Fixing Language Server Names</h3>
<p>I hit my first real error when trying to configure language servers. The Mason package names don't always match what you'd expect. I tried to install <code>lua_ls</code> but got an error:</p>
<pre><code class="lang-plaintext">Cannot find package "lua_ls"
</code></pre>
<p>The correct name is <code>lua-language-server</code> (hyphens, not underscores). This pattern held for other servers too:</p>
<pre><code class="lang-lua">ensure_installed = {
    'lua-language-server',  -- Not lua_ls
    'stylua',
    'pyright',              -- Python
    'gopls',                -- Go
    'bash-language-server', -- Shell scripts
}
</code></pre>
<p>Pro tip: Run <code>:Mason</code> in Neovim to browse all available packages and see their exact names.</p>
<h3 id="heading-understanding-which-key">Understanding Which-Key</h3>
<p>As mentioned earlier, one of the best discoveries I made was <strong>which-key</strong>.</p>
<p>Press <code>Space</code> and wait ~300ms? A popup shows all leader key commands. Press <code>g</code> and wait? See all the <code>g</code> commands like <code>gd</code> (go to definition).</p>
<p>It makes Neovim self-documenting. You don't need to memorize everything upfront—just start typing and which-key shows you what's possible. Sweet!</p>
<h2 id="heading-the-health-check">The Health Check</h2>
<p>Neovim includes a built-in health check system: <code>:checkhealth</code></p>
<p>This was useful for validating my setup. The output showed:</p>
<p><strong>Everything important working:</strong></p>
<ul>
<li><p><a target="_blank" href="https://langserver.org/">LSP</a> configured correctly</p>
</li>
<li><p><a target="_blank" href="https://neovim.io/doc/user/treesitter.html">Treesitter</a> parsers installed (Python, Go, Dockerfile, JSON, YAML, etc.)</p>
</li>
<li><p><a target="_blank" href="https://github.com/nvim-telescope/telescope.nvim">Telescope fuzzy finder</a> ready</p>
</li>
<li><p>All language servers installed</p>
</li>
</ul>
<p><strong>Warnings I could safely ignore:</strong></p>
<ul>
<li><p>Missing support for languages I don't use (PHP, Java, Julia)</p>
</li>
<li><p>Optional providers (Node.js, Perl, Ruby) - only needed for specific plugins</p>
</li>
<li><p>tree-sitter-cli not installed - even though it’s really only required if you're developing parsers, I decided to install it, but encountered an error due to missing C headers</p>
</li>
</ul>
<p>Installing tree-sitter (I already have Rust set up on my machine):</p>
<pre><code class="lang-bash">cargo install tree-sitter-cli
</code></pre>
<p>But it failed due to compilation errors which I resolved by installing the missing development packages:</p>
<pre><code class="lang-bash">sudo dnf install clang-devel libstdc++-devel
</code></pre>
<p>After that, <code>cargo install tree-sitter-cli</code> completed successfully, though, in hindsight, I could have skipped this entirely since it's only needed for parser development, not everyday use.</p>
<p>Kickstart's health check warned about the missing <code>neovim</code> npm package. Instead of using <code>npm install -g</code>, I used <a target="_blank" href="https://volta.sh/">Volta</a> (which I already use for Node.js version management):</p>
<pre><code class="lang-bash">volta install neovim
</code></pre>
<p>This keeps everything consistent with my existing toolchain.</p>
<h2 id="heading-reflections-why-bother">Reflections: Why Bother?</h2>
<p>This isn't about proving anything or cargo-culting what "real developers" use. It's about finding a workflow that matches how I think and work.</p>
<p>For me, that means having powerful text manipulation at my fingertips, and working in a fast, lightweight environment. I also want to understand my tools deeply instead of treating them as black boxes.</p>
<p>I'm glad I got started with Neovim via Kickstart, I think it was the right call. The single-file approach meant I could read and understand my entire config. Not abandoning my old vim setup removed the pressure, I had a fallback if I got stuck.</p>
<p><code>:checkhealth</code> immediately showed what was working and what needed attention. Turns out most things just worked. LSP, completion, fuzzy finding, git integration, all there from the start. And it was noticeably snappier than VSCode, even with plugins.</p>
<p>The learning curve wasn't as steep as I thought. With which-key and a good starter config, discovery is built into the workflow. You don't have to memorize keybindings, you learn them by seeing them.</p>
<p>Neovim with Kickstart gets me there while keeping the learning curve manageable. The setup took maybe an hour, but I already have a fully functional development environment that rivals VSCode's capabilities for my stack.</p>
<h2 id="heading-whats-next">What's Next</h2>
<p>Now I actually have to use it. Daily. On real projects. I want to learn the navigation patterns deeply, especially text objects. I'll add plugins gradually as I discover needs, not pre-emptively.</p>
<p>I need to get comfortable with Telescope for navigation instead of always reaching for the file tree. And I want to explore the debugging support (<a target="_blank" href="https://github.com/mfussenegger/nvim-dap">nvim-dap</a>) once I hit something I can't solve with print statements.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[From MS Word to LaTeX to Markdown: Taking the stress out of managing my CV]]></title><description><![CDATA[As developers, we often embark on projects to scratch our own itch. My journey to building a simple CV management tool is one such story - a tale of evolving needs, changing career paths, and the continuous quest for simpler and more efficient ways o...]]></description><link>https://blog.victor.co.zm/taking-the-stress-out-of-managing-my-cv</link><guid isPermaLink="true">https://blog.victor.co.zm/taking-the-stress-out-of-managing-my-cv</guid><category><![CDATA[markdown]]></category><category><![CDATA[Resume Templates]]></category><category><![CDATA[CV]]></category><category><![CDATA[vite]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[puppeteer]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Wed, 27 Aug 2025 03:00:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/9si2noVCVH8/upload/2395ce25b8bcb25c73aa13dfe7a36963.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As developers, we often embark on projects to scratch our own itch. My journey to building a simple CV management tool is one such story - a tale of evolving needs, changing career paths, and the continuous quest for simpler and more efficient ways of doing things.</p>
<h2 id="heading-the-problem-with-traditional-cv-management">The Problem with Traditional CV Management</h2>
<p>Like many professionals, I started my career using Microsoft Word for my CV. As a civil engineer, this seemed perfectly adequate at first. However, the problems quickly became apparent: multiple versions scattered across my computer, formatting inconsistencies, and the constant struggle to maintain a coherent document history. Which file had that perfect project description I wrote a couple of months ago?</p>
<p>The problem wasn't just about managing versions. As someone who values efficiency and automation, the traditional word processor approach felt increasingly at odds with modern development practices. I wanted something that would:</p>
<ul>
<li><p>Allow version control like my code</p>
</li>
<li><p>Support easy formatting without fighting with Word's layout engine</p>
</li>
<li><p>Generate a high quality PDF</p>
</li>
<li><p>Enable quick updates without needing a <a target="_blank" href="https://en.wikipedia.org/wiki/Word_processor">Word processor</a> or dealing with complex formatting</p>
</li>
</ul>
<h2 id="heading-the-latex-era">The LaTeX Era</h2>
<p>In August 2019, I made my first attempt at modernizing my CV workflow by moving to <a target="_blank" href="https://www.latex-project.org/">LaTeX</a>. Using the <a target="_blank" href="https://ctan.org/pkg/moderncv">moderncv</a> package, I created a professionally formatted document and, more importantly, put it under version control in a private GitLab repository. This was a significant improvement – my CV was now in plain text, versioned, and generated a consistent PDF output with professional typesetting.</p>
<p>However, LaTeX came with its own set of challenges:</p>
<ul>
<li><p>The TeX environment setup was non-trivial</p>
</li>
<li><p>Sometimes, making customizations meant diving deep into LaTeX internals</p>
</li>
<li><p>The development cycle was slow: edit, compile, check PDF, repeat</p>
</li>
</ul>
<h2 id="heading-career-transition-and-new-requirements">Career Transition and New Requirements</h2>
<p>When I began contemplating a switch to tech in 2021/2022, I realized I needed to update my CV and tailor it to suit a tech role. I still wanted to maintain my Civil Engineering focused CV. While doing some research on writing tech-centric CVs, I realised I needed to do more than just updating my CV -- I needed to completely rewrite it and structure it in a different way. I didn't want to use LaTeX, so I had to figure out another way. My research led me to explore various alternatives, including <a target="_blank" href="https://jsonresume.org/">JSON Resume</a> and Prateek Agarwal's <a target="_blank" href="https://github.com/prat0318/json_resume"><code>json_resume</code></a>. The idea was appealing: structured data in JSON format, separate from presentation concerns.</p>
<p>While JSON Resume is an excellent project, I couldn't find templates that matched my vision, and I lacked the time and skills to create my own. I settled on <code>json_resume</code>, but found myself still not entirely satisfied. My solution evolved into a web project with hand-customized HTML templates. The workflow was functional but hacky:.</p>
<ul>
<li><p><a target="_blank" href="https://www.npmjs.com/package/lite-server">lite-server</a> would serve the resume webpage</p>
</li>
<li><p><a target="_blank" href="https://pptr.dev/">Puppeteer</a> would generate PDFs from the served page</p>
</li>
<li><p>Changes required editing HTML and manual PDF regeneration</p>
</li>
</ul>
<p>While this solved some problems, it introduced new ones. The setup was complex, not user-friendly, and felt more like a temporary hack than a sustainable solution. Surely, there has to be a better way.</p>
<h2 id="heading-the-markdown-renaissance">The Markdown Renaissance</h2>
<p>By 2024, frustration with my existing setup led me back to the drawing board. I discovered Eliseo Papa's <a target="_blank" href="https://github.com/elipapa/markdown-cv"><code>markdown-cv</code></a> project, which resonated with my goals of simplicity and flexibility: simple content authoring with the ability to produce both PDF and web outputs. However, I wanted more control over styling and didn't want to depend on Ruby/Jekyll.</p>
<p>Initially, I got caught in the trap of overthinking the solution. I considered using <a target="_blank" href="https://www.11ty.dev/">11ty</a>, a static site generator I was already familiar with and very fond of, but found myself in <a target="_blank" href="https://en.wikipedia.org/wiki/Development_hell">development hell</a> – planning features without writing code.</p>
<p>The breakthrough came in September 2024 when I decided to strip away the complexity and focus on solving the core problem. I chose <a target="_blank" href="https://vite.dev/">Vite</a> for its simplicity and modern development experience, avoiding frameworks that might constrain the solution.</p>
<p>This led to my current project, which achieves several key goals:</p>
<ol>
<li><p><strong>Simple Content Authoring</strong>: A single markdown file (with <a target="_blank" href="https://docs.github.com/en/contributing/writing-for-github-docs/using-yaml-frontmatter">YAML frontmatter</a>) for all CV content -- parsed using <a target="_blank" href="https://marked.js.org/">Marked</a></p>
</li>
<li><p><strong>Modern Development Experience</strong>: Built with <a target="_blank" href="https://vite.dev/">Vite</a> for fast development and building</p>
</li>
<li><p><strong>Automated PDF Generation</strong>: CI/CD pipeline that automatically generates PDFs using <a target="_blank" href="https://pptr.dev/">Puppeteer</a> and optionally deploy a static site if I needed to link to an html version of my CV.</p>
</li>
<li><p><strong>Framework Independence</strong>: Vanilla JavaScript and <a target="_blank" href="https://sass-lang.com/">SCSS</a>, no frameworks</p>
</li>
<li><p><strong>Modern Styling</strong>: Using <a target="_blank" href="https://phosphoricons.com/">Phosphor Icons</a> and responsive design</p>
</li>
</ol>
<p>The architecture is intentionally simple:</p>
<ol>
<li><p>Write CV content in Markdown with YAML frontmatter</p>
</li>
<li><p>Parse and transform to a responsive webpage</p>
</li>
<li><p>Generate PDF through Puppeteer</p>
</li>
<li><p>Automate builds and deployments through CI</p>
</li>
</ol>
<h2 id="heading-technical-highlights">Technical Highlights</h2>
<p>The project taught me several valuable lessons:</p>
<h3 id="heading-1-sometimes-less-is-more">1. Sometimes Less is More</h3>
<p>Initially, I overthought the solution, considering frameworks like <a target="_blank" href="https://www.11ty.dev/">11ty</a>. By stepping back and focusing on core functionality, I created something more maintainable and useful.</p>
<h3 id="heading-2-expanding-technical-skills">2. Expanding Technical Skills</h3>
<p>As a Python developer, this project pushed me to improve my JavaScript and frontend development skills. I worked with:</p>
<ul>
<li><p><a target="_blank" href="https://vite.dev/">Vite</a> for building and development</p>
</li>
<li><p><a target="_blank" href="https://vitest.dev/">Vitest</a> for testing</p>
</li>
<li><p><a target="_blank" href="https://pdf-lib.js.org/">PDF-lib</a> for PDF manipulation</p>
</li>
<li><p><a target="_blank" href="https://sass-lang.com/">SCSS</a> for styling, including defining print styles</p>
</li>
</ul>
<h3 id="heading-3-cicd-integration">3. CI/CD Integration</h3>
<p>The automated PDF generation through <a target="_blank" href="https://github.com/features/actions">GitHub Actions</a> and <a target="_blank" href="https://docs.gitlab.com/ee/ci/">GitLab CI/CD</a> made the tool truly practical for regular use.</p>
<h2 id="heading-status-quo-and-looking-forward">Status Quo and Looking Forward</h2>
<p>The current solution achieves my primary goals:</p>
<ul>
<li><p>Single source of truth in Markdown</p>
</li>
<li><p>Version-controlled content</p>
</li>
<li><p>Automated PDF generation</p>
</li>
<li><p>Easy customization</p>
</li>
<li><p>Responsive web output</p>
</li>
<li><p>Simple development workflow</p>
</li>
</ul>
<p>While the tool isn't perfect, it solves the real-world problem of maintaining a professional CV in a developer-friendly way. The journey from Word to this current solution reflects not just my personal growth as a developer but also the evolution of web technologies and development practices.</p>
<p>If you find this useful or have suggestions for improvement, I'd love to hear from you! The project is now open-source and <a target="_blank" href="https://github.com/engineervix/cv">available</a> for anyone facing similar challenges in managing their CV. I'm sharing it with the community in hopes that others might find it useful or even contribute improvements.</p>
<p>Remember, sometimes the best solution isn't the most sophisticated one - it's the one that solves your problem while being maintainable and enjoyable to use.</p>
<hr />
<p>Wanna try it out? Check out the <a target="_blank" href="https://github.com/engineervix/cv">GitHub repository</a> for a demo, installation instructions and documentation.</p>
]]></content:encoded></item><item><title><![CDATA[Adding Your Own Context Menu Entries to GNOME Files (Nautilus)]]></title><description><![CDATA[Nautilus or "Files" is the default file manager that ships with GNOME. The stock context menu entries are sufficient for general purposes. However, there are times when you want the ability to add other entries to enable you perform custom actions. I...]]></description><link>https://blog.victor.co.zm/custom-nautilus-context-menu-python-extension</link><guid isPermaLink="true">https://blog.victor.co.zm/custom-nautilus-context-menu-python-extension</guid><category><![CDATA[Gnome]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Fedora]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Sat, 26 Apr 2025 16:24:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745683729187/2e09e72b-884a-4d51-b57c-187424c61202.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://apps.gnome.org/en/Nautilus/">Nautilus</a> or "Files" is the default file manager that ships with GNOME. The stock context menu entries are sufficient for general purposes. However, there are times when you want the ability to add other entries to enable you perform custom actions. In my case, I wanted the ability to open the current directory in a new tab within my currently active terminal application window. The default available entry, at least in Fedora 41, is "Open in Console", which opens a new terminal application window, instead of adding a tab to an existing one.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745683485703/e1109078-052e-4af9-8749-5f37724c8ef5.png" alt="Screenshot of GNOME Files (Nautilus) default context menu showing only the standard &quot;Open in Console&quot; option without any custom extensions." class="image--center mx-auto" /></p>
<p>I was constantly getting annoyed by this. Every time this happened, I'd end up with terminal application windows scattered across my desktop, or I'd resort to manually opening a new tab and typing <code>cd /path/to/long/directory/name</code>. Neither solution was efficient. I decided to <a target="_blank" href="https://open.spotify.com/track/6by0au2JlDXmdE1Xh2jo9U?si=0667337715674c84">do something about it</a>.</p>
<h2 id="heading-kowalski-optionshttpsmadagascarfandomcomwikioptionsgivenbykowalski"><a target="_blank" href="https://madagascar.fandom.com/wiki/Options_\(given_by_Kowalski\)">Kowalski, options!</a></h2>
<p>I searched online and found several approaches:</p>
<ul>
<li><p><strong>Nautilus scripts</strong> are a powerful feature. <a target="_blank" href="https://fedoramagazine.org/integrating-scripts-nautilus/">This post on Fedora Magazine</a> gives a practical example of converting images to a particular format. I also found another good example by Neil Brown <a target="_blank" href="https://neilzone.co.uk/2023/04/automating-actions-in-nautilus-gnomes-file-manager-with-scripts/">here</a>. However, for my particular use case, I didn't want to have to right-click, then go to "Scripts", then "Open in Console Tab". I wanted a top-level entry, not something hidden in a sub-menu.</p>
</li>
<li><p><strong>Nautilus actions</strong>, which was deprecated and renamed to <strong>FileManager Actions</strong>, then later <a target="_blank" href="https://gitlab.gnome.org/Archive/filemanager-actions">archived</a> because <a target="_blank" href="https://gitlab.gnome.org/Infrastructure/Infrastructure/-/issues/671">there were no contributions for several years</a>. If you are interested in the details, please see <a target="_blank" href="https://askubuntu.com/questions/1138673/is-filemanager-actions-working-with-19-04/">this askubuntu.com post</a>, <a target="_blank" href="https://askubuntu.com/questions/1030940/nautilus-actions-in-18-04">this one</a> and <a target="_blank" href="https://askubuntu.com/questions/1405687/how-to-install-filemanager-actions-in-ubuntu-22-04">this one</a>. Martin Bartlett has written a full-function replacement for this extension that works with the current version of Gnome Files. It's called <strong>Actions For Nautilus</strong> and it's at <a target="_blank" href="https://github.com/bassmanitram/actions-for-nautilus">https://github.com/bassmanitram/actions-for-nautilus</a>. As my use case was a very simple one, I thought this route was going to be too complex for my needs. So I continued digging through the rabbit hole that is the internet!</p>
</li>
<li><p><strong>Nautilus Python</strong>, an extension for Nautilus that allows further extending it with Python scripts with the help of Nautilus’s <a target="_blank" href="https://docs.gtk.org/gobject/">GObject API</a>. I think I may have stumbled upon it via a Reddit post <a target="_blank" href="https://www.reddit.com/r/gnome/comments/rf0leo/is_there_a_replacement_for_nautilusactions/">Is there a replacement for Nautilus-actions / Filemanager-actions?</a>. This is an official GNOME project, you can check out the <a target="_blank" href="https://gitlab.gnome.org/GNOME/nautilus-python/">project's git repository</a>.</p>
</li>
</ul>
<p>As an aside, when I asked <a target="_blank" href="https://claude.ai/">Claude</a> for a solution to my problem, the initial response I got was to use Nautilus actions. After raising the issue of it being deprecated, Claude then suggested using Nautilus scripts, which I didn't want. It was only then that Claude suggested Nautilus python (among others which included FileManager Actions and creating a custom GNOME extension).</p>
<h3 id="heading-and-we-have-a-winner">And, we have a winner!</h3>
<p>I decided to go with <strong>Nautilus Python</strong>, as it seemed to be the most practical solution for my use case, and was a perfect fit for me as a Python developer.</p>
<h2 id="heading-procedure">Procedure</h2>
<ol>
<li>First, install the Nautilus Python extension. This is for Fedora. If you're using a different distro, please adapt accordingly.</li>
</ol>
<pre><code class="lang-plaintext">sudo dnf install nautilus-python
</code></pre>
<ol start="2">
<li>Create the extensions directory (if it doesn't exist):</li>
</ol>
<pre><code class="lang-plaintext">mkdir -p ~/.local/share/nautilus-python/extensions/
</code></pre>
<ol start="3">
<li>Create a new file in the extensions directory. In my case, I called it <code>ptyxis_tab_extension.py</code>, because <a target="_blank" href="https://gitlab.gnome.org/chergert/ptyxis/">Ptyxis</a> is the default terminal application in Fedora (as of Fedora 41), replacing the long-standing GNOME Terminal. This change came after a <a target="_blank" href="https://pagure.io/fedora-workstation/issue/417">Fedora Workstation discussion</a> about adopting this newer terminal emulator, as <a target="_blank" href="https://www.reddit.com/r/Fedora/comments/1ew2rmg/ptyxis_formerly_known_as_prompt_will_replace/">shared in this Reddit thread</a>.</li>
</ol>
<pre><code class="lang-plaintext">touch ~/.local/share/nautilus-python/extensions/ptyxis_tab_extension.py
</code></pre>
<ol start="4">
<li>Write your code. You can see some examples <a target="_blank" href="https://gitlab.gnome.org/GNOME/nautilus-python/-/tree/master/examples?ref_type=heads">here</a>. Here's the code for opening a new <code>ptyxis</code> terminal tab at the current location:</li>
</ol>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>

<span class="hljs-keyword">import</span> subprocess
<span class="hljs-keyword">from</span> gi.repository <span class="hljs-keyword">import</span> Nautilus, GObject


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PtyxisTabMenuProvider</span>(<span class="hljs-params">GObject.GObject, Nautilus.MenuProvider</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">pass</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_file_items</span>(<span class="hljs-params">self, files</span>):</span>
        <span class="hljs-comment"># Only show for folders</span>
        <span class="hljs-keyword">if</span> len(files) != <span class="hljs-number">1</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> files[<span class="hljs-number">0</span>].is_directory():
            <span class="hljs-keyword">return</span> []

        item = Nautilus.MenuItem(
            name=<span class="hljs-string">"PtyxisTabExtension::open_in_tab"</span>,
            label=<span class="hljs-string">"Open in Console Tab"</span>,
            tip=<span class="hljs-string">"Open the folder in a Ptyxis console tab"</span>,
        )

        item.connect(<span class="hljs-string">"activate"</span>, self.open_in_ptyxis_tab, files[<span class="hljs-number">0</span>])
        <span class="hljs-keyword">return</span> [item]

    <span class="hljs-comment"># Handle right-click on background (empty space)</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_background_items</span>(<span class="hljs-params">self, current_folder</span>):</span>
        item = Nautilus.MenuItem(
            name=<span class="hljs-string">"PtyxisTabExtension::open_current_in_tab"</span>,
            label=<span class="hljs-string">"Open in Console Tab"</span>,
            tip=<span class="hljs-string">"Open this folder in a Ptyxis console tab"</span>,
        )

        item.connect(<span class="hljs-string">"activate"</span>, self.open_current_in_ptyxis_tab, current_folder)
        <span class="hljs-keyword">return</span> [item]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">open_in_ptyxis_tab</span>(<span class="hljs-params">self, menu, file</span>):</span>
        filepath = file.get_location().get_path()
        subprocess.Popen([<span class="hljs-string">"ptyxis"</span>, <span class="hljs-string">"--tab"</span>, <span class="hljs-string">"-d"</span>, filepath])

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">open_current_in_ptyxis_tab</span>(<span class="hljs-params">self, menu, folder</span>):</span>
        filepath = folder.get_location().get_path()
        subprocess.Popen([<span class="hljs-string">"ptyxis"</span>, <span class="hljs-string">"--tab"</span>, <span class="hljs-string">"-d"</span>, filepath])
</code></pre>
<ol start="5">
<li>Make the file executable:</li>
</ol>
<pre><code class="lang-plaintext">chmod +x ~/.local/share/nautilus-python/extensions/ptyxis_tab_extension.py
</code></pre>
<ol start="6">
<li>Restart Nautilus:</li>
</ol>
<pre><code class="lang-plaintext">nautilus -q
</code></pre>
<ol start="7">
<li>Enjoy!</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745683610111/92159b01-1453-4da1-9e3f-8a9d74c69311.png" alt="Screenshot of GNOME Files (Nautilus) context menu showing both the standard &quot;Open in Console&quot; option and the custom &quot;Open in Console Tab&quot; option implemented with nautilus-python extension." class="image--center mx-auto" /></p>
<h2 id="heading-verba-finalia">Verba Finalia</h2>
<p>With very few lines of code, we have an extension that adds the "Open in Console Tab" option in two places:</p>
<ul>
<li><p>When right-clicking on a folder</p>
</li>
<li><p>When right-clicking in the empty space within a directory</p>
</li>
</ul>
<p>When clicked, it runs <code>ptyxis --tab -d /path/to/folder</code> which opens the selected directory in a new tab in your existing Ptyxis window (or open a New window if there isn't any).</p>
<p>How cool is that? No more scattered terminal windows or tedious directory typing! Now I can quickly navigate my file system and open directories in terminal tabs with just a right-click.</p>
<p>The current implementation works well, but there are a few potential enhancements:</p>
<ul>
<li><p>Positioning the menu option closer to the original "Open in Console" option</p>
</li>
<li><p>Adding an icon to match the Ptyxis style</p>
</li>
</ul>
<p>But that's something for another day! For now, my problem is solved and I am happy!</p>
]]></content:encoded></item><item><title><![CDATA[Build it Yourself: When a 2MB Solution Beats a 1GB Installation]]></title><description><![CDATA[The Catalyst: Fedora 41 and Wayland
When I upgraded from Fedora 40 to 41, I didn't expect it to lead me down a path of discovery in Linux package development. The upgrade brought several changes, but the most impactful was the complete shift to Wayla...]]></description><link>https://blog.victor.co.zm/build-it-yourself-native-rpm-vs-flatpak</link><guid isPermaLink="true">https://blog.victor.co.zm/build-it-yourself-native-rpm-vs-flatpak</guid><category><![CDATA[Linux]]></category><category><![CDATA[Fedora]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[package manager]]></category><category><![CDATA[wayland]]></category><category><![CDATA[rpm]]></category><category><![CDATA[flatpak]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Wed, 18 Dec 2024 00:02:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734479977528/2cb0942a-4a4b-4ba1-bcc7-5ce1e8f6643b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-catalyst-fedora-41-and-wayland">The Catalyst: Fedora 41 and Wayland</h2>
<p>When I upgraded from Fedora <a target="_blank" href="https://fedoramagazine.org/announcing-fedora-linux-40/">40</a> to <a target="_blank" href="https://fedoramagazine.org/announcing-fedora-linux-41/">41</a>, I didn't expect it to lead me down a path of discovery in Linux package development. The upgrade brought several changes, but the most impactful was the <a target="_blank" href="https://news.itsfoss.com/fedora-41-gnome-wayland/">complete shift to Wayland from X11</a>. As someone who regularly used screen recording tools, this change immediately affected my workflow - I suddenly couldn't use my trusty <a target="_blank" href="https://github.com/phw/peek">Peek</a> screen recorder, which relied on X11.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><a target="_self" href="https://www.x.org/wiki/">X11 (also known as X Window System)</a> and <a target="_self" href="https://wayland.freedesktop.org/">Wayland</a> are display server protocols - they're responsible for handling how applications draw things on your screen and interact with input devices like your mouse and keyboard. X11 has been the standard for decades, but it was designed in the 1980s and carries a lot of legacy baggage. Wayland is its modern replacement, designed to be more secure, efficient, and better suited for today's graphics and display technologies. Think of it like upgrading from an old tube TV to a modern smart TV - same basic function, but with better technology under the hood.</div>
</div>

<h2 id="heading-the-search-for-alternatives">The Search for Alternatives</h2>
<p>After some searching, I found <a target="_blank" href="https://github.com/SeaDve/Kooha">Kooha</a>, a modern screen recorder designed with Wayland in mind.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734477986629/fc0c8529-281b-41e6-84c9-89743c65e05f.png" alt="Kooha's clean, modern interface makes screen recording simple" class="image--center mx-auto" /></p>
<p>Perfect! But there was a catch: it was only available as a <a target="_blank" href="https://flatpak.org/">Flatpak</a>. For those unfamiliar, Flatpak is a package format that bundles an application with all its dependencies, making it work across different Linux distributions. While this is excellent for compatibility, it came with significant overhead:</p>
<pre><code class="lang-plaintext">Flatpak Installation:
- GNOME Platform (46): 364.0 MB
- GNOME Platform Locale: 379.2 MB
- GL/Graphics drivers: 350.2 MB
  • GL default: 168.3 MB
  • GL default extra: 168.3 MB
  • VAAPI Intel: 13.6 MB
- Codecs (openh264): 976.5 KB
- Application Locale: 125.4 KB
- Kooha application itself: 2.2 MB
Total: ~1.1 GB

Native RPM Package:
- Package size: 2.2 MB
- Uses existing system libraries
Total: 2.2 MB
</code></pre>
<p>For someone mindful of disk space, this felt like overkill for a simple screen recorder. My system already had most of these dependencies installed. Why download them again?</p>
<h2 id="heading-taking-the-plunge-into-package-building">Taking the Plunge into Package Building</h2>
<p>That's when I had an idea: why not build Kooha natively for Fedora? With zero <a target="_blank" href="https://rpm-software-management.github.io/rpm/manual/">RPM</a> packaging experience but plenty of curiosity, I set out to create a native Fedora package. Thankfully, with the help of <a target="_blank" href="https://claude.ai/">claude.ai</a>, I was able to do this in under an hour.</p>
<p>The first challenge was creating a <a target="_blank" href="https://fedoramagazine.org/how-rpm-packages-are-made-the-spec-file/">spec file</a> - the recipe that tells the RPM build system how to create the package. Here's what I learned:</p>
<pre><code class="lang-plaintext"># Basic information about the package
Name:           kooha
Version:        2.3.0
Release:        1%{?dist}
Summary:        Elegantly record your screen

# Licensing and meta
License:        GPL-3.0
URL:            https://github.com/SeaDve/Kooha
Source0:        %{url}/archive/refs/tags/v%{version}.tar.gz

# Build requirements
BuildRequires:  meson
BuildRequires:  ninja-build
BuildRequires:  cargo
BuildRequires:  rust
BuildRequires:  appstream
BuildRequires:  pkgconfig(gstreamer-1.0)
BuildRequires:  pkgconfig(gstreamer-plugins-base-1.0)
BuildRequires:  pkgconfig(gtk4)
BuildRequires:  pkgconfig(libadwaita-1)
BuildRequires:  pkgconfig(glib-2.0)

# Runtime requirements
Requires:       pipewire
Requires:       gstreamer1-plugins-base
Requires:       gstreamer1-plugins-ugly-free
Requires:       gstreamer1-plugins-bad-free
Requires:       pipewire-gstreamer
Requires:       xdg-desktop-portal
Requires:       gtk4
Requires:       libadwaita

%description
Kooha is a simple screen recorder with a minimal interface. Record your screen
in an intuitive and straightforward way without distractions. Features include
recording microphone and desktop audio, support for WebM, MP4, GIF, and Matroska
formats, and the ability to select a monitor or portion of the screen to record.

%prep
%autosetup -n Kooha-%{version}

%build
%meson
%meson_build

%install
%meson_install

%check
appstreamcli validate %{buildroot}/%{_datadir}/metainfo/*.metainfo.xml

%files
%license COPYING
%doc README.md
%{_bindir}/%{name}
%{_datadir}/applications/*.desktop
%{_datadir}/metainfo/*.metainfo.xml
%{_datadir}/icons/hicolor/*/apps/*.svg
%{_datadir}/glib-2.0/schemas/*.gschema.xml
%{_datadir}/locale/*/LC_MESSAGES/*.mo
%{_datadir}/dbus-1/services/*.service
%{_datadir}/%{name}/resources.gresource

%changelog
</code></pre>
<p>This spec file tells the RPM build system everything it needs to know about the package: what it's called, what version we're building, what dependencies it needs, and how to build it. Getting the dependencies right was particularly tricky - I had to understand both what was needed to build the application (<code>BuildRequires</code>) and what was needed to run it (<code>Requires</code>). With the spec file in place, I could then set up the build environment:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Setting up the RPM build environment</span>
rpmdev-setuptree

<span class="hljs-comment"># Installing build dependencies</span>
dnf install rpmdevtools rpm-build meson ninja-build \
    gstreamer1-devel gtk4-devel libadwaita-devel

<span class="hljs-comment"># Building the package</span>
rpmbuild -ba kooha.spec
</code></pre>
<p>What followed was a fascinating journey into the heart of Linux packaging. Along the way, I discovered three things that changed how I think about Linux applications:</p>
<p>First, I gained a deep appreciation for the <a target="_blank" href="https://www.freedesktop.org/wiki/">freeDesktop.org</a> ecosystem. Building a native Linux desktop application isn't just about the code - it's about integrating with a rich framework of standards and specifications that make the Linux desktop experience seamless.</p>
<p>Second, package management turned out to be an intricate dance of dependencies. Understanding the relationship between build-time and runtime requirements, and how they all fit together, was both challenging and enlightening.</p>
<p>Finally, I came face-to-face with the challenges of cross-distribution compatibility - which gave me a whole new appreciation for why the original author chose Flatpak in the first place.</p>
<h2 id="heading-automating-the-process">Automating the process</h2>
<p>I decided to not end at just building the application locally. I wanted to automate the process so that</p>
<ul>
<li><p>I had a way to ensure that I had the latest updates</p>
</li>
<li><p>I had a reference in case I (or others) encountered a similar challenge in future</p>
</li>
<li><p>Others could benefit from it, and perhaps adapt it to other distros</p>
</li>
</ul>
<p>I therefore went ahead and built an <a target="_blank" href="https://github.com/engineervix/kooha-rpm/">automated RPM package builder for Kooha</a>. The automation process involved creating GitHub Actions workflows to:</p>
<ul>
<li><p>Check for new Kooha releases periodically</p>
</li>
<li><p>Build fresh RPM packages when updates are available</p>
</li>
<li><p>Create GitHub releases with the built packages</p>
</li>
</ul>
<p>The result was a fully automated system that keeps the RPM package updated with minimal maintenance overhead. The RPM packages are available on the <a target="_blank" href="https://github.com/engineervix/kooha-rpm/releases">project's GitHub releases page</a>. If you're interested in contributing or learning more about RPM packaging, the project's README provides detailed information to get you started.</p>
<h2 id="heading-learning-through-building">Learning Through Building</h2>
<p>This project taught me so much about Linux application development:</p>
<ul>
<li><p>The complexities of building for different distributions</p>
</li>
<li><p>The role of desktop integration through FreeDesktop.org standards</p>
</li>
<li><p>The importance of automated builds and release processes</p>
</li>
<li><p>The beauty of being able to customize and build software to suit your needs</p>
</li>
</ul>
<h2 id="heading-the-sustainability-challenge">The Sustainability Challenge</h2>
<p>Working on this project opened my eyes to the challenges of maintaining open source software. What looks simple on the surface often requires significant ongoing effort:</p>
<ul>
<li><p><strong>Continuous Updates</strong>: Every new OS release, dependency update, or security patch requires testing and potentially updates</p>
</li>
<li><p><strong>User Support</strong>: Responding to issues, helping with installation problems, and documenting solutions</p>
</li>
<li><p><strong>Infrastructure Costs</strong>: Build systems, hosting, and distribution all have associated costs</p>
</li>
<li><p><strong>Time Investment</strong>: Maintainers often work on these projects in their spare time</p>
</li>
</ul>
<p>This brings us to an important point: open source software needs financial support to thrive. While the software is free to use, its development and maintenance aren't free. There are several ways to support the open source ecosystem:</p>
<ol>
<li><p><strong>Direct Support</strong>:</p>
<ul>
<li><p>Donate to projects you use regularly</p>
</li>
<li><p>Subscribe to maintainers' GitHub Sponsors or Patreon accounts</p>
</li>
<li><p>Pay for support or additional features when offered</p>
</li>
</ul>
</li>
<li><p><strong>Indirect Support</strong>:</p>
<ul>
<li><p>Report bugs and provide detailed feedback</p>
</li>
<li><p>Contribute documentation or translations</p>
</li>
<li><p>Share and promote projects you find valuable</p>
</li>
<li><p>Help other users in community forums</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-the-power-of-open-source">The Power of Open Source</h2>
<p>What strikes me most about this journey is how it exemplifies the power of open source. When faced with a problem, I had the freedom to:</p>
<ol>
<li><p>Examine how the original application was built</p>
</li>
<li><p>Create a different distribution method</p>
</li>
<li><p>Share my solution with others</p>
</li>
<li><p>Learn about Linux packaging in the process</p>
</li>
</ol>
<p>While Flatpak is an excellent solution (and I now understand why the Kooha developer chose it), having alternatives is what makes open source special. Someone might prefer the Flatpak version for its portability, while others might want an RPM (or <a target="_blank" href="https://manpages.debian.org/unstable/dpkg-dev/deb.5.en.html">deb</a>, etc.) version for its smaller footprint.</p>
<p>Most importantly, this experience taught me that sometimes the best way to learn is to dive in and build something, even if you've never done it before. The Linux ecosystem might seem complex, but it's also incredibly rewarding to explore and contribute to.</p>
<h2 id="heading-getting-involved">Getting Involved</h2>
<p>If you're inspired to explore Linux package development:</p>
<ol>
<li><p><strong>Start Small</strong>: Pick a simple application you use and try building it from source</p>
</li>
<li><p><strong>Learn the Tools</strong>: Familiarize yourself with packaging tools like <code>rpmbuild</code> or <code>debuild</code></p>
</li>
<li><p><strong>Join Communities</strong>: Participate in distribution-specific packaging groups</p>
</li>
<li><p><strong>Support Projects</strong>: Consider supporting Kooha and other open source projects you use</p>
</li>
</ol>
<hr />
<p><em>Have you built packages for Linux before? What was your experience like? Please share your thoughts in the comments below!</em></p>
]]></content:encoded></item><item><title><![CDATA[Automating Atomic Poetry Dependency Updates with Bash]]></title><description><![CDATA[Dependency management is a crucial aspect of any project. Keeping your dependencies up to date ensures security, stability, and access to the latest features. However, simply running poetry update can be risky. What if you have 25 outdated packages, ...]]></description><link>https://blog.victor.co.zm/automating-atomic-poetry-dependency-updates-with-bash</link><guid isPermaLink="true">https://blog.victor.co.zm/automating-atomic-poetry-dependency-updates-with-bash</guid><category><![CDATA[Python]]></category><category><![CDATA[python-poetry]]></category><category><![CDATA[Bash]]></category><category><![CDATA[shell script]]></category><category><![CDATA[dependency management]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Wed, 18 Sep 2024 18:40:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/gp8BLyaTaA0/upload/1eec3194d017d6674263292efbf49c6c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dependency management is a crucial aspect of any project. Keeping your dependencies up to date ensures security, stability, and access to the latest features. However, simply running <code>poetry update</code> can be risky. What if you have 25 outdated packages, and one of them breaks your project? Rolling back becomes tricky because all updates are bundled into one commit.</p>
<p>This is where <strong>atomic updates</strong> come in. By updating one package at a time and committing each change separately, you get fine-grained control over your dependencies. In this post, I’ll demonstrate how to automate this process with a Bash script.</p>
<h4 id="heading-why-automate-the-process">Why Automate the Process?</h4>
<p>Manually updating packages with <code>poetry update &lt;package-name&gt;</code> can be time-consuming and repetitive, especially if your project has several outdated dependencies. Imagine running this command for each of 25 packages, and then manually committing each change. It becomes tedious, error-prone, and slows down your workflow.</p>
<p>Here’s why <strong>automating</strong> the process is important:</p>
<ul>
<li><p><strong>Efficiency</strong>: Instead of running individual update commands and making commits for each, the script automates the entire process in a loop. It checks for outdated packages, runs the update, commits the changes, and moves on to the next—without manual intervention.</p>
</li>
<li><p><strong>Consistency</strong>: Automation ensures that all updates follow a consistent process. You won’t forget to commit a change or make inconsistent commit messages—everything is handled systematically by the script.</p>
</li>
<li><p><strong>Focus on What Matters</strong>: Rather than spending your time running commands, you can focus on reviewing important changes, like release notes or resolving breaking changes introduced by updates. Let the script handle the grunt work.</p>
</li>
</ul>
<h4 id="heading-the-developers-responsibility">The Developer's Responsibility</h4>
<p>While automation makes dependency updates easier, it’s still <strong>the developer’s responsibility</strong> to ensure compatibility. Your project’s <code>pyproject.toml</code> file should specify dependency version constraints that prevent major breaking changes. For example, specifying something like:</p>
<pre><code class="lang-ini"><span class="hljs-attr">wagtail</span> = <span class="hljs-string">"&gt;=6.1,&lt;6.2"</span>
</code></pre>
<p>ensures that when you update <a target="_blank" href="https://wagtail.org/">Wagtail</a>, only compatible versions within this range will be installed. This minimizes the risk of major updates that could break your project.</p>
<p>Even with these safeguards, it’s essential to thoroughly <strong>test your project</strong> after updating dependencies to ensure nothing breaks. Automation can save you time, but you need to verify that everything works as expected.</p>
<h4 id="heading-tools-like-renovate-and-dependabot">Tools Like Renovate and Dependabot</h4>
<p>There are dedicated tools like <a target="_blank" href="https://www.mend.io/renovate/"><strong>Renovate</strong></a> and <a target="_blank" href="https://github.com/dependabot"><strong>Dependabot</strong></a> that help automate dependency updates by creating pull requests whenever an update is available. These tools integrate with GitHub and other platforms, providing a more structured way to handle updates.</p>
<p>However, there are times when such tools might not be available or suitable for the project you're working on—whether due to limitations in the development environment or specific project needs. This is where having a custom solution like a custom Bash script can be particularly useful.</p>
<h4 id="heading-the-script">The Script</h4>
<p>Here’s the Bash script I wrote</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Checking for outdated packages..."</span>

<span class="hljs-comment"># Checking outdated packages with Poetry</span>
outdated_packages=$(poetry show --outdated | awk <span class="hljs-string">'{print $1, $2, $3}'</span>)

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Starting the update process..."</span>

<span class="hljs-comment"># Read the outdated packages list line by line</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$outdated_packages</span>"</span> | <span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r line; <span class="hljs-keyword">do</span>
    <span class="hljs-built_in">read</span> -ra ADDR &lt;&lt;&lt;<span class="hljs-string">"<span class="hljs-variable">$line</span>"</span>
    package_name=<span class="hljs-variable">${ADDR[0]}</span>
    current_version=<span class="hljs-variable">${ADDR[1]}</span>
    latest_version=<span class="hljs-variable">${ADDR[2]}</span>

    <span class="hljs-comment"># Update package</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Attempting to update <span class="hljs-variable">$package_name</span> from <span class="hljs-variable">$current_version</span> to <span class="hljs-variable">$latest_version</span>..."</span>
    <span class="hljs-keyword">if</span> poetry update <span class="hljs-string">"<span class="hljs-variable">$package_name</span>"</span>; <span class="hljs-keyword">then</span>
        git add poetry.lock
        git commit -m <span class="hljs-string">"🐍📦 Update <span class="hljs-variable">$package_name</span> (<span class="hljs-variable">$current_version</span> -&gt; <span class="hljs-variable">$latest_version</span>)"</span>
        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Successfully updated and committed <span class="hljs-variable">$package_name</span>"</span>
    <span class="hljs-keyword">else</span>
        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to update <span class="hljs-variable">$package_name</span>. Continuing to next package..."</span>
    <span class="hljs-keyword">fi</span>

<span class="hljs-keyword">done</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Dependency update process completed."</span>
</code></pre>
<h4 id="heading-how-it-works">How It Works</h4>
<ol>
<li><p><strong>Check for outdated packages</strong>: The script starts by listing all outdated packages using <code>poetry show --outdated</code>. This command outputs the package name, current version, and the latest available version.</p>
</li>
<li><p><strong>Update each package</strong>: For each outdated package, the script runs <code>poetry update &lt;package-name&gt;</code>. If the update succeeds, the changes are committed to Git with a message that clearly shows the version upgrade (e.g., <code>🐍📦 Update requests (2.25.0 -&gt; 2.32.3)</code>).</p>
</li>
<li><p><strong>Handle errors</strong>: If an update fails (e.g., due to incompatibility issues), the script skips that package and moves on to the next, allowing the rest of the updates to proceed.</p>
</li>
</ol>
<h4 id="heading-why-atomic-updates-matter">Why Atomic Updates Matter</h4>
<p>The key advantage of this approach is that it provides <strong>granularity</strong>. When you commit each update individually, you can easily revert just one problematic package update without affecting others. For instance, if <code>requests</code> v2.32.3 breaks your project, you can roll back only that update, while keeping other updates intact.</p>
<p>Here’s how you would revert a single update:</p>
<pre><code class="lang-bash">git revert &lt;commit-hash&gt;
</code></pre>
<p>This undo command creates a new commit that reverses the changes from the specified commit—leaving the rest of your updates untouched.</p>
<h4 id="heading-conclusion">Conclusion</h4>
<p>By using this Bash script, you can keep your Poetry dependencies up to date while minimizing the risk of breaking changes. The use of atomic commits ensures that each update is easily traceable, and if something goes wrong, you have the power to roll back only the problematic update.</p>
<p>If you have access to tools like Renovate or Dependabot, they can provide even more automation and control over your updates. However, for projects where those tools aren’t available, this script provides a powerful and flexible alternative.</p>
<p>I've created a <a target="_blank" href="https://gist.github.com/engineervix/d428d835a43d3ff31a2cfc9bbc67e4ab">GitHub Gist</a> with the full script—feel free to try it out in your own project!</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[How is it going?]]></title><description><![CDATA[In my previous post in this series, I talked about enrolling to Coursera Plus and actually getting started on this journey to learn Computer Science. It's been close to two years since I wrote that post, and a lot has happened between then and now, s...]]></description><link>https://blog.victor.co.zm/how-is-it-going</link><guid isPermaLink="true">https://blog.victor.co.zm/how-is-it-going</guid><category><![CDATA[Computer Science]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[learning]]></category><category><![CDATA[Junior developer ]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Tue, 03 Sep 2024 17:52:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ZaeTg1PnZPk/upload/90b486ec153e652b70aec5ec06c3ad6c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a target="_blank" href="https://blog.victor.co.zm/and-so-it-begins">previous post</a> in <a target="_blank" href="https://blog.victor.co.zm/series/cs">this series</a>, I talked about enrolling to <a target="_blank" href="https://www.coursera.org/courseraplus">Coursera Plus</a> and actually getting started on this journey to learn Computer Science. It's been close to two years since I wrote that post, and a lot has happened between then and now, so let's get to it!</p>
<p>Firstly, some disappointing news – I never actually made significant progress with my learning on Coursera. Yes, I enrolled in a couple of programmes, and actually started going through the lessons and so on, but I struggled to do it <strong>consistently</strong>, and so I was always playing catch-up. In the end, I never got to complete any of those courses I had enrolled in, which is a shame. What excuse do I really have? I could say that the pressures and demands of life and work got the best of me, but don't we all have pressure from time to time? Even then, it's not like one is constantly under pressure – there are highs and lows in life, different seasons, just as the Preacher puts it in <a target="_blank" href="https://www.esv.org/Ecclesiastes+3/">Ecclesiastes 3</a></p>
<blockquote>
<p>For everything there is a season, and a time for every matter under heaven ...</p>
</blockquote>
<p>So that would be a pretty lame excuse. As I write this and reflect, I think the truth is that I didn't set my priorities right, and wasn't focused enough to ensure that I dedicated the required amount of time and effort into this. The fact that I had just <a target="_blank" href="https://blog.victor.co.zm/dev-retro-2022">started a new career in tech</a> should have been a good motivation for me to pursue this diligently, but alas, here I am, lamenting on my failures!</p>
<p>I should have done better, but I didn't. I think the lesson for me is to make every effort to ensure that I stick to all the commitments I have made. Sometimes we rush into making a commitment without carefully considering all the options on the table, and evaluating the feasibility of actually sticking to that commitment. However, in my case, I think I did spend some time carefully <a target="_blank" href="https://blog.victor.co.zm/planning-to-study-cs">planning and thinking about studying Computer Science</a>, so that's not the issue here. It's easy to make plans, but committing to those plans is another thing, and that's what I failed to do.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The lesson? It's easy to make plans, but sticking to them requires intentional prioritization and effort.</div>
</div>

<p>Now, as I delved deeper into the tech industry, I started re-evaluating my career goals and thinking about the future and where I wanted to be. You learn new things and unlearn some of the old stuff, the preconceived notions and ideas you had previously, and the misconceptions. When I realised that my self-directed approach wasn't working, I started to think about the next steps. What am I going to do? I was still keen on gaining a solid foundation in CS, not just learning a programming language or framework. So I went back to the drawing board and decided to reconsider enrolling into a University to study, despite my <a target="_blank" href="https://blog.victor.co.zm/planning-to-study-cs#heading-only-1-option">earlier thoughts and conclusions</a>. This brings me to my second item – I decided that enrolling in a formal MSc Computer Science program was the best way forward. I have therefore enrolled into a <a target="_blank" href="https://online.sunderland.ac.uk/online-course/msc-computer-science/">MSc Computer Science programme at the University of Sunderland, UK</a>.</p>
<p>As I said in my <a target="_blank" href="https://blog.victor.co.zm/planning-to-study-cs">first post</a> in this series, the biggest challenges with enrolling to a University are finances and time. This is why I considered the University of Sunderland programme, because of its balance between affordability and flexibility. On the financial side of things, it is generally much more affordable than its competitors, with flexible payment options. On the time aspect, I get to focus on just one module at a time (rather than say 2 or 3, as is typically the case with similar programmes), for about 7-8 weeks. Obviously, I have to manage my time wisely and ensure that I dedicate sufficient time to the programme so that I don't lag behind.</p>
<p>The <a target="_blank" href="https://online.sunderland.ac.uk/online-course/msc-computer-science/">programme</a> is ...</p>
<blockquote>
<p>designed for ambitious individuals who aren’t from a computer science background and who want to launch a new career in computer science or want to incorporate computer science expertise and knowledge into their current field as a means of accelerating their employability and career progression.</p>
</blockquote>
<p>Further, it is ...</p>
<blockquote>
<p>also open to professionals who work in computer science roles and want to gain an academic qualification to enhance their credentials and career prospects.</p>
</blockquote>
<p>Which means that I am a suitable candidate, right?</p>
<p>Anyway, today is the first day of the programme, and I saw it fit to continue with this series on Computer Science, as I've been too quiet! As I embark on this new chapter, I’m both nervous and excited. Have you ever faced similar challenges in your learning journey? I'd love to hear your experiences or any advice you might have!</p>
]]></content:encoded></item><item><title><![CDATA[Markdown-powered emails in Django]]></title><description><![CDATA[Programmatically sending "nice-looking" HTML emails with minimal effort is hard. This is why projects like MJML exist. MJML is cool, but I think it comes with some bit of overhead, as you have to learn (and write) the markup and design the layouts (y...]]></description><link>https://blog.victor.co.zm/markdown-powered-emails-in-django</link><guid isPermaLink="true">https://blog.victor.co.zm/markdown-powered-emails-in-django</guid><category><![CDATA[Django]]></category><category><![CDATA[markdown]]></category><category><![CDATA[email]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Mon, 01 Apr 2024 19:58:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1711999639496/6535ae5c-b699-4048-b197-9e6bb05b1f2d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Programmatically sending "nice-looking" HTML emails with minimal effort is hard. This is why projects like <a target="_blank" href="https://mjml.io/">MJML</a> exist. MJML is cool, but I think it comes with some bit of overhead, as you have to learn (and write) the markup and design the layouts (you can make use of available <a target="_blank" href="https://mjml.io/templates">templates</a> if they fit your use-case). This is fine if you have the time, and email visual quality is a core feature of your project. However, if you don't have time, and you just want to send decent looking emails with minimal effort, there is another way.</p>
<p>You probably already use <a target="_blank" href="https://daringfireball.net/projects/markdown/">Markdown</a> to write your docs, and even if you don't often write docs (which you should be doing!), your project's README is most likely written in Markdown. I have become so used to writing in Markdown that I even use the <a target="_blank" href="https://github.com/adam-p/markdown-here">markdown-here</a> browser extension so I can write emails in Markdown right within the Gmail interface! As <a target="_blank" href="https://en.wikipedia.org/wiki/John_Gruber">John Gruber</a> said in his <a target="_blank" href="https://daringfireball.net/projects/markdown/">introduction to Markdown</a>:</p>
<blockquote>
<p>Markdown allows you to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid XHTML (or HTML).</p>
</blockquote>
<p>This is why I find it appealing — you just write, with less worries about layouts and formatting. Wouldn't it be cool if we could write our Django email templates in Markdown, and render nice looking emails without breaking a sweat? Well, we can! So let's dive in and see how we can get this done.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">TLDR; Here's a <a target="_blank" href="https://github.com/engineervix/blog-post--django-markdown-powered-emails">GitHub repo</a> where you can see the code in action, and try it out for yourself.</div>
</div>

<p>Suppose, in our Django project, we want to send notification emails whenever somebody registers for an event. We have a plain text email template as follows:</p>
<pre><code class="lang-plaintext">Howdy {{ user }},

This is to confirm that you have successfully registered for “{{ event.title }}”. Here are the details, for your reference:

-----------------------------
Event: {{ event.title }}
Description: {{ event.description }}
Date and Time: {{ event.date_and_time }}
Location: {{ event.location }}
-----------------------------

We look forward to seeing you there!

Cheers,

— The Project Team
</code></pre>
<p>This is a <a target="_blank" href="https://docs.djangoproject.com/en/5.0/ref/templates/language/">Django template</a>, with <a target="_blank" href="https://docs.djangoproject.com/en/5.0/ref/templates/language/#variables">context variables</a> <code>user</code> and <code>event</code>. This template is rendered using the <a target="_blank" href="https://docs.djangoproject.com/en/5.0/topics/templates/#django.template.loader.render_to_string"><code>render_to_string</code></a> function in the <code>django.template.loader</code> module. Here's a custom <code>send_mail</code> function, where this is done:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> List, Optional

<span class="hljs-keyword">from</span> django.conf <span class="hljs-keyword">import</span> settings
<span class="hljs-keyword">from</span> django.core.mail <span class="hljs-keyword">import</span> EmailMessage
<span class="hljs-keyword">from</span> django.template.loader <span class="hljs-keyword">import</span> render_to_string


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_email</span>(<span class="hljs-params">
    subject: str,
    to_email_list: List[str],
    template: str,
    context: Optional[dict] = None,
</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    <span class="hljs-string">"""
    Sends an email with the specified subject,
    to the specified list of email recipients,
    using the supplied text template with optional context.

    Args:
        subject: The subject line of the email.
        to_email_list: A list of email addresses to send the email to.
        template: The path to the text template to use.
        context: A dictionary containing values to pass to the template (optional).

    Returns:
        None
    """</span>

    <span class="hljs-comment"># If context is None, set it to an empty dictionary</span>
    <span class="hljs-keyword">if</span> context <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        context = {}

    <span class="hljs-comment"># Render the text template using the provided context</span>
    text_content = render_to_string(template, context)

    <span class="hljs-comment"># Create the email message object</span>
    email = EmailMessage(
        subject=subject,
        body=text_content,
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=to_email_list,
    )

    <span class="hljs-comment"># Send the email</span>
    email.send()
</code></pre>
<p>So, in order to send an email, we'd call our custom <code>send_mail</code> function like this:</p>
<pre><code class="lang-python">send_email(
    subject=<span class="hljs-string">f"Event Registration for <span class="hljs-subst">{event.title}</span>"</span>,
    to_email_list=[user.email],
    template=<span class="hljs-string">"events/event_registration_notification_email.txt"</span>,
    context={<span class="hljs-string">"event"</span>: event, <span class="hljs-string">"user"</span>: user},
)
</code></pre>
<p>And the user will get a plain text email like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711794406664/8acc2e83-15ce-420a-9810-1ac8242457fa.png" alt class="image--center mx-auto" /></p>
<p>Now, in order to bring Markdown into the mix, we need to make a couple of changes:</p>
<ol>
<li><p>Write our template in Markdown</p>
</li>
<li><p>Install a Markdown parsing and rendering library</p>
</li>
<li><p>Modify our <code>send_mail</code> function to use the library above</p>
</li>
</ol>
<p>So, let's get to it. Here's a slightly modified version of the template:</p>
<pre><code class="lang-plaintext">Howdy {{ user }},

This is to confirm that you have successfully registered for **{{ event.title }}**. Here are the details, for your reference:

---

- _Event_: {{ event.title }}
- _Description_: {{ event.description }}
- _Date and Time_: **{{ event.date_and_time }}**
- _Location_: {{ event.location }}

---

We look forward to seeing you there!

Cheers,

— The Project Team
</code></pre>
<p>Regarding a Markdown parsing and rendering library, the choice is entirely up to you! There are several options available, including <a target="_blank" href="https://github.com/Python-Markdown/markdown">Python-Markdown</a> and <a target="_blank" href="https://github.com/trentm/python-markdown2">Python-Markdown2</a>, which are quite popular. These are Python implementations of John Gruber's original Perl-implemented Markdown. In my case, I wanted something that was based on <a target="_blank" href="https://github.com/github/cmark-gfm">GitHub's fork of <code>cmark</code></a>, so I could use <a target="_blank" href="https://github.github.com/gfm/">GitHub Flavoured Markdown (GFM)</a> out-of-the-box (without installing / configuring additional extensions), if I needed to. This led me to <a target="_blank" href="https://github.com/theacodes/cmarkgfm"><code>cmarkgfm</code></a> and <a target="_blank" href="https://github.com/zopieux/pycmarkgfm"><code>pycmarkgfm</code></a>. I went with <code>pycmarkgfm</code>, which is similar to <code>cmarkgfm</code>, but with support for additional features such as <a target="_blank" href="https://github.github.com/gfm/#task-list-items-extension-">task lists</a>.</p>
<p>Install the library:</p>
<pre><code class="lang-bash">pip install pycmarkgfm
</code></pre>
<p>Now let's modify the <code>send_mail</code> function so we can make use of <code>pycmarkgfm</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> List, Optional

<span class="hljs-keyword">import</span> pycmarkgfm
<span class="hljs-keyword">from</span> django.conf <span class="hljs-keyword">import</span> settings
<span class="hljs-keyword">from</span> django.core.mail <span class="hljs-keyword">import</span> EmailMessage
<span class="hljs-keyword">from</span> django.template.loader <span class="hljs-keyword">import</span> render_to_string


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_email</span>(<span class="hljs-params">
    subject: str,
    to_email_list: List[str],
    template: str,
    context: Optional[dict] = None,
    md_to_html: Optional[bool] = False,
</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    <span class="hljs-string">"""
    Sends an email with the specified subject,
    to the specified list of email recipients,
    using the supplied text template with optional context.

    Args:
        subject: The subject line of the email.
        to_email_list: A list of email addresses to send the email to.
        template: The path to the text template to use.
        context: A dictionary containing values to pass to the template (optional).
        md_to_html: Whether the template content is written in Markdown format and
        should be rendered as HTML.

    Returns:
        None
    """</span>

    <span class="hljs-comment"># If context is None, set it to an empty dictionary</span>
    <span class="hljs-keyword">if</span> context <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        context = {}

    <span class="hljs-comment"># Render the text template using the provided context</span>
    text_content = render_to_string(template, context)

    <span class="hljs-keyword">if</span> md_to_html:
        text_content = pycmarkgfm.gfm_to_html(text_content)

    <span class="hljs-comment"># Create the email message object</span>
    email = EmailMessage(
        subject=subject,
        body=text_content,
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=to_email_list,
    )

    <span class="hljs-comment"># If markdown to html is enabled, set the content type to HTML</span>
    <span class="hljs-keyword">if</span> md_to_html:
        email.content_subtype = <span class="hljs-string">"html"</span>

    <span class="hljs-comment"># Send the email</span>
    email.send()
</code></pre>
<p>We have introduced a new Boolean <code>md_to_html</code> keyword argument, so that, when it's <code>True</code>:</p>
<ul>
<li><p>we apply <code>pycmarkgfm</code>'s <code>gfm_to_html</code> function to the text rendered by <code>django.template.loader.render_to_string</code>, and</p>
</li>
<li><p>change the email <code>content_subtype</code> to <code>"html"</code>.</p>
</li>
</ul>
<p>We could use the <code>EmailMultiAlternatives</code> class here, in order to send both text and HTML versions of our email. However, we'll keep things simple and just send the HTML version. You can <a target="_blank" href="https://docs.djangoproject.com/en/5.0/topics/email/#sending-alternative-content-types">check the Django docs</a> for details of how to use the <code>EmailMultiAlternatives</code> class.</p>
<p>Here's what the email looks like</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711800689070/83f6331d-13d6-429a-a288-7c12af14ae29.png" alt class="image--center mx-auto" /></p>
<p>Pretty cool, right? We could end here and call it a day! However, we can make it even better, by adding a custom stylesheet. We can replicate the GitHub Markdown style by using <a target="_blank" href="https://sindresorhus.com/">Sindre Sorhus</a>' <a target="_blank" href="https://github.com/sindresorhus/github-markdown-css">github-markdown-css</a>.</p>
<p>For a web page, we can easily use an external stylesheet via</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"example.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
</code></pre>
<p>Or we could even embed the stylesheet via the <code>&lt;style&gt;</code> tag.</p>
<p>However, with emails, things are a little bit more complicated, mainly because</p>
<ul>
<li><p>most email clients do not support linking to external stylesheets due to security and privacy concerns</p>
</li>
<li><p>email client CSS support for embedded stylesheets is limited (at the time of writing this post)</p>
</li>
</ul>
<p>For more information about CSS in emails, please see the following resources, which I found quite helpful:</p>
<ul>
<li><p><a target="_blank" href="https://myemma.com/blog/css-in-html-emails-what-you-need-to-know-to-get-started/">https://myemma.com/blog/css-in-html-emails-what-you-need-to-know-to-get-started/</a></p>
</li>
<li><p><a target="_blank" href="https://mailtrap.io/blog/email-css/">https://mailtrap.io/blog/email-css/</a></p>
</li>
<li><p><a target="_blank" href="https://customer.io/blog/how-to-make-css-play-nice-in-html-emails-without-breaking-everything/">https://customer.io/blog/how-to-make-css-play-nice-in-html-emails-without-breaking-everything/</a></p>
</li>
</ul>
<p>To ensure your emails render properly across different email clients, it is recommended to use <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style">inline CSS</a>. That sounds like too much work! Fortunately, someone has written an excellent tool to do all this work for us. Enter <a target="_blank" href="https://premailer.io/">premailer</a>, by <a target="_blank" href="https://www.peterbe.com/about">Peter Bengtsson</a>!</p>
<p>From the <a target="_blank" href="https://github.com/peterbe/premailer">project's README</a>:</p>
<blockquote>
<p>[Premailer] parses an HTML page, looks up <code>style</code> blocks and parses the CSS. It then uses the <code>lxml.html</code> parser to modify the DOM tree of the page accordingly.</p>
</blockquote>
<p>Exactly what we need! Let's start by installing the package</p>
<pre><code class="lang-bash">pip install premailer
</code></pre>
<p>Next up, we'll need to download the <a target="_blank" href="https://github.com/sindresorhus/github-markdown-css">github-markdown-css</a> stylesheet and update our <code>send_mail</code> function to use <code>premailer</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> List, Optional

<span class="hljs-keyword">import</span> pycmarkgfm
<span class="hljs-keyword">from</span> django.conf <span class="hljs-keyword">import</span> settings
<span class="hljs-keyword">from</span> django.core.mail <span class="hljs-keyword">import</span> EmailMessage
<span class="hljs-keyword">from</span> django.template.loader <span class="hljs-keyword">import</span> render_to_string
<span class="hljs-keyword">from</span> premailer <span class="hljs-keyword">import</span> transform


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_email</span>(<span class="hljs-params">
    subject: str,
    to_email_list: List[str],
    template: str,
    context: Optional[dict] = None,
    md_to_html: Optional[bool] = False,
</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    <span class="hljs-string">"""
    Sends an email with the specified subject,
    to the specified list of email recipients,
    using the supplied text template with optional context.

    Args:
        subject: The subject line of the email.
        to_email_list: A list of email addresses to send the email to.
        template: The path to the text template to use.
        context: A dictionary containing values to pass to the template (optional).
        md_to_html: Whether the template content is written in Markdown format and
        should be rendered as HTML.

    Returns:
        None
    """</span>

    <span class="hljs-comment"># If context is None, set it to an empty dictionary</span>
    <span class="hljs-keyword">if</span> context <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        context = {}

    <span class="hljs-comment"># Render the text template using the provided context</span>
    text_content = render_to_string(template, context)

    <span class="hljs-keyword">if</span> md_to_html:
        html = pycmarkgfm.gfm_to_html(text_content)
        <span class="hljs-keyword">with</span> open(
            os.path.join(settings.PROJECT_DIR, <span class="hljs-string">"assets/css/github-markdown.min.css"</span>),
            <span class="hljs-string">"r"</span>,
            encoding=<span class="hljs-string">"utf-8"</span>,
        ) <span class="hljs-keyword">as</span> f:
            github_markdown_css = f.read()
        <span class="hljs-comment"># see https://github.com/sindresorhus/github-markdown-css?tab=readme-ov-file#usage</span>
        formatted_content = <span class="hljs-string">f"""
        &lt;!DOCTYPE html&gt;
        &lt;html&gt;
            &lt;head&gt;
                &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
                &lt;style&gt;<span class="hljs-subst">{github_markdown_css}</span>&lt;/style&gt;
                &lt;style&gt;
                    .markdown-body {{
                        box-sizing: border-box;
                        min-width: 200px;
                        max-width: 980px;
                        margin: 0 auto;
                        padding: 45px;
                    }}
                    @media (max-width: 767px) {{
                        .markdown-body {{
                            padding: 15px;
                        }}
                    }}
                &lt;/style&gt;
            &lt;/head&gt;
            &lt;body class="markdown-body"&gt;<span class="hljs-subst">{html}</span>&lt;/body&gt;
        &lt;/html&gt;
        """</span>
        text_content = transform(formatted_content)

    <span class="hljs-comment"># Create the email message object</span>
    email = EmailMessage(
        subject=subject,
        body=text_content,
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=to_email_list,
    )

    <span class="hljs-comment"># If markdown to html is enabled, set the content type to HTML</span>
    <span class="hljs-keyword">if</span> md_to_html:
        email.content_subtype = <span class="hljs-string">"html"</span>

    <span class="hljs-comment"># Send the email</span>
    email.send()
</code></pre>
<p>Notes:</p>
<ol>
<li><p><strong>Asset Loading</strong>: We load the <a target="_blank" href="https://github.com/sindresorhus/github-markdown-css">github-markdown-css</a> stylesheet from a file (<code>github-markdown.min.css</code>), and embed the stylesheet via the <code>&lt;style&gt;</code> tag. The file can be located anywhere (it doesn't need to be within the <a target="_blank" href="https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs"><code>STATICFILES_DIRS</code></a> because it's not used on the frontend — we are only using it when generating emails). I minified the file using <a target="_blank" href="https://sass-lang.com/documentation/cli/"><code>sass</code></a> in order to reduce the file size.</p>
</li>
<li><p><strong>HTML Content Generation</strong>: We create a formatted HTML string based on the example in the <a target="_blank" href="https://github.com/sindresorhus/github-markdown-css">github-markdown-css</a> README.</p>
</li>
<li><p><strong>Inlining CSS Styles</strong>: The <code>premailer.transform</code> function is applied to the formatted HTML content to inline CSS styles.</p>
</li>
</ol>
<p>This is the result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711997990978/689a3a96-21a5-486d-8299-bd3350a0ce29.png" alt class="image--center mx-auto" /></p>
<p>Pretty sweet, right? Well, there you have it — decent-looking emails with minimal effort. After all, <a target="_blank" href="https://github.com/python/peps/blob/3cdfdd90c512978ec631d8ad7b9455bcdd74f32d/peps/pep-0020.rst?plain=1#L26">simple is better than complex</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Conditional Display of Fields in Wagtail Admin]]></title><description><![CDATA[There may be times when you need to conditionally hide or show some fields in the Wagtail Page Editor. This post illustrates one approach to achieve this, using JavaScript.

💡
This post has an accompanying GitHub repo at github.com/engineervix/blog-...]]></description><link>https://blog.victor.co.zm/conditional-display-of-fields-in-wagtail-admin</link><guid isPermaLink="true">https://blog.victor.co.zm/conditional-display-of-fields-in-wagtail-admin</guid><category><![CDATA[wagtail]]></category><category><![CDATA[Django]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[cms]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Sun, 17 Mar 2024 18:50:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710022739811/ab3ec8a3-7a26-4907-b20f-6371e5ffb0c8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There may be times when you need to conditionally hide or show some fields in the <a target="_blank" href="https://youtu.be/qD6reQ7T8fQ">Wagtail Page Editor</a>. This post illustrates one approach to achieve this, using JavaScript.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This post has an accompanying GitHub repo at <a target="_blank" href="https://github.com/engineervix/blog-post--wagtailadmin-field-visibility-toggle/">github.com/engineervix/blog-post--wagtailadmin-field-visibility-toggle</a></div>
</div>

<p>Suppose we have a <code>toys</code> app and a <code>ToyPage</code> model with fields defined as shown below:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToyPage</span>(<span class="hljs-params">Page</span>):</span>
    description = RichTextField(features=[<span class="hljs-string">"bold"</span>, <span class="hljs-string">"italic"</span>, <span class="hljs-string">"link"</span>])
    designer = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=<span class="hljs-literal">True</span>, blank=<span class="hljs-literal">True</span>, related_name=<span class="hljs-string">"toys"</span>
    )
    use_designer_email = models.BooleanField(
        default=<span class="hljs-literal">False</span>,
        verbose_name=<span class="hljs-string">"Use designer's email"</span>,
        help_text=<span class="hljs-string">"Use the designer's email address as the contact email"</span>,
    )
    contact_email = models.EmailField(blank=<span class="hljs-literal">True</span>)
</code></pre>
<p>We want to show the <code>contact_email</code> field by default, and hide it when <code>use_designer_email</code> is <code>True</code>.</p>
<p>We can do this via JavaScript, by <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener">registering an event listener</a> on the <code>use_designer_email</code> checkbox, and toggling the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/display">display</a> of the <code>use_designer_email</code> <a target="_blank" href="https://docs.wagtail.org/en/stable/reference/pages/panels.html#wagtail.admin.panels.FieldPanel"><code>FieldPanel</code></a>.</p>
<p>Wagtail provides the <a target="_blank" href="https://docs.wagtail.org/en/v6.0.1/reference/hooks.html#insert-editor-js"><code>insert_editor_js</code></a> hook, which facilitates addition of extra JavaScript files or code snippets to the page editor. In our app's <code>wagtail_hooks.py</code>, we register a function with the above hook:</p>
<pre><code class="lang-python"><span class="hljs-comment"># /path/to/project/toys/wagtail_hooks.py</span>
<span class="hljs-keyword">from</span> django.templatetags.static <span class="hljs-keyword">import</span> static
<span class="hljs-keyword">from</span> django.utils.html <span class="hljs-keyword">import</span> format_html
<span class="hljs-keyword">from</span> wagtail <span class="hljs-keyword">import</span> hooks


<span class="hljs-meta">@hooks.register("insert_editor_js")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">editor_js</span>():</span>
    <span class="hljs-keyword">return</span> format_html(<span class="hljs-string">'&lt;script src="{}"&gt;&lt;/script&gt;'</span>, static(<span class="hljs-string">"js/page-editor.js"</span>))
</code></pre>
<p>We could write the JavaScript code directly in the <code>editor_js</code> function above, but for ease of maintenance I think it's better to just write the JavaScript code in its own file, which, in this case, is <code>js/page-editor.js</code> within the <a target="_blank" href="https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs"><code>STATICFILES_DIRS</code></a>. Now, before we write the JavaScript code, we need to use our web browser's <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools">developer tools</a> to inspect the DOM so we understand the panel layout and get the correct selectors for the elements we'll be working with.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710092583454/d46d72d2-adef-4e84-8b27-4fa208e80293.png" alt class="image--center mx-auto" /></p>
<p>In the screenshot above, the element representing the <code>use_designer_email</code> <code>BooleanField</code> is highlighted. In this case, it's a <code>checkbox</code> input:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"checkbox"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"use_designer_email"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_use_designer_email"</span>
  <span class="hljs-attr">aria-describedby</span>=<span class="hljs-string">"panel-child-content-child-designer-child-use_designer_email-helptext"</span>
/&gt;</span>
</code></pre>
<p>And now, let's take a closer look at the <code>contact_email</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710093269319/f17fa9dd-622c-4bf6-a1c2-f4ba9c7d812f.png" alt class="image--center mx-auto" /></p>
<p>The green rectangle represents the <code>email</code> input, while the red rectangle represents the containing <code>div</code> (with class <code>w-panel__wrapper</code>) which we actually want to hide/show — we don't want to hide just the input, but also the label and everything else associated with it. Here's the markup, for reference:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-panel__wrapper"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">label</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"w-field__label"</span>
    <span class="hljs-attr">for</span>=<span class="hljs-string">"id_contact_email"</span>
    <span class="hljs-attr">id</span>=<span class="hljs-string">"id_contact_email-label"</span>
  &gt;</span>
    Contact email
  <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-field__wrapper"</span> <span class="hljs-attr">data-field-wrapper</span>=<span class="hljs-string">""</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"w-field w-field--email_field w-field--email_input w-field--commentable"</span>
      <span class="hljs-attr">data-field</span>=<span class="hljs-string">""</span>
      <span class="hljs-attr">data-contentpath</span>=<span class="hljs-string">"contact_email"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"w-field__errors"</span>
        <span class="hljs-attr">data-field-errors</span>=<span class="hljs-string">""</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"panel-child-content-child-designer-child-contact_email-errors"</span>
      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"w-field__help"</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"panel-child-content-child-designer-child-contact_email-helptext"</span>
        <span class="hljs-attr">data-field-help</span>=<span class="hljs-string">""</span>
      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-field__input"</span> <span class="hljs-attr">data-field-input</span>=<span class="hljs-string">""</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
          <span class="hljs-attr">name</span>=<span class="hljs-string">"contact_email"</span>
          <span class="hljs-attr">maxlength</span>=<span class="hljs-string">"254"</span>
          <span class="hljs-attr">id</span>=<span class="hljs-string">"id_contact_email"</span>
        /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"w-field__comment-button w-field__comment-button--add"</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>
          <span class="hljs-attr">data-component</span>=<span class="hljs-string">"add-comment-button"</span>
          <span class="hljs-attr">data-comment-add</span>=<span class="hljs-string">""</span>
          <span class="hljs-attr">aria-label</span>=<span class="hljs-string">"Add comment"</span>
          <span class="hljs-attr">aria-describedby</span>=<span class="hljs-string">"id_contact_email-label"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"icon icon-comment-add icon"</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">use</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#icon-comment-add"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">use</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"icon icon-comment-add-reversed icon"</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">use</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#icon-comment-add-reversed"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">use</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>With this information, we can now go ahead and write our code:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">"DOMContentLoaded"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> checkbox = <span class="hljs-built_in">document</span>.querySelector(
    <span class="hljs-string">'input[type="checkbox"][name="use_designer_email"][id="id_use_designer_email"]'</span>
  );
  <span class="hljs-keyword">const</span> emailField = <span class="hljs-built_in">document</span>.querySelector(
    <span class="hljs-string">'input[type="email"][name="contact_email"][id="id_contact_email"]'</span>
  );

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toggleEmailField</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> panelWrapper = emailField.closest(<span class="hljs-string">".w-panel__wrapper"</span>);
    panelWrapper.style.display = checkbox.checked ? <span class="hljs-string">"none"</span> : <span class="hljs-string">"block"</span>;
  }

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initializePage</span>(<span class="hljs-params"></span>) </span>{
    checkbox.addEventListener(<span class="hljs-string">"change"</span>, toggleEmailField);
  }

  initializePage();
});
</code></pre>
<p>A few things to note here:</p>
<ol>
<li><p>Adding an event listener for the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event"><code>DOMContentLoaded</code> event</a> ensures that the code inside this function runs only when the DOM content is fully loaded and parsed.</p>
</li>
<li><p>The <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Element/closest"><code>closest()</code> method</a> is applied on the <code>emailField</code> in order to find the closest ancestor element with the class <code>"w-panel__wrapper"</code> relative to the <code>emailField</code>. This is particularly important, because, as you may have noticed from the screenshots above, there are several <code>div</code>s with class <code>"w-panel__wrapper"</code>, so we need to ensure that we are hiding/showing the correct one!</p>
</li>
<li><p>We dynamically toggle the visibility of the <code>panelWrapper</code> by adjusting it's <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style"><code>style</code> property</a> to control the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/display"><code>display</code> CSS property</a>. This involves switching between <code>'display: none;'</code> and <code>'display: block;'</code> based on the checked state of the checkbox.</p>
</li>
<li><p>We listen for the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event"><code>change</code> event</a> on the checkbox to trigger the <code>toggleEmailField</code> function when the checkbox state changes.</p>
</li>
</ol>
<p>Here's what this looks like:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/GdqYn7KrITI">https://youtu.be/GdqYn7KrITI</a></div>
<p> </p>
<p>Pretty cool, right? Well, let's look at one more contrived example.</p>
<p>Now, going back to our <code>ToyPage</code> model, suppose we have an <a target="_blank" href="https://docs.wagtail.org/en/v6.0.1/topics/pages.html#inline-models">inline model</a> on our <code>ToyPage</code>, and we want to control visibility of a field within the inline model. Here's the <code>ToyCampaign</code> inline model, and the updated <code>ToyPage</code> model:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToyCampaign</span>(<span class="hljs-params">Orderable</span>):</span>
    page = ParentalKey(<span class="hljs-string">"ToyPage"</span>, related_name=<span class="hljs-string">"campaigns"</span>)

    title = models.CharField(max_length=<span class="hljs-number">255</span>)

    start_date = models.DateTimeField()
    end_date_is_known = models.BooleanField(default=<span class="hljs-literal">False</span>)
    end_date = models.DateTimeField(blank=<span class="hljs-literal">True</span>, null=<span class="hljs-literal">True</span>)

    panels = [
        FieldPanel(<span class="hljs-string">"title"</span>),
        MultiFieldPanel(
            [
                FieldPanel(<span class="hljs-string">"start_date"</span>),
                FieldPanel(<span class="hljs-string">"end_date_is_known"</span>),
                FieldPanel(<span class="hljs-string">"end_date"</span>),
            ],
            heading=<span class="hljs-string">"Dates"</span>,
        ),
    ]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">clean</span>(<span class="hljs-params">self</span>):</span>
        errors = defaultdict(list)
        super().clean()

        end_date = self.end_date

        <span class="hljs-keyword">if</span> self.end_date_is_known <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> end_date:
            errors[<span class="hljs-string">"end_date"</span>].append(
                _(<span class="hljs-string">"Please specify the end date, since it is known!"</span>)
            )

        <span class="hljs-keyword">if</span> end_date <span class="hljs-keyword">and</span> end_date &lt;= self.start_date:
            errors[<span class="hljs-string">"end_date"</span>].append(_(<span class="hljs-string">"End date must be after start date"</span>))

        <span class="hljs-keyword">if</span> errors:
            <span class="hljs-keyword">raise</span> ValidationError(errors)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__str__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Toy Campaign ‘{}’ on Page “{}”"</span>.format(self.title, self.page.title)


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToyPage</span>(<span class="hljs-params">Page</span>):</span>
    description = RichTextField(features=[<span class="hljs-string">"bold"</span>, <span class="hljs-string">"italic"</span>, <span class="hljs-string">"link"</span>])
    designer = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=<span class="hljs-literal">True</span>, blank=<span class="hljs-literal">True</span>, related_name=<span class="hljs-string">"toys"</span>
    )
    use_designer_email = models.BooleanField(
        default=<span class="hljs-literal">False</span>,
        verbose_name=<span class="hljs-string">"Use designer's email"</span>,
        help_text=<span class="hljs-string">"Use the designer's email address as the contact email"</span>,
    )
    contact_email = models.EmailField(blank=<span class="hljs-literal">True</span>)

    content_panels = Page.content_panels + [
        FieldPanel(<span class="hljs-string">"description"</span>),
        MultiFieldPanel(
            [
                FieldPanel(<span class="hljs-string">"designer"</span>),
                FieldPanel(<span class="hljs-string">"use_designer_email"</span>),
                FieldPanel(<span class="hljs-string">"contact_email"</span>),
            ],
            heading=<span class="hljs-string">"Designer"</span>,
        ),
        InlinePanel(<span class="hljs-string">"campaigns"</span>, heading=<span class="hljs-string">"Campaigns"</span>, label=<span class="hljs-string">"Campaign"</span>),
    ]

    search_fields = Page.search_fields + [
        index.SearchField(<span class="hljs-string">"description"</span>),
        index.FilterField(<span class="hljs-string">"designer"</span>),
        index.FilterField(<span class="hljs-string">"use_designer_email"</span>),
    ]

<span class="hljs-meta">    @cached_property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">email</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">if</span> (designer := self.designer) <span class="hljs-keyword">and</span> self.use_designer_email:
            <span class="hljs-keyword">return</span> designer.email
        <span class="hljs-keyword">return</span> self.contact_email
</code></pre>
<p>Here's what this looks like (Most of the panels have been deliberately collapsed in order to fit everything in one view, for the sake of this illustration) :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710099368956/9c809506-616c-4d00-b9a9-f78e5eb58af1.png" alt class="image--center mx-auto" /></p>
<p>Now, here, we want to hide the <strong>End date</strong> by default, and only display it when the <strong>End date is known</strong> checkbox is ticked. Generally the same approach we used earlier applies even here. However, we are now dealing with <a target="_blank" href="https://docs.wagtail.org/en/v6.0.1/reference/pages/panels.html#wagtail.admin.panels.InlinePanel"><code>InlinePanel</code>s</a>, which introduces some challenges:</p>
<ol>
<li><p>We have no control over the number of campaigns, which means we have to be very careful about how we select the elements to work with.</p>
</li>
<li><p>When the page editor is loaded, there may be no campaigns at all, and additional campaigns can be added at any point.</p>
</li>
</ol>
<p>Once again, the developer tools' <em>DOM inspector</em> is our friend here:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710222288380/f068fb81-9431-4e4a-829e-1e1cd534c239.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710222321358/ddf346dd-0d61-479b-b90b-d156c08b52b7.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710222347845/256f5978-59c4-425f-85e5-f2971594cb86.png" alt class="image--center mx-auto" /></p>
<p>You will notice from the above screenshots that the markup for the <code>end_date</code> inputs in each <code>InlinePanel</code> looks like this (comments added for clarity):</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- Campaign 1 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"campaigns-0-end_date"</span>
  <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"off"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_campaigns-0-end_date"</span>
/&gt;</span>

<span class="hljs-comment">&lt;!-- Campaign 2 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"campaigns-1-end_date"</span>
  <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"off"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_campaigns-1-end_date"</span>
/&gt;</span>

<span class="hljs-comment">&lt;!-- Campaign 3 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"campaigns-2-end_date"</span>
  <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"off"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_campaigns-2-end_date"</span>
/&gt;</span>
</code></pre>
<p>Notice the incremental pattern in the <code>name</code> and <code>id</code>. Similarly, the checkboxes have this markup:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- Campain 1 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"checkbox"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"campaigns-0-end_date_is_known"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_campaigns-0-end_date_is_known"</span>
/&gt;</span>

<span class="hljs-comment">&lt;!-- Campain 2 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"checkbox"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"campaigns-1-end_date_is_known"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_campaigns-1-end_date_is_known"</span>
/&gt;</span>

<span class="hljs-comment">&lt;!-- Campain 3 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"checkbox"</span>
  <span class="hljs-attr">name</span>=<span class="hljs-string">"campaigns-2-end_date_is_known"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_campaigns-2-end_date_is_known"</span>
/&gt;</span>
</code></pre>
<p>This makes things easier, right? We can see a pattern in the convention used for the input <code>name</code> and <code>id</code>. While this may help us address the first challenge, it may not entirely help us to address the second one. Why? Well, remember we said earlier that</p>
<blockquote>
<p>Adding an event listener for the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event"><code>DOMContentLoaded</code> event</a> ensures that the code inside this function runs only when the DOM content is fully loaded and parsed.</p>
</blockquote>
<p>When additional campaigns are added after the DOM content is fully loaded and parsed, our code will not work, if we follow the same approach as before. We therefore need to register an event listener on the "add campaign" button:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710222701378/5a27dad3-c0e1-48b5-b681-49f1857b67e3.png" alt class="image--center mx-auto" /></p>
<p>The button's markup is as follows:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"button button-small button-secondary chooser__choose-button"</span>
  <span class="hljs-attr">id</span>=<span class="hljs-string">"id_campaigns-ADD"</span>
&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"icon icon-plus-inverse icon"</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">use</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#icon-plus-inverse"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">use</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>
  &gt;</span>Add campaign
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>Alright, talk is cheap, show me the code already! Well, I'm glad you asked, here it is, this time, we'll implement <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_programming">OOP</a> to help keep things more structured:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Toggles visibility of an `end_date` field's parent panel based on
 * its counterpart `end_date_is_known` checkbox state'.
 *
 * This is used on the page editor for ToyPages, specifically
 * on the **campaigns** InlinePanel.
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EndDateVisibilityHandler</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">this</span>.namePrefix = <span class="hljs-string">'[name^="campaigns-"]'</span>;
    <span class="hljs-built_in">this</span>.idPrefix = <span class="hljs-string">'[id^="id_campaigns-"]'</span>;
    <span class="hljs-built_in">this</span>.checkboxes = <span class="hljs-built_in">document</span>.querySelectorAll(
      <span class="hljs-string">`input[type="checkbox"]<span class="hljs-subst">${<span class="hljs-built_in">this</span>.namePrefix}</span><span class="hljs-subst">${<span class="hljs-built_in">this</span>.idPrefix}</span>[id$="-end_date_is_known"]`</span>
    );
    <span class="hljs-built_in">this</span>.addButton = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#id_campaigns-ADD"</span>);
  }

  toggleEndDateField(checkbox) {
    <span class="hljs-keyword">const</span> match = checkbox.id.match(<span class="hljs-regexp">/-(\d+)-end_date_is_known/</span>);
    <span class="hljs-keyword">const</span> identifier = match ? match[<span class="hljs-number">1</span>] : <span class="hljs-literal">null</span>;

    <span class="hljs-keyword">if</span> (identifier !== <span class="hljs-literal">null</span>) {
      <span class="hljs-keyword">const</span> endDateField = <span class="hljs-built_in">document</span>.getElementById(
        <span class="hljs-string">`id_campaigns-<span class="hljs-subst">${identifier}</span>-end_date`</span>
      );
      <span class="hljs-keyword">const</span> panelWrapper = endDateField.closest(<span class="hljs-string">".w-panel__wrapper"</span>);
      panelWrapper.style.display = checkbox.checked ? <span class="hljs-string">"block"</span> : <span class="hljs-string">"none"</span>;
    }
  }

  initializeFields(checkboxes) {
    checkboxes.forEach(<span class="hljs-function">(<span class="hljs-params">checkbox</span>) =&gt;</span> {
      checkbox.addEventListener(<span class="hljs-string">"change"</span>, <span class="hljs-function">() =&gt;</span>
        <span class="hljs-built_in">this</span>.toggleEndDateField(checkbox)
      );
    });
  }

  initializePage() {
    <span class="hljs-built_in">this</span>.initializeFields(<span class="hljs-built_in">this</span>.checkboxes);

    <span class="hljs-built_in">this</span>.addButton.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">const</span> newCheckboxes = <span class="hljs-built_in">this</span>.addButton
        .closest(<span class="hljs-string">".w-panel__content"</span>)
        .querySelectorAll(
          <span class="hljs-string">`input[type="checkbox"]<span class="hljs-subst">${<span class="hljs-built_in">this</span>.namePrefix}</span><span class="hljs-subst">${<span class="hljs-built_in">this</span>.idPrefix}</span>[id$="-end_date_is_known"]`</span>
        );
      <span class="hljs-built_in">this</span>.initializeFields(newCheckboxes);
    });
  }
}

<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">"DOMContentLoaded"</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> edvh = <span class="hljs-keyword">new</span> EndDateVisibilityHandler();
  edvh.initializePage();
});
</code></pre>
<p>A few things to note:</p>
<ol>
<li><p>Notice how we use the attribute selectors <code>[id^=]</code> and <code>[id$=]</code> (<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors/Attribute_selectors#substring_matching_selectors">Reference</a>) in selecting checkboxes with <code>id</code>s that have an incremental pattern. We know that they have a constant prefix and constant suffix, so we make use of this convention.</p>
</li>
<li><p>Once we select a checkbox, we use <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match"><code>String.prototype.match()</code></a> to extract the number from the checkbox's <code>id</code>, which we use to select the accompanying <code>endDateField</code>.</p>
</li>
<li><p>When the <code>addButton</code> is clicked, a new <code>InlinePanel</code> is created above the button. Once again, the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Element/closest"><code>closest()</code> method</a> comes in handy here.</p>
</li>
</ol>
<p>Here's what this looks like:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/ecDkF6fiQNc">https://youtu.be/ecDkF6fiQNc</a></div>
<p> </p>
<p>Awesome, right? Well, that's all folks! Have fun customizing your Wagtail project!</p>
<p>Please check out the accompanying <a target="_blank" href="https://github.com/engineervix/blog-post--wagtailadmin-field-visibility-toggle/">GitHub repo</a> if you would like to take a closer look and quickly try things out for yourself.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying HedgeDoc to Dokku]]></title><description><![CDATA[💡
This applies to HedgeDoc version 1.x, which is the stable version as of December 2023.


HedgeDoc is an open-source, web-based, self-hosted, collaborative markdown editor. I stumbled upon it while looking for a self-hosted markdown editor. I parti...]]></description><link>https://blog.victor.co.zm/deploying-hedgedoc-to-dokku</link><guid isPermaLink="true">https://blog.victor.co.zm/deploying-hedgedoc-to-dokku</guid><category><![CDATA[Dokku]]></category><category><![CDATA[markdown]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Sat, 23 Dec 2023 15:28:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/-EnI0H6Wm6s/upload/ab5e7de481cded41639b2104b793ec68.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This applies to HedgeDoc version 1.x, which is the stable version as of December 2023.</div>
</div>

<p><a target="_blank" href="https://hedgedoc.org/">HedgeDoc</a> is an open-source, web-based, self-hosted, collaborative markdown editor. I stumbled upon it while looking for a self-hosted markdown editor. I particularly like it for it's simple and straightforward UI and wide array of features, such as</p>
<ul>
<li><p>diagrams and charts</p>
</li>
<li><p>media embeds / uploads</p>
</li>
<li><p>presentations, powered by <a target="_blank" href="https://revealjs.com/">reveal.js</a></p>
</li>
</ul>
<p><a target="_blank" href="https://dokku.com/">Dokku</a> is a platform-as-a-service (PaaS) solution that allows you to easily deploy and manage applications on your own infrastructure. It is an open-source alternative to platforms like <a target="_blank" href="https://www.heroku.com/">Heroku</a>, and is designed to be lightweight and straightforward, making it easy for developers to set up and use.</p>
<p>I already have a Dokku instance on an <a target="_blank" href="https://www.hetzner.com/press-release/arm64-cloud">arm64 VPS provided by Hetzner</a>. If you are new to Dokku and would like to give it a spin, please see <a target="_blank" href="https://dokku.com/docs/getting-started/installation/">the official docs</a>. You might also want to have a look at <a target="_blank" href="https://github.com/engineervix/pre-dokku-server-setup">this script</a> I wrote to bootstrap an Ubuntu VPS server prior to installation of Dokku (Use the <a target="_blank" href="https://github.com/engineervix/pre-dokku-server-setup/tree/arm64"><code>arm64</code></a> branch if you are setting up on an arm64 machine).</p>
<p>I only recently started using servers based on the arm64 architecture, as they have low power consumption and high energy efficiency, which makes them cheaper compared to their <a target="_blank" href="https://www.lenovo.com/us/en/glossary/x86/">x86</a> counterparts.</p>
<h2 id="heading-heroku-buildpack-approach">Heroku buildpack approach</h2>
<p>Dokku has this concept of <strong>builders</strong> – which are a way of customizing how an app is built from a source. There are <a target="_blank" href="https://dokku.com/docs/deployment/builders/builder-management/#builder-selection">several built-in builders available</a>. However, by default, Dokku normally uses <a target="_blank" href="https://devcenter.heroku.com/articles/buildpacks">Heroku buildpacks</a> (via <a target="_blank" href="https://github.com/gliderlabs/herokuish#buildpacks">Herokuish</a>) for deployment.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">It is important to note that many buildpacks either download pre-compiled things for x86 or only target x86. See <a target="_blank" href="https://github.com/gliderlabs/herokuish/issues/87">this Herokuish issue</a> for details. Therefore, by default, the herokuish builder <a target="_blank" href="https://dokku.com/blog/2022/dokku-0.29.0/#initial-herokuish-support-on-arm-servers">is disabled on arm/arm64 servers</a>.</div>
</div>

<p>Now, before I switched to arm64, I had an x86 Dokku server with HedgeDoc and other apps running on it. I had deployed HedgeDoc using the Heroku buildpacks approach, and it worked very well. How did I do this? Well, in a nutshell, I</p>
<ul>
<li><p>forked the <a target="_blank" href="https://github.com/hedgedoc/hedgedoc">https://github.com/hedgedoc/hedgedoc</a> repository to <a target="_blank" href="https://github.com/engineervix/hedgedoc">https://github.com/engineervix/hedgedoc</a></p>
</li>
<li><p>switched to the <code>master</code> branch, and from there created a <code>dokku</code> branch, where I made some small changes (please see <a target="_blank" href="https://github.com/engineervix/hedgedoc/commit/0a8385ed54a5057e7d345ecb7f8700c0c2d871b7">this commit</a> for the details) – addition of a <a target="_blank" href="https://devcenter.heroku.com/articles/procfile"><code>Procfile</code></a> and installation of <a target="_blank" href="https://pm2.keymetrics.io/"><code>pm2</code></a>, although the latter isn't necessary.</p>
</li>
<li><p>deployed the <code>dokku</code> branch as follows (all commands are executed on the server, unless otherwise indicated)</p>
<pre><code class="lang-bash">  <span class="hljs-comment"># create app</span>
  dokku apps:create hedgedoc

  <span class="hljs-comment"># add a domain to your app</span>
  dokku domains:add hedgedoc example.com

  <span class="hljs-comment"># setup postgres | https://github.com/dokku/dokku-postgres</span>
  <span class="hljs-comment"># (feel free to use your preferred database backend)</span>
  dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres
  <span class="hljs-comment"># feel free to choose the image &amp; image version of your choice here</span>
  dokku postgres:create postgres-hedgedoc --image <span class="hljs-string">"postgres"</span> --image-version <span class="hljs-string">"13.4"</span>
  dokku postgres:link postgres-hedgedoc hedgedoc
  <span class="hljs-comment"># Take note of the DATABASE_URL, which you will need to assign to CMD_DB_URL later</span>

  <span class="hljs-comment"># set up authentication for backups on the postgres service</span>
  <span class="hljs-comment"># Datastore backups are supported via AWS S3 and S3 compatible services</span>
  <span class="hljs-comment"># like https://github.com/minio/minio and https://www.backblaze.com/b2/cloud-storage.html</span>
  <span class="hljs-comment"># dokku postgres:backup-auth &lt;service&gt; &lt;aws-access-key-id&gt; &lt;aws-secret-access-key&gt; &lt;aws-default-region&gt; &lt;aws-signature-version&gt; &lt;endpoint-url&gt;</span>
  dokku postgres:backup-auth postgres-hedgedoc aws-access-key-id aws-secret-access-key us-west-004 v4 https://s3.us-west-004.backblazeb2.com
  <span class="hljs-comment"># postgres:backup postgres-hedgedoc &lt;bucket-name&gt;</span>
  dokku postgres:backup postgres-hedgedoc bucket-name
  <span class="hljs-comment"># everyday at 2:45AM, 10:45AM, 6:45PM</span>
  dokku postgres:backup-schedule postgres-hedgedoc <span class="hljs-string">"45 2,10,18 * * *"</span> bucket-name

  <span class="hljs-comment"># Persistent Storage &lt;https://dokku.com/docs/advanced-usage/persistent-storage/&gt;</span>
  <span class="hljs-comment"># Create a persistent storage directory in the recommended storage path</span>
  dokku storage:ensure-directory --chown heroku hedgedoc
  <span class="hljs-comment"># Create a new bind mount</span>
  dokku storage:mount hedgedoc /var/lib/dokku/data/storage/hedgedoc/uploads:/home/node/app/public/uploads

  <span class="hljs-comment"># set env variables for your app</span>
  dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_DOMAIN=example.com &amp;&amp; \
  dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_ALLOW_ORIGIN=<span class="hljs-string">'["example.com"]'</span> &amp;&amp; \
  dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_SESSION_SECRET=somethingsecret &amp;&amp; \
  dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_PROTOCOL_USESSL=<span class="hljs-literal">true</span> &amp;&amp; \
  dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_PORT=3000 &amp;&amp; \
  dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_ALLOW_ANONYMOUS=<span class="hljs-literal">false</span> &amp;&amp; \
  dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_ALLOW_EMAIL_REGISTER=<span class="hljs-literal">false</span> &amp;&amp; \
  <span class="hljs-comment"># <span class="hljs-doctag">NOTE:</span> set CMD_DB_URL to whatever the value of DATABASE_URL is</span>
  dokku config:<span class="hljs-built_in">set</span> hedgedoc CMD_DB_URL=...

  <span class="hljs-comment"># configure buildpacks</span>
  dokku buildpacks:add hedgedoc https://github.com/heroku/heroku-buildpack-nodejs.git

  <span class="hljs-comment"># Customize Nginx | set `client_max_body_size`, to make upload feature work better, for example</span>
  dokku nginx:<span class="hljs-built_in">set</span> hedgedoc client-max-body-size 50m
  <span class="hljs-comment"># regenerate config</span>
  dokku proxy:build-config hedgedoc

  <span class="hljs-comment"># ports</span>
  dokku ports:add hedgedoc http:80:3000 &amp;&amp; \
  dokku ports:add hedgedoc https:443:3000 &amp;&amp; \
  dokku ports:remove hedgedoc http:80:5000 &amp;&amp; \
  dokku ports:remove hedgedoc https:443:5000

  <span class="hljs-comment"># letsencrypt</span>
  dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
  dokku config:<span class="hljs-built_in">set</span> --no-restart --global DOKKU_LETSENCRYPT_EMAIL=user@example.com
  dokku letsencrypt:<span class="hljs-built_in">set</span> hedgedoc email user@example.com
  dokku letsencrypt:<span class="hljs-built_in">enable</span> hedgedoc
  <span class="hljs-comment"># this would setup a cron job to renew letsencrypt certificate</span>
  dokku letsencrypt:cron-job --add

  <span class="hljs-comment"># (on local machine) add a remote `dokku` to your git repo, and deploy 🚀</span>
  <span class="hljs-comment"># git remote add dokku dokku@your-server:hedgedoc</span>
  <span class="hljs-comment"># git push dokku dokku:master</span>

  <span class="hljs-comment"># after successful deployment, you can add a user</span>
  dokku run hedgedoc bin/manage_users --add user@email.com
</code></pre>
</li>
</ul>
<p>This worked pretty well, until <a target="_blank" href="https://knowyourmeme.com/memes/everything-changed-when-the-fire-nation-attacked"><s>the fire nation attacked</s></a> I switched to an arm64 server, where I encountered issues during the deployment, because the <a target="_blank" href="https://github.com/heroku/heroku-buildpack-nodejs">Heroku buildpack for Node.js</a> <a target="_blank" href="https://github.com/heroku/heroku-buildpack-nodejs/issues/964">did not work on arm64</a>. I therefore had to consider another approach.</p>
<h2 id="heading-dockerfile-approach">Dockerfile approach</h2>
<p>At the time of writing this post, HedgeDoc <a target="_blank" href="https://docs.hedgedoc.org/setup/docker/">has a Docker image</a>, which doesn't support the arm64 architecture. However, the good folks at <a target="_blank" href="https://linuxserver.io">LinuxServer.io</a> have created an <a target="_blank" href="https://docs.hedgedoc.org/setup/community/#linuxserverio-docker-image">Alpine-based multi-arch container image</a> which supports arm64.</p>
<p>I tried two approaches:</p>
<ol>
<li><p>deploy the <a target="_blank" href="https://github.com/linuxserver/docker-hedgedoc">linuxserver.io image</a> to my Dokku instance using the <a target="_blank" href="https://dokku.com/docs/deployment/methods/image/">Docker Image Deployment approach</a>. I failed to make it work.</p>
</li>
<li><p>create my own Dockerfile based on the above image, and deploy via the <a target="_blank" href="https://dokku.com/docs/deployment/builders/dockerfiles/">Dockerfile deployment approach</a>. After <a target="_blank" href="https://github.com/engineervix/hedgedoc/commits/dokku/?author=engineervix&amp;since=2023-10-01&amp;until=2023-10-31">many desperate attempts</a>, I still couldn't get things to work.</p>
</li>
</ol>
<p>I ended up giving up. This was in October 2023. The main issue I encountered was that when I ran the application, I couldn't connect to the database, as the application somehow couldn't read the <a target="_blank" href="https://docs.hedgedoc.org/configuration/">environment variables</a> I had set.</p>
<p>Two months later, I decided to revisit the problem, because I really missed HedgeDoc, having become accustomed to it after previously using it for about a year. I couldn't find any suitable replacement and I was getting frustrated.</p>
<p>I realised that the <a target="_blank" href="https://github.com/linuxserver/docker-hedgedoc">linuxserver.io image</a> was <a target="_blank" href="https://github.com/linuxserver/docker-hedgedoc#docker-compose-recommended-click-here-for-more-info">primarily meant to be deployed via docker compose</a>, so I decided to focus on creating my own Dockerfile, where I would have more control.</p>
<p><a target="_blank" href="https://stackoverflow.com/questions/63277210/get-env-variables-with-dockerized-vue-app-in-dokku">This Stack Overflow post</a> was my eureka moment! It seems the environment variables I set (via <code>dokku config:set hedgedoc KEY=VALUE</code>) were not available at build time for non-buildpack-based deploys. According to the <a target="_blank" href="https://dokku.com/docs/configuration/environment-variables/">Dokku docs</a>:</p>
<blockquote>
<p>Environment variables are available both at run time and during the application build/compilation step for buildpack-based deploys.</p>
</blockquote>
<p>Even before I stumbled upon the aforementioned Stack Overflow post, I was setting <a target="_blank" href="https://dokku.com/docs/deployment/builders/dockerfiles/#build-time-configuration-variables">build-time configuration variables</a> (via <code>dokku docker-options:add hedgedoc build '--build-arg KEY=VALUE</code>'), but things were still broken.</p>
<p>What was missing is using both <code>ARG</code> and <code>ENV</code> in the Dockerfile (I was only using <code>ENV</code>), like this:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">ARG</span> CMD_DOMAIN
<span class="hljs-keyword">ENV</span> CMD_DOMAIN=${CMD_DOMAIN}
</code></pre>
<p>And with this, I was finally able to have a working HedgeDoc deployment 🎉!</p>
<p>So, what does the final code look like? Well, <a target="_blank" href="https://github.com/hedgedoc/hedgedoc/compare/master...engineervix:hedgedoc:dokku?expand=1">here's a diff</a> between the <code>master</code> branch on the official HedgeDoc repository and the <code>dokku</code> branch on my clone (click on the <strong>Files changed</strong> tab). The key changes are:</p>
<ul>
<li><p>addition of a <code>Dockerfile</code></p>
<pre><code class="lang-dockerfile">  <span class="hljs-keyword">FROM</span> node:<span class="hljs-number">18.18</span>.<span class="hljs-number">2</span>-bullseye

  <span class="hljs-keyword">RUN</span><span class="bash"> mkdir -p /home/node/app &amp;&amp; chown -R node:node /home/node/app</span>

  <span class="hljs-comment"># Set the working directory in the container</span>
  <span class="hljs-keyword">WORKDIR</span><span class="bash"> /home/node/app</span>

  <span class="hljs-comment"># Port used by this container to serve HTTP.</span>
  <span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">3000</span>

  <span class="hljs-comment"># set environment variables</span>
  <span class="hljs-comment"># reference: https://docs.hedgedoc.org/configuration/</span>
  <span class="hljs-keyword">ARG</span> CMD_DOMAIN \
      CMD_SESSION_SECRET \
      CMD_ALLOW_ORIGIN \
      CMD_IMAGE_UPLOAD_TYPE \
      CMD_DB_URL
  <span class="hljs-keyword">ENV</span> PORT=<span class="hljs-number">3000</span> \
      CMD_PORT=<span class="hljs-number">3000</span> \
      CMD_IMAGE_UPLOAD_TYPE=${CMD_IMAGE_UPLOAD_TYPE} \
      CMD_ALLOW_ANONYMOUS=false \
      CMD_PROTOCOL_USESSL=true \
      CMD_URL_ADDPORT=false \
      CMD_ALLOW_EMAIL_REGISTER=false \
      CMD_DB_DIALECT=postgres \
      NODE_ENV=production \
      CMD_DOMAIN=${CMD_DOMAIN} \
      CMD_SESSION_SECRET=${CMD_SESSION_SECRET} \
      CMD_ALLOW_ORIGIN=${CMD_ALLOW_ORIGIN} \
      CMD_DB_URL=${CMD_DB_URL} \
      <span class="hljs-comment"># necessary on ARM because puppeteer doesn't provide a prebuilt binary</span>
      PUPPETEER_SKIP_DOWNLOAD=true

  <span class="hljs-comment"># Yarn 3 requires corepack to be enabled</span>
  <span class="hljs-keyword">RUN</span><span class="bash"> corepack <span class="hljs-built_in">enable</span></span>

  <span class="hljs-comment"># Switch to the non-root user</span>
  <span class="hljs-keyword">USER</span> node

  <span class="hljs-comment"># Copy the code to the container</span>
  <span class="hljs-keyword">COPY</span><span class="bash"> --chown=node:node . .</span>
  <span class="hljs-keyword">COPY</span><span class="bash"> --chown=node:node config.json.example ./config.json</span>

  <span class="hljs-comment"># Remove the "saml" section from the config.json file</span>
  <span class="hljs-comment"># because of https://community.hedgedoc.org/t/change-certificate-file-path-of-idp-in-pem-format/108/3</span>
  <span class="hljs-keyword">RUN</span><span class="bash"> sed -i <span class="hljs-string">'/"saml": {/,/},/d'</span> ./config.json</span>

  <span class="hljs-comment"># Install the application dependencies</span>
  <span class="hljs-keyword">RUN</span><span class="bash"> yarn workspaces focus --production</span>

  <span class="hljs-comment"># # Build the frontend bundle</span>
  <span class="hljs-keyword">RUN</span><span class="bash"> yarn install --immutable &amp;&amp; \
      yarn run build</span>

  <span class="hljs-comment"># Runtime command that executes when "docker run" is called</span>
  <span class="hljs-comment"># do nothing - exec commands elsewhere</span>
  <span class="hljs-keyword">CMD</span><span class="bash"> tail -f /dev/null</span>
</code></pre>
</li>
<li><p>addition of a <code>Procfile</code></p>
<pre><code class="lang-plaintext">  web: yarn start
</code></pre>
</li>
<li><p>addition of a <code>.dockerignore</code> file</p>
<pre><code class="lang-plaintext">  .git
  .gitignore
  node_modules
  build
  Dockerfile
  .dockerignore
  .gitignore
  .env
</code></pre>
</li>
</ul>
<p>As part of the troubleshooting process, I did also modify some files:</p>
<ul>
<li><p><code>lib/config/default.js</code></p>
<pre><code class="lang-diff">  -  dbURL: '',
  +  dbURL: process.env.DATABASE_URL || '',
</code></pre>
</li>
<li><p><code>lib/models/index.js</code></p>
<pre><code class="lang-diff">    if (config.dbURL) {
  +    logger.info(config.dbURL)
      sequelize = new Sequelize(config.dbURL, dbconfig)
    } else {
  +    logger.warn('config.dbURL is not set')
      sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig)
    }
</code></pre>
</li>
</ul>
<p>I ended up leaving those modifications (I was probably tired and just wanted to use my HedgeDoc!), you don't need to make them, though they might be useful.</p>
<p>Here is how I deployed to Dokku (again, all commands are executed on the server, unless otherwise indicated).</p>
<pre><code class="lang-bash"><span class="hljs-comment"># create app</span>
dokku apps:create hedgedoc

<span class="hljs-comment"># add a domain to your app</span>
dokku domains:add hedgedoc example.com

<span class="hljs-comment"># setup postgres | https://github.com/dokku/dokku-postgres</span>
<span class="hljs-comment"># (feel free to use your preferred database backend)</span>
dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres
<span class="hljs-comment"># feel free to choose the image &amp; image version of your choice here</span>
dokku postgres:create postgres-hedgedoc --image <span class="hljs-string">"postgres"</span> --image-version <span class="hljs-string">"13.4"</span>
dokku postgres:link postgres-hedgedoc hedgedoc
<span class="hljs-comment"># Take note of the DATABASE_URL, which you will need to assign to CMD_DB_URL later</span>

<span class="hljs-comment"># set up authentication for backups on the postgres service</span>
<span class="hljs-comment"># Datastore backups are supported via AWS S3 and S3 compatible services</span>
<span class="hljs-comment"># like https://github.com/minio/minio and https://www.backblaze.com/b2/cloud-storage.html</span>
<span class="hljs-comment"># dokku postgres:backup-auth &lt;service&gt; &lt;aws-access-key-id&gt; &lt;aws-secret-access-key&gt; &lt;aws-default-region&gt; &lt;aws-signature-version&gt; &lt;endpoint-url&gt;</span>
dokku postgres:backup-auth postgres-hedgedoc aws-access-key-id aws-secret-access-key us-west-004 v4 https://s3.us-west-004.backblazeb2.com
<span class="hljs-comment"># postgres:backup postgres-hedgedoc &lt;bucket-name&gt;</span>
dokku postgres:backup postgres-hedgedoc bucket-name
<span class="hljs-comment"># everyday at 2:45AM, 10:45AM, 6:45PM</span>
dokku postgres:backup-schedule postgres-hedgedoc <span class="hljs-string">"45 2,10,18 * * *"</span> bucket-name

<span class="hljs-comment"># Persistent Storage &lt;https://dokku.com/docs/advanced-usage/persistent-storage/&gt;</span>
<span class="hljs-comment"># Create a persistent storage directory in the recommended storage path</span>
dokku storage:ensure-directory --chown heroku hedgedoc
<span class="hljs-comment"># Create a new bind mount</span>
dokku storage:mount hedgedoc /var/lib/dokku/data/storage/hedgedoc/uploads:/home/node/app/public/uploads

<span class="hljs-comment"># set env variables for your app</span>
<span class="hljs-comment"># I wonder whether this is even necessary, since we are actually using Docker build args</span>
dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_DOMAIN=example.com &amp;&amp; \
dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_ALLOW_ORIGIN=<span class="hljs-string">'["example.com"]'</span> &amp;&amp; \
dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_SESSION_SECRET=somethingsecret &amp;&amp; \
dokku config:<span class="hljs-built_in">set</span> --no-restart hedgedoc CMD_IMAGE_UPLOAD_TYPE=filesystem &amp;&amp; \
<span class="hljs-comment"># <span class="hljs-doctag">NOTE:</span> set CMD_DB_URL to whatever the value of DATABASE_URL is</span>
dokku config:<span class="hljs-built_in">set</span> hedgedoc CMD_DB_URL=...

<span class="hljs-comment"># customize Docker Build-time configuration variables</span>
<span class="hljs-comment"># https://dokku.com/docs/deployment/builders/dockerfiles/#build-time-configuration-variables</span>
<span class="hljs-comment"># --------------------------------------------------------------------------------------------</span>
dokku docker-options:add hedgedoc build <span class="hljs-string">'--build-arg CMD_DOMAIN=example.com'</span> &amp;&amp; \
dokku docker-options:add hedgedoc build <span class="hljs-string">'--build-arg CMD_ALLOW_ORIGIN=["example.com"]'</span> &amp;&amp; \
dokku docker-options:add hedgedoc build <span class="hljs-string">'--build-arg CMD_SESSION_SECRET=somethingsecret'</span> &amp;&amp; \
dokku docker-options:add hedgedoc build <span class="hljs-string">'--build-arg CMD_IMAGE_UPLOAD_TYPE=filesystem'</span> &amp;&amp; \
dokku docker-options:add hedgedoc build <span class="hljs-string">'--build-arg CMD_DB_URL=postgres://username:password@host:port/database'</span>
<span class="hljs-comment"># add others based on your requirements. See https://docs.hedgedoc.org/configuration</span>
<span class="hljs-comment"># <span class="hljs-doctag">NOTE:</span> I just saw [from the docs](https://dokku.com/docs/deployment/builders/dockerfiles/#build-time-configuration-variables) as I was writing this, that:</span>
<span class="hljs-comment"># "All environment variables set by the config plugin are automatically exported during a docker build,</span>
<span class="hljs-comment">#  and thus --build-arg only requires setting a key without a value.</span>
<span class="hljs-comment">#</span>
<span class="hljs-comment">#  dokku docker-options:add node-js-app build '--build-arg NODE_ENV'</span>
<span class="hljs-comment"># "</span>

<span class="hljs-comment"># Customize Nginx | set `client_max_body_size`, to make upload feature work better, for example</span>
dokku nginx:<span class="hljs-built_in">set</span> hedgedoc client-max-body-size 50m
<span class="hljs-comment"># regenerate config</span>
dokku proxy:build-config hedgedoc

<span class="hljs-comment"># ports</span>
dokku ports:add hedgedoc http:80:3000 &amp;&amp; \
dokku ports:add hedgedoc https:443:3000 &amp;&amp; \
dokku ports:remove hedgedoc http:80:5000 &amp;&amp; \
dokku ports:remove hedgedoc https:443:5000

<span class="hljs-comment"># letsencrypt</span>
dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
dokku config:<span class="hljs-built_in">set</span> --no-restart --global DOKKU_LETSENCRYPT_EMAIL=user@example.com
dokku letsencrypt:<span class="hljs-built_in">set</span> hedgedoc email user@example.com
dokku letsencrypt:<span class="hljs-built_in">enable</span> hedgedoc
<span class="hljs-comment"># this would setup a cron job to renew letsencrypt certificate</span>
dokku letsencrypt:cron-job --add

<span class="hljs-comment"># (on local machine) add a remote `dokku` to your git repo, and deploy 🚀</span>
<span class="hljs-comment"># git remote add dokku dokku@your-server:hedgedoc</span>
<span class="hljs-comment"># git push dokku dokku:master</span>

<span class="hljs-comment"># after successful deployment, you can add a user</span>
dokku run hedgedoc bin/manage_users --add user@email.com
</code></pre>
<p>You will observe that the commands executed above are not so different from the ones for the Heroku buildpack approach. The main differences:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Heroku buildpack approach</td><td>Dockerfile approach</td></tr>
</thead>
<tbody>
<tr>
<td>We specify a buildpack <code>(dokku buildpacks:add hedgedoc https://github.com/heroku/heroku-buildpack-nodejs.git)</code>.</td><td>We do not specify a buildback. Because the repo has a <code>Dockerfile</code>, Dokku will detect it and use the <a target="_blank" href="https://dokku.com/docs/deployment/builders/dockerfiles/"><code>builder-dockerfile</code></a>.</td></tr>
<tr>
<td>No need to specify build time configuration variables, because environment variables are available both at run time and during the application build/compilation step for buildpack-based deploys.</td><td>We have to specify build time configuration variables.</td></tr>
</tbody>
</table>
</div><div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You may have noticed from the git diff that I specified a couple of <code>CMD<em>S3</em><em></em></code> variables and other <code>CMD<em>DB</em></code> variables in the <code>Dockerfile</code>, which I have not defined on the server. I initially wanted to use an <a target="_blank" href="https://www.backblaze.com/">S3-compatible cloud storage</a> for uploads. Having configured a private bucket on <a target="_blank" href="https://www.backblaze.com/">Backblaze</a>, I was able to upload files, however, I couldn't preview them, due to <a target="_blank" href="https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules#cors-on-private-buckets">the way Backblaze handles CORS on private buckets</a>. So I resorted to using the filesystem (I plan to configure automated backups via <a target="_blank" href="https://rclone.org">rclone</a>). Regarding the <code>CMD<em>DB</em>*</code> variables, if <code>CMD_DB_URL</code> is set, then we really do not need the rest. I only set these as I was troubleshooting database connection issues.</div>
</div>

<hr />
<p>Well, I hope you find this useful. If you have any suggestions, questions or comments, please feel free to comment below. Enjoy your HedgeDoc!</p>
]]></content:encoded></item><item><title><![CDATA[Building zed-news – an automated news podcast]]></title><description><![CDATA[I have always been terrible when it comes to staying up to date with the latest news. One day, while reading the Hacker Newsletter, I came across Hackercast – an AI-generated podcast summary of Hacker News. I was deeply fascinated, and this triggered...]]></description><link>https://blog.victor.co.zm/building-zed-news-an-automated-news-podcast</link><guid isPermaLink="true">https://blog.victor.co.zm/building-zed-news-an-automated-news-podcast</guid><category><![CDATA[Python]]></category><category><![CDATA[11ty]]></category><category><![CDATA[llm]]></category><category><![CDATA[podcast]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Sat, 22 Jul 2023 07:53:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1689540165556/839f29bc-afc9-4eee-970a-9321a27027d2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have always been terrible when it comes to staying up to date with the latest news. One day, while reading the <a target="_blank" href="https://hackernewsletter.com/">Hacker Newsletter</a>, I came across <a target="_blank" href="https://camrobjones.com/hackercast/">Hackercast</a> – an AI-generated podcast summary of Hacker News. I was deeply fascinated, and this triggered a 💡 moment – this could be the solution to my <em>staying up-to-date with the news</em> problem!</p>
<p>So, without any knowledge and experience in building things with <a target="_blank" href="https://history-computer.com/what-are-large-language-models/">LLMs</a>, I decided to embark on a quest to build a tool that automatically gathers news from various sources in <a target="_blank" href="https://www.google.co.zm/maps/place/Zambia/@-13.0863147,22.5642455,6z/data=!3m1!4b1!4m6!3m5!1s0x1940f4a5fcfc0b71:0xf19ff9ac7e583e45!8m2!3d-13.133897!4d27.849332!16zL20vMDg4dmI?entry=ttu">my country</a> 🇿🇲, summarizes the news articles and produces a podcast, without my input!</p>
<p>TLDR; here's the <a target="_blank" href="https://zednews.pages.dev/">result</a>, and here's the <a target="_blank" href="https://github.com/engineervix/zed-news">code</a>.</p>
<h2 id="heading-the-rationale">The rationale</h2>
<p>When starting this project, I already knew from the Hackercast project that I needed to</p>
<ul>
<li><p>scrape various news websites,</p>
</li>
<li><p>use <a target="_blank" href="https://python.langchain.com/en/latest/index.html">Langchain</a> to summarize the news articles and</p>
</li>
<li><p>use <a target="_blank" href="https://aws.amazon.com/polly/">AWS Polly</a> to convert text to speech.</p>
</li>
</ul>
<p>I was already familiar with using <a target="_blank" href="https://pypi.org/project/requests/">requests</a> and <a target="_blank" href="https://pypi.org/project/beautifulsoup4/">Beautiful Soup</a> for scraping, but I had to learn how to use Langchain and AWS Polly. I tried to consider alternatives to AWS Polly but after doing some bit of searching and experimentation, I concluded that AWS Polly was the best tool for the job, because of its</p>
<ul>
<li><p>reasonable pricing</p>
</li>
<li><p>nice, straightforward API via <a target="_blank" href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html">Boto3</a>.</p>
</li>
<li><p>many language options and features</p>
</li>
<li><p>nice documentation</p>
</li>
<li><p>excellent integration with <a target="_blank" href="https://aws.amazon.com/s3/">S3</a>, which means that I have fewer headaches about where to store the audio files.</p>
</li>
</ul>
<p>However, scraping, summarization and text-to-speech were only a small piece of the puzzle. I had to think about how the whole thing was going to work. Remember, the problem I'm trying to solve here is <em>staying up to date with the latest news</em>. I do not have time to scour through news websites and read the news, so this tool I'm building should do the job, and tell me what's going on! From the onset, I wanted</p>
<ul>
<li><p>the news to be automatically delivered every weekday in the afternoon, towards the end of my work day, so I can listen to the news on my commute or something along those lines. This has <a target="_blank" href="https://cronitor.io/guides/cron-jobs">cron</a> written all over it!</p>
</li>
<li><p>the news to be accessible from anywhere, on multiple podcast platforms. This meant I had to build my own web application with <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio">HTML Audio</a> and a proper <a target="_blank" href="https://en.wikipedia.org/wiki/RSS">RSS feed</a> which would be submitted to the various platforms.</p>
</li>
</ul>
<p>AWS Polly, S3 and OpenAI are not free. It didn't make sense to build a web application that would require additional expenses for hosting, since the main content would be audio files, which would be on an S3 bucket. I therefore concluded that I would have to build a static site and deploy it for free via services like <a target="_blank" href="https://pages.github.com/">Github Pages</a>, <a target="_blank" href="https://pages.cloudflare.com/">Cloudflare Pages</a>, <a target="_blank" href="https://render.com/">render</a>, etc.</p>
<p>Now, which <a target="_blank" href="https://jamstack.org/generators/">static site generator</a> do I use? This being a Python project, I could have used the likes of <a target="_blank" href="https://getpelican.com/">Pelican</a> or even <a target="_blank" href="https://pypi.org/project/Frozen-Flask/">Frozen Flask</a>. However, it's been a while since I played around with Pelican, and I've never even used Frozen Flask. Besides, using Flask would be overkill for this. I was already learning so many things, and didn't want to spend too much time tinkering with shiny new toys, at the expense of actually solving the problem at hand and getting things done. So I resorted to using <a target="_blank" href="https://www.11ty.dev/">11ty</a>, which I was very familiar with and had used recently. I particularly love 11ty as I find it simple to use, yet very powerful and unopinionated, which means I can build anything with it, with the freedom to choose which template engine to use, how to structure my project, which build tools to use, etc.</p>
<p>To sum it up, I broke down the project into two main components:</p>
<ul>
<li><p>the <strong>core</strong> – which consists of the Python code that handles the fetching of news items, summarization, audio production and creation of content to be passed to the web app.</p>
</li>
<li><p>the <strong>web app</strong> – which renders the generated content.</p>
</li>
</ul>
<p>Now, you may ask, "What's with the name?". Well, I chose <strong>zed-news</strong> because <em>Zed</em> is for Zambia, and this whole project is a podcast consisting of news curated from Zambian sources. That makes sense, right? Well, <a target="_blank" href="https://martinfowler.com/bliki/TwoHardThings.html">naming things is hard</a>!</p>
<h2 id="heading-the-experience">The experience</h2>
<p>Building zed-news was an interesting and fun challenge. I learnt a lot of things along the way, and I had to rewrite some things and make some important implementation decisions. I will highlight a few noteworthy aspects here.</p>
<h3 id="heading-making-things-less-robotic-and-more-natural">Making things less "robotic" and more "natural"</h3>
<p>The result of this project is an audio file consisting of the news read by an AWS Polly voice. To minimise monotony and avoid having each episode sound the same,</p>
<ul>
<li><p>I grouped several phrases/sentences and picked a random one at various points when constructing the transcript. Here's an example</p>
<pre><code class="lang-python">  <span class="hljs-keyword">import</span> random

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">random_dig_in</span>():</span>
      variations = [
          <span class="hljs-string">"Without any more delay, let's jump right in."</span>,
          <span class="hljs-string">"No time to waste, let's get started."</span>,
          <span class="hljs-string">"Let's not wait any longer, it's time to delve in."</span>,
          <span class="hljs-string">"Without further ado, let's dive in."</span>,
          <span class="hljs-string">"Without prolonging the anticipation, let's begin our exploration."</span>,
          <span class="hljs-string">"Time to embark on our news journey. Let's get to it."</span>,
      ]

      <span class="hljs-keyword">return</span> random.choice(variations)
</code></pre>
</li>
<li><p>I used the <a target="_blank" href="https://github.com/savoirfairelinux/num2words">num2words</a> library to convert numbers to words. This was particularly useful for generating ordinal numbers like <strong>seventh</strong> or <strong>thirty-first</strong>. Here's an example of how I used it</p>
<pre><code class="lang-python">  <span class="hljs-keyword">import</span> random
  <span class="hljs-keyword">from</span> num2words <span class="hljs-keyword">import</span> num2words

  read = <span class="hljs-string">""</span>
  <span class="hljs-comment"># ...</span>
  <span class="hljs-comment"># ...</span>
  <span class="hljs-comment"># Iterate over each article in the source</span>
  <span class="hljs-keyword">for</span> index, article <span class="hljs-keyword">in</span> enumerate(articles_by_source[source], start=<span class="hljs-number">1</span>):
      count = num2words(index)
      count_ordinal = num2words(index, to=<span class="hljs-string">"ordinal"</span>)
      count_variations = [
          <span class="hljs-string">f"Entry number <span class="hljs-subst">{count}</span> "</span>,
          <span class="hljs-string">f"The <span class="hljs-subst">{count_ordinal}</span> entry "</span>,
      ]
      title = article[<span class="hljs-string">"title"</span>]
      read += <span class="hljs-string">f"<span class="hljs-subst">{random.choice(count_variations)}</span> is entitled '<span class="hljs-subst">{title}</span>' "</span>
</code></pre>
</li>
</ul>
<h3 id="heading-keeping-track-of-the-episode-number">Keeping track of the episode number</h3>
<p>Because I wanted to have this project run on auto-pilot, I needed a way for it to know that today's episode is number 8, for example. Initially, I thought I was going to run this every day, so I based the episode number on the first date the podcast was published. However, I quickly saw that this would be a problem because anything could go wrong and no podcasts would be published on certain days. Besides, I later decided to just run this on weekdays. Therefore, I needed to keep track of the episode number so that I could just increment when publishing a new episode.</p>
<h3 id="heading-serverless-postgres-with-neon">Serverless Postgres with Neon</h3>
<p>I decided to not only keep track of the episode number but also all the pertinent episode information, and thought a database would be a good solution. I have always wanted to try out the Serverless Postgres offering by <a target="_blank" href="https://neon.tech/">neon.tech</a>, so this was a good opportunity to do so. The whole process has been pretty smooth so far.</p>
<p>Choosing an ORM was difficult, I ended up settling for <a target="_blank" href="https://tortoise.github.io/">Tortoise ORM</a> because I'd previously played around with it while learning <a target="_blank" href="https://fastapi.tiangolo.com/">FastAPI</a>. The main reason I liked Tortoise ORM is because it has a syntax similar to the <a target="_blank" href="https://docs.djangoproject.com/en/4.2/topics/db/queries/">Django ORM</a>, which means a lower learning curve for me, or so I thought ...</p>
<h3 id="heading-async-await">Async / Await</h3>
<p>... You see, Tortoise ORM is an <a target="_blank" href="https://docs.python.org/3/library/asyncio.html">asyncio</a> ORM, which introduces additional complexities into the mix (See <a target="_blank" href="https://charlesleifer.com/blog/asyncio/">this article</a> by Charles Leifer, and <a target="_blank" href="https://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/">this one</a> by Armin Ronacher). I spent a lot of time fixing <code>async</code> / <code>await</code> issues, and I still don't really know what's going on under the hood! But hey, it works, right?😀 Well, that's a subject for another day!</p>
<h3 id="heading-summarization">Summarization</h3>
<p>It wasn't hard to get up and running with <a target="_blank" href="https://python.langchain.com/en/latest/index.html">Langchain</a>, which provides a very nice API to work with LLMs. The resource "<a target="_blank" href="https://github.com/gkamradt/langchain-tutorials/blob/main/data_generation/5%20Levels%20Of%20Summarization%20-%20Novice%20To%20Expert.ipynb">5 Levels Of Summarization: Novice to Expert</a>" was very helpful. I went with the simplest approach, as it was sufficient for this project.</p>
<p>I tried playing around with <a target="_blank" href="https://cohere.com/">Cohere</a>'s <a target="_blank" href="https://docs.cohere.com/reference/summarize-2">Summarize API</a>, but I found it a bit disappointing because of crazy hallucinations where it mixed up content based on old events.</p>
<p>One issue I encountered was exceeding OpenAI's token limit of 4096 tokens. This happened when pricing longer articles. The typical solution for this is to split the content into smaller chunks that fit within the token limit, summarize each chunk, and then get a summary of the summaries. The technical term is referred to as <strong>Map - Reduce</strong>, and falls under <em>Level 3</em> in the above resource. However, I didn't bother to go for this technique, because I didn't have the time. So I just wrote a quick and dirty hack, where I truncate the content to fit within the token limit and ignore the rest of the content:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> logging
<span class="hljs-keyword">import</span> math
<span class="hljs-keyword">import</span> textwrap

<span class="hljs-keyword">from</span> langchain <span class="hljs-keyword">import</span> OpenAI, PromptTemplate

<span class="hljs-keyword">from</span> app.core.utilities <span class="hljs-keyword">import</span> OPENAI_API_KEY

llm = OpenAI(temperature=<span class="hljs-number">0</span>, openai_api_key=OPENAI_API_KEY)
MAX_TOKENS = <span class="hljs-number">4096</span>


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">summarize</span>(<span class="hljs-params">content: str, title: str</span>) -&gt; str:</span>
    <span class="hljs-string">"""Summarize the content using OpenAI's language model."""</span>

    template = <span class="hljs-string">"""
    Please provide a very short, sweet, informative and engaging summary of the following news entry, in not more than two sentences.
    Please provide your output in a manner suitable for reading as part of a podcast.

    {entry}
    """</span>

    <span class="hljs-comment"># Calculate the maximum number of tokens available for the prompt</span>
    max_prompt_tokens = MAX_TOKENS - llm.get_num_tokens(template)

    <span class="hljs-comment"># Trim the content if it exceeds the available tokens</span>
    <span class="hljs-comment"># <span class="hljs-doctag">TODO:</span> Instead of truncating the content, split it</span>
    <span class="hljs-comment"># see &lt;https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/split_by_token&gt;</span>
    chars = int(max_prompt_tokens * <span class="hljs-number">3.75</span>)  <span class="hljs-comment"># Assuming 1 token ≈ 4 chars</span>
    <span class="hljs-comment"># round down max_chars to the nearest 100</span>
    max_chars = math.floor(chars / <span class="hljs-number">100</span>) * <span class="hljs-number">100</span>
    <span class="hljs-keyword">if</span> len(content) &gt; max_chars:
        content = textwrap.shorten(content, width=max_chars, placeholder=<span class="hljs-string">" ..."</span>)

    prompt = PromptTemplate(input_variables=[<span class="hljs-string">"entry"</span>], template=template)

    summary_prompt = prompt.format(entry=content)

    num_tokens = llm.get_num_tokens(summary_prompt)
    logging.info(<span class="hljs-string">f"'<span class="hljs-subst">{title}</span>' and its prompt has <span class="hljs-subst">{num_tokens}</span> tokens"</span>)

    <span class="hljs-keyword">return</span> llm(summary_prompt)
</code></pre>
<h3 id="heading-defining-the-toolchain-and-cron-job">Defining the toolchain and cron job</h3>
<p>I'm a big fan of <a target="_blank" href="http://www.pyinvoke.org/">Invoke</a>, which I wrote about <a target="_blank" href="https://importthis.tech/task-execution-and-automation-using-invoke">in this post</a>. After coding all the bits and pieces, I tied everything together with Invoke. Well, I actually just created a <code>run.py</code> script with a <code>main()</code> function that runs when the <code>run.py</code> script is executed directly. Then I created an Invoke task:</p>
<pre><code class="lang-python"><span class="hljs-meta">@task</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">toolchain</span>(<span class="hljs-params">c</span>):</span>
    <span class="hljs-string">"""The toolchain for creating the podcast"""</span>
    c.run(<span class="hljs-string">"python app/core/run.py"</span>, pty=<span class="hljs-literal">True</span>)
</code></pre>
<p>The idea was to run <code>inv toolchain</code> as part of a cron job script on an Ubuntu VPS. I wrote a <code>cron.sh</code> BASH script and added a bunch of commands there. I made use of <a target="_blank" href="https://healthchecks.io/">healthchecks.io</a> to ensure that I'm aware when the script fails to run. Here's what the script looks like:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/usr/bin/env bash</span>

<span class="hljs-comment"># ==========================================================</span>
<span class="hljs-comment"># Logical steps</span>
<span class="hljs-comment"># 1. cd to project directory</span>
<span class="hljs-comment"># 2. Activate virtual environment</span>
<span class="hljs-comment"># 3. git pull</span>
<span class="hljs-comment"># 4. Run script inside docker container (cleanup afterwards)</span>
<span class="hljs-comment"># 5. commit changes</span>
<span class="hljs-comment"># 6. push changes to remote</span>
<span class="hljs-comment"># ==========================================================</span>

<span class="hljs-built_in">set</span> -e  <span class="hljs-comment"># Exit immediately if any command fails</span>

<span class="hljs-comment"># 1. cd to project directory</span>
<span class="hljs-built_in">cd</span> <span class="hljs-string">"<span class="hljs-variable">${HOME}</span>/SITES/zed-news"</span> || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to change directory."</span>; <span class="hljs-built_in">exit</span> 1; }

<span class="hljs-comment"># Source the .env file so we can retrieve healthchecks.io ping URL</span>
<span class="hljs-comment"># shellcheck source=/dev/null</span>
<span class="hljs-built_in">source</span> .env

<span class="hljs-comment"># Function to send success signal to healthchecks.io</span>
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">send_healthcheck_success</span></span>() {
  curl -fsS --retry 3 <span class="hljs-string">"<span class="hljs-variable">${HEALTHCHECKS_PING_URL}</span>"</span> &gt; /dev/null
}

<span class="hljs-comment"># Function to send failure signal to healthchecks.io</span>
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">send_healthcheck_failure</span></span>() {
  curl -fsS --retry 3 <span class="hljs-string">"<span class="hljs-variable">${HEALTHCHECKS_PING_URL}</span>/fail"</span> &gt; /dev/null
}

<span class="hljs-comment"># 2. Activate virtual environment</span>
<span class="hljs-comment"># shellcheck source=/dev/null</span>
<span class="hljs-built_in">source</span> <span class="hljs-string">"<span class="hljs-variable">${HOME}</span>/Env/zed-news/bin/activate"</span> || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to activate virtual environment."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }

<span class="hljs-comment"># 3. git pull</span>
git pull || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to pull changes from Git."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }

<span class="hljs-comment"># 4. Run script inside docker container</span>
inv up --build || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to build Docker container."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }
docker-compose run --rm app invoke toolchain || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to run script inside Docker container."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }
inv down || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to stop Docker container."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }

<span class="hljs-comment"># 5. commit changes</span>
today_iso=$(date --iso)
git add . || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to stage changes for commit."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }
git commit --no-verify -m <span class="hljs-string">"chore: ✨ new episode 🎙️ » <span class="hljs-variable">${today_iso}</span>"</span> || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to commit changes."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }

<span class="hljs-comment"># 6. push changes to remote</span>
git push origin main || { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to push changes to remote repository."</span>; send_healthcheck_failure; <span class="hljs-built_in">exit</span> 1; }

<span class="hljs-comment"># Send success signal to healthchecks.io</span>
send_healthcheck_success

<span class="hljs-comment"># Pause for 5 minutes</span>
sleep 300

<span class="hljs-comment"># Notify Admin via Telegram</span>
<span class="hljs-comment"># See next section ...</span>
</code></pre>
<p>Pushing the changes to remote triggers a Cloudflare Pages build &amp; deployment. I pause the script for 5 minutes to allow this process to complete, before notifying myself.</p>
<h3 id="heading-notifying-myself-when-a-new-episode-is-published">Notifying myself when a new episode is published</h3>
<p>As I have already mentioned, this project was really a fantastic learning opportunity for me. I learned how to use <a target="_blank" href="https://github.com/caronc/apprise">apprise</a>, by configuring it to send myself a message via <a target="_blank" href="https://telegram.org/">Telegram</a> when a new episode was published. This is the last step in the <code>cron.sh</code> script:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Notify Admin via Telegram</span>
today_human_readable=$(date +<span class="hljs-string">"%a %d %b %Y"</span>)
apprise -vv -t <span class="hljs-string">"🎙️ New Episode » <span class="hljs-variable">${today_human_readable}</span>"</span> \
  -b <span class="hljs-string">"📻 Listen now at <span class="hljs-variable">${BASE_URL}</span>/episode/<span class="hljs-variable">${today_iso}</span>/"</span> \
  tgram://<span class="hljs-string">"<span class="hljs-variable">${TELEGRAM_BOT_TOKEN}</span>"</span>
</code></pre>
<h3 id="heading-switching-from-pip-tools-to-poetry">Switching from pip-tools to poetry</h3>
<p>I have been using <a target="_blank" href="https://github.com/jazzband/pip-tools">pip-tools</a> in my projects for a while, I even wrote about it <a target="_blank" href="https://importthis.tech/python-deps-management-using-pip-tools">here</a>. However, I use <a target="_blank" href="https://python-poetry.org/">Poetry</a> a lot at work, and have come to love it! Initially, I just wanted to update my pip-tools setup to use <code>pyproject.toml</code> to manage dependencies, instead of multiple <code>requirements*</code> files. However, I ended up convincing myself to just switch to Poetry, which I did, and it was fun porting from pip-tools!</p>
<h3 id="heading-switching-from-flake8-isort-pycodestyle-to-ruff">Switching from flake8 / isort/ pycodestyle to ruff</h3>
<p>Having heard about <a target="_blank" href="https://github.com/astral-sh/ruff">ruff</a>, I decided to give it a try on this project. One of the motivations for this was that I would have fewer config files in my project root since ruff uses <code>pyproject.toml</code>! Also, it's one linting command instead of several! The switch was fairly smooth, I didn't encounter any issues that I can immediately recall. My Invoke linting task looks like this:</p>
<pre><code class="lang-python"><span class="hljs-meta">@task(help={"fix": "let black and ruff format your files"})</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lint</span>(<span class="hljs-params">c, fix=False</span>):</span>
    <span class="hljs-string">"""ruff and black"""</span>

    <span class="hljs-keyword">if</span> fix:
        c.run(<span class="hljs-string">"black ."</span>, pty=<span class="hljs-literal">True</span>)
        c.run(<span class="hljs-string">"ruff check --fix ."</span>, pty=<span class="hljs-literal">True</span>)
    <span class="hljs-keyword">else</span>:
        c.run(<span class="hljs-string">"black . --check"</span>, pty=<span class="hljs-literal">True</span>)
        c.run(<span class="hljs-string">"ruff check ."</span>, pty=<span class="hljs-literal">True</span>)
</code></pre>
<p>And the GitHub Actions job looks like this</p>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">linter_ruff:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-22.04</span>
    <span class="hljs-attr">container:</span> <span class="hljs-string">python:3.10-slim-bullseye</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">Code</span> <span class="hljs-string">Repository</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Dependencies</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          pip install -q ruff==0.0.270
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">ruff</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|</span>
          <span class="hljs-string">ruff</span> <span class="hljs-string">check</span> <span class="hljs-string">--format=github</span> <span class="hljs-string">.</span>
</code></pre>
<h3 id="heading-audio-mixing-on-auto-pilot">Audio mixing on auto-pilot</h3>
<p>In December 2022, I wrote about <a target="_blank" href="https://importthis.tech/multimedia-cli-tools">using CLI tools to manage audio, video, images and PDF files</a>. One of these tools is <a target="_blank" href="https://www.ffmpeg.org/">FFmpeg</a>, and it came in quite handy in this project because I had to automate various aspects of the audio production, as follows:</p>
<ol>
<li><p>Adjustment of the AWS Polly generated audio file's <a target="_blank" href="https://www.adobe.com/uk/creativecloud/video/discover/audio-sampling.html">sample rate</a> from 24 kHz to 44.1 kHz. This is the standard for most consumer audio and is used for formats like CDs.</p>
</li>
<li><p>Converting the result from (1) above from <a target="_blank" href="https://en.wikipedia.org/wiki/Monaural">mono</a> to <a target="_blank" href="https://www.adobe.com/uk/creativecloud/video/discover/audio-bitrate.html">128 kb/s</a> <a target="_blank" href="https://en.wikipedia.org/wiki/Stereophonic_sound">stereo</a>.</p>
</li>
<li><p>Adjustment of the treble (high frequency).</p>
</li>
<li><p>Addition of both an intro and outro instrumental.</p>
</li>
<li><p>Addition of <a target="_blank" href="https://en.wikipedia.org/wiki/ID3">ID3 tags</a> to the final audio file</p>
</li>
</ol>
<p>I wrote a function to do these things:</p>
<pre><code class="lang-python"><span class="hljs-comment"># For more details, see </span>
<span class="hljs-comment"># https://github.com/engineervix/zed-news/blob/v0.3.1/app/core/podcast/mix.py</span>

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mix_audio</span>(<span class="hljs-params">voice_track, intro_track, outro_track, dest=<span class="hljs-string">f"<span class="hljs-subst">{DATA_DIR}</span>/<span class="hljs-subst">{today_iso_fmt}</span>_podcast_dist.mp3"</span></span>):</span>
    <span class="hljs-string">"""
    Mix the voice track, intro track, and outro track into a single audio file
    """</span>

    voice_track_file_name = os.path.splitext(voice_track)[<span class="hljs-number">0</span>]
    mix_44100 = <span class="hljs-string">f"<span class="hljs-subst">{voice_track_file_name}</span>.44.1kHz.mp3"</span>
    voice_track_in_stereo = <span class="hljs-string">f"<span class="hljs-subst">{voice_track_file_name}</span>.stereo.mp3"</span>
    eq_mix = <span class="hljs-string">f"<span class="hljs-subst">{voice_track_file_name}</span>.eq-mix.mp3"</span>
    initial_mix = <span class="hljs-string">f"<span class="hljs-subst">{voice_track_file_name}</span>.mix-01.mp3"</span>

    <span class="hljs-comment"># change the voice track sample rate to 44.1 kHz</span>
    subprocess.run(
        <span class="hljs-string">f"ffmpeg -i <span class="hljs-subst">{voice_track}</span> -ar 44100 <span class="hljs-subst">{mix_44100}</span>"</span>,
        shell=<span class="hljs-literal">True</span>,
    )

    <span class="hljs-comment"># convert voice track from mono to 128 kb/s stereo</span>
    subprocess.run(
        <span class="hljs-string">f'ffmpeg -i <span class="hljs-subst">{mix_44100}</span> -af "pan=stereo|c0=c0|c1=c0" -b:a 128k <span class="hljs-subst">{voice_track_in_stereo}</span>'</span>,
        shell=<span class="hljs-literal">True</span>,
    )

    <span class="hljs-comment"># adjust the treble (high-frequency).</span>
    <span class="hljs-comment"># The g=3 parameter specifies the gain in decibels (dB) to be applied to the treble frequencies.</span>
    subprocess.run(
        <span class="hljs-string">f'ffmpeg -i <span class="hljs-subst">{voice_track_in_stereo}</span> -af "treble=g=3" <span class="hljs-subst">{eq_mix}</span>'</span>,
        shell=<span class="hljs-literal">True</span>,
    )

    <span class="hljs-comment"># initial mix: the intro + voice track</span>
    subprocess.run(
        <span class="hljs-string">f'ffmpeg -i <span class="hljs-subst">{eq_mix}</span> -i <span class="hljs-subst">{intro_track}</span> -filter_complex amix=inputs=2:duration=longest:dropout_transition=0:weights="1 0.25":normalize=0 <span class="hljs-subst">{initial_mix}</span>'</span>,
        shell=<span class="hljs-literal">True</span>,
    )

    <span class="hljs-comment"># get duration of the initial mix</span>
    command_1 = <span class="hljs-string">f'ffmpeg -i <span class="hljs-subst">{initial_mix}</span> 2&gt;&amp;1 | grep "Duration"'</span>
    output_1 = run_ffmpeg_command(command_1)
    duration_1 = extract_duration_in_milliseconds(output_1)

    <span class="hljs-comment"># get duration of the outro instrumental</span>
    command_2 = <span class="hljs-string">f'ffmpeg -i <span class="hljs-subst">{outro_track}</span> 2&gt;&amp;1 | grep "Duration"'</span>
    output_2 = run_ffmpeg_command(command_2)
    duration_2 = extract_duration_in_milliseconds(output_2)

    <span class="hljs-comment"># pad the outro instrumental with silence, using initial mix duration and</span>
    <span class="hljs-comment"># the outro instrumental's duration</span>
    <span class="hljs-comment"># adelay = (duration of initial mix - outro instrumental duration) in milliseconds</span>
    <span class="hljs-keyword">if</span> duration_1 != <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> duration_2 != <span class="hljs-number">0</span>:
        padded_outro = <span class="hljs-string">f"<span class="hljs-subst">{voice_track_file_name}</span>.mix-02.mp3"</span>

        adelay = duration_1 - duration_2
        subprocess.run(<span class="hljs-string">f'ffmpeg -i <span class="hljs-subst">{outro_track}</span> -af "adelay=<span class="hljs-subst">{adelay}</span>|<span class="hljs-subst">{adelay}</span>" <span class="hljs-subst">{padded_outro}</span>'</span>, shell=<span class="hljs-literal">True</span>)

        <span class="hljs-comment"># final mix: the initial mix + the padded outro</span>
        subprocess.run(
            <span class="hljs-string">f'ffmpeg -i <span class="hljs-subst">{initial_mix}</span> -i <span class="hljs-subst">{padded_outro}</span> -filter_complex amix=inputs=2:duration=longest:dropout_transition=0:weights="1 0.25":normalize=0 <span class="hljs-subst">{dest}</span>'</span>,
            shell=<span class="hljs-literal">True</span>,
        )

        <span class="hljs-comment"># add Id3 tags</span>
        episode = <span class="hljs-keyword">await</span> get_episode_number()
        audio_file = dest
        tag = eyed3.load(audio_file).tag
        tag.artist = <span class="hljs-string">"Victor Miti"</span>
        tag.album = <span class="hljs-string">"Zed News"</span>
        tag.title = <span class="hljs-string">f"Zed News Podcast, Episode <span class="hljs-subst">{episode:<span class="hljs-number">03</span>}</span> (<span class="hljs-subst">{today_human_readable}</span>)"</span>
        tag.track_num = episode
        tag.release_date = eyed3.core.Date(today.year, today.month, today.day)
        tag.genre = <span class="hljs-string">"Podcast"</span>
        album_art_file = <span class="hljs-string">f"<span class="hljs-subst">{IMAGE_DIR}</span>/album-art.jpg"</span>
        <span class="hljs-keyword">with</span> open(album_art_file, <span class="hljs-string">"rb"</span>) <span class="hljs-keyword">as</span> cover_art:
            <span class="hljs-comment"># The value 3 indicates that the front cover shall be set</span>
            <span class="hljs-comment"># # https://eyed3.readthedocs.io/en/latest/eyed3.id3.html#eyed3.id3.frames.ImageFrame</span>
            tag.images.set(<span class="hljs-number">3</span>, cover_art.read(), <span class="hljs-string">"image/jpeg"</span>)
        tag.save()

        <span class="hljs-comment"># Clean up</span>
        <span class="hljs-keyword">for</span> f <span class="hljs-keyword">in</span> [voice_track_in_stereo, mix_44100, eq_mix, initial_mix, padded_outro]:
            delete_file(f)
</code></pre>
<h3 id="heading-the-rss-feed">The RSS Feed</h3>
<p>People typically use podcast management tools such as <a target="_blank" href="https://www.simplecast.com/">SimpleCast</a>, <a target="_blank" href="https://www.buzzsprout.com/">Buszzsprout</a>, <a target="_blank" href="https://www.podbean.com/">Podbean</a>, etc. These manage various aspects of the podcast publishing process, including file hosting, and submission to Google Podcasts, Apple Podcasts and all the other major podcast platforms. In my case, I was essentially rolling out my own solution, which meant that I had to deal with all these tasks myself. I had already sorted out the file hosting by using AWS S3. The main thing I needed to do next was automatic submission of my podcast to various platforms. It turns out that the key to doing this properly is having a well-structured <a target="_blank" href="https://en.wikipedia.org/wiki/RSS">RSS Feed</a> with the right metadata. I found the following resources to be very helpful:</p>
<ul>
<li><p><a target="_blank" href="https://help.apple.com/itc/podcasts_connect/#/itcb54353390">A Podcaster's Guide to RSS</a>, by Apple</p>
</li>
<li><p><a target="_blank" href="https://support.google.com/podcast-publishers/answer/9889544">RSS feed guidelines for Google Podcasts</a>, by Google</p>
</li>
</ul>
<p>Getting this part of the project right involved a bit of trial and error, this is the Nunjucks template I ended up with for generating the <code>feed.xml</code> file:</p>
<pre><code class="lang-xml">---json
{
  "permalink": "feed.xml",
  "eleventyExcludeFromCollections": true,
  "metadata": {
    "title": "Zed News Podcast",
    "description": "Your weekday source for the latest happenings in Zambia (and beyond), the Zed News Podcast is an automated news podcast consisting of AI-powered updates from various Zambian sources. New episodes Monday to Friday between 16:45 and 17:00 CAT.",
    "url": "https://example.com",
    "feedUrl": "https://example.com/feed.xml",
    "author": {
      "name": "Victor Miti",
      "email": "victormiti@gmail.com"
    }
  }
}
---
<span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">rss</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"2.0"</span>
     <span class="hljs-attr">xmlns:itunes</span>=<span class="hljs-string">"http://www.itunes.com/dtds/podcast-1.0.dtd"</span>
     <span class="hljs-attr">xmlns:content</span>=<span class="hljs-string">"http://purl.org/rss/1.0/modules/content/"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">channel</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>{{ metadata.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>{{ metadata.description }}<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">itunes:owner</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">itunes:name</span>&gt;</span>{{ metadata.author.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">itunes:name</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">itunes:email</span>&gt;</span>{{ metadata.author.email }}<span class="hljs-tag">&lt;/<span class="hljs-name">itunes:email</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">itunes:owner</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">itunes:author</span>&gt;</span>{{ metadata.author.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">itunes:author</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">itunes:image</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{ site.base_url }}/img/zed-news-podcast-album-art.jpg"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">itunes:category</span> <span class="hljs-attr">text</span>=<span class="hljs-string">"News <span class="hljs-symbol">&amp;amp;</span> Politics"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">itunes:explicit</span>&gt;</span>no<span class="hljs-tag">&lt;/<span class="hljs-name">itunes:explicit</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">language</span>&gt;</span>en<span class="hljs-tag">&lt;/<span class="hljs-name">language</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{ site.base_url }}"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"self"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"{{ site.base_url }}"</span>/&gt;</span>
        {%- for episode in collections.episode | reverse -%}
        {% set absolutePostUrl %}{{ episode.url | url | absoluteUrl(site.base_url) }}{% endset %}
        <span class="hljs-tag">&lt;<span class="hljs-name">item</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>{{ episode.data.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">description</span>&gt;</span>{{ episode.data.description }} It is presented by {{ episode.data.presenter }} in {{ episode.data.locale.name }} and consists of {{ episode.data.references.count }} articles from {{ episode.data.references.sources }} sources. The size of the audio file is {{ episode.data.mp3.size }}. Running time is {{ episode.data.mp3.length }}.Check out the &lt;![CDATA[ &lt;a href="{{ absolutePostUrl }}"&gt;episode notes&lt;/a&gt; ]]&gt; for more information, including links to the news sources.<span class="hljs-tag">&lt;/<span class="hljs-name">description</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">pubDate</span>&gt;</span>{{ episode.date | dateToRfc822 }}<span class="hljs-tag">&lt;/<span class="hljs-name">pubDate</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">enclosure</span> <span class="hljs-attr">url</span>=<span class="hljs-string">"{{ episode.data.mp3.url }}"</span>
                       <span class="hljs-attr">type</span>=<span class="hljs-string">"audio/mpeg"</span> <span class="hljs-attr">length</span>=<span class="hljs-string">"{{ episode.data.rss.enclosure_length }}"</span>/&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">itunes:duration</span>&gt;</span>{{ episode.data.rss.itunes_duration }}<span class="hljs-tag">&lt;/<span class="hljs-name">itunes:duration</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">link</span>&gt;</span>{{ absolutePostUrl }}<span class="hljs-tag">&lt;/<span class="hljs-name">link</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">guid</span>&gt;</span>{{ absolutePostUrl }}<span class="hljs-tag">&lt;/<span class="hljs-name">guid</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">itunes:explicit</span>&gt;</span>no<span class="hljs-tag">&lt;/<span class="hljs-name">itunes:explicit</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">item</span>&gt;</span>
        {%- endfor %}
    <span class="hljs-tag">&lt;/<span class="hljs-name">channel</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">rss</span>&gt;</span>
</code></pre>
<p>With this in place, I just submitted <code>https://zednews.pages.dev/feed.xml</code> to the relevant platforms and that was it.</p>
<h3 id="heading-rendering-a-different-button-depending-on-os">Rendering a different button depending on OS</h3>
<p>I wanted to show a "Listen on Apple Podcasts" button when the site was accessed from an Apple device, and "Listen on Google Podcasts" for all other devices. I used <a target="_blank" href="https://www.npmjs.com/package/ua-parser-js">UAParser.js</a> to achieve this:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generatePodcastListenButton</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> parser = <span class="hljs-keyword">new</span> UAParser();
  <span class="hljs-keyword">const</span> result = parser.getResult();

  <span class="hljs-keyword">const</span> platform = result.os.name.toLowerCase();
  <span class="hljs-keyword">let</span> buttonHTML = <span class="hljs-string">""</span>;

  <span class="hljs-keyword">if</span> (platform === <span class="hljs-string">"mac os"</span> || platform === <span class="hljs-string">"ios"</span>) {
    buttonHTML = <span class="hljs-string">`
      &lt;a class="podcast-listen-btn" href="https://podcasts.apple.com/us/podcast/zed-news-podcast/id1690709989"&gt;
        &lt;img src="/img/apple-podcasts-badge.svg" alt="Listen on Apple Podcasts" height="40" /&gt;
      &lt;/a&gt;
    `</span>;
  } <span class="hljs-keyword">else</span> {
    buttonHTML = <span class="hljs-string">`
      &lt;a class="podcast-listen-btn" href="https://podcasts.google.com/feed/aHR0cHM6Ly96ZWRuZXdzLnBhZ2VzLmRldi9mZWVkLnhtbA"&gt;
        &lt;img src="/img/google-podcasts-badge.svg" alt="Listen on Google Podcasts" height="38" /&gt;
      &lt;/a&gt;
    `</span>;
  }

  <span class="hljs-keyword">return</span> buttonHTML;
}
</code></pre>
<h3 id="heading-writing-unit-tests-without-using-pytest">Writing unit tests without using pytest</h3>
<p>I am used to writing tests using <a target="_blank" href="https://docs.pytest.org">pytest</a>. However, I decided to challenge myself to improve my knowledge and understanding of <a target="_blank" href="https://docs.python.org/3/library/unittest.html">unittest</a>, which is part of the standard library. I recently came to know about the <a target="_blank" href="https://www.theregister.com/2016/03/23/npm_left_pad_chaos/">left-pad chaos</a>, and was challenged by this thought-provoking <a target="_blank" href="https://www.davidhaney.io/npm-left-pad-have-we-forgotten-how-to-program/">article</a>. I already have too many dependencies on this project, and I didn't want to install more stuff and start configuring <code>pytest</code>, when I could achieve the same thing with <code>unittest</code> and even learn one thing or two in the process. I did spend quite some time writing tests, and I must say I enjoyed the process and gained valuable knowledge and experience, particularly regarding mocking.</p>
<h3 id="heading-cloudflare-pages-issues">Cloudflare Pages Issues</h3>
<p>I use Cloudflare for my DNS setups. I prefer Cloudflare Pages for deploying static sites because it is part of the Cloudflare suite of tools, which makes it easy for me to manage everything in one place. However, at the time of deploying the project, Cloudflare Pages' <strong>v1 build system</strong> (<em>stable</em>) supported old versions of various software. For example, the default Python version is 2.7, and only Python versions 2.7, 3.5 and 3.7 were supported. I didn't think this would affect me, because the build command for the generated static site did not need Python at all. However, I was wrong! The build system looks for certain files in the project root and makes assumptions based on those files. In my case, it "knew" this was a Python project and therefore attempted to install the project dependencies. This failed because the project uses Python 3.10.</p>
<p>I therefore had to change the build settings to use the <strong>v2 build system</strong> (<em>beta</em>). The build process still failed, because it couldn't install dependencies using poetry! So I had to generate a <code>requirements.txt</code> file from poetry:</p>
<pre><code class="lang-bash">poetry <span class="hljs-built_in">export</span> --with dev --without-hashes --format requirements.txt --output requirements.txt
</code></pre>
<p>The other issue, which I actually encountered on a different project, had to do with the Node version. I experienced build failures using Node.js v18. I only fixed the problem by downgrading to v16. With this in mind, I maintained the project's Node.js version to v16, as I wasn't prepared to experiment with v18 again. Perhaps this may have been fixed by now, I'll need to check at some point.</p>
<h2 id="heading-the-result">The result</h2>
<p>Well, here it is ...</p>
<p><strong>On Mobile</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689986068157/c374464d-455d-44b3-8b54-610078b46750.png" alt="Mobile screenshot" class="image--center mx-auto" /></p>
<p><strong>On Desktop</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689986150044/c50ebbb5-3652-4d85-957f-7519e663bc76.jpeg" alt="Desktop screenshot" class="image--center mx-auto" /></p>
<p>It's not perfect, but it works! <a target="_blank" href="https://zednews.pages.dev/">Check it out</a> and let me know what you think. The <a target="_blank" href="https://github.com/engineervix/zed-news">code is open</a>, feel free to <a target="_blank" href="https://github.com/engineervix/zed-news/blob/v0.3.1/CONTRIBUTING.md">add your contributions</a> to make it better!</p>
<hr />
<p><em>Cover image generated by</em> <a target="_blank" href="https://www.bing.com/create"><em>Bing</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Dev Retro 2022 — a tech retrospective]]></title><description><![CDATA[2022 was a very interesting and busy year for me. Interesting because

I never thought I'd be switching careers and becoming a developer

I met and worked with so many new people

I learnt a lot of new things and unlearned some old ways of doing thin...]]></description><link>https://blog.victor.co.zm/dev-retro-2022</link><guid isPermaLink="true">https://blog.victor.co.zm/dev-retro-2022</guid><category><![CDATA[#DevRetro2022]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Sun, 01 Jan 2023 22:59:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2d4f91c496203505ebc3ef0ec79d711a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>2022 was a very interesting and busy year for me. Interesting because</p>
<ol>
<li><p>I never thought I'd be switching careers and becoming a developer</p>
</li>
<li><p>I met and worked with so many new people</p>
</li>
<li><p>I learnt a lot of new things and unlearned some old ways of doing things</p>
</li>
<li><p>I presented a talk at a tech conference for the first time</p>
</li>
<li><p>... I could go on and on ... but, let's keep it simple 😃</p>
</li>
</ol>
<p>Now, let's break things down a little bit 🙂</p>
<h3 id="heading-getting-my-first-role-as-a-developer">Getting my first role as a developer</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/qgQUggAC3Pfv687qPC/giphy.gif">https://media.giphy.com/media/qgQUggAC3Pfv687qPC/giphy.gif</a></div>
<p> </p>
<p>In March 2022, I started work as a junior developer at <a target="_blank" href="https://torchbox.com/">Torchbox</a>, working remotely and building interesting stuff with <a target="_blank" href="https://www.python.org/">Python</a>, <a target="_blank" href="https://www.djangoproject.com/">Django</a> and <a target="_blank" href="https://wagtail.org/">Wagtail</a>! I'm grateful to The Lord for this opportunity. I never imagined myself leaving my previous job in 2022 to become a developer. God willing, I will write about this in a separate post. Suffice it to say, it's not easy to leave behind a career you've built for a decade, to start afresh in a completely new field. Notwithstanding, it's been a great experience so far, and I hope I can continue to grow in this new craft, and become a better developer one step at a time 🏋💪!</p>
<p>In the meantime, check out our <a target="_blank" href="https://torchbox.com/careers/jobs">careers page</a>, you might just be my teammate! There are various roles there, across various departments.</p>
<h3 id="heading-new-people-new-ways-of-working">New people, new ways of working</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/oBYB0gqUy3xxBf89aT/giphy.gif">https://media.giphy.com/media/oBYB0gqUy3xxBf89aT/giphy.gif</a></div>
<p> </p>
<p>Starting a new job not only entails meeting and working with new people, but also new ways of working! In my case, all my colleagues are at least 10,000km away, on various continents outside Africa, and I haven't even met any of them in person! However, they are all fantastic people, and it has been such a joy working and interacting with them. I thought working remotely would be all fun and games ...</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672611220422/9281bf74-3b51-4c94-b1af-46ba82efbaf5.webp" alt="source: https://www.instagram.com/p/CCpiNNGJeKE/" class="image--center mx-auto" /></p>
<p>It requires a lot of discipline, focus and hard work, otherwise you won't get anything done!</p>
<p>The whole <a target="_blank" href="https://www.atlassian.com/agile">agile</a> thing was new to me — <a target="_blank" href="https://www.wrike.com/project-management-guide/faq/what-is-a-sprint-in-agile/">sprints</a>, <a target="_blank" href="https://www.atlassian.com/agile/scrum/standups">stand-ups</a>, <a target="_blank" href="https://www.agilealliance.org/glossary/backlog-refinement/">backlog refinements</a>, etc. Took a while to get used to this, and I'm still learning!</p>
<p>Prior to this experience, most of the development work I did was solo. No teams, no collaborators, except, of course, for open source contributions. Now, I learnt all about <a target="_blank" href="https://www.atlassian.com/git/tutorials/comparing-workflows">Git workflows</a>, and the various nuances of having several pieces of interdependent work going on simultaneously ... interesting stuff!</p>
<p>All in all, a lot of adjustments and adaptation, but all good so far 👍</p>
<h3 id="heading-learning-learning-and-learning">Learning, learning and learning</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/c1Mj7g9HQ8IGq4fzu6/giphy.gif">https://media.giphy.com/media/c1Mj7g9HQ8IGq4fzu6/giphy.gif</a></div>
<p> </p>
<p>I've already highlighted some of the things I've had to learn. I think there's been so much learning in 2022 for me. At the end of the year, I was certainly a better developer than I was at the beginning of 2022. I even subscribed to <a target="_blank" href="https://www.coursera.org/courseraplus">Coursera Plus</a> at the end of the year, so that I could continue to level up my skills! I started a <a target="_blank" href="https://importthis.tech/series/cs">Computer Science Series</a>, where I talk about these things. You'll also see from my <a target="_blank" href="https://www.classcentral.com/@engineervix">Class Central profile</a> that I completed the following two courses:</p>
<ul>
<li><p><a target="_blank" href="https://learndigital.withgoogle.com/digitalskills/course/digital-marketing">Fundamentals of Digital Marketing</a>, by Google.</p>
</li>
<li><p><a target="_blank" href="https://courses.minnalearn.com/en/courses/startingup/">Starting Up</a>, by <a target="_blank" href="https://www.aalto.fi/en">Aalto University</a> and <a target="_blank" href="https://minnalearn.com/">MinnaLearn</a>.</p>
</li>
</ul>
<p>"But these courses don't even have anything to do with programming", you might say! Well, that isn't entirely correct! Being a developer is much more than writing good code. Ask <a target="_blank" href="https://twitter.com/simpleprogrammr">John Sonmez</a>, he has <a target="_blank" href="https://www.amazon.com/Soft-Skills-Software-Developers-Manual-dp-0999081446/dp/0999081446/">a lot to say</a> about this! I have been reading his book and I have found it to be quite insightful.</p>
<h3 id="heading-first-pycon-talk">First PyCon talk</h3>
<p>I submitted a last-minute application to present a remote talk at <a target="_blank" href="https://2022.za.pycon.org/">PyCon ZA 2022</a>, and I couldn't believe it when I got the email confirmation of acceptance! You can check out my talk in the video below:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=KPXJ94C6Xw0">https://www.youtube.com/watch?v=KPXJ94C6Xw0</a></div>
<p> </p>
<h3 id="heading-and-thats-a-wrap">And that's a wrap!</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/l0IycQmt79g9XzOWQ/giphy.gif">https://media.giphy.com/media/l0IycQmt79g9XzOWQ/giphy.gif</a></div>
<p> </p>
<p>Well, I'll pause here for now. I need to get some sleep 😴, a busy week ahead, lots of things to do, and more interesting things to learn! By God's grace, we keep moving!</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Using CLI tools to manage audio, video, images and PDF files]]></title><description><![CDATA[I'm going to cut straight to the chase here, as the main reason for this post isn't to compare and contrast available tools and so on, but rather to highlight a few useful commands that often come in handy for various situations. This is more like a ...]]></description><link>https://blog.victor.co.zm/multimedia-cli-tools</link><guid isPermaLink="true">https://blog.victor.co.zm/multimedia-cli-tools</guid><category><![CDATA[cli]]></category><category><![CDATA[terminal]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[multimedia]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Mon, 19 Dec 2022 18:21:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1671455749356/FnQqqD4Sr.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm going to cut straight to the chase here, as the main reason for this post isn't to compare and contrast available tools and so on, but rather to highlight a few useful commands that often come in handy for various situations. This is more like a quick reference for some specific tasks. The focus here is on the following popular tools, which I use quite often:</p>
<ul>
<li><p><a target="_blank" href="https://www.ffmpeg.org/">FFmpeg</a>: A complete, cross-platform solution to record, convert and stream audio and video</p>
</li>
<li><p><a target="_blank" href="https://www.lcdf.org/gifsicle/">Gifsicle</a>: for creating, editing, and getting information about GIF images and animations</p>
</li>
<li><p><a target="_blank" href="https://imagemagick.org/">ImageMagick</a>: create, edit, compose, or convert digital images</p>
</li>
<li><p><a target="_blank" href="https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/">PDFtk</a>: for doing everyday things with PDF documents.</p>
</li>
<li><p><a target="_blank" href="https://github.com/ocrmypdf/OCRmyPDF">OCRmyPDF</a>: adds an <a target="_blank" href="https://en.wikipedia.org/wiki/Optical_character_recognition">OCR</a> text layer to scanned PDF files, allowing them to be searched</p>
</li>
<li><p><a target="_blank" href="https://www.ghostscript.com/">ghostscript</a>: an interpreter for PostScript® and Portable Document Format (PDF) files.</p>
</li>
<li><p><a target="_blank" href="https://github.com/mozilla/mozjpeg">mozjpeg</a>: improves JPEG compression efficiency achieving higher visual quality and smaller file sizes at the same time</p>
</li>
<li><p><a target="_blank" href="https://pngquant.org/">pngquant</a>: a utility and library for lossy compression of PNG images. The conversion reduces file sizes significantly (often as much as 70%) and preserves <strong>full alpha transparency</strong>.</p>
</li>
<li><p><a target="_blank" href="https://github.com/svg/svgo">svgo</a>: a Node.js-based tool for optimizing SVG vector graphics files.</p>
</li>
</ul>
<p>This is a work in progress. I'll be updating the commands from time to time, as I encounter them.</p>
<h2 id="heading-ffmpeg">FFmpeg</h2>
<h3 id="heading-convert-video-from-x-to-y">convert video from x to y</h3>
<p>Generally speaking, you should be able to convert a video from one format to another by using the following convention:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># x and y here are the file extensions, for instance; mp4, mkv, ogv</span>
ffmpeg -i input.x -c copy output.y
</code></pre>
<p>If you encounter any issues, a quick Google search for <strong>How to convert x to y using FFmpeg</strong> will give you the solution.</p>
<p><strong>Reference(s)</strong></p>
<ul>
<li><a target="_blank" href="https://stackoverflow.com/questions/18123376/webm-to-mp4-conversion-using-ffmpeg">webm to mp4 conversion using ffmpeg</a></li>
</ul>
<h3 id="heading-compress-video-reduce-video-size-without-a-noticeable-reduction-in-quality">compress video (reduce video size, without a <em>noticeable</em> reduction in quality)</h3>
<p>Here, I'm using mp4 videos. If you have other video types (mkv, ogv, webm, etc. then you might need to convert those to mp4 first</p>
<pre><code class="lang-bash"><span class="hljs-comment"># using the libx265 codec</span>
ffmpeg -i input.mp4 -vcodec libx265 -crf 28 output.mp4

<span class="hljs-comment"># if you don't have libx265, you can use the libx264 codec</span>
ffmpeg -i input.mp4 -vcodec libx264 -crf 28 output.mp4
</code></pre>
<blockquote>
<p>For more info &amp; technical details, see this <strong>Unix &amp; Linux Stack Exchange</strong> post: <a target="_blank" href="https://unix.stackexchange.com/questions/28803/how-can-i-reduce-a-videos-size-with-ffmpeg">How can I reduce a video's size with ffmpeg?</a></p>
</blockquote>
<h3 id="heading-speed-up-a-video">speed up a video</h3>
<p>I find this useful in situations where I record a screen capture (no audio) while doing a demo, and I end up with, say, a 5-minute video that has several periods of no activity (e.g. waiting for a process to complete). I could speed up the video and hence reduce its overall time to, say 2.5 minutes:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># the 0.5 here means we're reducing the video to half the original time.</span>
<span class="hljs-comment"># adjust this to suit your preference. If the value is above 1.0, then you're making your video slower</span>
ffmpeg -i input.mp4 -filter:v <span class="hljs-string">"setpts=0.5*PTS"</span> output.mp4
</code></pre>
<p>If you wanna speed up video and audio at the same time:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># note here that the audio tempo is the inverse of the factor modifying frame timestamps. In English,</span>
<span class="hljs-comment"># if you're speeding up the video 2x, divide 1 by 2 to get 0.5 for the video, then</span>
<span class="hljs-comment"># use 2.0 as the factor for the audio tempo (this is 1 divided by video factor)</span>
ffmpeg -i input.mp4 -filter_complex <span class="hljs-string">"[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]"</span> -map <span class="hljs-string">"[v]"</span> -map <span class="hljs-string">"[a]"</span> output.mp4
</code></pre>
<p>For more info and technical details, see</p>
<ul>
<li><p><a target="_blank" href="https://trac.ffmpeg.org/wiki/How%20to%20speed%20up%20/%20slow%20down%20a%20video">How to speed up / slow down a video</a> on the FFmpeg Bug Tracker and Wiki.</p>
</li>
<li><p><a target="_blank" href="https://superuser.com/questions/1247462/speed-up-video-x1-5-but-keep-all-frames">Speed up video x1.5 but keep all frames</a> on superuser.com.</p>
</li>
</ul>
<h3 id="heading-concatenate-video-audio">concatenate video / audio</h3>
<p>The assumption here is that you want to concatenate files with the same codecs. If this isn't the case, see <a target="_blank" href="https://trac.ffmpeg.org/wiki/Concatenate">this page</a> on the FFmpeg Bug Tracker and Wiki.</p>
<p>What you wanna do is create a text file with all the files (whose paths can be either relative or absolute) you want to have concatenated in the following form:</p>
<pre><code class="lang-plaintext">file '/path/to/file1.mp3'
file '/path/to/file2.mp3'
file '/path/to/file3.mp3'
</code></pre>
<p>It is possible to generate this file with a bash for loop, or using <code>printf</code>. <strong>Either</strong> of the following would generate a file containing every *.mp3 in the working directory:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># with a bash for loop</span>
<span class="hljs-keyword">for</span> f <span class="hljs-keyword">in</span> *.mp3; <span class="hljs-keyword">do</span> <span class="hljs-built_in">echo</span> <span class="hljs-string">"file '<span class="hljs-variable">$f</span>'"</span> &gt;&gt; text_file.txt; <span class="hljs-keyword">done</span>

<span class="hljs-comment"># or with printf</span>
<span class="hljs-built_in">printf</span> <span class="hljs-string">"file '%s'\n"</span> *.mp3 &gt; mylist.txt
</code></pre>
<p>Then, using FFmpeg:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># The -safe 0 is not required if the paths are relative</span>
ffmpeg -f concat -safe 0 -i text_file.txt -c copy output.mp3
</code></pre>
<p>For more info and technical details, see</p>
<ul>
<li><p><a target="_blank" href="https://trac.ffmpeg.org/wiki/Concatenate">Concatenating media files</a> on Stackoverflow</p>
</li>
<li><p><a target="_blank" href="https://stackoverflow.com/questions/7333232/how-to-concatenate-two-mp4-files-using-ffmpeg">How to concatenate two MP4 files using FFmpeg?</a> on the FFmpeg Bug Tracker and Wiki</p>
</li>
</ul>
<h3 id="heading-extract-audio-from-video">extract audio from video</h3>
<p>If we don't wanna re-encode, then we first have to run <code>ffprobe</code> on the video file to list available audio streams and their types:</p>
<pre><code class="lang-console">❯ ffprobe video.mp4
ffprobe version 5.1.2 Copyright (c) 2007-2022 the FFmpeg developers
  built with gcc 12 (SUSE Linux)
  configuration: --prefix=/usr --libdir=/usr/lib64 --shlibdir=/usr/lib64 --incdir=/usr/include/ffmpeg --extra-cflags='-O2 -Wall -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -fstack-protector-strong -funwind-tables -fasynchronous-unwind-tables -fstack-clash-protection -Werror=return-type -flto=auto -ffat-lto-objects -g' --optflags='-O2 -Wall -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -fstack-protector-strong -funwind-tables -fasynchronous-unwind-tables -fstack-clash-protection -Werror=return-type -flto=auto -ffat-lto-objects -g' --disable-htmlpages --enable-pic --disable-stripping --enable-shared --disable-static --enable-gpl --enable-version3 --disable-openssl --enable-gnutls --enable-ladspa --enable-libshaderc --enable-vulkan --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libdc1394 --enable-libdrm --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libjack --enable-libjxl --enable-librist --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopenh264-dlopen --enable-libopus --enable-libpulse --enable-librav1e --enable-librubberband --enable-libsvtav1 --enable-libsoxr --enable-libspeex --enable-libssh --enable-libsrt --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libxml2 --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lto --enable-lv2 --enable-libmfx --enable-vaapi --enable-vdpau --enable-version3 --enable-libfdk-aac-dlopen --enable-nonfree --enable-libvo-amrwbenc --enable-libx264 --enable-libx265 --enable-libxvid
  libavutil      57. 28.100 / 57. 28.100
  libavcodec     59. 37.100 / 59. 37.100
  libavformat    59. 27.100 / 59. 27.100
  libavdevice    59.  7.100 / 59.  7.100
  libavfilter     8. 44.100 /  8. 44.100
  libswscale      6.  7.100 /  6.  7.100
  libswresample   4.  7.100 /  4.  7.100
  libpostproc    56.  6.100 / 56.  6.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.29.100
  Duration: 00:02:32.96, start: 0.000000, bitrate: 452 kb/s
  Stream #0:0[0x1](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 126 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 1280x720, 317 kb/s, 30 fps, 30 tbr, 15360 tbn (default)
    Metadata:
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
</code></pre>
<p>From above, we see that the audio stream format is <a target="_blank" href="https://en.wikipedia.org/wiki/Advanced_Audio_Coding">aac</a>. Then we can go ahead and</p>
<pre><code class="lang-bash">ffmpeg -i video.mp4 -map 0:a -acodec copy audio.aac
</code></pre>
<p>However, if we want to extract the audio in a specific format, say mp3, then we'll have to re-encode:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># here we're using libmp3lame &lt;https://www.ffmpeg.org/ffmpeg-codecs.html#libmp3lame-1&gt; for mp3 encoding</span>
ffmpeg -i video.mp4 -map 0:a -acodec libmp3lame audio.mp3
</code></pre>
<h3 id="heading-compress-audio">compress audio</h3>
<p>You have an hour-long 320kbps mp3 file weighing in at 144Mb. Let's say you want to reduce the target <a target="_blank" href="https://en.wikipedia.org/wiki/Bit_rate#MP3">bitrate</a> to 96kbps:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># -q:a 7 gives you an average bitrate of 100kbps</span>
<span class="hljs-comment"># for reference, see &lt;https://trac.ffmpeg.org/wiki/Encode/MP3&gt;</span>
ffmpeg -i input.mp3 -codec:a libmp3lame -q:a 7 output.mp3
</code></pre>
<p>That should give you a resulting file ~45Mb! You'll have to play the file to determine if you are happy with the quality. Note that audio quality improves with increasing bitrate. Here's a quick guide (taken from <a target="_blank" href="https://en.wikipedia.org/wiki/Bit_rate#MP3">wikipedia.org/wiki/Bit_rate#MP3</a>):</p>
<ul>
<li><p>32 kbps – generally acceptable only for speech</p>
</li>
<li><p>96kbps – generally used for speech or low-quality streaming</p>
</li>
<li><p>128 or 160 kbps – mid-range bitrate quality</p>
</li>
<li><p>192 kbps – medium-quality bitrate</p>
</li>
<li><p>256 kbps – a commonly used high-quality bitrate</p>
</li>
<li><p>320 kbps – highest level supported by the <a target="_blank" href="https://en.wikipedia.org/wiki/MP3">MP3</a> standard</p>
</li>
</ul>
<p><strong>Note(s)</strong></p>
<ul>
<li><p>For additional compression, you could mix down to <a target="_blank" href="https://en.wikipedia.org/wiki/Monaural">mono</a> (use 1 audio channel) and set the <a target="_blank" href="https://manual.audacityteam.org/man/sample_rates.html">sample rate</a> to 22050 Hz (which is half the standard 44.1kHz) by using <code>-ac 1</code> for the former and <code>-ar 22050</code> for the latter.</p>
</li>
<li><p>Refer to the <a target="_blank" href="https://trac.ffmpeg.org/wiki/Encode/MP3">FFmpeg MP3 Encoding Guide</a> for more info and technical details</p>
</li>
<li><p>Also, check out <a target="_blank" href="https://trac.ffmpeg.org/wiki/AudioChannelManipulation#a2stereostereo">Manipulating audio channels</a> on the FFmpeg Bug Tracker and Wiki</p>
</li>
</ul>
<h3 id="heading-extract-part-of-a-media-file">extract part of a media file</h3>
<p>Say you have a 45min audio file and you only need the part between 2min and 44min:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># time format is HH:MM:SS or HH:MM:SS.xx, where xx expresses decimal value for SS</span>
<span class="hljs-comment"># see &lt;https://ffmpeg.org/ffmpeg-utils.html#Time-duration&gt;</span>
ffmpeg -i input.mp3 -ss 00:02:00 -to 00:44:00.00 [other options here] output.mp3
</code></pre>
<h2 id="heading-gifsicle">Gifsicle</h2>
<h3 id="heading-compress-a-gif-file">compress a GIF file</h3>
<pre><code class="lang-bash">gifsicle -O3 --colors 256 --lossy=30 -o output.gif input.gif
</code></pre>
<p><strong>Reference</strong>: <a target="_blank" href="https://superuser.com/questions/1107200/optimize-animated-gif-size-in-command-line">Optimize animated GIF size in command-line</a> on superuser.com</p>
<h2 id="heading-imagemagick">Imagemagick</h2>
<h3 id="heading-resize-images">resize images</h3>
<p>Using image width as a reference</p>
<pre><code class="lang-bash"><span class="hljs-comment"># we want to resize the image, keeping the width at 1280px</span>
convert -resize 1280x input.jpg output.jpg
</code></pre>
<p>Using image height as a reference</p>
<pre><code class="lang-bash"><span class="hljs-comment"># we want to resize the image, keeping the height at 1280px</span>
convert -resize x1280 input.jpg output.jpg
</code></pre>
<h3 id="heading-convert-x-to-y">convert X to Y</h3>
<p>Convert, say, a JPG file to PNG</p>
<pre><code class="lang-bash">convert image.png image.jpg
</code></pre>
<p>Convert PNG with transparency to JPG</p>
<pre><code class="lang-bash"><span class="hljs-comment"># -flatten, by default, results in a white background</span>
<span class="hljs-comment"># see &lt;http://www.imagemagick.org/script/command-line-options.php#flatten&gt;</span>
convert image.png -flatten image.jpg
</code></pre>
<p>Converting all pixels of a given color to transparent</p>
<pre><code class="lang-bash"><span class="hljs-comment"># we want to make white transparent</span>
convert image.jpg -fuzz 5% -transparent white image.png
</code></pre>
<p><strong>Some References</strong></p>
<ul>
<li><p><a target="_blank" href="http://www.fmwconcepts.com/imagemagick/tidbits/image.php#alpha">Fred's ImageMagick Tidbits</a></p>
</li>
<li><p><a target="_blank" href="https://stackoverflow.com/questions/47954470/convert-png-with-transparency-to-jpg">Convert PNG with transparency to JPG (Stackoverflow)</a></p>
</li>
<li><p>[ImageMagick Snippets by Mohamed A. Hassan](https://gist.github.com/MohamedAlaa/d9b54cd856b3edce1510]</p>
</li>
</ul>
<h2 id="heading-mozjpeg">mozjpeg</h2>
<h3 id="heading-compress-jpeg-images">compress JPEG images</h3>
<p>Batch compress a bunch of JPG files in the current directory</p>
<pre><code class="lang-bash"><span class="hljs-keyword">for</span> img <span class="hljs-keyword">in</span> *.jpg; <span class="hljs-keyword">do</span> mozjpeg -outfile /path/to/output/directory/<span class="hljs-variable">$img</span> <span class="hljs-variable">$img</span>; <span class="hljs-keyword">done</span>
</code></pre>
<h2 id="heading-pngquant">pngquant</h2>
<h3 id="heading-compress-png-images">compress PNG images</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># this will compress image.png file and save it as a new file with name "image-fs8.png"</span>
pngquant image.png

<span class="hljs-comment"># change output filename suffix. This will result in a new file image.min.png</span>
pngquant image.png --ext .min.png

<span class="hljs-comment"># save converted files in different location, instead of current directory</span>
pngquant image.png --output path/to/output/directory/image.png

<span class="hljs-comment"># strip Image metadata</span>
pngquant --strip image.png

<span class="hljs-comment"># skip saving files if the size of compressed files are larger than original files</span>
pngquant --skip-if-larger image.png

<span class="hljs-comment"># specify min/max quality</span>
<span class="hljs-comment"># set image quality in range 0 (worst) to 100 (perfect)</span>
<span class="hljs-comment"># here we set the minimum image quality as 60 and maximum quality as 80</span>
pngquant --quality=60-80 image.png
</code></pre>
<h2 id="heading-pdftk">PDFtk</h2>
<h3 id="heading-concatenate-various-pdf-files">concatenate various PDF files</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># this concatenates all pdf files in current directory, saving them as filename.pdf</span>
pdftk *.pdf cat output filename.pdf

<span class="hljs-comment"># concatenate specific PDF files</span>
pdftk file1.pdf file2 cat output filename.pdf

<span class="hljs-comment"># if you want to see what's happening,</span>
<span class="hljs-comment"># add the verbose option at the end of your command. This is true for any pdftk operation</span>
pdftk *.pdf cat output filename.pdf verbose
</code></pre>
<h3 id="heading-rotate-pdf">rotate PDF</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># rotate page 1 by 90 degrees clockwise</span>
pdftk input.pdf cat 1east output output.pdf

<span class="hljs-comment"># rotate page 1 by 90 degrees anti-clockwise</span>
pdftk input.pdf cat 1west output output.pdf

<span class="hljs-comment"># rotate page 1 by 180 degrees</span>
pdftk input.pdf cat 1south output output.pdf

<span class="hljs-comment"># rotate all pages 90 degrees clockwise</span>
pdftk input.pdf cat 1-endeast output output.pdf

<span class="hljs-comment"># rotate all pages 90 degrees anti-clockwise</span>
pdftk input.pdf cat 1-endwest output output.pdf

<span class="hljs-comment"># rotate all pages 180 degrees</span>
pdftk input.pdf cat 1-endsouth output output.pdf
</code></pre>
<h3 id="heading-encrypt-pdf">encrypt PDF</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># No. 1: Encrypt a PDF using 128-bit strength (the default), withhold all permissions (the default)</span>
pdftk file1.pdf output file1.128.pdf owner_pw your_password

<span class="hljs-comment"># No. 2: Same as above, except printing is allowed</span>
pdftk file1.pdf output file1.128.pdf owner_pw foo owner_pw your_password allow printing

No. 3: Same as No. 1, except password baz must also be used to open output PDF
pdftk file1.pdf output file1.128.pdf owner_pw foo user_pw baz
</code></pre>
<h3 id="heading-extract-pages-from-pdf">extract pages from PDF</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># extract page 1, 4 and 5 from input.pdf, save them as one document, output.pdf</span>
pdftk input.pdf cat 1 4 5 output output.pdf verbose

<span class="hljs-comment"># same as above, but as separate files</span>
<span class="hljs-keyword">for</span> pages <span class="hljs-keyword">in</span> {1,4,5};<span class="hljs-keyword">do</span> pdftk input.pdf cat <span class="hljs-variable">$pages</span> output extracted-<span class="hljs-variable">$pages</span>.pdf verbose;<span class="hljs-keyword">done</span>

<span class="hljs-comment"># you can also extract a range, like page 3 to 15, or page 19 to the end of the document, or both:</span>
pdftk input.pdf cat 3-15 output output.pdf verbose
pdftk input.pdf cat 19-end output output.pdf verbose
pdftk input.pdf cat 3-15 19-end output output.pdf verbose

<span class="hljs-comment"># in one operation, you can extract pages from separate documents and combine them as one</span>
<span class="hljs-comment"># here, we have two files, file1.pdf and file2.pdf, and we're</span>
<span class="hljs-comment"># extracting pages 1-3 from file1.pdf, and extracting pages 4 to the end from file2.pdf</span>
pdftk A=file1.pdf B=file2.pdf cat A1-3 B4-end output newfile.pdf verbose
</code></pre>
<h3 id="heading-stamp-a-pdf-file-with-another-pdf-file">"stamp" a PDF file with another PDF file</h3>
<p>There are many practical use cases for this. For example, you want to add a watermark to a PDF, or you want to add a signature to a PDF.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># this usually works best if both files are of the same size</span>
pdftk page_to_be_stamped.pdf stamp stamp.pdf output stamped.pdf

<span class="hljs-comment"># same as above, but for a multi-page document</span>
<span class="hljs-comment"># see &lt;https://www.pdflabs.com/docs/pdftk-man-page/#dest-op-multistamp&gt;</span>
pdftk document_to_be_stamped.pdf multistamp stamp.pdf output stamped.pdf
</code></pre>
<h2 id="heading-ghostscript">Ghostscript</h2>
<h3 id="heading-compress-pdf">compress pdf</h3>
<p>Ghostscript is super powerful, and to be honest, the first time I encountered it I felt intimidated by the myriad of options available. Fortunately, <a target="_blank" href="https://github.com/aklomp">Alfred Klomp</a> has written an excellent wrapper around Ghostscript to reduce a PDF's file size. It comes as a BASH script called <a target="_blank" href="https://github.com/aklomp/shrinkpdf">shrinkpdf</a>, and has proved to be very handy for me over the years. I have even included it as part of my <a target="_blank" href="https://github.com/engineervix/ubuntu-server-setup">BASH setup script for Ubuntu servers</a></p>
<p>I normally add the script in a <code>bin</code> directory inside my <code>$HOME</code> directory, and ensure that</p>
<ol>
<li><p>the script is executable (<code>chmod +x ~/bin/shrinkpdf</code>)</p>
</li>
<li><p><code>~/bin/</code> is on my <code>$PATH</code> (<code>export PATH="$PATH:$HOME/bin"</code>)</p>
</li>
</ol>
<p>Then use it as follows:</p>
<pre><code class="lang-bash">shrinkpdf -o output.pdf input.pdf

<span class="hljs-comment"># And an output resolution in DPI (default is 72 DPI) with the -r option</span>
shrinkpdf -r 150 -o output.pdf input.pdf
</code></pre>
<h2 id="heading-ocrmypdf">OCRmyPDF</h2>
<p>Add an OCR layer and convert to <a target="_blank" href="https://en.wikipedia.org/wiki/PDF/A">PDF/A</a></p>
<pre><code class="lang-bash">ocrmypdf input.pdf output.pdf
</code></pre>
<p>Add an OCR layer and output a standard PDF</p>
<pre><code class="lang-bash">ocrmypdf --output-type pdf input.pdf output.pdf
</code></pre>
<p>Correct page rotation</p>
<pre><code class="lang-bash">ocrmypdf --rotate-pages input.pdf output.pdf
</code></pre>
<p>Produce PDF and text file containing OCR text</p>
<pre><code class="lang-bash">ocrmypdf --sidecar output.txt input.pdf output.pdf
</code></pre>
<p>OCRmyPDF can also convert single images to PDFs on its own</p>
<pre><code class="lang-bash"><span class="hljs-comment"># If the resolution (dots per inch, DPI) of an image is not set or is incorrect,</span>
<span class="hljs-comment"># it can be overridden with --image-dpi, e.g. --image-dpi 300</span>
ocrmypdf image.png myfile.pdf
</code></pre>
<p>If you have multiple images, use <a target="_blank" href="https://gitlab.mister-muffin.de/josch/img2pdf">img2pdf</a> to convert the images to PDF. Here's an example, where you convert your images to PDFs, and then pipe the results to run <code>ocrmypdf</code>. The <code>-</code> tells OCRmyPDF to read standard input:</p>
<pre><code class="lang-bash">img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
</code></pre>
<h2 id="heading-svgo">SVGO</h2>
<p>Compress a single SVG file</p>
<pre><code class="lang-bash"><span class="hljs-comment"># you can skip the optional -i argument</span>
svgo -i input.svg -o output.svg
</code></pre>
<p>Compress multiple SVG files in a directory</p>
<pre><code class="lang-bash">svgo -f path/to/dir/with/svg/files -o path/to/dir/with/svg/output
</code></pre>
<hr />
<p>It's worth mentioning that some of these commands are too verbose, and it's not easy to remember all the arguments and options for some of them. In such cases, I often find it useful to have <a target="_blank" href="https://linuxize.com/post/how-to-create-bash-aliases/#creating-bash-aliases-with-arguments-bash-functions">BASH functions</a> for some of these. For instance, I have one for encrypting PDFs:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># encrypt pdf, allow printing</span>
<span class="hljs-function"><span class="hljs-title">encrypt_pdf</span></span>() {
  encrypted_pdf=<span class="hljs-string">"<span class="hljs-variable">${1%.pdf}</span>.128.pdf"</span>
  pdftk <span class="hljs-string">"<span class="hljs-variable">$1</span>"</span> output <span class="hljs-variable">${encrypted_pdf}</span> owner_pw <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span> allow printing verbose

  <span class="hljs-comment"># rename the files after encryption</span>
  mv -v <span class="hljs-string">"<span class="hljs-variable">$1</span>"</span> <span class="hljs-string">"<span class="hljs-variable">${1%.pdf}</span>_src.pdf"</span>
  mv -v <span class="hljs-variable">${encrypted_pdf}</span> <span class="hljs-string">"<span class="hljs-variable">${encrypted_pdf%.128.pdf}</span>.pdf"</span>
}
</code></pre>
<p>I call it like this:</p>
<pre><code class="lang-bash">encrypt_pdf filename.pdf $(openssl rand -base64 12)
</code></pre>
<p>Hope you found this post useful! If you have any cool tricks and time-saving techniques, please share them in the comments!</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[And so it begins]]></title><description><![CDATA[In my previous post in this series, I talked about my desire and plans to study Computer Science. I considered the various available options and I concluded that it was probably best for me to "craft my own CS degree". So in this post, I will briefly...]]></description><link>https://blog.victor.co.zm/and-so-it-begins</link><guid isPermaLink="true">https://blog.victor.co.zm/and-so-it-begins</guid><category><![CDATA[Computer Science]]></category><category><![CDATA[learning]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[studying]]></category><category><![CDATA[university]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Sun, 04 Dec 2022 20:18:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1670178714452/ERhLOBRDS.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a target="_blank" href="https://blog.victor.co.zm/planning-to-study-cs">previous post</a> in <a target="_blank" href="https://blog.victor.co.zm/series/cs">this series</a>, I talked about my desire and plans to study Computer Science. I considered the various available options and I concluded that it was probably best for me to "craft my own CS degree". So in this post, I will briefly discuss this and outline the process that I went through in deciding on how to go about this.</p>
<p>My initial plan was to spend some time looking at different CS curricula from various high-profile universities – comparing the structure, looking at common themes and so on, but I realised that that would be quite a significant undertaking and I didn't have that much time – I needed to make a decision quickly and get started immediately. In the past, I have often wasted a lot of time over-analysing things and coming up with very intricate plans – only to later on miserably fail to implement them😒! Therefore, this time I decided not to waste time over-analysing and overthinking things, especially given that people smarter than I have already encountered this and done the work already! I mean, I already talked about <a target="_blank" href="https://teachyourselfcs.com">teachyourselfcs.com</a> in my previous post and I agree with their suggested 9 subjects as being <em>essential for every practising software engineer</em>. The <a target="_blank" href="https://github.com/ossu/computer-science">OSSU curriculum</a> is great, but I think it's too detailed and rigorous. It's very easy to lose focus and go down the rabbit hole. As I mentioned in my previous post, I have very limited free time, so I have to make the most of it. Following the OSSU curriculum would not be a prudent use of the limited time I have. 10 years ago, it would have made perfect sense to delve into the OSSU pathway, but things have changed now. A clearer, more succinct and optimum pathway is what I need, and <a target="_blank" href="https://teachyourselfcs.com">teachyourselfcs.com</a> does it for me.</p>
<p>While I'm set on following the <a target="_blank" href="https://teachyourselfcs.com">teachyourselfcs.com</a> approach as a guide, I will not necessarily follow the recommended videos/lectures suggested for each subject. I decided to tweak things a little bit and come up with my own structure based on what I was trying to achieve at the end of the day.</p>
<p>I would like to learn the foundational CS principles and concepts but I would also like to earn a qualification while doing so because God-willing, I'd like to enrol in a Computer Science Master's programme in the near future. The Folks at <a target="_blank" href="https://teachyourselfcs.com">teachyourselfcs.com</a> say that a Master's degree is not important. Well, I respect their view, but for me, I would like to pursue a Master's degree – not that I need it but I think it's something nice to have as there are certain kinds of clientele that specifically demand a Bachelor's or Master's degree.</p>
<p>In light of the foregoing, I will be selecting those courses that allow me to earn a certificate as that will greatly help me as I seek to gain admission to a Master's programme at some point in the future. This means my primary focus will be looking at the various available <a target="_blank" href="https://en.wikipedia.org/wiki/Massive_open_online_course">MOOCs</a> such as <a target="_blank" href="https://www.coursera.org/">Coursera</a>, <a target="_blank" href="https://edx.org/">edX</a>, <a target="_blank" href="https://www.udacity.com/">Udacity</a> and so on. So I went over to <a target="_blank" href="https://classcentral.com">classcentral.com</a>, created a <a target="_blank" href="https://www.classcentral.com/@engineervix">profile</a> and used it to find courses and make comparisons. Class Central is quite an excellent resource, you should check it out if you haven't already!</p>
<p>After making comparisons of the various platforms, I settled on Coursera because for $399 a year with the <a target="_blank" href="https://www.coursera.org/courseraplus">Coursera Plus</a> subscription (I was fortunate to get a $100 Black Friday discount for the first year, so I actually paid $299 ☺️), I have a wide array of courses to choose from and the freedom to pursue them in my own time and at my own pace. I concluded that this was the <strong>most cost-effective</strong> way for me. The key thing is to manage my time wisely and pick the courses that would provide the most value.</p>
<p>Of course, Coursera isn't the solution for everything – I will certainly have to look at other platforms (and find the money 💵 to pay) for specific courses of interest.</p>
<p>I'm looking at a timeline of about 2 years to cover these 9 subjects. In the first year, I intend to focus on the first 4 items on the list:</p>
<ol>
<li><p>Programming</p>
</li>
<li><p>Computer Architecture</p>
</li>
<li><p>Data Structures and Algorithms</p>
</li>
<li><p>Math for CS</p>
</li>
</ol>
<p>However, before rushing to get started, and before I even paid for the Coursera Plus subscription, I decided to give myself a challenge by enrolling in a course with a free certificate (outside Coursera) and ensuring that I completed it within the indicative timeframe. I've enrolled in such courses (and programming tutorials) before – there are very very few that I've actually completed 🙈! I thought this would be a good indication of my readiness to do this. I told myself that if I struggled to complete this one course, then perhaps I wasn't quite ready to commit myself. I enrolled in the <a target="_blank" href="https://courses.minnalearn.com/en/courses/startingup/">Starting Up</a> course offered by <a target="_blank" href="https://www.aalto.fi/">Aalto University</a> and <a target="_blank" href="https://www.minnalearn.com/">MinnaLearn</a> and I <a target="_blank" href="https://twitter.com/engineervix/status/1589521116955316224?s=20&amp;t=cqqcJGkBAHFK_vA6EfIOcQ">tweeted about this</a> (it's been said that people tend to be more committed to their goals after they share them). I was able to complete it in just under 4 weeks, which was great because the indicative timeframe was 6 weeks. This gave me some confidence and helped me identify my weaknesses and plan my time, so I think I'm now ready to roll!</p>
<p>Because of the limited Black Friday deal (which I find out via ClassCentral), I actually ended up subscribing to Coursera Plus before I even completed the Starting Up course.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/3ohzdDVC6SAniR17l6/giphy.gif">https://media.giphy.com/media/3ohzdDVC6SAniR17l6/giphy.gif</a></div>
<p> </p>
<p>Since this is no trivial undertaking, involving learning lots of new stuff, I thought it necessary to spend some time <strong>learning how to learn</strong>. Therefore, before I delve into the 4 focus areas for the first year, I enrolled in <a target="_blank" href="https://www.coursera.org/learn/learning-how-to-learn">Learning How to Learn: Powerful mental tools to help you master tough subjects</a>, a <a target="_blank" href="https://www.classcentral.com/report/top-moocs-2022-edition/#personal-development">highly recommended course</a>. I know, I'm supposed to get started with CS already, right? Well, I need to make the most of the limited time available, which means I also need to master the technique of learning so I can learn new things effectively. So this is something important, and my future self will surely be most grateful that I did this!</p>
<p>I intend to begin the first-year journey with <a target="_blank" href="https://www.coursera.org/specializations/computer-fundamentals">Fundamentals of Computing Specialization</a> offered by <a target="_blank" href="https://www.rice.edu/">Rice University</a>. This covers 2 of the 4 subjects (<em>Programming</em> + <em>Data Structures and Algorithms</em>). There's an indicative completion timeframe of 6 months, we'll see how it goes – hopefully, I can do it in less time. I'll be reviewing progress and evaluating things along the way, let's see how it goes! Until the next time .... adios!</p>
<hr />
<p>Photo by <a target="_blank" href="https://unsplash.com/@bradencollum?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Braden Collum</a> on <a target="_blank" href="https://unsplash.com/s/photos/start?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Planning to study computer science]]></title><description><![CDATA[I happen to be one of those developers who started their career in a totally different field, then at a later stage made the switch to software engineering.
I learnt how to code while still studying civil engineering, and continued to slowly (and inc...]]></description><link>https://blog.victor.co.zm/planning-to-study-cs</link><guid isPermaLink="true">https://blog.victor.co.zm/planning-to-study-cs</guid><category><![CDATA[Computer Science]]></category><category><![CDATA[studying]]></category><category><![CDATA[learning]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[university]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Fri, 04 Nov 2022 20:00:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1667589839508/VlJiF5j2V.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I happen to be one of those developers who started their career in a totally different field, then at a later stage made the switch to software engineering.</p>
<p>I learnt how to code while still studying <a target="_blank" href="https://en.wikipedia.org/wiki/Civil_engineering">civil engineering</a>, and continued to slowly (and inconsistently) learn one or two things after <a target="_blank" href="https://www.cbu.ac.zm/">university</a>, in my free time. Fast-forward to early 2022, a couple of months before my 37th birthday, I landed an opportunity to work as a software developer, and now, 8 months later, I'm at a point where I have to make an important decision on how to navigate this new career.</p>
<p>While it's possible to have a thriving career in tech without a CS degree, I think it's important to be well grounded in the fundamental CS concepts and principles. In the words of <a target="_blank" href="https://twitter.com/oznova_">Oz Nova</a> and <a target="_blank" href="https://twitter.com/quackingduck">Myles Byrne</a> from <a target="_blank" href="https://teachyourselfcs.com/">teachyourselfcs.com</a></p>
<blockquote>
<p>If you’re a self-taught engineer or bootcamp grad, you owe it to yourself to learn computer science.</p>
</blockquote>
<p>And again,</p>
<blockquote>
<p>There are <strong>2 types of software engineer</strong>: those who understand computer science well enough to do challenging, innovative work, and those who just get by because they’re familiar with a few high level tools.</p>
</blockquote>
<p>I don't want to be the <em>type 2</em> guy 😀, so I have to <a target="_blank" href="https://open.spotify.com/track/6by0au2JlDXmdE1Xh2jo9U?si=19e5b6f0adb44388">do something about it</a>!</p>
<h2 id="heading-what-are-my-available-options">What are my available options?</h2>
<h3 id="heading-physical-or-online">Physical or Online?</h3>
<p>At this stage in my life, I do not have the luxury of going to university full time, so that's out of the question. This only leaves me with online options.</p>
<h3 id="heading-pursue-a-degree-or-learn-on-your-own">Pursue a degree, or learn on your own?</h3>
<p>Now, do I want to pursue an actual CS degree, or do I just want to learn the core subjects on my own? Well, I think it's nice to have an actual CS degree. However, one has to </p>
<ol>
<li>find a well accredited University with good rankings</li>
<li>have the money to pay for university education</li>
<li>have the time to actually invest into learning, doing assessments, exams, etc.</li>
</ol>
<p>Item № 1 is not difficult. The challenge is № 2 and 3.</p>
<p><em>Most</em> of the really good universities out there aren't cheap. There are some institutions which I've found to be affordable, but I'm not quite sure about credibility. I do not want to spend 3+ years pursuing an academic qualification which will only be recognized in very few places. That's a definite no-no for me!</p>
<p>If I'm going to enroll to study at a university, I have to dedicate about 25 hours per week towards studies, that's quite a lot of time for someone who has a family, has a full time job and other responsibilities.</p>
<p>So there has to be a balance between <strong>quality</strong>, <strong>affordability</strong> and <strong>time</strong>.</p>
<h3 id="heading-if-degree-bachelors-or-masters">If degree – Bachelor's or Master's?</h3>
<p>Having already earned a Bachelor's degree, the thought of going to pursue another Bachelor's degree kinda feels counter productive. It's more desirable to move forward and pursue a higher qualification.</p>
<p>There are a lot of excellent institutions that offer CS Master's degrees to people whose first degree is not necessarily a CS degree. However, I have also found that admission into these programs generally isn't easy. Further, I looked at a few curricula and saw that either</p>
<ol>
<li>the content is generally "basic" and broad, not going in depth, since the target students are not coming from a CS background. </li>
<li>the content is advanced and rigorous, as one is expected to somehow already have the foundational concepts that are typically taught at undergraduate level.</li>
</ol>
<p>Going for a program with a category 1 curriculum just for the sake of "having a Master's degree" would not do me any good. At this stage, it's hard for me to get into a university offering Category 2 curriculum, so I have to rule out a Master's degree for now. I need to be well grounded in the core CS concepts, so it would be better for me to pursue a Bachelor's degree.</p>
<p>However, even if I had the money, enrolling at a University to pursue an undergraduate CS degree may be difficult for me because of time constraints. I cannot commit to 25 hours per week at this point.</p>
<p>So, what do I do?</p>
<h2 id="heading-only-1-option">Only 1 option!</h2>
<p>Well, clearly, my best option (unless you can convince me otherwise) is to self-study, in my own time, at my own pace. However, I have to do so in a <strong>well defined and structured</strong> manner, not haphazardly and only doing so when I feel like. What would really help is to be part of a community of other people in a similar situation, so that we study and learn together, encouraging and challenging each other along the way.</p>
<p>Fortunately, there are many resources and guidelines out there to help people like me pursue a CS education on their own. I have already referred to <a target="_blank" href="https://teachyourselfcs.com/">teachyourselfcs.com</a>, which suggests that one studies the following nine core subjects, aiming for 100-200 hours of study of each, then revisiting favorites throughout one's career:</p>
<ol>
<li>Programming</li>
<li>Computer Architecture</li>
<li>Algorithms and Data Structures</li>
<li>Math for CS</li>
<li>Operating Systems</li>
<li>Computer Networking</li>
<li>Databases</li>
<li>Languages and Compilers</li>
<li>Distributed Systems</li>
</ol>
<p>There's also the <a target="_blank" href="https://github.com/ossu/computer-science">OSSU curriculum</a>, which is described as a <em>complete education in computer science using online materials</em>. OSSU has a <a target="_blank" href="https://discord.gg/wuytwK5s9h">discord server</a> where one can interact with other OSSU students.</p>
<p>Looks like I'm sorted, right? Well, not quite!</p>
<h2 id="heading-next-steps">Next steps</h2>
<p>I need to look at the various guidelines and curricula, also comparing with actual CS programmes at selected universities. </p>
<p>I need to think critically about the end goal and structure my learning accordingly.</p>
<p>I'll put up a follow-up post where I document what I'll come up with — essentially, I'll be talking about building my own CS degree!</p>
<p>Until then, adios! 👋</p>
<hr />
<p>Cover image by <a target="_blank" href="https://unsplash.com/@domlafou?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Dom Fou</a> on <a target="_blank" href="https://unsplash.com/s/photos/university?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Programmatically creating Documents in Wagtail]]></title><description><![CDATA[It's very easy to manage PDFs, spreadsheets, Microsoft Word documents and so on through Wagtail's admin interface. What if you generated such documents on-the-fly, and wanted them to be available in the Documents section of the Admin interface? I was...]]></description><link>https://blog.victor.co.zm/programmatically-creating-documents-in-wagtail</link><guid isPermaLink="true">https://blog.victor.co.zm/programmatically-creating-documents-in-wagtail</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[cms]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Fri, 31 Dec 2021 23:10:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640990092162/O1z5G6bSe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's very easy to manage PDFs, spreadsheets, Microsoft Word documents and so on through <a target="_blank" href="https://wagtail.io/">Wagtail</a>'s admin interface. What if you generated such documents on-the-fly, and wanted them to be available in the <a target="_blank" href="https://docs.wagtail.io/en/stable/editor_manual/documents_images_snippets/documents.html">Documents</a> section of the Admin interface? I was faced with such a situation, and this is how I did it ...</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> django.core.files.base <span class="hljs-keyword">import</span> File
<span class="hljs-keyword">from</span> wagtail.documents.models <span class="hljs-keyword">import</span> Document

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_wagtail_document</span>(<span class="hljs-params">f, title</span>):</span>
    <span class="hljs-string">"""
    programmatically create a wagtail document which will
    automatically be available in the Documents section of wagtailadmin
    """</span>
    doc_file = File(open(f, <span class="hljs-string">"rb"</span>), name=os.path.basename(f))
    doc = Document(
        title=title,
        file=doc_file,
    )
    doc.save()

f = os.path.join(<span class="hljs-string">"your_file"</span>)
today = datetime.now().strftime(<span class="hljs-string">"%Y-%b-%d-%a"</span>)
now = datetime.now().strftime(<span class="hljs-string">"%-I:%M%p"</span>)
doc_title = <span class="hljs-string">f"My awesome document created on <span class="hljs-subst">{today}</span> at <span class="hljs-subst">{now}</span>"</span>
create_wagtail_document(f, doc_title)
</code></pre>
<p>The solution was inspired by <a target="_blank" href="https://www.twitter.com/pieterclaerhout">Pieter Claerhout</a>'s approach in <a target="_blank" href="https://www.yellowduck.be/posts/programatically-importing-images-wagtail/">Programatically importing images in Wagtail</a>.</p>
]]></content:encoded></item><item><title><![CDATA[an opinionated list of essential VS Code extensions]]></title><description><![CDATA[VS Code is one of the most popular editors/IDEs out there (see, for instance, WakaTime 2020 Programming Stats, Stack Overflow 2021 Developer Survey & Python Developers Survey 2020 Results). It is currently my favourite code editor – I use it for almo...]]></description><link>https://blog.victor.co.zm/an-opinionated-list-of-essential-vs-code-extensions</link><guid isPermaLink="true">https://blog.victor.co.zm/an-opinionated-list-of-essential-vs-code-extensions</guid><category><![CDATA[Visual Studio Code]]></category><category><![CDATA[vscode extensions]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Text Editors]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Sat, 20 Nov 2021 09:48:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637400021337/u_XGTRgBV.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>VS Code is one of the most popular editors/IDEs out there (see, for instance, <a target="_blank" href="https://wakatime.com/blog/43-wakatime-2020-programming-stats">WakaTime 2020 Programming Stats</a>, <a target="_blank" href="https://insights.stackoverflow.com/survey/2021#technology-most-loved-dreaded-and-wanted">Stack Overflow 2021 Developer Survey</a> &amp; <a target="_blank" href="https://www.jetbrains.com/lp/python-developers-survey-2020/#DevelopmentTools">Python Developers Survey 2020 Results</a>). It is currently my favourite code editor – I use it for almost everything!</p>
<p>In this post, I present a list of VS Code extensions that I generally depend on, and always ensure that they are installed in my favourite text editor! So, without further ado, here we go ...</p>
<h2 id="heading-general-purpose">General Purpose ⚙️</h2>
<h3 id="heading-spell-checking">Spell Checking ✍🏽</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker"><code>streetsidesoftware.code-spell-checker</code></a> – <strong>Code Spell Checker</strong> › Spelling checker for source code</li>
</ul>
<h3 id="heading-comments">Comments 💬</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments"><code>aaron-bond.better-comments</code></a> – <strong>Better Comments</strong> › Improve your code commenting by annotating with alert, informational, TODOs, and more!</li>
</ul>
<h3 id="heading-changelogs">Changelogs 📜</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=axetroy.vscode-changelog-generator"><code>axetroy.vscode-changelog-generator</code></a> – <strong>changelog-generator</strong> › An extension to generate changelog.</li>
</ul>
<h3 id="heading-add-a-splash-of-colour">Add a splash of colour 🎨</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2"><code>CoenraadS.bracket-pair-colorizer-2</code></a> – <strong>Bracket Pair Colorizer 2</strong> › A customizable extension for colorizing matching brackets</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=kamikillerto.vscode-colorize"><code>kamikillerto.vscode-colorize</code></a> – <strong>colorize</strong> › A vscode extension to help visualize css colors in files.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=naumovs.color-highlight"><code>naumovs.color-highlight</code></a> – <strong>Color Highlight</strong> › Highlight web colors in your editor</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow"><code>oderwat.indent-rainbow</code></a> – <strong>indent-rainbow</strong> › Makes indentation easier to read</li>
</ul>
<h3 id="heading-lorem-ipsum">Lorem ipsum ❇️</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=deerawan.vscode-faker"><code>deerawan.vscode-faker</code></a> – <strong>vscode-faker</strong> › Generate fake data for name, address, lorem ipsum, commerce and much more</li>
</ul>
<h3 id="heading-coding-style">Coding style 📄</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig"><code>EditorConfig.EditorConfig</code></a> – <strong>EditorConfig for VS Code</strong> › EditorConfig Support for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode"><code>esbenp.prettier-vscode</code></a> – <strong>Prettier - Code formatter</strong> › Code formatter using prettier</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=HookyQR.beautify"><code>HookyQR.beautify</code></a> – <strong>Beautify</strong> › Beautify code in place for VS Code</li>
</ul>
<h3 id="heading-code-execution">Code execution 🚀</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=formulahendry.code-runner"><code>formulahendry.code-runner</code></a> – <strong>Code Runner</strong> › Run C, C++, Java, JS, PHP, Python, Perl, Ruby, Go, Lua, Groovy, PowerShell, CMD, BASH, F#, C#, VBScript, TypeScript, CoffeeScript, Scala, Swift, Julia, Crystal, OCaml, R, AppleScript, Elixir, VB.NET, Clojure, Haxe, Obj-C, Rust, Racket, Scheme, AutoHotkey, AutoIt, Kotlin, Dart, Pascal, Haskell, Nim, ...</li>
</ul>
<h3 id="heading-metrics">Metrics 📈</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=WakaTime.vscode-wakatime"><code>WakaTime.vscode-wakatime</code></a> – <strong>WakaTime</strong> › Metrics, insights, and time tracking automatically generated from your programming activity.</li>
</ul>
<h3 id="heading-git">Git 🗃️</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=rubbersheep.gi"><code>rubbersheep.gi</code></a> – <strong>gi</strong> › Generating .gitignore files made easy</li>
</ul>
<h3 id="heading-screen-capture">Screen capture 📸</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=pnp.polacode"><code>pnp.polacode</code></a> – <strong>Polacode</strong> › 📸  Polaroid for your code</li>
</ul>
<h3 id="heading-miscellaneous-tools">Miscellaneous Tools 🛠️</h3>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=alefragnani.Bookmarks"><code>alefragnani.Bookmarks</code></a> – <strong>Bookmarks</strong> › Mark lines and jump to them</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree"><code>Gruntfuggly.todo-tree</code></a> – <strong>Todo Tree</strong> › Show TODO, FIXME, etc. comment tags in a tree view</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=humao.rest-client"><code>humao.rest-client</code></a> – <strong>REST Client</strong> › REST Client for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=kisstkondoros.vscode-gutter-preview"><code>kisstkondoros.vscode-gutter-preview</code></a> – <strong>Image preview</strong> › Shows image preview in the gutter and on hover</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=tombonnike.vscode-status-bar-format-toggle"><code>tombonnike.vscode-status-bar-format-toggle</code></a> – <strong>Formatting Toggle</strong> › A VS Code extension that allows you to toggle the formatter (Prettier, Beautify, …) ON and OFF with a simple click.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=wmaurer.vscode-jumpy"><code>wmaurer.vscode-jumpy</code></a> – <strong>jumpy</strong> › Jumpy provides fast cursor movement, inspired by Atom's package of the same name.</li>
</ul>
<h2 id="heading-themes">Themes 💄</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=akamud.vscode-theme-onedark"><code>akamud.vscode-theme-onedark</code></a> – <strong>Atom One Dark Theme</strong> › One Dark Theme based on Atom</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme"><code>PKief.material-icon-theme</code></a> – <strong>Material Icon Theme</strong> › Material Design Icons for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=sdras.night-owl"><code>sdras.night-owl</code></a> – <strong>Night Owl</strong> › A VS Code theme for the night owls out there. Now introducing Light Owl theme for daytime usage. Decisions were based on meaningful contrast for reading comprehension and for optimal razzle dazzle. ✨</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons"><code>vscode-icons-team.vscode-icons</code></a> – <strong>vscode-icons</strong> › Icons for Visual Studio Code</li>
</ul>
<h2 id="heading-language-support">Language Support ✨</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=asciidoctor.asciidoctor-vscode"><code>asciidoctor.asciidoctor-vscode</code></a> – <strong>AsciiDoc</strong> › Provides rich language support for AsciiDoc.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml"><code>attilabuti.vscode-mjml</code></a> – <strong>MJML</strong> › MJML preview, lint, compile for Visual Studio Code.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=bibhasdn.django-html"><code>bibhasdn.django-html</code></a> – <strong>Django Template</strong> › Django template language support for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=bungcip.better-toml"><code>bungcip.better-toml</code></a> – <strong>Better TOML</strong> › Better TOML Language support</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code"><code>Dart-Code.dart-code</code></a> – <strong>Dart</strong> › Dart language support and debugger for Visual Studio Code.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter"><code>Dart-Code.flutter</code></a> – <strong>Flutter</strong> › Flutter support and debugger for Visual Studio Code.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=DotJoshJohnson.xml"><code>DotJoshJohnson.xml</code></a> – <strong>XML Tools</strong> › XML Formatting, XQuery, and XPath Tools for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=GrapeCity.gc-excelviewer"><code>GrapeCity.gc-excelviewer</code></a> – <strong>Excel Viewer</strong> › View Excel spreadsheets and CSV files within Visual Studio Code workspaces.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=IBM.output-colorizer"><code>IBM.output-colorizer</code></a> – <strong>Output Colorizer</strong> › Syntax highlighting for log files</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=idleberg.nsis"><code>idleberg.nsis</code></a> – <strong>NSIS</strong> › Language syntax, IntelliSense and build system for Nullsoft Scriptable Install System (NSIS)</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=lextudio.restructuredtext"><code>lextudio.restructuredtext</code></a> – <strong>reStructuredText</strong> › reStructuredText language support (RST/ReST linter, preview, IntelliSense and more)</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv"><code>mikestead.dotenv</code></a> – <strong>DotENV</strong> › Support for dotenv file syntax</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp"><code>ms-dotnettools.csharp</code></a> – <strong>C#</strong> › C# for Visual Studio Code (powered by OmniSharp).</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools"><code>ms-vscode.cmake-tools</code></a> – <strong>CMake Tools</strong> › Extended CMake support in Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools"><code>ms-vscode.cpptools</code></a> – <strong>C/C++</strong> › C/C++ IntelliSense, debugging, and code browsing.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=naco-siren.gradle-language"><code>naco-siren.gradle-language</code></a> – <strong>Gradle Language Support</strong> › Add Gradle language support for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=NativeScript.nativescript"><code>NativeScript.nativescript</code></a> – <strong>NativeScript</strong> › NativeScript support for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml"><code>redhat.vscode-yaml</code></a> – <strong>YAML</strong> › YAML Language Support by Red Hat, with built-in Kubernetes syntax support</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=redhat.vscode-xml"><code>redhat.vscode-xml</code></a> – <strong>XML</strong> › XML Language Support by Red Hat</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=samuelcolvin.jinjahtml"><code>samuelcolvin.jinjahtml</code></a> – <strong>Better Jinja</strong> › Syntax highlighting for jinja(2) including HTML, Markdown, YAML, Ruby and LaTeX templates</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=shanoor.vscode-nginx"><code>shanoor.vscode-nginx</code></a> – <strong>nginx.conf</strong> › Syntax highlighter for nginx conf files.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Syler.sass-indented"><code>Syler.sass-indented</code></a> – <strong>Sass</strong> › Indented Sass syntax Highlighting, Autocomplete &amp; Formatter</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=twxs.cmake"><code>twxs.cmake</code></a> – <strong>CMake</strong> › CMake langage support for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=william-voyek.vscode-nginx"><code>william-voyek.vscode-nginx</code></a> – <strong>NGINX Configuration</strong> › Syntax highlighting for NGINX configuration files</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=xshrim.txt-syntax"><code>xshrim.txt-syntax</code></a> – <strong>Txt Syntax</strong> › highlight text files(.txt, .out .tmp, .log, .ini, .cnf ...) and provide general utility tools for text documents</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=XadillaX.viml"><code>XadillaX.viml</code></a> – <strong>VimL (Vim Language, Vim Script)</strong> › Vim Script language support for VSCode.</li>
</ul>
<h2 id="heading-snippets">Snippets 🎉</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=bibhasdn.django-snippets"><code>bibhasdn.django-snippets</code></a> – <strong>Django Snippets</strong> › Common Django snippets for everyday use</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Keno.uikit-3-snippets"><code>Keno.uikit-3-snippets</code></a> – <strong>UIkit 3.0 Snippets</strong> › UIkit 3.0 Snippets based on official documentation</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=msaelices.nativescript-vue-snippets"><code>msaelices.nativescript-vue-snippets</code></a> – <strong>NativeScript-Vue Snippets</strong> › Snippets for Telerik's NativeScript mobile framework with Vue</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=sdras.vue-vscode-snippets"><code>sdras.vue-vscode-snippets</code></a> – <strong>Vue VSCode Snippets</strong> › Snippets that will supercharge your Vue workflow</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=tsvetan-ganev.nativescript-xml-snippets"><code>tsvetan-ganev.nativescript-xml-snippets</code></a> – <strong>NativeScript XML Snippets</strong> › XML snippets for Telerik's NativeScript cross-platform mobile applications development framework.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets"><code>xabikos.JavaScriptSnippets</code></a> – <strong>JavaScript (ES6) code snippets</strong> › Code snippets for JavaScript in ES6 syntax</li>
</ul>
<h2 id="heading-containers">Containers 🚢</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker"><code>ms-azuretools.vscode-docker</code></a> – <strong>Docker</strong> › Makes it easy to create, manage, and debug containerized applications.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers"><code>ms-vscode-remote.remote-containers</code></a> – <strong>Remote - Containers</strong> › Open any folder or repository inside a Docker container and take advantage of Visual Studio Code's full feature set.</li>
</ul>
<h2 id="heading-python">Python 🐍</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-python.python"><code>ms-python.python</code></a> – <strong>Python</strong> › IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance"><code>ms-python.vscode-pylance</code></a> – <strong>Pylance</strong> › A performant, feature-rich language server for Python in VS Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter"><code>ms-toolsai.jupyter</code></a> – <strong>Jupyter</strong> › Jupyter notebook support, interactive programming and computing that supports Intellisense, debugging and more.</li>
</ul>
<h2 id="heading-tex">TeX ✒️</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=geoffkaile.latex-count"><code>geoffkaile.latex-count</code></a> – <strong>latex-count</strong> › A word counter for latex files (.tex)</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop"><code>James-Yu.latex-workshop</code></a> – <strong>LaTeX Workshop</strong> › Boost LaTeX typesetting efficiency with preview, compile, autocomplete, colorize, and more.</li>
</ul>
<h2 id="heading-web-development">Web development 🌐</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense"><code>christian-kohler.npm-intellisense</code></a> – <strong>npm Intellisense</strong> › Visual Studio Code plugin that autocompletes npm modules in import statements</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ecmel.vscode-html-css"><code>ecmel.vscode-html-css</code></a> – <strong>HTML CSS Support</strong> › CSS Intellisense for HTML</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint"><code>dbaeumer.vscode-eslint</code></a> – <strong>ESLint</strong> › Integrates ESLint JavaScript into VS Code.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=eg2.vscode-npm-script"><code>eg2.vscode-npm-script</code></a> – <strong>npm</strong> › npm support for VS Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=lonefy.vscode-JS-CSS-HTML-formatter"><code>lonefy.vscode-JS-CSS-HTML-formatter</code></a> – <strong>JS-CSS-HTML Formatter</strong> › Format, prettify and beautify JS, CSS, HTML code by using shortcuts, context menu or CLI</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.sublime-keybindings"><code>ms-vscode.sublime-keybindings</code></a> – <strong>Sublime Text Keymap and Settings Importer</strong> › Import Sublime Text settings and keybindings into VS Code.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin"><code>ms-vscode.vscode-typescript-tslint-plugin</code></a> – <strong>TSLint</strong> › TSLint support for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer"><code>ritwickdey.LiveServer</code></a> – <strong>Live Server</strong> › Launch a development local Server with live reload feature for static &amp; dynamic pages</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=sidthesloth.html5-boilerplate"><code>sidthesloth.html5-boilerplate</code></a> – <strong>HTML Boilerplate</strong> › A basic HTML5 boilerplate snippet generator.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint"><code>stylelint.vscode-stylelint</code></a> – <strong>stylelint</strong> › Modern CSS/SCSS/Less linter</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=webhint.vscode-webhint"><code>webhint.vscode-webhint</code></a> – <strong>webhint</strong> › Run <a target="_blank" href="https://webhint.io/">webhint</a> in Visual Studio Code.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Zignd.html-css-class-completion"><code>Zignd.html-css-class-completion</code></a> – <strong>IntelliSense for CSS class names in HTML</strong> › CSS class name completion for the HTML class attribute based on the definitions found in your workspace.</li>
</ul>
<h2 id="heading-markdown">Markdown 📝</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint"><code>DavidAnson.vscode-markdownlint</code></a> – <strong>markdownlint</strong> › Markdown linting and style checking for Visual Studio Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.Theme-MarkdownKit"><code>ms-vscode.Theme-MarkdownKit</code></a> – <strong>Markdown Theme Kit</strong> › Theme Kit for VS Code optimized for Markdown. Based on the TextMate themes.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount"><code>ms-vscode.wordcount</code></a> – <strong>Word Count</strong> › Markdown Word Count Example - a status bar contribution that reports out the number of works in a Markdown document as you interact with it.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one"><code>yzhang.markdown-all-in-one</code></a> – <strong>Markdown All in One</strong> › All you need to write Markdown (keyboard shortcuts, table of contents, auto preview and more)</li>
</ul>
<h2 id="heading-vuejs">Vue.js 💚</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=sdras.vue-vscode-extensionpack"><code>sdras.vue-vscode-extensionpack</code></a> – <strong>Vue VS Code Extension Pack</strong> › A collection of extensions for working with Vue Applications in VS Code</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=octref.vetur"><code>octref.vetur</code></a> – <strong>Vetur</strong> › Vue tooling for VS Code</li>
</ul>
<h2 id="heading-android">Android 📱</h2>
<ul>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=jeroen-meijer.pubspec-assist"><code>jeroen-meijer.pubspec-assist</code></a> – <strong>Pubspec Assist</strong> › Easily add and update dependencies to your Dart and Flutter project.</li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-gradle"><code>richardwillis.vscode-gradle</code></a> – <strong>Gradle Tasks</strong> › Run Gradle tasks in VS Code</li>
</ul>
<p>Well, there you have it! What VS Code extensions do you depend on that aren't on this list? Do you have alternatives to some of the extensions I've listed? Well, let me know in the comments below 🙂.</p>
]]></content:encoded></item><item><title><![CDATA[Branching workflows in Wagtail]]></title><description><![CDATA[I wrote this post in order to demonstrate and document a working solution to this Stack Overflow question which I asked on September 2nd, 2021. Big ups to LB Ben Johnston, who outlined the solution and provided a proof of concept.
Contents

The probl...]]></description><link>https://blog.victor.co.zm/wagtail-branching-workflows</link><guid isPermaLink="true">https://blog.victor.co.zm/wagtail-branching-workflows</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[cms]]></category><category><![CDATA[workflow]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Mon, 11 Oct 2021 19:40:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1633806480885/iYsetSQFm.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wrote this post in order to demonstrate and document a working solution to <a target="_blank" href="https://stackoverflow.com/questions/69028083/">this Stack Overflow question</a> which I asked on September 2<sup>nd</sup>, 2021. Big ups to <a target="_blank" href="https://lb.ee/">LB Ben Johnston</a>, who outlined the solution and provided a proof of concept.</p>
<h3 id="contents">Contents</h3>
<ul>
<li><a class="post-section-overview" href="#the-problem">The problem</a></li>
<li><a class="post-section-overview" href="#the-solution">The solution</a><ul>
<li><a class="post-section-overview" href="#0-first-things-first">0. First things first</a></li>
<li><a class="post-section-overview" href="#1-update-the-dailyreflectionpage-model">1. Update the <code>DailyReflectionPage</code> model</a></li>
<li><a class="post-section-overview" href="#2-create-a-new-task">2. Create a new <code>Task</code></a></li>
<li><a class="post-section-overview" href="#3-email-notifications">3. Email Notifications</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#demo">Demo</a></li>
</ul>
<h3 id="the-problem">The problem</h3>
<p>Suppose we have an organization that annually publishes a booklet consisting of <strong>daily reflections</strong>. This booklet is published towards the end of the year, for use in the coming year. The daily reflections are written by different authors, and we would like to have a publishing <a target="_blank" href="https://docs.wagtail.io/en/stable/editor_manual/administrator_tasks/managing_workflows.html">workflow</a> where, for instance, the daily reflections from January to June are reviewed by one group, and those from July to December are reviewed by another group, as illustrated below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1633808469404/drmzXkwWV.png" alt="Branching Workflows based on value of specified Page field" /></p>
<p>We have a <code>DailyReflectionPage</code> Model with a <code>reflection_date</code> field that forms the basis for the Page's slug, which is in the form <code>YYYY-MM-DD</code>. Here's an extract of the Page model:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DailyReflectionPage</span>(<span class="hljs-params">Page</span>):</span>
    <span class="hljs-string">"""
    The Daily Reflection Model
    """</span>
    ...
    ...

    reflection_date = models.DateField(<span class="hljs-string">"Reflection Date"</span>, max_length=<span class="hljs-number">254</span>)
    ...
    ...
<span class="hljs-meta">    @cached_property</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">date</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""
        Returns the Reflection's date as a string in %Y-%m-%d format
        """</span>
        fmt = <span class="hljs-string">"%Y-%m-%d"</span>
        date_as_string = (self.reflection_date).strftime(fmt)
        <span class="hljs-keyword">return</span> date_as_string      
    ...
    ...
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">full_clean</span>(<span class="hljs-params">self, *args, **kwargs</span>):</span>
        <span class="hljs-comment"># first call the built-in cleanups (including default slug generation)</span>
        super(DailyReflectionPage, self).full_clean(*args, **kwargs)

        <span class="hljs-comment"># now make your additional modifications</span>
        <span class="hljs-keyword">if</span> self.slug <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> self.date:
            self.slug = self.date
    ...
    ...
</code></pre>
<p>If you want to see the complete Page Model, then have a look at the <a target="_blank" href="https://github.com/engineervix/wagtail-branching-workflows/blob/5745dfde910ea98e9cfebc7c44c0a6f9aee6491f/app/mysite/reflections/models.py"><code>models.py</code> file in the <code>reflections</code> app</a>. This link points to a commit before implementation of the solution outlined herein.</p>
<h3 id="the-solution">The solution</h3>
<p>I initially <a target="_blank" href="https://gist.github.com/engineervix/c0896359b82c07e3a383bd32b4d4e2ce">created a gist</a> showing the full implementation of LB's solution. It only consists of the<code>models.py</code> file, so if you just want to jump straight to the basic solution, then you can look at it. However, if you would like to see a more complete solution within the wider context of an actual Wagtail project, you can have a look at the sample Wagtail project I created to accompany this post. The source code is available at <a target="_blank" href="https://github.com/engineervix/wagtail-branching-workflows">github.com/engineervix/wagtail-branching-workflows</a>.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/engineervix/wagtail-branching-workflows">https://github.com/engineervix/wagtail-branching-workflows</a></div>
<h4 id="0-first-things-first">0. First things first</h4>
<p>If you would like to follow along, starting from the point before implementing the solution, you can clone the repo and checkout commit <a target="_blank" href="https://github.com/engineervix/wagtail-branching-workflows/tree/5745dfde910ea98e9cfebc7c44c0a6f9aee6491f"><code>5745df</code></a></p>
<pre><code>git <span class="hljs-keyword">clone</span> https:<span class="hljs-comment">//github.com/engineervix/wagtail-branching-workflows.git</span>
cd wagtail-branching-workflows
git checkout <span class="hljs-number">5745</span>df
</code></pre><p>The project uses <a target="_blank" href="https://docs.docker.com/get-docker/">docker</a> and <a target="_blank" href="https://docs.docker.com/compose/">docker-compose</a>, so make sure that these are installed and working on your machine:</p>
<pre><code class="lang-sh"><span class="hljs-comment"># check that you have docker on your machine</span>
docker -v

<span class="hljs-comment"># check that you have docker-compose on your machine</span>
docker-compose -v
</code></pre>
<p>Then create the required <code>.env</code> files:</p>
<pre><code class="lang-sh">cp -v app/.envs/.dev.env.sample app/.envs/.dev.env
cp -v app/.envs/.test.env.sample app/.envs/.test.env
</code></pre>
<p>Build the images and spin up the containers:</p>
<pre><code class="lang-sh">docker-compose up -d --build
</code></pre>
<p>You'll have to wait a few seconds for some processes to initialize / run (postgres, database migrations, browser-sync, Django server, etc.). You can check the status via</p>
<pre><code class="lang-sh">docker-compose logs web
</code></pre>
<p>When all set, you should see something like this:</p>
<pre><code class="lang-txt">web_1  | Performing system checks...
web_1  | 
web_1  | [Browsersync] Proxying: http://127.0.0.1:8000
web_1  | [Browsersync] Access URLs:
web_1  |  -----------------------------------
web_1  |        Local: http://localhost:3000
web_1  |     External: http://172.19.0.3:3000
web_1  |  -----------------------------------
web_1  |           UI: http://localhost:3001
web_1  |  UI External: http://localhost:3001
web_1  |  -----------------------------------
web_1  | [Browsersync] Watching files...
web_1  | System check identified no issues (0 silenced).
web_1  | 
web_1  | Django version 3.2.8, using settings 'config.settings.dev'
web_1  | Development server is running at http://0.0.0.0:8000/
web_1  | Using the Werkzeug debugger (http://werkzeug.pocoo.org/)
web_1  | Quit the server with CONTROL-C.
web_1  | [Browsersync] Reloading Browsers... (buffered 2 events)
web_1  |  * Debugger is active!
web_1  |  * Debugger PIN: 104-102-219
</code></pre>
<p>You can now proceed to create a superuser:</p>
<pre><code class="lang-sh">docker-compose <span class="hljs-built_in">exec</span> web ./manage.py createsuperuser
</code></pre>
<p>Load initial data:</p>
<pre><code class="lang-sh">docker-compose <span class="hljs-built_in">exec</span> web ./manage.py load_initial_data
</code></pre>
<p>This initial data includes 6 users with the following details:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>No.</td><td>Email Address</td><td>Password</td><td>Group</td><td>First Name</td><td>Last Name</td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>john.doe@example.com</td><td>WriterPassword1</td><td>Writers</td><td>John</td><td>Doe</td></tr>
<tr>
<td>2</td><td>jane.doe@example.com</td><td>WriterPassword2</td><td>Writers</td><td>Jane</td><td>Doe</td></tr>
<tr>
<td>3</td><td>another.writer@example.com</td><td>WriterPassword3</td><td>Writers</td><td>Another</td><td>Writer</td></tr>
<tr>
<td>4</td><td>moderator.one@example.org</td><td>ModeratorPassword1</td><td>Moderators</td><td>Gina</td><td>Stephenson</td></tr>
<tr>
<td>5</td><td>moderator.two@example.org</td><td>ModeratorPassword2</td><td>Moderators</td><td>George</td><td>Benson</td></tr>
<tr>
<td>6</td><td>chief@example.org</td><td>ApproverPassword0</td><td>Approvers</td><td>Connie</td><td>Montgomery</td></tr>
</tbody>
</table>
</div><p>You can access the dev server at <a target="_blank" href="http://127.0.0.1:3009">http://127.0.0.1:3009</a>. This project uses <a target="_blank" href="https://github.com/maildev/maildev">MailDev</a> for viewing and testing emails generated during development. The MailDev server is accessible at <a target="_blank" href="http://localhost:1089">http://localhost:1089</a>. </p>
<h4 id="1-update-the-dailyreflectionpage-model">1. Update the <code>DailyReflectionPage</code> model</h4>
<p>First, we add a method called <code>date_in_first_semester</code>:</p>
<pre><code class="lang-python"><span class="hljs-meta">@cached_property</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">date_in_first_semester</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-string">"""
    Returns True if Reflection's date
    is in the first half of the year
    """</span>
    month = (self.reflection_date).month
    <span class="hljs-keyword">return</span> month &lt;= <span class="hljs-number">6</span>
</code></pre>
<p>Then, we add a method called <code>get_approval_group_key</code> which will return a simple Boolean or maybe something like 'A' or 'B' (feel free to suit this to your liking).</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_approval_group_key</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-comment"># custom logic here that checks all the date stuff</span>
    <span class="hljs-keyword">if</span> self.date_in_first_semester:
        <span class="hljs-keyword">return</span> <span class="hljs-string">"A"</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">"B"</span>
</code></pre>
<h4 id="2-create-a-new-task">2. Create a new <code>Task</code></h4>
<p>We will now create a new <code>Task</code> that extends the Wagtail <code>Task</code> class.</p>
<p>The following official Wagtail resources are essential in gaining an understanding of what's going on:</p>
<ol>
<li><a target="_blank" href="https://docs.wagtail.io/en/stable/advanced_topics/custom_tasks.html">how to add a new Task Type</a></li>
<li><a target="_blank" href="https://docs.wagtail.io/en/stable/reference/pages/model_reference.html#task">Task model reference</a>.</li>
<li>source code of the built-in <a target="_blank" href="https://github.com/wagtail/wagtail/blob/main/wagtail/core/models/__init__.py#L3425"><code>GroupApprovalTask</code></a></li>
</ol>
<pre><code class="lang-python"><span class="hljs-comment"># only **additional** imports are shown here</span>
<span class="hljs-keyword">from</span> django <span class="hljs-keyword">import</span> forms
...
...
<span class="hljs-comment"># from wagtail.core.models import Page</span>
<span class="hljs-keyword">from</span> wagtail.core.models <span class="hljs-keyword">import</span> Group, Page, Task, TaskState, WorkflowState
...
...

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SplitGroupApprovalTask</span>(<span class="hljs-params">Task</span>):</span>

    <span class="hljs-comment">## note: this is the simplest approach, two fields of linked groups, you could further refine this approach as needed.</span>

    groups_a = models.ManyToManyField(
        Group,
        verbose_name=<span class="hljs-string">"for Jan - June Daily Reflections"</span>,
        help_text=<span class="hljs-string">"Pages at this step in a workflow will be moderated or approved by these groups of users"</span>,
        related_name=<span class="hljs-string">"split_task_group_a"</span>,
    )
    groups_b = models.ManyToManyField(
        Group,
        verbose_name=<span class="hljs-string">"for Jul - Dec Daily Reflections"</span>,
        help_text=<span class="hljs-string">"Pages at this step in a workflow will be moderated or approved by these groups of users"</span>,
        related_name=<span class="hljs-string">"split_task_group_b"</span>,
    )

    admin_form_fields = Task.admin_form_fields + [<span class="hljs-string">"groups_a"</span>, <span class="hljs-string">"groups_b"</span>]
    admin_form_widgets = {
        <span class="hljs-string">"groups_a"</span>: forms.CheckboxSelectMultiple,
        <span class="hljs-string">"groups_b"</span>: forms.CheckboxSelectMultiple,
    }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_approval_groups</span>(<span class="hljs-params">self, page</span>):</span>
        <span class="hljs-string">"""This method gets used by all checks when determining what group to allow/assign this Task to"""</span>

        <span class="hljs-comment"># recommend some checks here, what if `get_approval_group` is not on the Page?</span>

        <span class="hljs-comment"># here's a simple check</span>
        <span class="hljs-keyword">if</span> hasattr(page.specific, <span class="hljs-string">"get_approval_group_key"</span>):
            approval_group = page.specific.get_approval_group_key()
        <span class="hljs-keyword">else</span>:
            <span class="hljs-comment"># arbitrarily assign to group A </span>
            <span class="hljs-comment"># (you could instead do something else)</span>
            approval_group = <span class="hljs-string">"A"</span>

        <span class="hljs-keyword">if</span> approval_group == <span class="hljs-string">"A"</span>:
            <span class="hljs-keyword">return</span> self.groups_a

        <span class="hljs-keyword">return</span> self.groups_b

    <span class="hljs-comment"># each of the following methods will need to be implemented, all checking for the correct groups for the Page when called</span>
    <span class="hljs-comment"># def start(self, ...etc)</span>
    <span class="hljs-comment"># def user_can_access_editor(self, ...etc)</span>
    <span class="hljs-comment"># def page_locked_for_user(self, ...etc)</span>
    <span class="hljs-comment"># def user_can_lock(self, ...etc)</span>
    <span class="hljs-comment"># def user_can_unlock(self, ...etc)</span>
    <span class="hljs-comment"># def get_task_states_user_can_moderate(self, ...etc)</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">start</span>(<span class="hljs-params">self, workflow_state, user=None</span>):</span>
        <span class="hljs-comment"># essentially a copy of this method on `GroupApprovalTask` but with the ability to have a dynamic 'group' returned.</span>
        approval_groups = self.get_approval_groups(workflow_state.page)

        <span class="hljs-keyword">if</span> workflow_state.page.locked_by:
            <span class="hljs-comment"># If the person who locked the page isn't in one of the groups, unlock the page</span>
            <span class="hljs-comment"># if not workflow_state.page.locked_by.groups.filter(id__in=self.groups.all()).exists():</span>
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> approval_groups.filter(id__in=self.groups.all()).exists():
                workflow_state.page.locked = <span class="hljs-literal">False</span>
                workflow_state.page.locked_by = <span class="hljs-literal">None</span>
                workflow_state.page.locked_at = <span class="hljs-literal">None</span>
                workflow_state.page.save(
                    update_fields=[<span class="hljs-string">"locked"</span>, <span class="hljs-string">"locked_by"</span>, <span class="hljs-string">"locked_at"</span>]
                )

        <span class="hljs-keyword">return</span> super().start(workflow_state, user=user)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_can_access_editor</span>(<span class="hljs-params">self, page, user</span>):</span>
        <span class="hljs-comment"># essentially a copy of this method on `GroupApprovalTask` but with the ability to have a dynamic 'group' returned.</span>
        approval_groups = self.get_approval_groups(page)

        <span class="hljs-comment"># return self.groups.filter(id__in=user.groups.all()).exists() or user.is_superuser</span>
        <span class="hljs-keyword">return</span> (
            approval_groups.filter(id__in=user.groups.all()).exists()
            <span class="hljs-keyword">or</span> user.is_superuser
        )

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">page_locked_for_user</span>(<span class="hljs-params">self, page, user</span>):</span>
        <span class="hljs-comment"># essentially a copy of this method on `GroupApprovalTask` but with the ability to have a dynamic 'group' returned.</span>
        approval_groups = self.get_approval_groups(page)

        <span class="hljs-comment"># return not (self.groups.filter(id__in=user.groups.all()).exists() or user.is_superuser)</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">not</span> (
            approval_groups.filter(id__in=user.groups.all()).exists()
            <span class="hljs-keyword">or</span> user.is_superuser
        )

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_can_lock</span>(<span class="hljs-params">self, page, user</span>):</span>
        <span class="hljs-comment"># essentially a copy of this method on `GroupApprovalTask` but with the ability to have a dynamic 'group' returned.</span>
        approval_groups = self.get_approval_groups(page)

        <span class="hljs-comment"># return self.groups.filter(id__in=user.groups.all()).exists()</span>
        <span class="hljs-keyword">return</span> approval_groups.filter(id__in=user.groups.all()).exists()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_can_unlock</span>(<span class="hljs-params">self, page, user</span>):</span>
        <span class="hljs-comment"># essentially a copy of this method on `GroupApprovalTask` but with the ability to have a dynamic 'group' returned.</span>
        <span class="hljs-comment"># approval_groups = self.get_approval_groups(page)</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_actions</span>(<span class="hljs-params">self, page, user</span>):</span>
        <span class="hljs-comment"># essentially a copy of this method on `GroupApprovalTask` but with the ability to have a dynamic 'group' returned.</span>
        approval_groups = self.get_approval_groups(page)

        <span class="hljs-keyword">if</span> (
            approval_groups.filter(id__in=user.groups.all()).exists()
            <span class="hljs-keyword">or</span> user.is_superuser
        ):
            <span class="hljs-keyword">return</span> [
                (<span class="hljs-string">"reject"</span>, <span class="hljs-string">"Request changes"</span>, <span class="hljs-literal">True</span>),
                (<span class="hljs-string">"approve"</span>, <span class="hljs-string">"Approve"</span>, <span class="hljs-literal">False</span>),
                (<span class="hljs-string">"approve"</span>, <span class="hljs-string">"Approve with comment"</span>, <span class="hljs-literal">True</span>),
            ]

        <span class="hljs-keyword">return</span> super().get_actions(page, user)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_task_states_user_can_moderate</span>(<span class="hljs-params">self, user, **kwargs</span>):</span>

        <span class="hljs-comment"># not a very DRY approach, but it works!</span>

        <span class="hljs-keyword">if</span> user.is_superuser:
            <span class="hljs-keyword">return</span> TaskState.objects.filter(
                status=TaskState.STATUS_IN_PROGRESS, task=self.task_ptr
            )
        <span class="hljs-keyword">elif</span> self.groups_a.filter(id__in=user.groups.all()).exists():
            <span class="hljs-keyword">return</span> TaskState.objects.filter(
                status=TaskState.STATUS_IN_PROGRESS,
                task=self.task_ptr,
                workflow_state__in=WorkflowState.objects.filter(
                    page__in=DailyReflectionPage.objects.filter(
                        reflection_date__month__lte=<span class="hljs-number">6</span>
                    )
                ),
            )
        <span class="hljs-keyword">elif</span> self.groups_b.filter(id__in=user.groups.all()).exists():
            <span class="hljs-keyword">return</span> TaskState.objects.filter(
                status=TaskState.STATUS_IN_PROGRESS,
                task=self.task_ptr,
                workflow_state__in=WorkflowState.objects.filter(
                    page__in=DailyReflectionPage.objects.filter(reflection_date__month__gt=<span class="hljs-number">6</span>)
                ),
            )
        <span class="hljs-keyword">else</span>:
            <span class="hljs-keyword">return</span> TaskState.objects.none()

<span class="hljs-meta">    @classmethod</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_description</span>(<span class="hljs-params">cls</span>):</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Groups are assigned to approve this task based on reflection month"</span>

    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meta</span>:</span>
        verbose_name = <span class="hljs-string">"reflection-month-dependent approval task"</span>
        verbose_name_plural = <span class="hljs-string">"reflection-month-dependent approval tasks"</span>
</code></pre>
<h4 id="3-email-notifications">3. Email Notifications</h4>
<p>By default, email notifications are sent upon workflow submission, approval and rejection, and upon submission to the built-in <code>GroupApprovalTask</code>. We will replicate this behaviour in our <code>SplitGroupApprovalTask</code>. This is a three-step process:</p>
<ol>
<li>Create a <code>mail.py</code> file within our <code>reflections</code> app and add the following classes:<ul>
<li>A base <a target="_blank" href="https://github.com/wagtail/wagtail/blob/da1e4d9a99e87a020c778ad2f3142b89c2e8a732/wagtail/admin/mail.py#L141"><code>Notifier</code></a> to send updates for <code>SplitGroupApprovalTask</code> events</li>
<li>A notifier to send updates for <code>SplitGroupApprovalTask</code> submission events</li>
</ul>
</li>
<li>Create a <code>signal_handlers.py</code>file within our <code>reflections</code> app. In here, we instantiate the notifier, and connect it to the <a target="_blank" href="https://github.com/wagtail/wagtail/blob/da1e4d9a99e87a020c778ad2f3142b89c2e8a732/wagtail/core/signals.py#L43"><code>task_submitted</code></a> signal via the <code>register_signal_handlers()</code> method.</li>
<li>Run <code>register_signal_handlers()</code> upon loading our <code>reflections</code> app</li>
</ol>
<p>Here's the code:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Step 1: reflections/mail.py</span>

<span class="hljs-keyword">from</span> django.conf <span class="hljs-keyword">import</span> settings
<span class="hljs-keyword">from</span> django.contrib.auth <span class="hljs-keyword">import</span> get_user_model
<span class="hljs-keyword">from</span> wagtail.admin.mail <span class="hljs-keyword">import</span> EmailNotificationMixin, Notifier
<span class="hljs-keyword">from</span> wagtail.core.models <span class="hljs-keyword">import</span> TaskState

<span class="hljs-keyword">from</span> mysite.reflections.models <span class="hljs-keyword">import</span> SplitGroupApprovalTask


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseSplitGroupApprovalTaskStateEmailNotifier</span>(<span class="hljs-params">EmailNotificationMixin, Notifier</span>):</span>
    <span class="hljs-string">"""A base notifier to send updates for SplitGroupApprovalTask events"""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Allow TaskState to send notifications</span>
        super().__init__((TaskState))

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">can_handle</span>(<span class="hljs-params">self, instance, **kwargs</span>):</span>
        <span class="hljs-keyword">if</span> super().can_handle(instance, **kwargs) <span class="hljs-keyword">and</span> isinstance(
            instance.task.specific, SplitGroupApprovalTask
        ):
            <span class="hljs-comment"># Don't send notifications if a Task has been cancelled and then resumed - ie page was updated to a new revision</span>
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">not</span> TaskState.objects.filter(
                workflow_state=instance.workflow_state,
                task=instance.task,
                status=TaskState.STATUS_CANCELLED,
            ).exists()
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_context</span>(<span class="hljs-params">self, task_state, **kwargs</span>):</span>
        context = super().get_context(task_state, **kwargs)
        context[<span class="hljs-string">"page"</span>] = task_state.workflow_state.page
        context[<span class="hljs-string">"task"</span>] = task_state.task.specific
        <span class="hljs-keyword">return</span> context

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_recipient_users</span>(<span class="hljs-params">self, task_state, **kwargs</span>):</span>

        triggering_user = kwargs.get(<span class="hljs-string">"user"</span>, <span class="hljs-literal">None</span>)

        approval_groups = task_state.task.specific.get_approval_groups(
            task_state.workflow_state.page
        )

        group_members = get_user_model().objects.filter(
            groups__in=approval_groups.all()
        )

        recipients = group_members

        include_superusers = getattr(
            settings, <span class="hljs-string">"WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS"</span>, <span class="hljs-literal">True</span>
        )
        <span class="hljs-keyword">if</span> include_superusers:
            superusers = get_user_model().objects.filter(is_superuser=<span class="hljs-literal">True</span>)
            recipients = recipients | superusers

        <span class="hljs-keyword">if</span> triggering_user:
            recipients = recipients.exclude(pk=triggering_user.pk)

        <span class="hljs-keyword">return</span> recipients


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SplitGroupApprovalTaskStateSubmissionEmailNotifier</span>(<span class="hljs-params">
    BaseSplitGroupApprovalTaskStateEmailNotifier
</span>):</span>
    <span class="hljs-string">"""A notifier to send updates for SplitGroupApprovalTask submission events"""</span>

    notification = <span class="hljs-string">"submitted"</span>
</code></pre>
<pre><code class="lang-python"><span class="hljs-comment"># Step 2: reflections/signal_handlers.py</span>

<span class="hljs-keyword">from</span> wagtail.core.signals <span class="hljs-keyword">import</span> task_submitted

<span class="hljs-keyword">from</span> mysite.reflections.mail <span class="hljs-keyword">import</span> (
    SplitGroupApprovalTaskStateSubmissionEmailNotifier,
)

task_submission_email_notifier = SplitGroupApprovalTaskStateSubmissionEmailNotifier()


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">register_signal_handlers</span>():</span>
    task_submitted.connect(
        task_submission_email_notifier,
        dispatch_uid=<span class="hljs-string">"task_submitted_email_notification"</span>,
    )
</code></pre>
<pre><code class="lang-python"><span class="hljs-comment"># Step 3: reflections/apps.py</span>

<span class="hljs-keyword">from</span> django.apps <span class="hljs-keyword">import</span> AppConfig


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReflectionsConfig</span>(<span class="hljs-params">AppConfig</span>):</span>
    name = <span class="hljs-string">"mysite.reflections"</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ready</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">from</span> .signal_handlers <span class="hljs-keyword">import</span> register_signal_handlers

        register_signal_handlers()
</code></pre>
<p>You can check out the  <a target="_blank" href="https://docs.wagtail.io/en/stable/advanced_topics/custom_tasks.html#adding-notifications">Extending Wagtail --&gt; How to add new Task types --&gt; Adding notifications</a> section of the Wagtail Docs for more information.</p>
<blockquote>
<p>In my initial stages of trying to figure this out, the example source code in the docs was not up to date. I <a target="_blank" href="https://github.com/wagtail/wagtail/pull/7526">submitted a PR</a> to fix this, and it was merged.</p>
</blockquote>
<p>You might also wanna have a look at the <a target="_blank" href="https://docs.wagtail.io/en/stable/reference/settings.html#workflow">workflow settings reference</a> for further workflow customization.</p>
<p>Well, that's about it! You can run migrations, log into the Wagtail Admin, create some content, some workflows and tasks ... check out the video below to see this in action!</p>
<h3 id="demo">Demo</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/qx1LOqJkt9Y">https://youtu.be/qx1LOqJkt9Y</a></div>
<hr />
]]></content:encoded></item><item><title><![CDATA[My technical writing journey]]></title><description><![CDATA[An inclination towards writing
I see myself as one of those people who are better able to express themselves in writing rather than speaking. Growing up as an introvert, I discovered that I enjoyed expressing my thoughts and ideas on paper. In my Hig...]]></description><link>https://blog.victor.co.zm/my-technical-writing-journey</link><guid isPermaLink="true">https://blog.victor.co.zm/my-technical-writing-journey</guid><category><![CDATA[Technical writing ]]></category><category><![CDATA[writing]]></category><category><![CDATA[hashnodebootcamp]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Tue, 14 Sep 2021 22:48:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1631655676397/BUS55-P-o.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="an-inclination-towards-writing">An inclination towards writing</h3>
<p>I see myself as one of those people who are better able to express themselves in writing rather than speaking. Growing up as an introvert, I discovered that I enjoyed expressing my thoughts and ideas on paper. In my High School days, I would usually top my class in <a target="_blank" href="https://learn.org/articles/What_is_English_Composition.html">English composition</a>. I remember once getting a score of 18/20 in an essay, while most people scored in the 10 – 13 range!</p>
<p>My tech stack choices are sometimes heavily influenced by the quality of the documentation. I actually enjoy writing docs myself, and I tend to put some decent effort into the process. I think this is well reflected in some of my projects, for example, <a target="_blank" href="https://github.com/engineervix/readme-coverage-badger">readme-coverage-badger</a>, <a target="_blank" href="https://github.com/engineervix/cookiecutter-wagtail-vix">cookiecuter-wagtail-vix</a> and <a target="_blank" href="https://ubuntu-server-setup.readthedocs.io">ubuntu-server-setup</a>.</p>
<h3 id="why-write">Why write?</h3>
<p>When I got into software development, I realized that I learnt a lot from reading, and a huge part of that reading was from other developers' blogs! Sometimes, you read a tutorial, and as you follow along, you discover that you have to make so many modifications to get things working on your machine. Or perhaps you are trying to solve a specific dev problem which requires some bit of research – scouring the inter-webs for information and combining this and that to build a solution to your problem. What happens when you have to do the same thing again, say, two years from now? "Well, go back to your old code" – you would say! But we all know that future you may look at the code (s)he wrote in the past and have absolutely no clue what is going on!</p>
<iframe src="https://me.me/embed/i/12954474" width="500" height="613" class="meme-embed" style="max-width:100%;margin:0 auto"></iframe>

<p>This is why I thought that it would be a good idea to start a blog, so that I could have a place where I could document the things I learned, not only for the sake of future me, but also for others who might encounter the same challenges I faced.</p>
<h3 id="early-attempts-at-technical-blogging">Early attempts at technical blogging</h3>
<p>I setup my first blog sometime in 2012/2013, using <a target="_blank" href="https://wordpress.org/">Wordpress</a>. I wrote a few articles (less than 10!), but was very inconsistent, and I ended up shutting down the site.</p>
<p>After learning <a target="_blank" href="https://www.python.org/">Python</a>, I decided to build my own blog, and settled on <a target="_blank" href="https://blog.getpelican.com/">Pelican</a> – a powerful and popular python-based <a target="_blank" href="https://jamstack.org/generators/">static site generator</a>. This was around 2014/2015. Again, I wrote very few articles, mostly haphazardly and without a proper plan and schedule.</p>
<p>I started getting frustrated with my failure to write consistently and maintain a decent level of organization around my writing and learning. After a long period of inactivity on the blog, I embarked on a challenge to rebrand myself and rebuild my blog. I even wanted to write my own Pelican theme and make it open source. I got started developing a custom theme using <a target="_blank" href="https://getuikit.com/">UIkit</a>, but the project went into <a target="_blank" href="https://en.wikipedia.org/wiki/Development_hell">development hell</a> and I ended up abandoning the whole notion of building my own blog!</p>
<p>I was getting super busy at work and I didn't have sufficient time to devote to building my own blog. I learnt about platforms like <a target="_blank" href="https://medium.com">medium.com</a> and <a target="_blank" href="https://dev.to/">dev.to</a>, where I was quick to sign up, but never wrote a single article on either platform! Notwithstanding, I still had the desire to write and have my own space on the web where I could pen down not only my thoughts and ideas, but also share what I learnt in my dev journey.</p>
<h3 id="hello-hashnode">Hello Hashnode</h3>
<p>Well, fast-forward to the year 2020, and I discover <a target="_blank" href="https://hashnode.com/">Hashnode</a> through <a target="_blank" href="https://twitter.com/sikaili99">@sikaili99</a>! I was immediately blown away – custom domain, a fantastic and growing dev community, SEO, I get to own my content, GitHub integration ... <em>Wow! Such awesome!</em> I thought to myself, "this is it, it's time to get serious!" </p>
<iframe src="https://me.me/embed/i/03c39432edbb4d7f947a5f7341a620df" width="500" height="564" class="meme-embed" style="max-width:100%;margin:0 auto"></iframe>

<p>Towards the end of 2020, I sat down and did some bit of planning to decide how I was gonna proceed, seeing that my previous writing efforts were mostly unsuccessful. Here are two important things I did:</p>
<ul>
<li>I thought carefully about the <strong>content</strong> &amp; <strong>focus</strong> of my technical blog. Because I love Python, I decided that I would <em>largely</em> focus on Python and related technologies. This is why I chose <a target="_blank" href="http://importthis.tech/">importthis.tech</a> as my domain!</li>
<li>I resolved to start by writing at least 12 articles in 2021 – that's one article per month.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1631658043053/LdnxBOy1L.png" alt="import_this.png" /></p>
<p>This is my 8<sup>th</sup> article, and I wrote it in the 9<sup>th</sup> month of 2021! Not too bad, right?</p>
<h3 id="motivation">Motivation</h3>
<p>I have received some positive feedback on a few of my articles, and this has inspired me to keep writing. Getting to sit down to write an article can be a challenge, but the feeling of satisfaction that I get when I hit that <strong>Publish</strong> button is exhilarating, to say the least! It's like running or any other physical exercise – getting up to go for a run or hit the gym isn't easy, but we all feel good when we're back from exercise, and are often glad that we went!</p>
<p>Being a self-taught developer seeking to switch careers (from civil engineering to software engineering), I see technical writing as a way to demonstrate my abilities and document my growth in the craft. I see this as one of the means to showcase my work to a potential employer!</p>
<p>Further, the prospect of actually earning income from technical writing is another motivation factor. I have read a lot of stories of developers like myself who earn passive income from their writing.</p>
<h3 id="shout-outs">Shout-outs</h3>
<p>This is my first year of maintaining a decent level of consistency in my technical writing, and I am learning a lot of things along the way. All of this would not have been possible without Hashnode. You guys are the best, and I'm really grateful for the many opportunities that you've brought my way, including the <a target="_blank" href="https://hashnode.com/bootcamp">Technical Writing Bootcamp</a>, which I attended for the first time on September 13<sup>th</sup>, 2021. I was really encouraged not only by <a target="_blank" href="https://hashnode.com/@didicodes">@didicodes</a> and <a target="_blank" href="https://hashnode.com/@tanoaksam">@tanoaksam</a>, but also by my fellow participants, most notably <a target="_blank" href="https://hashnode.com/@winnekes">@winnekes</a> – who's <a target="_blank" href="https://blog.winnekes.com/from-scrubs-to-slacks">story</a> is very inspiring!</p>
<h3 id="where-do-we-go-from-here">Where do we go from here?</h3>
<p>As the year comes to a close, I'll need to sit down again to reflect on how I fared this year, address some of my weaknesses and work on building my personal brand. In the meantime, the writing continues ... 🚀</p>
<hr />
<blockquote>
<p>Cover image by <a target="_blank" href="https://unsplash.com/@cathrynlavery?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Cathryn Lavery</a> on <a target="_blank" href="https://unsplash.com/s/photos/technical-writing?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[an opinionated list of essential Node.js global packages]]></title><description><![CDATA[When Javascript came on the scene in 1995, it was meant to be used as a scripting language for Web pages. However, we all know that it's use has gone way past that, and even if you are not a web developer, there are very high chances that you use too...]]></description><link>https://blog.victor.co.zm/an-opinionated-list-of-essential-nodejs-global-packages</link><guid isPermaLink="true">https://blog.victor.co.zm/an-opinionated-list-of-essential-nodejs-global-packages</guid><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[cli]]></category><category><![CDATA[tools]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Fri, 27 Aug 2021 23:39:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1630107264860/cFy6NM6kb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When Javascript came on the scene in 1995, it was meant to be used as a scripting language for Web pages. However, we all know that it's use has gone way past that, and even if you are not a web developer, there are very high chances that you use tools built on top of Javascript, particularly <a target="_blank" href="https://nodejs.org/">Node.js</a> based tools in your dev workflow.</p>
<p>In this post, I present a list of Node.js tools that I generally depend on, and always ensure that they are <a target="_blank" href="https://docs.npmjs.com/downloading-and-installing-packages-globally">installed globally</a> on my development machine. So, without further ado, here we go ...</p>
<h3 id="web-development">Web development 🌐</h3>
<ul>
<li><a target="_blank" href="https://cli.vuejs.org/"><code>@vue/cli</code></a> – Standard Tooling for <a target="_blank" href="https://vuejs.org/">Vue.js</a> Development</li>
<li><a target="_blank" href="https://www.npmjs.com/package/@vue/cli-init"><code>@vue/cli-init</code></a> – <code>vue init</code> command addon for <code>@vue/cli</code>. This is an alias to the old <code>vue-cli@2.x</code>.</li>
<li><a target="_blank" href="https://browsersync.io/"><code>browser-sync</code></a> – Keep multiple browsers &amp; devices in sync when building websites.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/caniuse-cmd"><code>caniuse-cmd</code></a> – a <a target="_blank" href="https://caniuse.com/">caniuse.com</a> command line tool</li>
<li><a target="_blank" href="https://github.com/open-cli-tools/concurrently"><code>concurrently</code></a> – Run multiple commands concurrently. I typically use this to simultaneously run the <a target="_blank" href="https://docs.djangoproject.com/en/3.2/ref/django-admin/#runserver">Django dev server</a>, <a target="_blank" href="https://gulpjs.com/">gulp</a> and <a target="_blank" href="https://github.com/maildev/maildev">maildev</a>.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/clean-css-cli"><code>clean-css-cli</code></a> – a command-line interface to <a target="_blank" href="https://github.com/clean-css/clean-css">clean-css</a> - fast and efficient CSS optimizer for Node.js.</li>
<li><a target="_blank" href="https://expressjs.com/en/starter/generator.html"><code>express-generator</code></a> – <a target="_blank" href="https://expressjs.com/">Express</a> application generator tool,</li>
<li><a target="_blank" href="https://www.npmjs.com/package/firebase-tools"><code>firebase-tools</code></a> – Firebase CLI Tools for testing, managing, and deploying your <a target="_blank" href="https://firebase.google.com">Firebase</a> project from the command line</li>
<li><a target="_blank" href="https://gruntjs.com/"><code>grunt-cli</code></a> – JavaScript Task Runner. I have a couple of old projects that use Grunt, but I have since transitioned to Gulp, which seems to be more actively maintained and has wider plugin support.</li>
<li><a target="_blank" href="https://gulpjs.com/"><code>gulp-cli</code></a> – A toolkit to automate &amp; enhance your workflow.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/html-minifier"><code>html-minifier</code></a> – a highly configurable, well-tested, JavaScript-based HTML minifier.</li>
<li><a target="_blank" href="https://lerna.js.org/"><code>lerna</code></a> – A tool for managing JavaScript projects with multiple packages.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/lite-server"><code>lite-server</code></a> – Lightweight <em>development only</em> node server that serves a web app, opens it in the browser, refreshes when html or Javascript change, injects CSS changes using sockets, and has a fallback page when a route is not found</li>
<li><a target="_blank" href="https://www.npmjs.com/package/local-cors-proxy"><code>local-cors-proxy</code></a> – Simple proxy to bypass CORS issues. This was built as a local dev only solution to enable prototyping against existing APIs without having to worry about CORS.</li>
<li><a target="_blank" href="https://github.com/maildev/maildev"><code>maildev</code></a> – SMTP Server + Web Interface for viewing and testing emails during development. </li>
<li><a target="_blank" href="https://www.npmjs.com/package/nodemon"><code>nodemon</code></a> – a tool that helps develop Node.js based applications by automatically restarting the node application when file changes in the directory are detected.</li>
<li><a target="_blank" href="https://parceljs.org/"><code>parcel-bundler</code></a> – Blazing fast, zero configuration web application bundler</li>
<li><a target="_blank" href="https://www.npmjs.com/package/pm2"><code>pm2</code></a> – a production process manager for Node.js applications with a built-in load balancer. It allows you to keep applications alive forever, to reload them without downtime and to facilitate common system admin tasks.</li>
<li><a target="_blank" href="https://prettier.io/"><code>prettier</code></a> – an opinionated code formatter</li>
<li><a target="_blank" href="https://sass-lang.com/"><code>sass</code></a> – CSS with superpowers</li>
<li><a target="_blank" href="https://www.npmjs.com/package/serve"><code>serve</code></a> – Static file serving and directory listing </li>
<li><a target="_blank" href="https://www.typescriptlang.org/"><code>typescript</code></a> – JavaScript with syntax for types.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/uglify-js"><code>uglify-js</code></a> – a JavaScript parser, minifier, compressor and beautifier toolkit.</li>
</ul>
<h3 id="mobile-app-development">Mobile app development 📱</h3>
<ul>
<li><a target="_blank" href="https://ionicframework.com/docs/cli"><code>@ionic/cli</code></a> – command-line interface for developing <a target="_blank">Ionic</a> apps.</li>
<li><a target="_blank" href="https://quasar.dev/quasar-cli/installation"><code>@quasar/cli</code></a> – I've placed Quasar under the <strong>mobile app development</strong> category because that's what I've used it for. However, it is a multi-purpose tool. In fact, Quasar’s motto is: “<strong>write code once and simultaneously deploy</strong> it as a website, a Mobile App and/or an Electron App”.</li>
<li><a target="_blank" href="https://quasar.dev/icongenie/introduction"><code>@quasar/icongenie</code></a> – outputs a set of <strong>SQUARE</strong> favicons, webicons, pwa-icons and electron-icons as well as iOS, Windows Store and MacOS icons from an original 1240x1240 square icon that retains transparency and also <strong>minifies</strong> the assets. It will also create splash screens for Cordova/<a target="_blank" href="https://capacitorjs.com/">Capacitor</a> and even a minified svg.</li>
<li><a target="_blank" href="https://cordova.apache.org/"><code>cordova</code></a> – an open-source mobile development framework which allows you to use standard web technologies (HTML5, CSS3, and JavaScript) for cross-platform development.</li>
<li><a target="_blank" href="https://nativescript.org/"><code>nativescript</code></a> – access native APIs from JavaScript directly. The framework provides iOS and Android runtimes for rich mobile development.</li>
</ul>
<h3 id="git-repository-management">Git repository management 💻</h3>
<ul>
<li><a target="_blank" href="https://github.com/commitizen/cz-cli"><code>commitizen</code></a> – Simple commit conventions for internet citizens</li>
<li><a target="_blank" href="https://www.npmjs.com/package/conventional-changelog-cli"><code>conventional-changelog-cli</code></a> – Generate a changelog from git metadata</li>
<li><a target="_blank" href="https://www.npmjs.com/package/dependabot-config-generator"><code>dependabot-config-generator</code></a> – CLI tool for <a target="_blank" href="https://dependabot.com/docs/config-file/">Dependabot config</a> generate</li>
<li><a target="_blank" href="https://www.npmjs.com/package/standard-version"><code>standard-version</code></a> – A utility for versioning using <a target="_blank" href="https://semver.org/">semver</a> and CHANGELOG generation powered by <a target="_blank" href="https://conventionalcommits.org/">Conventional Commits</a>.</li>
</ul>
<h3 id="screen-recording-terminal-capture">Screen recording / terminal capture 📹</h3>
<ul>
<li><a target="_blank" href="https://www.npmjs.com/package/asciicast2gif"><code>asciicast2gif</code></a> – a tool for generating GIF animations from <a target="_blank" href="https://github.com/asciinema/asciinema/blob/master/doc/asciicast-v1.md">asciicast</a> files recorded by <a target="_blank" href="https://github.com/asciinema/asciinema">asciinema</a>.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/gifify"><code>gifify</code></a> – Convert any video file to an optimized animated GIF. This tool is no longer maintained, so I've switched to <a target="_blank" href="https://github.com/ImageOptim/gifski"><code>gifski</code></a>, a <a target="_blank" href="https://www.rust-lang.org/install.html">Rust</a>-based GIF encoder based on <a target="_blank" href="https://pngquant.org/lib/"><code>libimagequant</code></a></li>
<li><a target="_blank" href="https://www.npmjs.com/package/svg-term-cli"><code>svg-term-cli</code></a> – Share terminal sessions as razor-sharp animated SVG everywhere.</li>
<li><a target="_blank" href="https://github.com/faressoft/terminalizer"><code>terminalizer</code></a> – Record your terminal and generate animated gif images or share a web player.</li>
</ul>
<h3 id="document-processing-conversion">Document processing / conversion 🗎</h3>
<ul>
<li><a target="_blank" href="https://www.npmjs.com/package/doctoc"><code>doctoc</code></a> – Generates table of contents for markdown files inside local git repository.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/mdpdf"><code>mdpdf</code></a> – A command line markdown to pdf converter with support for page headers, footers, and custom stylesheets. Mdpdf is incredibly configurable and has a JavaScript API for more extravogant usage.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/puppeteer-pdf"><code>puppeteer-pdf</code></a> – HTML to PDF from the command line with <a target="_blank" href="https://pptr.dev/">Puppeteer</a></li>
</ul>
<h3 id="cv-resume-generation">CV / Resume generation 📃</h3>
<ul>
<li><a target="_blank" href="https://github.com/fluentdesk/FluentCV"><code>fluentcv</code></a> – a dev-friendly, local-only Swiss Army knife for resumes and CVs. It is the corporate-friendly fork of <a target="_blank" href="https://github.com/hacksalot/hackmyresume">HackMyResume</a>.</li>
<li><a target="_blank" href="https://www.npmjs.com/package/hackmyresume"><code>hackmyresume</code></a> – Create polished résumés and CVs in multiple formats from your command line or shell. Author in clean Markdown and JSON, export to Word, HTML, PDF, LaTeX, plain text, and other arbitrary formats</li>
</ul>
<h3 id="image-compression">Image compression 🖻</h3>
<ul>
<li><a target="_blank" href="https://www.npmjs.com/package/imagemin-cli"><code>imagemin-cli</code></a> – Minify images seamlessly</li>
<li><a target="_blank" href="https://github.com/imagemin/imagemin-advpng"><code>imagemin-advpng</code></a> – <a target="_blank" href="https://www.advancemame.it/doc-advpng.html">AdvPNG</a> plugin for imagemin </li>
<li><a target="_blank" href="https://github.com/imagemin/imagemin-jpegtran"><code>imagemin-jpegtran</code></a> – <a target="_blank" href="https://jpegclub.org/jpegtran/">jpegtran</a> plugin for imagemin</li>
<li><a target="_blank" href="https://github.com/imagemin/imagemin-mozjpeg"><code>imagemin-mozjpeg</code></a> – Imagemin plugin for mozjpeg</li>
<li><a target="_blank" href="https://github.com/imagemin/imagemin-optipng"><code>imagemin-optipng</code></a> – <a target="_blank" href="http://optipng.sourceforge.net/">optipng</a> plugin for imagemin</li>
<li><a target="_blank" href="https://github.com/imagemin/imagemin-pngcrush"><code>imagemin-pngcrush</code></a> – <a target="_blank" href="https://pmt.sourceforge.io/pngcrush/">pngcrush</a> plugin for imagemin</li>
<li><a target="_blank" href="https://github.com/imagemin/imagemin-pngquant"><code>imagemin-pngquant</code></a> – Imagemin plugin for <code>pngquant</code></li>
<li><a target="_blank" href="https://www.npmjs.com/package/mozjpeg"><code>mozjpeg</code></a> – a production-quality JPEG encoder that improves compression while maintaining compatibility with the vast majority of deployed decoders</li>
<li><a target="_blank" href="https://github.com/svg/svgo"><code>svgo</code></a> – Node.js tool for optimizing SVG files. I often also use a similar Python tool called <a target="_blank" href="https://github.com/scour-project/scour">Scour</a>. I'd run both tools on one file and get the smaller resulting file!</li>
</ul>
<p>Well, there you have it! What Node.js tools do you regularly use that aren't on this list? Do you have alternatives to some of the tools I've listed? Well, let me know in the comments below 🙂.</p>
]]></content:encoded></item><item><title><![CDATA[On publishing my first python package to PyPI]]></title><description><![CDATA[How it all started
Many developers use services like coveralls.io or codecov.io for test coverage analysis and reporting. These services are free for open source projects, but require a monthly subscription for private repos. Many times, we work on p...]]></description><link>https://blog.victor.co.zm/on-publishing-my-first-python-package-to-pypi</link><guid isPermaLink="true">https://blog.victor.co.zm/on-publishing-my-first-python-package-to-pypi</guid><category><![CDATA[Python]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[python projects]]></category><category><![CDATA[Deploy ]]></category><category><![CDATA[deployment automation]]></category><dc:creator><![CDATA[Victor Miti]]></dc:creator><pubDate>Wed, 28 Jul 2021 23:48:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1627515294219/zZYac_rBw.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="how-it-all-started">How it all started</h3>
<p>Many developers use services like <a target="_blank" href="https://coveralls.io/">coveralls.io</a> or <a target="_blank" href="https://codecov.io">codecov.io</a> for test coverage analysis and reporting. These services are free for open source projects, but require a monthly subscription for private repos. Many times, we work on private repos, and we wanna be able to automatically have coverage badges in our READMEs. What if you are unable to pay such subscription fees, or maybe you don't want to use a <a target="_blank" href="https://en.wikipedia.org/wiki/Software_as_a_service">SaaS</a>? Your solution becomes to generate your own badge!</p>
<p>Now, there are so many excellent coverage badge generation tools out there, but I couldn't find one to suit my needs. All the existing python tools (for example, <a target="_blank" href="https://github.com/dbrgn/coverage-badge">coverage-badge</a>) I had come across ended at generating SVG/PNG files/strings/Base64 images. What you do with this remains entirely up to you. Having used <a target="_blank" href="https://github.com/olavoparno/istanbul-badges-readme">istanbul-badges-readme</a> on Javascript projects, I wanted a python alternative but couldn't find anything, so I decided to write one myself!</p>
<p>Initially, I wrote a python script for use on a specific project. This script would be placed in a <code>scripts</code> or <code>utils</code> folder in the project root, and, I would use npm scripts to make it easy to run the python script, like this</p>
<pre><code class="lang-sh">npm readme-cov
</code></pre>
<p>I thought to myself, "what happens when I'm working on another project? Am I going to have to be copying this script around from project to project? No way!" And so I decided to package the script and publish it to <a target="_blank" href="https://pypi.org/">PyPI</a> so that I (and others who might find it useful) can just <code>pip install</code> it and have it in your PATH within a couple seconds, ready for use.</p>
<h3 id="the-process">The process</h3>
<h4 id="packaging-101">Packaging 101</h4>
<p>At this point, I knew very little about python packaging, so I had to do a bit of digging to learn how it works and how I could package and distribute my project. To start with, I used <a target="_blank" href="https://github.com/audreyfeldroy/cookiecutter-pypackage"><code>audreyfeldroy/cookiecutter-pypackage</code></a> as the base for my package. It comes with a lot of batteries included, and makes it easy to get up and running with a python package.</p>
<p>I also had a look at the official <a target="_blank" href="https://packaging.python.org/">Python Packaging User Guide</a>, maintained by the <a target="_blank" href="https://pypa.io/">Python Packaging Authority (PyPA)</a>. Now, I've seen that many python projects have been using the <code>setuptools</code> way of packaging Python modules using a <code>setup()</code> function within a <code>setup.py</code> script. Even the cookiecutter template I was using had this setup. However, it is now <a target="_blank" href="https://setuptools.readthedocs.io/en/latest/build_meta.html#how-to-use-it">recommended</a> to use the <a target="_blank" href="https://www.python.org/dev/peps/pep-0517/">PEP 517</a> approach – a <code>pyproject.toml</code> file and a <code>setup.cfg</code> file. This is the approach I settled for.</p>
<p>At this point, I was already familiar with <code>pyproject.toml</code>, having already used in in my projects to define configurations for <a target="_blank" href="https://github.com/psf/black">black</a> and <a target="_blank" href="https://commitizen-tools.github.io/commitizen/">commitizen-tools</a>. However, using <code>setup.cfg</code> was new for me, but by reading the docs and seeing how other python projects used it, I was able to figure things out. You can have a look at the <a target="_blank" href="https://github.com/engineervix/readme-coverage-badger/blob/master/pyproject.toml"><code>project.toml</code></a> and <a target="_blank" href="https://github.com/engineervix/readme-coverage-badger/blob/master/setup.cfg"><code>setup.cfg</code></a> files on the project's GitHub page.</p>
<p>I used <a target="_blank" href="https://pypa-build.readthedocs.io/en/latest/">PyPA build</a> to build my package and generate a distribution:</p>
<pre><code class="lang-sh">python -m build
</code></pre>
<p>This created a <code>dist</code> directory in the project root, with two files:</p>
<pre><code class="lang-txt">❯ ls dist/
    readme_coverage_badger-0.1.0-py3-none-any.whl 
    readme-coverage-badger-0.1.0.tar.gz
</code></pre>
<p>I found the following resources very helpful:</p>
<ul>
<li><a target="_blank" href="https://setuptools.readthedocs.io/en/latest/build_meta.html">https://setuptools.readthedocs.io/en/latest/build_meta.html</a></li>
<li><a target="_blank" href="https://packaging.python.org/key_projects/">https://packaging.python.org/key_projects/</a></li>
<li><a target="_blank" href="https://github.com/pyscaffold/pyscaffold/blob/master/setup.cfg">https://github.com/pyscaffold/pyscaffold/blob/master/setup.cfg</a></li>
</ul>
<p>Some key takeaways:</p>
<ul>
<li>Previously, I would have multiple python configurations in my project root; one for Pytest, one for Flake8, one for Coverage and then of course <code>pyproject.toml</code> for black and commitizen-tools. From this experience, I realized that I could ditch the former three config files and have the configurations defined in <code>setup.cfg</code>! This is great, now I can reduce the number of config files in my project!</li>
<li>I discovered that there are also other third-party tools one could use to package your python projects. One of the prominent ones is <a target="_blank" href="https://flit.readthedocs.io/en/latest/index.html">flit</a>, which is used on the <a target="_blank" href="https://github.com/tiangolo/fastapi">FastAPI</a> project. <a target="_blank" href="https://python-poetry.org/">Poetry</a> uses <code>project.toml</code> and has building and packaging capabilities.</li>
</ul>
<h4 id="supporting-multiple-python-versions">Supporting multiple Python versions</h4>
<p>Up to this point, most of my CI/CD workflows focused on one Python version. If I'm building something that I know will be used on a Python 3.8 server, why should I bother testing it on other Python versions? Well, if you're building something that will be used by not only yourself but also others and in different environments, you have to at least ensure that it actually works in different environments. Most Python projects these days seem to support at least Python 3.6 going up, so I also adopted this approach. For the first time, I learnt how to configure and use <a target="_blank" href="https://tox.readthedocs.io/en/latest/"><code>tox</code></a> in my project.</p>
<p>I already had <a target="_blank" href="https://github.com/pyenv/pyenv"><code>pyenv</code></a> installed on my machine, with one or two python versions. I made sure I had Python 3.6 to 3.9 installed, and created a <code>.python-version</code> file in my project root with the details of the python versions I installed:</p>
<pre><code class="lang-txt">3.6.10
3.7.9
3.8.6
3.9.0
</code></pre>
<p>It was such a magical experience to see <code>tox</code> do its thing! Of course things didn't work right the first time, for instance, it took a couple of failures and Google searches to learn that I had to create the above <code>.python-version</code> file!</p>
<p>The cookiecutter template comes with a <code>travis.yml</code> config which uses <a target="_blank" href="https://github.com/tox-dev/tox-travis">tox-travis</a> for seamless integration of tox into <a target="_blank" href="https://travis-ci.com/">Travis CI</a>. Even though different Python versions are covered, all of these are on a Linux machine. I wanted to setup a test suite that also runs on Windows and Mac OS. However, my initial attempts were unsuccessful and I temporarily gave up on this, incurring technical debt in the process (it's one of my <a target="_blank" href="https://github.com/engineervix/readme-coverage-badger/tree/69b65ccae0d2767c8ddec04019a5d25b46321280#-todo">TODO items on the project</a>)!</p>
<p>Key takeaway:</p>
<ul>
<li>I learnt how to use <code>tox</code>, a very powerful tool which, among other things, helps in checking that your package installs correctly with different Python versions and interpreters.</li>
</ul>
<h4 id="uploading-to-testpypi-then-pypi">Uploading to TestPyPI, then PyPI</h4>
<p>Before officially releasing your package to PyPI, it is recommended that you first test it on <a target="_blank" href="https://test.pypi.org/">TestPyPI</a>. After creating an account, I was able to upload my package to TestPyPI using <a target="_blank" href="https://packaging.python.org/key_projects/#twine">twine</a>:</p>
<pre><code class="lang-sh">twine upload -r testpypi dist/*
</code></pre>
<p>Running the above command uploads everything in your <code>dist</code> directory to TestPyPI. You'll be prompted for your username and password. In order to avoid the username/password prompts, you can define your package indexes configuration in a <a target="_blank" href="https://packaging.python.org/specifications/pypirc/#the-pypirc-file"><code>.pypirc</code></a> file in your home directory.</p>
<p>I was able to see my project immediately on <a target="_blank" href="https://test.pypi.org">https://test.pypi.org</a>, and even install as follows:</p>
<pre><code class="lang-sh">pip install -i https://test.pypi.org/simple/ readme-coverage-badger
</code></pre>
<p>In addition, I was able to</p>
<ul>
<li>see how the package's README is rendered,</li>
<li>see whether I had correctly set the <a target="_blank" href="https://pypi.org/classifiers/">trove classifiers</a> for the package.</li>
</ul>
<p>As of June 2021, markdown checkboxes are not rendered at all on PyPI. They instead show up as a bullet followed by <code>&lt;input type="checkbox" disabled="" /&gt;</code>, which is so annoying. Perhaps this could be one of the reasons why a number of packages maintain the use of restructuredtext for all forms of documentation. Even the <code>README</code>, <code>CONTRIBUTING</code>, <code>AUTHORS</code> and <code>HISTORY</code> files generated by <a target="_blank" href="https://github.com/audreyfeldroy/cookiecutter-pypackage"><code>audreyfeldroy/cookiecutter-pypackage</code></a> are in restructuredtext format. Well, as of July 2021, I am more comfortable writing in markdown (Isn't this the same reason <a target="_blank" href="https://www.mkdocs.org/">mkdocs</a> was born, and why <a target="_blank" href="https://squidfunk.github.io/mkdocs-material/">Material for MkDocs</a> has gained popularity?). So, for now, it's <code>README.md</code> for me, and not <code>README.rst</code>!</p>
<p>After making corrections and rebuilding the package, I was ready to publish to the actual PyPI. So I went ahead and created an account at <a target="_blank" href="https://pypi.org/">PyPI</a>, then I uploaded my package:</p>
<pre><code class="lang-sh">twine upload dist/*
</code></pre>
<p><strong>Side note</strong>:</p>
<p>The silly thing is that, while I spent time checking "secondary" things like text rendering, spellings, trove classifiers, etc., I didn't actually <strong>run</strong> my application after installing it from TestPyPI! I was comfortable with the fact that I had executed the application before building it, that I had written tests which passed and that I was able to install the app without encountering errors! But I never ran the application after building it! I only decided to do this after I had already pushed <a target="_blank" href="https://github.com/engineervix/readme-coverage-badger/releases/tag/v0.1.0">version <code>0.1.0</code></a> to PyPI, and to my shame, the app was broken:</p>
<pre><code class="lang-txt">ERROR: 06-Jul-21 22:02:03 There's no README.md or README at this location
INFO: 06-Jul-21 22:02:03 Run this in a directory containing either a README.md or README file
</code></pre>
<p>Wait a minute? What do you mean "there's no README.md ..."? I double-checked, and there was a README.md in the current directory. I went back to the code and discovered that the problem was in the way I defined the <em>README location</em>. I initially defined it using relative paths, and if you run the code from the project root, it works! However, this changes when you build the application, due to the notion of <a target="_blank" href="https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html">entry points</a>. So, instead of using relative paths to define the README location, I used <code>os.getcwd()</code>, so that the script checks for the README file in the <em>current directory</em>, rather than checking for it in the <em>script's directory's parent directory</em>:</p>
<p>Buggy code:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">readme_location</span>(<span class="hljs-params">filename: Union[str, str] = <span class="hljs-string">"README.md"</span></span>) -&gt; Path:</span>
    <span class="hljs-string">"""Path to the README file"""</span>
    current_dir = Path(__file__).resolve().parent
    parent_dir = current_dir.parents[<span class="hljs-number">0</span>]
    readme_file = parent_dir / filename

    <span class="hljs-keyword">return</span> readme_file
</code></pre>
<p>Fixed code:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">readme_location</span>(<span class="hljs-params">filename: Union[str, str] = <span class="hljs-string">"README.md"</span></span>) -&gt; str:</span>
    <span class="hljs-string">"""Path to the README file"""</span>
    current_dir = os.getcwd()
    readme_file = os.path.join(current_dir, filename)

    <span class="hljs-keyword">return</span> readme_file
</code></pre>
<p>Isn't the fixed code simpler and easier to read than the buggy one? Clearly, <strong>simple is better than complex</strong>!</p>
<p>After the bugfix and comprehensive testing to make sure that it actually worked, I released <a target="_blank" href="https://github.com/engineervix/readme-coverage-badger/releases/tag/v0.1.1">version <code>0.1.1</code></a>, which is the current version at the time of writing this post.</p>
<h4 id="automating-pypi-deployment">Automating PyPI deployment</h4>
<p>The Travis CI config that comes with the cookiecutter allows for automatic release to PyPI when you push a new tag to master. At this point, I was not very familiar with Travis CI, so I had to learn how to use it and configure it. The relevant section of the <code>.travis.yml</code> config is this one:</p>
<pre><code class="lang-yml"><span class="hljs-attr">deploy:</span>
  <span class="hljs-attr">provider:</span> <span class="hljs-string">pypi</span>
  <span class="hljs-attr">distributions:</span> <span class="hljs-string">sdist</span> <span class="hljs-string">bdist_wheel</span>
  <span class="hljs-attr">user:</span> {{ <span class="hljs-string">cookiecutter.pypi_username</span> }}
  <span class="hljs-attr">password:</span>
    <span class="hljs-attr">secure:</span> <span class="hljs-string">PLEASE_REPLACE_ME</span>
  <span class="hljs-attr">on:</span>
    <span class="hljs-attr">tags:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">repo:</span> {{ <span class="hljs-string">cookiecutter.github_username</span> }}<span class="hljs-string">/{{</span> <span class="hljs-string">cookiecutter.project_slug</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">python:</span> <span class="hljs-number">3.8</span>
</code></pre>
<p><a target="_blank" href="https://pypi.org/help/#apitoken">According to PyPI</a>:</p>
<blockquote>
<p>API tokens provide an alternative way (instead of username and password) to authenticate when <strong>uploading packages</strong> to PyPI</p>
</blockquote>
<p>So I created an API token for the project, and set the username (<code>user</code> in the above config) to <code>__token__</code> as per PyPI instructions. However, instead of putting the actual token in my config, I had to <a target="_blank" href="https://docs.travis-ci.com/user/encryption-keys/">encrypt it</a>:</p>
<pre><code class="lang-sh"><span class="hljs-comment"># first, ensure that you have the travis Ruby gem installed on your computer </span>
<span class="hljs-comment"># see &lt;https://github.com/travis-ci/travis.rb#installation&gt; for detailed installation instructions</span>
gem install travis --no-document
<span class="hljs-comment"># then, you need to create a [personal access token](https://github.com/settings/tokens) on Github</span>
<span class="hljs-comment"># following the [Travis CI guidelines](https://docs.travis-ci.com/user/github-oauth-scopes/)</span>
<span class="hljs-comment"># you can now sign in to Travis CI</span>
travis login --pro --github-token YOUR_GITHUB_PERSONAL_ACCESS_TOKEN
<span class="hljs-comment"># now you can encrypt your PyPI API token</span>
<span class="hljs-comment"># the `--add deploy.password` option automatically updates the `travis.yml` with the encrypted string</span>
travis encrypt YOUR_PYPI_API_TOKEN --add deploy.password --com
</code></pre>
<p>The good thing is, you can easily modify this configuration to upload to TestPyPI instead. This is done by adding <code>server: https://test.pypi.org/legacy/</code> under the <code>deploy</code> section of the config.</p>
<p>I modified the config slightly by adding a <code>before_deploy</code> section in the Travis config, which run <code>python -m build</code>, just as I would if I was deploying manually from my computer</p>
<h4 id="automating-github-releases">Automating GitHub releases</h4>
<p>I decided to take things further and also automate the process of creating <a target="_blank" href="https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/about-releases">GitHub releases</a>. Now, there are many ways to do this, including the use of GitHub Actions. However, since I was already using Travis CI, I decided to adopt a Travis solution, since Travis CI already comes with built-in deployment support for <a target="_blank" href="https://docs.travis-ci.com/user/deployment/#supported-providers">several providers</a>.</p>
<p>The GitHub releases are tied to the PyPI deployment process. So, immediately after deploying to PyPI, I wanted to create a GitHub release. Travis makes it easy to <a target="_blank" href="https://docs.travis-ci.com/user/deployment/#deploying-to-multiple-providers">upload to multiple providers</a>. There's even a dedicated page in the Travis docs for each provider. It is highly recommended to use <code>travis setup releases</code> to automatically create and encrypts a GitHub OAuth token with the correct scopes. However, this somehow didn't work for me, (I also tried adding <code>--com</code> and <code>--com --force</code> but to no avail) so I went with the <a target="_blank" href="https://docs.travis-ci.com/user/deployment/releases/#authenticating-with-an-oauth-token">manual approach</a>.</p>
<p>In order for automatic GitHub releases to work,</p>
<ul>
<li>I automated CHANGELOG generation using <a target="_blank" href="https://commitizen-tools.github.io/commitizen/">commitizen-tools</a> and <a target="_blank" href="https://github.com/conventional-changelog/standard-version">standard-version</a>.</li>
<li>I created an <a target="_blank" href="https://importthis.tech/task-execution-and-automation-using-invoke"><code>invoke</code></a> task called <code>get-release-notes</code> to extract the latest content from the CHANGELOG, which is basically everything under the current tag. <code>get-release-notes</code> saves this content to a file named <code>LATEST_RELEASE_NOTES.md</code> in the home directory.</li>
<li>I added <code>invoke get-release-notes</code> to the <code>before_deploy</code> section of the Travis config.</li>
</ul>
<p>The complete <code>.travis.yml</code> file looks like this:</p>
<pre><code class="lang-yml"><span class="hljs-attr">language:</span> <span class="hljs-string">python</span>
<span class="hljs-attr">os:</span> <span class="hljs-string">linux</span>
<span class="hljs-attr">dist:</span> <span class="hljs-string">focal</span>
<span class="hljs-attr">python:</span>
<span class="hljs-bullet">-</span> <span class="hljs-number">3.9</span>
<span class="hljs-bullet">-</span> <span class="hljs-number">3.8</span>
<span class="hljs-bullet">-</span> <span class="hljs-number">3.7</span>
<span class="hljs-bullet">-</span> <span class="hljs-number">3.6</span>
<span class="hljs-attr">install:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">pip</span> <span class="hljs-string">install</span> <span class="hljs-string">-U</span> <span class="hljs-string">tox-travis</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">pip</span> <span class="hljs-string">install</span> <span class="hljs-string">codecov</span>
<span class="hljs-attr">script:</span> <span class="hljs-string">tox</span>
<span class="hljs-attr">after_success:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">codecov</span>
<span class="hljs-attr">before_deploy:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">pip</span> <span class="hljs-string">install</span> <span class="hljs-string">-r</span> <span class="hljs-string">requirements_dev.txt</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">invoke</span> <span class="hljs-string">dist</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">invoke</span> <span class="hljs-string">get-release-notes</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">TODAY="($(TZ=Africa/Lusaka</span> <span class="hljs-string">date</span> <span class="hljs-string">--iso))"</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">RELEASE_NAME="$TRAVIS_TAG</span> <span class="hljs-string">$TODAY"</span>
<span class="hljs-attr">deploy:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">provider:</span> <span class="hljs-string">pypi</span>
    <span class="hljs-attr">distributions:</span> <span class="hljs-string">sdist</span> <span class="hljs-string">bdist_wheel</span>
    <span class="hljs-attr">user:</span> <span class="hljs-string">__token__</span>
    <span class="hljs-attr">password:</span>
      <span class="hljs-attr">secure:</span> <span class="hljs-string">...</span>
    <span class="hljs-attr">on:</span>
      <span class="hljs-attr">tags:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">repo:</span> <span class="hljs-string">engineervix/readme-coverage-badger</span>
      <span class="hljs-attr">python:</span> <span class="hljs-number">3.8</span>
    <span class="hljs-attr">skip_existing:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">skip-cleanup:</span> <span class="hljs-literal">true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">provider:</span> <span class="hljs-string">releases</span>
    <span class="hljs-attr">api_key:</span>
      <span class="hljs-attr">secure:</span> <span class="hljs-string">"..."</span>
    <span class="hljs-attr">file_glob:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">file:</span> <span class="hljs-string">dist/*</span>
    <span class="hljs-attr">skip_cleanup:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">edge:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">$RELEASE_NAME</span>
    <span class="hljs-attr">release_notes_file:</span> <span class="hljs-string">"$HOME/LATEST_RELEASE_NOTES.md"</span>
    <span class="hljs-attr">on:</span>
      <span class="hljs-attr">tags:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">repo:</span> <span class="hljs-string">engineervix/readme-coverage-badger</span>
      <span class="hljs-attr">python:</span> <span class="hljs-number">3.8</span>
</code></pre>
<h3 id="what-next">What next?</h3>
<p>Well, this was quite an interesting experience, I certainly learnt a lot of new things, and I'm still learning and improving. Now that I've released my package and it's out there, I have to <strong>maintain</strong> it! One of the important maintenance tasks is to deal with security vulnerabilities and ensure that dependencies are up to date. I previously used <a target="_blank" href="https://github.com/dependabot">Dependabot</a> as the primary tool for this, but have since switched to <a target="_blank" href="https://github.com/renovatebot/renovate">renovate</a>, not only because of the <em>auto-merge</em> feature (which GitHub's native Dependabot <a target="_blank" href="https://github.com/dependabot/dependabot-core/issues/1973">doesn't have</a>, as of July 2021), but also because of the greater flexibility in terms of configuration.</p>
<p>I have given myself (and whoever wants to contribute!) <a target="_blank" href="https://github.com/engineervix/readme-coverage-badger/tree/69b65ccae0d2767c8ddec04019a5d25b46321280#-todo">additional challenges</a> to improve the package, and I hope I can work on these and continue improving it.</p>
<hr />
]]></content:encoded></item></channel></rss>