<?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[Acel's Blog]]></title><description><![CDATA[Solving problems with Django]]></description><link>https://blog.acel.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 10:13:53 GMT</lastBuildDate><atom:link href="https://blog.acel.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[LM Studio on Windows, WSL2 for Development: The Setup Guide I Wish I Had]]></title><description><![CDATA[I went to a Build with Gemma event expecting to spend the day experimenting with local models. Instead, I spent most of the day trying to make a Windows-installed LM Studio talk to code running inside]]></description><link>https://blog.acel.dev/lm-studio-on-windows-wsl2-for-development-the-setup-guide-i-wish-i-had</link><guid isPermaLink="true">https://blog.acel.dev/lm-studio-on-windows-wsl2-for-development-the-setup-guide-i-wish-i-had</guid><category><![CDATA[gemma]]></category><category><![CDATA[lmstudio]]></category><category><![CDATA[wsl2]]></category><category><![CDATA[Python]]></category><category><![CDATA[Windows]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Fri, 14 Aug 2026 07:20:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fecb0a8b2cf2458fe324d89/e641d820-611b-4bd1-a151-6e51745c7407.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I went to a <strong>Build with Gemma</strong> event expecting to spend the day experimenting with local models. Instead, I spent most of the day trying to make a Windows-installed LM Studio talk to code running inside WSL2.</p>
<p>If your development environment also lives in WSL2, this is the setup path I wish I had before the workshop.</p>
<h2>The Problem</h2>
<p>The workshop instructions were straightforward: install a local model runner, download Gemma, and write a small Python application that talks to the model. The <a href="https://github.com/geektutor/gemma-workshop/blob/main/gemma4_basic_codelab.md">codelab</a> was based on Ollama, which made the Windows-native path look especially simple.</p>
<p>The problem was that my development environment was not on Windows. I use WSL2 for my terminal, Python projects, virtual environments, and Git repositories. LM Studio, however, was installed on the Windows side because that is where the desktop application and model runtime live.</p>
<p>That gives you two environments on one computer:</p>
<pre><code class="language-text">Windows
└── LM Studio
    └── Gemma model and local HTTP API

WSL2
└── Python application, terminal, files, and virtual environment
</code></pre>
<p>The model does not need to be installed twice. The Python application in WSL2 only needs to reach LM Studio's HTTP API.</p>
<p>That sounds simple. It was not obvious from the workshop instructions, and several different problems looked like the same problem:</p>
<ul>
<li>The LM Studio chat window worked, but the API server was stopped.</li>
<li><code>localhost</code> inside WSL2 was not initially the same as <code>localhost</code> on Windows.</li>
<li>I entered Windows WSL configuration into a Linux shell.</li>
<li>The workshop used Ollama model names, while LM Studio returned different model identifiers.</li>
<li>I spent time investigating a shell plugin that was unrelated to the basic API connection.</li>
</ul>
<p>The key was separating the layers instead of treating "LM Studio works" as one single test.</p>
<h2>My Environment</h2>
<p>This was the setup I used:</p>
<ul>
<li>Windows 11, build <code>26200</code></li>
<li>WSL version <code>2.7.11.0</code></li>
<li>LM Studio <code>1.0.7 (build 2)</code></li>
<li>16 GB RAM</li>
<li>Intel UHD Graphics 620</li>
<li>WSL2 as the development environment</li>
<li>Gemma 4 E2B served by LM Studio</li>
</ul>
<p>Your versions may differ. LM Studio's interface changes, so treat the concepts and verification commands as more important than the exact screenshots.</p>
<h2>First Important Distinction: Chat Is Not the API</h2>
<p>Being able to ask questions in LM Studio's chat window does not mean that a program can reach the model.</p>
<p><img src="image1.png" alt="The LM Studio chat window working with Gemma does not prove that the API is running." /></p>
<p>The chat interface and the Local Model API server are separate. In my version of LM Studio, the relevant setting was here. LM Studio also documents this server as an <a href="https://lmstudio.ai/docs/developer/openai-compat">OpenAI-compatible API</a>:</p>
<pre><code class="language-text">Settings -&gt; Local Model API -&gt; Local API server
</code></pre>
<p>The switch must be running before a Python program or <code>curl</code> can connect.</p>
<p>You can check the server from Windows PowerShell:</p>
<pre><code class="language-powershell">lms server status --json --quiet
curl.exe http://127.0.0.1:1234/v1/models
</code></pre>
<p>The <code>curl.exe</code> spelling matters in PowerShell. <code>curl</code> may resolve to PowerShell's <code>Invoke-WebRequest</code> alias rather than the normal curl executable.</p>
<p>A working response looks like this:</p>
<pre><code class="language-json">{
  "data": [
    {
      "id": "google/gemma-4-e2b",
      "object": "model",
      "owned_by": "organization_owner"
    }
  ],
  "object": "list"
}
</code></pre>
<p>If this Windows-side request fails, do not troubleshoot WSL2 yet. The API server or its port is the problem.</p>
<h2>The Setup That Worked</h2>
<p>WSL2 commonly starts in NAT networking mode. In that mode, a service running on Windows is not always reachable from Linux through <code>127.0.0.1</code>. Microsoft's <a href="https://learn.microsoft.com/en-us/windows/wsl/networking">WSL networking documentation</a> describes the difference between the default NAT and mirrored modes.</p>
<p>Windows 11 supports mirrored networking, which lets WSL2 and Windows reach each other's localhost services. That was the simplest option for this setup because LM Studio could continue listening on localhost instead of being exposed to the rest of my network.</p>
<h3>Step 1: Configure mirrored networking</h3>
<p>Open the WSL configuration file from PowerShell, not from Bash:</p>
<pre><code class="language-powershell">notepad.exe "$env:USERPROFILE\.wslconfig"
</code></pre>
<p>If the file already exists, add the setting under its existing <code>[wsl2]</code> section. Otherwise, create the file with:</p>
<pre><code class="language-ini">[wsl2]
networkingMode=mirrored
</code></pre>
<p>Save the file, then restart WSL from PowerShell:</p>
<pre><code class="language-powershell">wsl --shutdown
</code></pre>
<p>This was one of my mistakes. I initially pasted the <code>[wsl2]</code> lines into the WSL terminal, where Bash tried to execute them as commands. <code>.wslconfig</code> is a Windows file stored at <code>%UserProfile%\.wslconfig</code>.</p>
<h3>Step 2: Start the LM Studio API</h3>
<p>Open LM Studio and enable the <strong>Local API server</strong> switch under <strong>Settings -&gt; Local Model API</strong>.</p>
<p>The default port is usually <code>1234</code>. If you use another port, replace <code>1234</code> in every command and code sample below.</p>
<p>Confirm the server from PowerShell before testing from WSL:</p>
<pre><code class="language-powershell">lms server status --json --quiet
curl.exe http://127.0.0.1:1234/v1/models
</code></pre>
<h3>Step 3: Test from WSL2</h3>
<p>Open a new WSL terminal and run:</p>
<pre><code class="language-bash">curl --connect-timeout 5 http://127.0.0.1:1234/v1/models
</code></pre>
<p>The response should contain the same model list as the PowerShell request.</p>
<p>At this point, the important test has passed: a program running inside WSL2 can reach a model server running on Windows.</p>
<h2>What About NAT Networking?</h2>
<p>Mirrored networking is not the only solution.</p>
<p>With the default NAT configuration, WSL2 can reach the Windows host through the gateway address shown by this command:</p>
<pre><code class="language-bash">LM_STUDIO_HOST="$(ip route show default | awk '$1 == "default" {print $3; exit}')"
echo "$LM_STUDIO_HOST"
</code></pre>
<p>LM Studio must then listen beyond Windows localhost:</p>
<pre><code class="language-powershell">lms server start --port 1234 --bind 0.0.0.0
</code></pre>
<p>From WSL2, the request becomes:</p>
<pre><code class="language-bash">curl "http://${LM_STUDIO_HOST}:1234/v1/models"
</code></pre>
<p>This approach has two drawbacks:</p>
<ol>
<li>The WSL2 gateway address can change after a restart.</li>
<li>Binding to <code>0.0.0.0</code> exposes the server beyond localhost, so authentication and firewall rules become important.</li>
</ol>
<p>NAT is still useful when you want stronger network separation or do not want to change the global WSL networking mode. For this single-computer setup, mirrored networking was simpler and allowed LM Studio to remain local-only.</p>
<h2>Adapting the Workshop Code</h2>
<p>The workshop example uses Ollama. Its Python client and model name assume an Ollama server:</p>
<pre><code class="language-python">import ollama

response = ollama.chat(
    model="gemma4:e2b",
    messages=[{"role": "user", "content": prompt}],
)
</code></pre>
<p>LM Studio provides an OpenAI-compatible API, so the same application can use the OpenAI Python client instead.</p>
<p>Create a small project with <code>uv</code>:</p>
<pre><code class="language-bash">mkdir gemma4-local-app
cd gemma4-local-app
uv init
uv add openai==2.53.0
</code></pre>
<p><code>uv</code> creates and manages the project environment and resolves the Python dependencies. There is no separate virtual-environment setup to maintain.</p>
<p>Create <code>app.py</code>:</p>
<pre><code class="language-python">import os

from openai import OpenAI


MODEL = os.getenv("LM_STUDIO_MODEL", "google/gemma-4-e2b")
BASE_URL = os.getenv("LM_STUDIO_BASE_URL", "http://127.0.0.1:1234/v1")
API_KEY = os.getenv("LM_STUDIO_API_KEY", "lm-studio")

client = OpenAI(
    base_url=BASE_URL,
    api_key=API_KEY,
)


def ask_model(prompt: str) -&gt; str:
    response = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "user", "content": prompt}],
    )
    return response.choices[0].message.content or ""


if __name__ == "__main__":
    print(ask_model("Give me three creative local AI app ideas."))
</code></pre>
<p>Run it from WSL2:</p>
<pre><code class="language-bash">uv run app.py
</code></pre>
<p>The model stays loaded in LM Studio on Windows. The Python process, source code, and <code>uv</code> environment remain in WSL2.</p>
<p>No API token is needed when LM Studio authentication is disabled. The <code>lm-studio</code> value in the example is only a non-empty placeholder required by the OpenAI client; I did not create or enter a token. If you enable authentication in LM Studio, replace it with a real token:</p>
<pre><code class="language-bash">export LM_STUDIO_API_KEY="your-token"
</code></pre>
<p>Do not commit that token to the repository.</p>
<h2>Use the Model Identifier LM Studio Returns</h2>
<p>The workshop's <code>gemma4:e2b</code> is an Ollama-style model tag. It is not necessarily the identifier that LM Studio expects.</p>
<p>Always inspect the API response:</p>
<pre><code class="language-bash">curl http://127.0.0.1:1234/v1/models
</code></pre>
<p>In my case, the correct identifier was:</p>
<pre><code class="language-text">google/gemma-4-e2b
</code></pre>
<p>Use that exact value in the OpenAI client. Do not guess the model name from the chat display or from another model runner's documentation.</p>
<h2>The Failures That Cost Me Time</h2>
<h3>The API server was stopped</h3>
<p>My LM Studio chat session was working, but the Local Model API screen showed <code>Stopped</code>. No WSL networking configuration can fix a server that is not listening.</p>
<p><img src="image3.png" alt="The Local Model API server was stopped even though the chat window worked." /></p>
<p>The first diagnostic should always be the Windows-side request:</p>
<pre><code class="language-powershell">curl.exe http://127.0.0.1:1234/v1/models
</code></pre>
<h3>I used the wrong localhost</h3>
<p>Under default WSL2 NAT networking, <code>127.0.0.1</code> inside WSL2 refers to the Linux environment. It does not automatically mean the Windows host.</p>
<p>Mirrored networking changes this relationship so WSL2 can reach Windows localhost services through <code>127.0.0.1</code>.</p>
<h3>I entered a Windows configuration file into Bash</h3>
<p>These lines are not Bash commands:</p>
<pre><code class="language-ini">[wsl2]
networkingMode=mirrored
</code></pre>
<p>They belong in <code>%UserProfile%\.wslconfig</code>, and the WSL subsystem must be restarted after saving them.</p>
<h3>I investigated the wrong plugin</h3>
<p>I also tried to install a shell-access plugin for LM Studio. That plugin is designed to let a model execute commands on the host or in WSL. It does not solve the basic problem of making a Python program reach the model API.</p>
<p>For workshop code, the API connection is the first milestone. Tool execution and agent plugins are a separate concern.</p>
<h2>A Note on Performance</h2>
<p>Connectivity and inference speed are different problems.</p>
<p>After the connection worked, Gemma 4 E2B generated at roughly 2.8 to 3.3 completion tokens per second on my laptop. A simple coding request took several minutes because the model spent many tokens on reasoning and the machine had no discrete GPU.</p>
<p>That does not indicate a WSL2 networking failure. If <code>/v1/models</code> responds quickly but generation is slow, the connection is working. The likely causes are model size, reasoning behavior, quantization, context length, and available hardware.</p>
<p>For a workshop, benchmark a small non-reasoning or coding-focused model before assuming that the API setup is broken.</p>
<h2>The Checklist I Wish I Had</h2>
<p>Before the workshop, I would have run these checks in order:</p>
<ol>
<li>Start the model in LM Studio.</li>
<li>Enable <strong>Settings -&gt; Local Model API -&gt; Local API server</strong>.</li>
<li>Confirm the API from PowerShell with <code>curl.exe</code>.</li>
<li>Configure mirrored networking in <code>%UserProfile%\.wslconfig</code>.</li>
<li>Run <code>wsl --shutdown</code> from PowerShell.</li>
<li>Confirm the same API from WSL2 with <code>curl</code>.</li>
<li>Copy the exact model ID from <code>/v1/models</code>.</li>
<li>Use an OpenAI-compatible client from the WSL2 application.</li>
<li>Only after all of that, investigate tool calling or agent behavior.</li>
</ol>
<p>The entire distinction is this:</p>
<pre><code class="language-text">LM Studio chat working
        !=
LM Studio API reachable from WSL2
</code></pre>
<p>Once I treated those as separate systems, the setup became straightforward.</p>
<p>If you use Windows for your model runtime and WSL2 for development, I hope this saves you the day I lost.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[From O(N³) to O(1): Scaling M2M Validation in Django]]></title><description><![CDATA[Adding a few cities to a delivery zone is instant. Adding 5,000 cities ran over 5,000 database round-trips and regularly timed out the request. The root causes were two common Django patterns that don]]></description><link>https://blog.acel.dev/from-o-n-to-o-1-scaling-m2m-validation-in-django</link><guid isPermaLink="true">https://blog.acel.dev/from-o-n-to-o-1-scaling-m2m-validation-in-django</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[performance]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Thu, 23 Jul 2026 14:35:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fecb0a8b2cf2458fe324d89/00c600a2-c56a-4ae8-8750-bb54f96d08b1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Adding a few cities to a delivery zone is instant. Adding 5,000 cities ran over 5,000 database round-trips and regularly timed out the request. The root causes were two common Django patterns that don't scale: validating with per-row <code>.exists()</code> calls, and writing Many-to-Many relations with <code>.add()</code>. Here's the replacement.</p>
<hr />
<h2>The Problem</h2>
<p>A shipping group links a set of cities to a delivery zone. The model is straightforward:</p>
<pre><code class="language-python">class ShippingGroup(models.Model):
    name = models.CharField(max_length=255)
    country = models.ForeignKey(Country, on_delete=models.CASCADE)
    cities = models.ManyToManyField(City)
</code></pre>
<p>When an admin creates a new shipping group, the endpoint needs to validate that none of the selected cities already belong to a competing group in the same region. The original code did this with a triple-nested loop:</p>
<pre><code class="language-python">for storefront in storefronts:
    for state in states:
        for city in cities:
            if ShippingGroup.objects.filter(
                company=self.company, storefronts=storefront,
                country=country, states=state, cities=city
            ).exists():
                raise ParseError("A shipping group already covers this city.")
</code></pre>
<p>For 5 storefronts, 10 states, and 100 cities, that's 5 × 10 × 100 = 5,000 individual <code>.exists()</code> queries. Just to validate the write hasn't even happened yet. And when the validation passes, the write itself used <code>.add()</code>:</p>
<pre><code class="language-python">shipping_group.cities.add(*city_ids)
</code></pre>
<p>Under the hood, <code>.add()</code> first issues a <code>SELECT</code> to find which IDs don't already exist in the junction table, then issues a single <code>INSERT</code> for the remaining rows. Neither step is batched: the deduplication query grows to match the full ID set, and the single large <code>INSERT</code> can lock the table. For 5,000 cities, the SELECT + INSERT plus the 5,000 validation queries made the request timeout long before it finished.</p>
<hr />
<h2>The Solution</h2>
<p>Three pieces: validate with in-memory set intersection instead of per-row queries, write directly to the <code>.through</code> table in batches, and offload past a threshold to a background task.</p>
<h3>Step 1: Validate with set intersection, not per-row <code>.exists()</code></h3>
<p>Instead of checking each city against each existing group one query at a time, fetch all potentially conflicting groups in one query with a <code>Prefetch</code> that hoists the related IDs onto each instance. Then intersect locally:</p>
<pre><code class="language-python">from django.db.models import Prefetch

def validate_no_overlap(country, storefront_ids, state_ids, city_ids):
    # Fetch every group that might conflict, with all related IDs attached
    existing = ShippingGroup.objects.filter(
        country=country,
    ).prefetch_related(
        Prefetch(
            "storefronts",
            queryset=Storefront.objects.only("id"),
            to_attr="prefetched_storefronts",
        ),
        Prefetch(
            "states",
            queryset=State.objects.only("id"),
            to_attr="prefetched_states",
        ),
        Prefetch(
            "cities",
            queryset=City.objects.only("id"),
            to_attr="prefetched_cities",
        ),
    )

    request_storefronts = set(storefront_ids)
    request_states = set(state_ids)
    request_cities = set(city_ids)

    for group in existing:
        group_sfs = {sf.id for sf in group.prefetched_storefronts}
        group_sts = {st.id for st in group.prefetched_states}
        group_cts = {ct.id for ct in group.prefetched_cities}

        if (
            request_storefronts &amp; group_sfs
            and request_states &amp; group_sts
            and request_cities &amp; group_cts
        ):
            raise ValidationError(
                f"A shipping group already covers this region."
            )
</code></pre>
<p>One query to fetch the groups, one query per <code>Prefetch</code> for the related IDs. Typically 3 to 5 queries total regardless of how many storefronts, states, or cities are being checked. The intersection runs entirely in Python, where a set membership check is O(1).</p>
<p>The <code>to_attr</code> on each <code>Prefetch</code> is the key detail: it hoists the filtered queryset onto each <code>ShippingGroup</code> instance as a plain list of objects (<code>prefetched_storefronts</code>, etc.), rather than leaving it as a lazily-evaluated queryset that would re-query on access. The set comprehensions then extract just the IDs for intersection.</p>
<h3>Step 2: Write to the <code>.through</code> table directly</h3>
<p><code>.add()</code> looks like a bulk operation, but Django's Many-to-Many manager inserts rows one at a time. For a large set of IDs, the correct approach is to go directly to the auto-generated through model and use <code>bulk_create</code>:</p>
<pre><code class="language-python">through_model = ShippingGroup.cities.through
batch_size = 500

for i in range(0, len(city_ids), batch_size):
    chunk = city_ids[i : i + batch_size]

    # Verify these IDs actually exist in the database before inserting
    valid_ids = set(
        City.objects.filter(id__in=chunk, country=country_id)
        .values_list("id", flat=True)
    )

    objs = [
        through_model(shippinggroup_id=group_id, city_id=cid)
        for cid in valid_ids
    ]
    through_model.objects.bulk_create(objs, ignore_conflicts=True)
</code></pre>
<p>Three things happening here:</p>
<ul>
<li><strong>Batching in chunks of 500</strong> keeps the transaction size bounded and prevents a single massive <code>INSERT</code> from locking the table for too long.</li>
<li><strong><code>ignore_conflicts=True</code></strong> skips rows that already exist: no duplicate-key error, no need to pre-filter existing relations. If a city is already assigned to this group, the insert is silently ignored.</li>
<li><strong>Pre-validating IDs against the database</strong> (<code>City.objects.filter(id__in=chunk)</code>) ensures you're not inserting garbage IDs that would fail a foreign key constraint. It's one query per batch, not one per row.</li>
</ul>
<p>For 5,000 cities, that's 10 batches × 2 queries each = 20 queries instead of 5,000 inserts.</p>
<h3>Step 3: Offload past a threshold to a background task</h3>
<p>Even with batched bulk writes, processing 10,000+ rows synchronously will make the HTTP request hang. At some size, the operation belongs in a queue:</p>
<pre><code class="language-python">THRESHOLD = 500

def create_shipping_group(data):
    group = ShippingGroup.objects.create(name=data["name"], country=country)

    city_count = len(data["city_ids"])
    if city_count &gt; THRESHOLD:
        assign_cities_to_group.delay(
            city_ids=data["city_ids"],
            country_id=country.id,
            group_id=group.id,
        )
    else:
        # Small enough — do it inline. At this count the SELECT + INSERT
        # in .add() is fast enough and keeps the code simpler.
        cities = City.objects.filter(id__in=data["city_ids"], country=country)
        group.cities.add(*cities.values_list("id", flat=True))

    return group
</code></pre>
<p>Below 500 cities: synchronous <code>.add()</code> is fine. Above 500: a Celery task takes over, the request returns immediately with the group created but cities still being assigned in the background. The task uses the same batched <code>bulk_create</code> path from Step 2.</p>
<p>Threshold choice matters: 500 was picked because it's well under a typical request timeout even with a slow database, but it's a number you should tune against your own infrastructure.</p>
<hr />
<h2>Why This Works</h2>
<p><strong>Validation is constant-time relative to the input size.</strong> The <code>Prefetch</code> queries are bounded by the number of <em>existing</em> groups in the country, not the number of cities being validated. Set intersection is O(n) in Python and negligible at typical group counts.</p>
<p><strong><code>.through</code> + <code>bulk_create</code> skips the deduplication penalty and avoids table locks.</strong> Under the hood, <code>.add()</code> runs a large <code>SELECT</code> to find which IDs are already in the table, then issues one giant <code>INSERT</code> for the rest — both unbounded operations. With <code>ignore_conflicts=True</code>, <code>bulk_create</code> skips the deduplication step entirely, and batching keeps the transactions small enough to avoid locking.</p>
<p><strong><code>ignore_conflicts</code> means idempotent writes.</strong> You can retry the Celery task without worrying about partial progress from a previous attempt. Rows that were already inserted in a prior batch are silently skipped on retry.</p>
<p><strong>The threshold keeps the API responsive.</strong> Synchronous for small inputs (the common case), asynchronous for large ones without changing the interface. The caller always gets the same response shape. The group exists, cities are either assigned or being assigned.</p>
<hr />
<h2>Design Decision: The <code>.through</code> Model Changes What You're Responsible For</h2>
<p>Using <code>through_model.objects.bulk_create()</code> instead of <code>group.cities.add()</code> means you're bypassing Django's Many-to-Many manager entirely. That's the point: the manager isn't designed for batch writes. But it also means you lose two things the manager normally gives you for free:</p>
<ul>
<li><strong>Signal dispatch.</strong> <code>.add()</code> fires <code>m2m_changed</code> signals that might be expected by other parts of the system (audit logs, cache invalidation). Writing to the through table directly does not. If those signals matter, you need to fire them manually or postpone logging to a post-write step.</li>
<li><strong>Validation.</strong> The manager checks that the related object exists before inserting. Step 2 compensates with an explicit <code>City.objects.filter(id__in=chunk)</code> pre-check. If you skip that check and insert a non-existent ID, you get a <code>ForeignKeyViolation</code> in Postgres instead of a clean Django error.</li>
</ul>
<p>Neither of these is a bad trade. Just a trade you should know you're making.</p>
<hr />
<h2>The Result</h2>
<p>The validation step went from 5,000 queries — one per city-state-storefront combination — to about 5 regardless of input size. The write went from a single large <code>INSERT</code> with no batching to 10 batched chunks of 500 with idempotent conflict handling, or a background task for counts above the threshold.</p>
<p>The pattern (validate with set intersection, write to <code>.through</code> in batches, offload past a threshold) applies to any Django model with a large Many-to-Many field: tags, categories, user teams, regional assignments, or anywhere else you're adding hundreds or thousands of rows to a junction table.</p>
<p>How are you handling large Many-to-Many writes in your Django apps? Are you going through the through table directly, or have you found a different approach? Drop it in the comments.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[It Worked on Every Environment Except Prod: Getting Daily Reports Right Across Timezones]]></title><description><![CDATA[Every day, a report runs: "what did each store sell yesterday?" One of the stores is in Lagos, the other in London. The report showed both stores as having zero sales for the last two hours of the day]]></description><link>https://blog.acel.dev/it-worked-on-every-environment-except-prod-getting-daily-reports-right-across-timezones</link><guid isPermaLink="true">https://blog.acel.dev/it-worked-on-every-environment-except-prod-getting-daily-reports-right-across-timezones</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[timezone]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Mon, 13 Jul 2026 10:01:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fecb0a8b2cf2458fe324d89/342ac2d2-3dc0-4f9d-912e-9bd64be1fe96.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every day, a report runs: "what did each store sell yesterday?" One of the stores is in Lagos, the other in London. The report showed both stores as having zero sales for the last two hours of the day: wrong numbers, every day, in production. Here's the conversion pattern that fixed it.</p>
<hr />
<h2>The Problem</h2>
<p>You store your timestamps in UTC. Django recommends it, Postgres defaults to it, and it keeps your data unambiguous. Order 582 was placed at <code>2025-03-10 23:15:00+00:00</code>. Clean, portable, no ambiguity.</p>
<p>But a report that asks for "Tuesday's orders" needs to know what Tuesday <em>means</em>. Tuesday in Lagos starts at 11 PM Monday UTC. Tuesday in London starts at midnight UTC. If your reporting query uses UTC midnight for everyone, orders placed between 11 PM and midnight UTC get assigned to the wrong day for every store east of GMT.</p>
<p>The fix is deceptively simple: each store knows its timezone, and the reporting query converts the store's local day-bounds into a UTC range before filtering. But there's a landmine in the conversion, and it's one that only fires in production:</p>
<pre><code class="language-python">&gt;&gt;&gt; import pytz, datetime
&gt;&gt;&gt; store_tz = pytz.timezone("Africa/Lagos")
&gt;&gt;&gt; start = datetime.datetime(2025, 3, 10, 0, 0, 0)
&gt;&gt;&gt; store_tz.localize(start)
datetime.datetime(2025, 3, 10, 0, 0, tzinfo=&lt;DstTzInfo 'Africa/Lagos' WAT+1:00:00 STD&gt;)
</code></pre>
<p>That worked. But then the code runs again, maybe from a Celery retry, maybe from a helper that already called <code>make_aware</code>, and the datetime is already offset-aware:</p>
<pre><code class="language-python">&gt;&gt;&gt; aware_start = store_tz.localize(start)     # fine
&gt;&gt;&gt; store_tz.localize(aware_start)             # BOOM
ValueError: Not naive datetime (tzinfo is already set)
</code></pre>
<p>The same line fails because <code>pytz.localize()</code> refuses to touch an already-aware datetime. This didn't surface in development because the call path never produced an aware datetime in the dev environment, but in production, with re-queued tasks and retried jobs, it did.</p>
<p>The root cause isn't the <code>localize()</code> call itself: it's that an aware datetime reached the function at all. Guard the input before it hits <code>datetime.combine()</code>, and <code>localize()</code> never sees an aware value. The fix is one guard at the top of the function, and the lesson is that timezone-correct reporting needs both the right model and the right conversion hygiene.</p>
<blockquote>
<p><strong>Note on modern Django:</strong> The <code>ValueError</code> above is a <code>pytz</code>-specific quirk. Django 4.0 deprecated <code>pytz</code> in favour of Python's built-in <code>zoneinfo</code>, and Django 5.0 removed <code>pytz</code> support entirely. With <code>zoneinfo</code> you use <code>datetime.replace(tzinfo=tz)</code> instead of <code>.localize()</code>, which behaves differently. The code in this article uses <code>pytz</code> because the bug originated in a legacy codebase, and millions of Django apps are still in that position. But the underlying trap is the same regardless of library: passing an aware datetime that carries a UTC date into a function expecting to produce local midnight bounds will silently fetch the wrong day's data if you aren't guarding your inputs.</p>
</blockquote>
<hr />
<h2>The Solution</h2>
<p>Two pieces: a per-store timezone field, and a helper that safely turns a local calendar date into a UTC range for filtering.</p>
<h3>Step 1: Give each store a timezone</h3>
<pre><code class="language-python"># models.py
class Store(models.Model):
    name = models.CharField(max_length=255)
    timezone = models.CharField(
        max_length=50,
        default="Africa/Lagos",
        help_text="IANA timezone name, e.g. 'Africa/Lagos', 'Europe/London'.",
    )
</code></pre>
<p>One field. The Lagos store has <code>"Africa/Lagos"</code>, the London store has <code>"Europe/London"</code>. If a store never sets one, the default keeps it working (and wrong for anyone who notices, which is better than crashing).</p>
<h3>Step 2: Convert local day-bounds to a UTC range</h3>
<p>The reporting query needs a UTC <code>start</code> and <code>end</code> for the store's local day. A helper handles the conversion safely:</p>
<pre><code class="language-python">import pytz
from datetime import date, datetime, time, timedelta
from django.utils import timezone as dj_timezone

def local_day_bounds_utc(store_tz: str, target_date: date | datetime) -&gt; tuple[datetime, datetime]:
    """Return (utc_start, utc_end) for the store's local calendar day."""
    tz = pytz.timezone(store_tz)

    # Normalize the date input — if an aware datetime arrives from a
    # Celery retry or pre-processed helper, extract the calendar date
    # in the store's own timezone.
    #
    # This matters because an aware datetime carries a UTC date component,
    # not a local one. A Lagos store at 11:30 PM local time is already the
    # next calendar day in UTC — passing that directly to datetime.combine()
    # would silently produce bounds for the wrong day.
    if isinstance(target_date, datetime) and not dj_timezone.is_naive(target_date):
        target_date = target_date.astimezone(tz).date()

    naive_start = datetime.combine(target_date, time.min)
    naive_end = datetime.combine(target_date + timedelta(days=1), time.min)

    aware_start = tz.localize(naive_start)
    aware_end = tz.localize(naive_end)

    return aware_start.astimezone(pytz.UTC), aware_end.astimezone(pytz.UTC)
</code></pre>
<p><code>naive_end</code> is built as the start of the next day rather than <code>time.max</code> on the current day. This avoids microsecond edge cases on the upper boundary. The next day's midnight is unambiguous. For March 11 and a Lagos store, the UTC range becomes <code>2025-03-10 23:00+00:00</code> to <code>2025-03-11 23:00+00:00</code>. That's Lagos-Tuesday in UTC.</p>
<h3>Step 3: Filter with the UTC range</h3>
<p>The reporting query now reads cleanly:</p>
<pre><code class="language-python">from django.db.models import Sum

store = Store.objects.get(id=store_id)
store_tz = pytz.timezone(store.timezone)
target_date = datetime.now(store_tz).date() - timedelta(days=1)  # yesterday, store-local

utc_start, utc_end = local_day_bounds_utc(store.timezone, target_date)

orders = Order.objects.filter(
    store=store,
    created_at__gte=utc_start,
    created_at__lt=utc_end,
)
totals = orders.aggregate(total=Sum("amount"))
</code></pre>
<p>One query, timezone-correct by construction. "Yesterday" is derived in the store's own timezone, not the server's. The function converts to UTC once, and the database does the rest.</p>
<hr />
<h2>Why This Works</h2>
<p><strong>The conversion is a pure function of data you already have.</strong> The store's timezone is a column on the store model. No API calls, no user-preference lookups, no frontend involvement. The reporting query just reads the field and does the math.</p>
<p><strong>Postgres handles the UTC comparison natively.</strong> <code>created_at</code> is a <code>timestamptz</code> column, which stores everything as UTC internally. When Django passes two UTC-aware datetimes to <code>__gte</code>/<code>__lt</code>, Postgres can resolve the range with a plain index scan on the <code>created_at</code> column: no timezone arithmetic per row, no sequential scan. The conversion happens once per query, in Python. The database just compares two UTC values.</p>
<p><strong>It works for any date, not just "yesterday."</strong> The helper takes a date, not a relative expression. Daily EOD reports, weekly summaries, arbitrary date-picker queries. All call the same function.</p>
<hr />
<h2>Design Decision: Put the Guard on the Input, Not the Output</h2>
<p>The <code>local_day_bounds_utc</code> helper normalizes its input before doing anything else. <code>datetime.combine()</code> takes a <code>date</code> and a <code>time</code>. If you pass an aware <code>datetime</code> as the date argument, Python uses its date component, which is the UTC date, not the store-local date. A Lagos store at 11:30 PM local time on March 10 is already March 11 in UTC. Pass that aware datetime directly to <code>combine</code> and you silently get bounds for the wrong day, with no error raised.</p>
<p>In production, the input sometimes isn't a plain date. A Celery task retries and the retry wrapper has already called <code>make_aware</code> on the parameter. A helper upstream pre-processed the date into UTC. The <code>isinstance + is_naive</code> check at the top of the function catches this: if the incoming value is an aware datetime, it's converted to the store's own timezone first, and only the local calendar date is extracted. This means "yesterday" is always resolved in the store's timezone, regardless of what timezone the parameter arrived with.</p>
<p>The lesson: <strong>timezone correctness has two layers</strong>. The model (which timezone applies) and the conversion hygiene (is the input naive or aware, and does it carry the right date?). You need both. The second layer fails silently in every environment except the one where retries and re-queues happen.</p>
<hr />
<h2>The Result</h2>
<p>The daily sales report now returns correct totals for every store, regardless of timezone. Adding a new store in a new city is one field change on the store record. The reporting query picks it up automatically. The input guard means the report produces correct results even when a date parameter arrives through an unexpected code path.</p>
<p>How are you handling per-store or per-tenant timezone-aware reporting? Are you doing it at the DB level, in Python, or have you found a third approach? I'd love to hear what's worked for you in the comments.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[How to Prefetch Across GenericForeignKeys When You Can't Change the Schema]]></title><description><![CDATA[You don't always control the schema you work with. Sometimes you inherit a codebase where GenericForeignKey is threaded through the models, and a migration isn't on the table. Loading a page of 20 aud]]></description><link>https://blog.acel.dev/how-to-prefetch-across-genericforeignkeys-when-you-can-t-change-the-schema</link><guid isPermaLink="true">https://blog.acel.dev/how-to-prefetch-across-genericforeignkeys-when-you-can-t-change-the-schema</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[performance]]></category><category><![CDATA[database]]></category><category><![CDATA[django rest framework]]></category><category><![CDATA[optimization]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Mon, 06 Jul 2026 10:13:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fecb0a8b2cf2458fe324d89/4b544ea8-b76a-4e9c-82ad-548e27bb7252.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You don't always control the schema you work with. Sometimes you inherit a codebase where <code>GenericForeignKey</code> is threaded through the models, and a migration isn't on the table. Loading a page of 20 audit entries runs ~60 queries because Django's ORM can't prefetch across a GFK. Here's how to fix it without changing a single model.</p>
<hr />
<h2>The Problem</h2>
<p><code>GenericForeignKey</code> lets one table point at rows in multiple other tables without knowing which one at query time. The classic use case is an audit log, a notification feed, or a tagging system where the set of target types is open-ended.</p>
<p>But <code>select_related</code> and <code>prefetch_related</code> can't traverse a GFK. The ORM needs to know the target table ahead of time, and a <code>content_type_id</code>/<code>object_id</code> pair doesn't give it that until runtime. The result: every time you touch the GFK on an instance, you pay a query.</p>
<p>Take an activity log. One table, every row references some target: an order, a user, a product:</p>
<pre><code class="language-python"># models.py
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models

class ActivityLog(models.Model):
    actor = models.ForeignKey("User", on_delete=models.CASCADE)
    action = models.CharField(max_length=50)           # "created", "updated", "deleted"
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.CharField(max_length=255)
    target = GenericForeignKey("content_type", "object_id")
    created_at = models.DateTimeField(auto_now_add=True)

class Order(models.Model):
    number = models.CharField(max_length=20)
    status = models.CharField(max_length=20)
    logs = GenericRelation(ActivityLog)

class Product(models.Model):
    name = models.CharField(max_length=255)
    sku = models.CharField(max_length=50)
    logs = GenericRelation(ActivityLog)

class User(models.Model):
    email = models.EmailField()
    logs = GenericRelation(ActivityLog)
</code></pre>
<p>This is a legitimate use of GFK. Audit logs <em>should</em> be polymorphic. But rendering a page of 20 activity feed entries, each showing the target's name and a link, hits the database once per entry to resolve <code>log.target</code>: 20 individual single-row lookups.</p>
<p>If the audit trail also needs actor details, or the target's related data, the count multiplies fast. Twenty entries can easily become 60-80 queries before pagination.</p>
<hr />
<h2>The Solution</h2>
<p>Three steps. First, batch-resolve every GFK lookup in the page at once using compound queries. Second, attach the results to each instance as a cached private attribute. Third, let serializers read the cache with a live-query fallback, so the optimization is optional and the code never breaks if you forget to run it.</p>
<h3>Step 1: Resolve the targets in bulk, not one by one</h3>
<p>Collect the log entries you want to display, then group their <code>object_id</code> values by content type. Fire one query per target model, map the results back by ID, and attach the resolved instances:</p>
<pre><code class="language-python">from collections import defaultdict
from django.contrib.contenttypes.models import ContentType

def hydrate_activity_targets(entries):
    """Resolve all ActivityLog.target references in bulk."""
    if not entries:
        return

    # Collect all (content_type_id, object_id) pairs from the page
    refs_by_ct = defaultdict(set)
    for entry in entries:
        if entry.content_type_id and entry.object_id:
            refs_by_ct[entry.content_type_id].add(entry.object_id)

    # Resolve content types dynamically from the entries. One query,
    # no hardcoded model list — inherited codebases gain new types
    # and this adapts automatically.
    ct_by_id = {
        ct.id: ct
        for ct in ContentType.objects.filter(id__in=refs_by_ct.keys())
    }

    # One query per target model, map results back by (ct_id, str(id))
    target_map = {}
    for ct_id, obj_ids in refs_by_ct.items():
        ct = ct_by_id.get(ct_id)
        if ct is None:
            continue
        model = ct.model_class()
        if model is None:
            continue
        for obj in model.objects.filter(id__in=obj_ids):
            target_map[(ct_id, str(obj.id))] = obj

    # Attach to each entry
    for entry in entries:
        key = (entry.content_type_id, str(entry.object_id))
        entry._resolved_target = target_map.get(key)
</code></pre>
<p>This resolves every target reference on the page, regardless of type, with one query per content type. For a feed mixing 4 types, that's 4 queries instead of 20. The <code>ContentType</code> map is built from the entries themselves (no hardcoded model list), so the function automatically handles content types added after the hydration code was written.</p>
<h3>Step 2: Handle the related data with compound queries</h3>
<p>Resolving the target object is only the first layer. Audit entries also need the <em>actor's</em> name, or a display string that lives on a related model. You can extend the hydration to fetch those in bulk too. The pattern is the same: bucket by content type, compound <code>Q</code> filter across all buckets, map back by <code>(content_type_id, object_id)</code>:</p>
<pre><code class="language-python">from django.db.models import Q

def hydrate_activity_details(entries):
    """Bulk-fetch display details for all resolved targets on the page."""
    if not entries:
        return

    # Build a model → ct_id map up front. Unlike Step 1, display formatting
    # is inherently type-specific — a product shows name and SKU, a user
    # shows their full name — so hardcoding known types here is correct.
    ct_map = ContentType.objects.get_for_models(Product, User, Order)
    ct_id_by_model = {model: ct.id for model, ct in ct_map.items()}

    # Group entries by content type
    obj_ids_by_ct = defaultdict(list)
    entry_by_target_id = defaultdict(list)  # target.id → [entries with that target]
    for entry in entries:
        target = getattr(entry, "_resolved_target", None)
        if target is None:
            continue
        ct_id = ct_id_by_model.get(type(target))
        if ct_id is not None:
            obj_ids_by_ct[ct_id].append(target.id)
            entry_by_target_id[(ct_id, target.id)].append(entry)

    # Fetch products in one query
    product_ct = ct_id_by_model.get(Product)
    if product_ct in obj_ids_by_ct:
        product_display = {
            p.id: f"{p.name} ({p.sku})"
            for p in Product.objects.filter(
                id__in=obj_ids_by_ct[product_ct]
            )
        }
        for product_id, display in product_display.items():
            for entry in entry_by_target_id[(product_ct, product_id)]:
                entry._target_display = display

    # Fetch users with their profile in one query
    user_ct = ct_id_by_model.get(User)
    if user_ct in obj_ids_by_ct:
        user_display = {
            u.id: u.get_full_name()
            for u in User.objects.filter(
                id__in=obj_ids_by_ct[user_ct]
            ).select_related("profile")
        }
        for user_id, display in user_display.items():
            for entry in entry_by_target_id[(user_ct, user_id)]:
                entry._target_display = display

    # Repeat the same pattern for Order or any other content type.
    # Targets that don't match a handled type fall back gracefully through
    # the serializer's str(target) path.
</code></pre>
<p>Building the display map in a dict comprehension first keeps each pass over entries to O(n). One iteration per content type, not a nested loop over every object × entry combination. For 200 entries and 50 products, that's 50 + 200 checks instead of 50 × 200.</p>
<h3>Step 3: Serializers that read the cache with a fallback</h3>
<p>Serializers read the cached attributes when they're present, and fall back to a live query when they aren't, so the serializer works whether or not the hydration ran:</p>
<pre><code class="language-python">class ActivityLogSerializer(serializers.Serializer):
    target_name = serializers.SerializerMethodField()
    target_type = serializers.SerializerMethodField()

    def get_target_name(self, entry):
        display = getattr(entry, "_target_display", None)
        if display is not None:
            return display
        # Cache wasn't populated — fall back to a live GFK lookup.
        target = entry.target
        return str(target) if target else None

    def get_target_type(self, entry):
        target = getattr(entry, "_resolved_target", entry.target)
        if target is None:
            return None
        return type(target).__name__
</code></pre>
<p>The <code>getattr(entry, "_target_display", None)</code> pattern is the decoupling mechanism. If hydration ran, the attribute is present and <code>entry.target</code> never fires. If it didn't run, the serializer falls back to a live GFK query and everything still works. The hydration logic is an optional optimization — the serializer doesn't care whether it was called.</p>
<hr />
<h2>Why This Works</h2>
<p><strong>One query per content type, not per row.</strong> The <code>id__in</code> filter resolves all targets of a given type in a single query regardless of how many entries reference that type. A feed with 20 entries across 4 types goes from 20+ queries to 4.</p>
<p><strong>The private-attribute convention is lazy and safe.</strong> <code>_resolved_target</code> and <code>_target_display</code> are set when hydration runs and silently absent when it doesn't. The <code>getattr(..., fallback)</code> pattern means no code path breaks. The serializer degrades to live queries without any conditional branching.</p>
<p><strong>No schema change required.</strong> This pattern doesn't touch the models, doesn't need a migration, and doesn't alter how GFK works. It's a presentation-layer optimization. You can introduce it incrementally to the slowest pages and leave the rest untouched.</p>
<p><strong>It composes with existing <code>select_related</code>/<code>prefetch_related</code>.</strong> If your queryset already uses ORM-level prefetch for non-GFK relationships (like <code>select_related("actor")</code>), the hydration runs <em>after</em> that, filling in only the gaps the ORM can't reach.</p>
<hr />
<h2>Design Decision: The Fallback Is Silent, and That's Both a Feature and a Risk</h2>
<p>Because <code>getattr(entry, "_resolved_target", entry.target)</code> swallows a missing attribute silently, a typo in the attribute name (<code>_resolvedTarget</code>) will never raise an error. It'll just quietly bypass the cache and fire a live query on every call. The page still renders correctly, but your query count silently regresses.</p>
<p>Mitigations: keep the <code>_</code> prefix convention consistent, test the query count in integration tests (not just correctness), and consider a debug-mode assertion that logs a warning when hydration is expected but the cache attribute is absent.</p>
<p>One more operational note: if the set of target types is large, the <code>id__in</code> per-content-type approach fires many small queries rather than one big one. At dozens of content types, a single query that fetches all targets of any type via <code>UNION</code> may be faster — but for the typical audit log with half a dozen entity types, the per-type approach is simpler and easier to debug.</p>
<hr />
<h2>The Result</h2>
<p>An activity feed that used to cost ~60 queries for 20 entries — the initial queryset (1), a per-entry GFK target lookup (20), an actor display-name lookup per entry (20), and a related-field fetch per target (20+, e.g. user profiles, product details) — now costs about 6: one per target content type plus the initial fetch.</p>
<p>The two hydration functions compose between the queryset and the serializer:</p>
<pre><code class="language-python">entries = ActivityLog.objects.select_related("actor").order_by("-created_at")[:20]
hydrate_activity_targets(entries)
hydrate_activity_details(entries)
return ActivityLogSerializer(entries, many=True).data
</code></pre>
<p>No schema change, no migration, no ORM tricks. The hydration functions are drop-ins that run between the queryset and the serializer, and if you forget to call them, nothing breaks. The serializers fall back to live queries.</p>
<p>How are you handling N+1 across GenericForeignKeys in your apps? Are you hand-rolling prefetches, or have you found a different pattern? Drop your approach in the comments.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[How I Moved Real-Time Out of Django (and Made Everything Simpler)]]></title><description><![CDATA[Your users want live updates: an order flips to paid, a notification pops, a dashboard number ticks up without a refresh. The obvious answer in Django is Channels. I reached for it first too. But as o]]></description><link>https://blog.acel.dev/how-i-moved-real-time-out-of-django-and-made-everything-simpler</link><guid isPermaLink="true">https://blog.acel.dev/how-i-moved-real-time-out-of-django-and-made-everything-simpler</guid><category><![CDATA[Django]]></category><category><![CDATA[websocket]]></category><category><![CDATA[architecture]]></category><category><![CDATA[Python]]></category><category><![CDATA[Centrifugo]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Tue, 30 Jun 2026 08:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fecb0a8b2cf2458fe324d89/97808897-0438-4bed-9cc5-46967ab53756.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Your users want live updates: an order flips to <em>paid</em>, a notification pops, a dashboard number ticks up without a refresh. The obvious answer in Django is <a href="https://channels.readthedocs.io/">Channels</a>. I reached for it first too. But as our scale grew, the costs of keeping connections in-process pushed me to rethink the architecture. I let Django stop handling WebSockets entirely, and almost everything got simpler. Here's the pattern that replaced it.</p>
<hr />
<h2>The Problem</h2>
<p>Django is a request/response framework. It processes a request, returns a response, and moves on. Real-time notifications — a dashboard ticking up, an order status flipping live — need a server that <em>holds a connection open</em> for every connected client. That is a fundamentally different job.</p>
<p>The standard answer in the Django ecosystem is <a href="https://channels.readthedocs.io/">Channels</a>. It moves you to ASGI, gives you consumers, and works well, especially when your real-time needs are deep and bidirectional.</p>
<p>Ours weren't. We needed server→client push: notify a user when an order completed, or when a payment landed. The connections were mostly idle. Channels gave us that, but it came with costs that started hurting at scale.</p>
<p>The concrete problem was this: every open WebSocket connection consumed memory and a small slice of CPU time on our app servers. A thousand connected dashboards was fine. Ten thousand, across dozens of tenants, each holding a persistent socket. Suddenly, our workers were doing two very different jobs at once. Serving fast API responses <em>and</em> babysitting long-lived, mostly-quiet connections. Both suffered.</p>
<p>What made it brittle was the reconnect storm. Deploy a new build, restart a server, or hit a brief network partition, and every connected client disconnects and <em>immediately reconnects</em>. All at once. That thundering herd of TLS handshakes, authentication, and subscription requests would spike CPU to 100% on every app server as it tried to spin up thousands of ASGI consumer instances simultaneously, occasionally knocking them over entirely before the retry backoff kicked in.</p>
<p>There's also the smaller but telling friction of auth. Authenticating the WebSocket handshake means reaching your existing auth (DRF/JWT) from inside an ASGI scope. I ended up writing middleware that reconstructed a DRF <code>Request</code> out of the raw scope just to reuse the auth I already had. It worked, but it was a sign: I was bending the framework to do something it wasn't built for.</p>
<p>The turning point was asking: <em>why is my web framework in the business of holding connections at all?</em></p>
<p>For a fire-and-forget server→client push, it shouldn't be. There are servers built specifically to hold millions of open connections and do nothing else. The only two things your backend needs to do are <strong>say who's allowed to listen</strong>, and <strong>publish events</strong>.</p>
<hr />
<h2>The Solution</h2>
<p>Put a dedicated pub/sub gateway in front of your app. I used <a href="https://centrifugal.dev/">Centrifugo</a>, a standalone real-time server. Clients connect to <em>it</em>, not to Django. Django goes back to being a plain, boring WSGI/sync app and plays two small roles:</p>
<ol>
<li><p><strong>Issue a short-lived connection token</strong> that embeds <em>which channels this user may subscribe to</em>.</p>
</li>
<li><p><strong>Publish events</strong> to the gateway over its HTTP API.</p>
</li>
</ol>
<p>The gateway holds the connections. Django holds nothing.</p>
<h3>Step 1: Issue a token that carries its own permissions</h3>
<p>When a client wants to connect, it asks your backend (over a normal authenticated request) for a connection token. The token is a JWT whose claims list the exact channels the user may subscribe to. The gateway verifies the signature and authorizes subscriptions <strong>without a single database lookup</strong>:</p>
<pre><code class="language-python"># realtime/auth.py
import time
import jwt
from django.conf import settings


class RealtimeTokenService:
    """Issues short-lived JWTs for connecting to the realtime gateway."""

    def generate_connection_token(self, user) -&gt; str:
        tenant_id = user.tenant_id

        # Channel grants are baked into the token, so the gateway authorizes
        # subscriptions with zero database lookups on connect.
        channels = [
            f"user_{user.id}",
            f"critical:user_{user.id}",
            f"tenant_{tenant_id}",
            f"critical:tenant_{tenant_id}",
        ]

        claims = {
            "sub": str(user.id),
            "exp": int(time.time()) + settings.REALTIME_TOKEN_TTL_SECONDS,
            "info": {"name": user.get_full_name(), "email": user.email},
            "channels": channels,
        }
        return jwt.encode(claims, settings.REALTIME_HMAC_SECRET, algorithm="HS256")
</code></pre>
<p>This is the key idea: <strong>token-as-permission</strong>. The channel names encode your authorization model. A user gets their own <code>user_{id}</code> channel and their tenant's <code>tenant_{id}</code> channel, and nothing else. Multi-tenant isolation falls out of the channel naming: a user simply has no grant for another tenant's channel, so the gateway will refuse the subscription.</p>
<h3>Step 2: Hand the token out behind your normal auth</h3>
<p>The endpoint that mints the token is an ordinary authenticated view. Your existing auth stack protects it; no ASGI gymnastics:</p>
<pre><code class="language-python"># realtime/views.py
class RealtimeTokenView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        token = RealtimeTokenService().generate_connection_token(request.user)
        return Response(
            {"token": token, "socket_url": settings.REALTIME_SOCKET_URL}
        )
</code></pre>
<p>The client takes <code>{token, socket_url}</code>, opens a WebSocket to the gateway, and subscribes to its channels. Django is now out of the connection entirely.</p>
<h3>Step 3: Publish events over HTTP</h3>
<p>To push an update, your backend POSTs to the gateway's publish API. No connection state, no sockets: just an HTTP call:</p>
<pre><code class="language-python"># realtime/publisher.py
import requests
from django.conf import settings


def push_event(channel, event_type, payload, *, recoverable=False, timeout=10.0):
    # The "critical:" namespace is configured on the gateway with message
    # history + recovery, so clients can catch up after a reconnect.
    final_channel = f"critical:{channel}" if recoverable else channel

    resp = requests.post(
        f"{settings.REALTIME_API_URL}/publish",
        json={
            "channel": final_channel,
            "data": {"type": event_type, "payload": payload},
        },
        headers={"X-API-Key": settings.REALTIME_API_KEY},
        timeout=timeout,
    )
    resp.raise_for_status()
</code></pre>
<p>Notice the <code>critical:</code> channel prefix. On the gateway, that namespace is configured to keep a short history so a client that briefly drops can recover missed messages on reconnect:</p>
<pre><code class="language-json">{
  "namespaces": [
    { "name": "critical", "history_size": 50, "history_ttl": "120s", "force_recovery": true }
  ]
}
</code></pre>
<p>Ephemeral events (a "user is typing" blip) publish to the bare channel; events you can't afford to lose (a completed order) publish to <code>critical:</code> and survive a reconnect.</p>
<h3>Step 4: Make publishing reliable, and don't block the request</h3>
<p>Publishing is a network call to a separate service. You do <strong>not</strong> want it on the request's critical path, and you want it to survive a transient blip. Wrap it in a Celery task with retries and backoff:</p>
<pre><code class="language-python"># realtime/tasks.py
import logging
from requests.exceptions import RequestException, HTTPError
from celery import shared_task
from .publisher import push_event

logger = logging.getLogger(__name__)


@shared_task(bind=True, max_retries=3, default_retry_delay=5, retry_backoff=True)
def publish_realtime_event(self, channel, event_type, payload, recoverable=False):
    try:
        push_event(channel, event_type, payload, recoverable=recoverable)
    except HTTPError as exc:
        # A 4xx error (bad API key, malformed payload) is a deterministic
        # failure that retries can't fix. Log it and drop it.
        # A 5xx error might be transient, so retry it.
        if 400 &lt;= exc.response.status_code &lt; 500:
            logger.error("Deterministic 4xx error on %s, dropping.", channel)
            return
        raise self.retry(exc=exc)
    except RequestException as exc:
        # Pure network timeouts or connection errors — retry with backoff.
        raise self.retry(exc=exc)
    except Exception:
        # A serialization bug won't fix itself on a retry. Log and drop it,
        # rather than burning three attempts on a guaranteed failure.
        # By not re-raising, Celery marks the task as successful and removes
        # it from the queue — no poison pill looping forever.
        logger.exception("Dropping unrecoverable realtime event on %s", channel)
</code></pre>
<p>That <code>except</code> split matters: <strong>retry the transient, drop the deterministic.</strong> A flaky network deserves another try; a payload that can't be JSON-encoded will fail identically three times in a row, so retrying it is just noise.</p>
<p>Firing an event from anywhere in your app is now a one-liner:</p>
<pre><code class="language-python">publish_realtime_event.delay(
    channel=f"tenant_{order.tenant_id}",
    event_type="order.completed",
    payload={"order_id": order.id, "total": str(order.total)},
    recoverable=True,
)
</code></pre>
<hr />
<h2>Why This Works</h2>
<p><strong>Your web tier goes back to being stateless.</strong> Django stays on WSGI. No ASGI migration, no async server, no sockets held in your workers. You can scale, restart, and deploy the web tier like any other stateless service. The open connections live on the gateway and aren't disturbed.</p>
<p><strong>Connections scale independently.</strong> Holding a lot of idle connections is a specialized job. A purpose-built gateway does it on its own box, tuned for that, while your app servers stay sized for request throughput.</p>
<p><strong>Auth has zero connect-time cost.</strong> Because the token carries its channel grants, the gateway never calls back into your database to authorize a subscription. Ten thousand clients reconnecting after a blip is ten thousand signature checks, not ten thousand DB queries.</p>
<p><strong>Multi-tenancy is just naming.</strong> Tenant isolation isn't a separate access-control layer. It's the channel names in the token. There's no code path by which a user can subscribe to a channel they weren't granted.</p>
<p><strong>Delivery is fire-and-forget but durable.</strong> Publishing happens off the request path via Celery, retries on transient failure, and the <code>critical:</code> namespace lets clients recover what they missed across a reconnect.</p>
<hr />
<h2>Design Decision: The Token Is a Cache, So It Can Go Stale</h2>
<p>Baking permissions into the token is what buys you DB-free connects. But a token is a snapshot. If a user's access changes (you remove them from a tenant, say) and their token still has 24 hours to live, the gateway will keep honoring the <em>old</em> grants until it expires. You've traded <strong>revocation freshness</strong> for <strong>connect-time performance</strong>.</p>
<p>That's usually the right trade for a notification stream, but be deliberate about it:</p>
<ul>
<li><p><strong>Keep the TTL short</strong> and have the client transparently re-fetch a token. Shorter tokens = smaller stale window.</p>
</li>
<li><p><strong>For true revocation</strong>, the gateway can be told to force-disconnect a user or refuse channels server-side. Reach for that only when a stream is sensitive enough to need it.</p>
</li>
</ul>
<p>One more operational gotcha: a single gateway is tempting to share across environments, but <code>tenant_42</code> in staging and <code>tenant_42</code> in production are the same channel name: cross-talk waiting to happen. If you must share one broker, <strong>prefix channels with the environment</strong> (<code>prod_tenant_42</code>, <code>staging_tenant_42</code>) so the namespaces can't collide.</p>
<p>And to be honest about the path here: I shipped the Channels version first. It worked. But the friction of running an async server and bridging my existing auth into the ASGI scope is exactly what made the gateway approach worth it. If your real-time needs are deep and bidirectional (collaborative editing, presence), in-process Channels may genuinely fit better. For "push updates to subscribed clients," letting something else hold the sockets is the simpler system.</p>
<hr />
<h2>The Result</h2>
<p>Adding a live update anywhere in the app is now one <code>.delay()</code> call. The web tier never learned what a WebSocket is. Connections, fan-out, history, and recovery are the gateway's problem; authorization is a signed list of channel names; delivery is a retrying background task. Django went back to doing the thing it's good at. And the real-time problem moved to a server built for it.</p>
<p>How are you doing real-time in your Django apps — in-process with Channels, or have you pushed connections out to a gateway? I'd love to hear what's worked for you in the comments.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[How I Built a "Set It and Forget It" Sync System with Django Signals]]></title><description><![CDATA[Change a product's price anywhere in your app, and it instantly syncs to a third-party marketplace. No manual triggers, no polling, no fragile save() overrides. Here's the signal pattern that powers i]]></description><link>https://blog.acel.dev/how-i-built-a-set-it-and-forget-it-sync-system-with-django-signals</link><guid isPermaLink="true">https://blog.acel.dev/how-i-built-a-set-it-and-forget-it-sync-system-with-django-signals</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[architecture]]></category><category><![CDATA[webdev]]></category><category><![CDATA[signals]]></category><category><![CDATA[ third party integration]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Mon, 15 Jun 2026 22:51:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fecb0a8b2cf2458fe324d89/26b39bc4-1b31-4634-8220-9d675984775c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Change a product's price anywhere in your app, and it instantly syncs to a third-party marketplace. No manual triggers, no polling, no fragile <code>save()</code> overrides. Here's the signal pattern that powers it.</p>
<hr />
<h2>The Problem</h2>
<p>In our app, a product's name or price can change from eight different places. The edit page. Bulk import. The variant editor. A pricing rule engine. An API endpoint that processes webhooks from suppliers. Every few weeks, someone adds a new feature and creates yet another code path that mutates a product.</p>
<p>We needed every one of those changes, no matter where they came from, to sync to an external marketplace. The naive approach would be to add a sync call to each code path. That's eight places to maintain (and counting), eight chances to forget, and eight places that break if the sync API changes.</p>
<p>Django's <a href="https://docs.djangoproject.com/en/stable/ref/signals/#post-save"><code>post_save</code></a> signals solve the discovery problem: hook into a model's save event and you catch every change, from every code path, in one place. Signals get a bad rap — fairly, they make control flow hard to trace. But when you need to react to changes from <em>everywhere</em> without touching <em>anywhere</em>, this specific discovery problem is exactly what they were designed for.</p>
<p>There's a catch, though. If ten prices change in a single request, say, a bulk import, a naive signal handler fires ten times. That's ten API calls in rapid succession. At best, you are wasting resources. At worst, the external API rate-limits you.</p>
<p>We needed signals to <em>detect</em> changes, but we needed to batch them.</p>
<hr />
<h2>The Solution</h2>
<p>Here's the three-part pattern:</p>
<ol>
<li><p><strong>A signal handler</strong> that collects change references instead of acting on them immediately.</p>
</li>
<li><p><strong>A per-thread set</strong> that deduplicates for free — adding the same product twice does nothing.</p>
</li>
<li><p><strong>A flush callback</strong> deferred to <a href="https://docs.djangoproject.com/en/stable/topics/db/transactions/#django.db.transaction.on_commit"><code>transaction.on_commit</code></a> that processes everything once the database transaction lands.</p>
</li>
</ol>
<h3>Step 1: Register the signal</h3>
<p>In your app's <code>apps.py</code>, connect <code>post_save</code> to the model that holds pricing:</p>
<pre><code class="language-python"># shopping/apps.py
from django.apps import AppConfig

class ShoppingAppConfig(AppConfig):
    name = "shopping"

    def ready(self):
        from django.db.models.signals import post_save
        from shopping.models import PriceRecord
        from shopping.signals import on_price_change

        post_save.connect(
            on_price_change,
            sender=PriceRecord,
            dispatch_uid="shopping_price_change",
        )
</code></pre>
<p><a href="https://docs.djangoproject.com/en/stable/topics/signals/#preventing-duplicate-signals"><code>dispatch_uid</code></a> prevents duplicate connections if <code>ready()</code> runs twice, a common gotcha during development with auto-reload.</p>
<h3>Step 2: Collect, don't act</h3>
<p>The signal handler doesn't call an API. It just adds a reference to a set and registers a flush callback:</p>
<pre><code class="language-python"># shopping/signals.py
import threading
from django.db import transaction

_local = threading.local()


def _get_pending():
    if not hasattr(_local, "pending"):
        _local.pending = set()
    return _local.pending


def on_price_change(sender, instance, **kwargs):
    # Record just enough to look up the product later
    _get_pending().add(instance.object_id)
    transaction.on_commit(flush_changes)
</code></pre>
<p>That's it. Four lines of logic. The handler doesn't care how many prices changed or where the change came from. It just records what changed and defers action to the flush.</p>
<p>Notice that we use <code>threading.local()</code> instead of a standard module-level variable. This ensures that each thread gets its own isolated storage.</p>
<p>Why <code>transaction.on_commit</code>? If the transaction rolls back, the price change never happened, so the flush callback is discarded. You never sync data that wasn't committed.</p>
<h3>Step 3: Flush once per transaction</h3>
<p>When the transaction lands, the flush handler fires. It snapshots the set, clears it immediately, and processes everything in one batch:</p>
<pre><code class="language-python">def flush_changes():
    pending = _get_pending()
    refs = pending.copy()
    pending.clear()

    if not refs:
        return

    from shopping.services import SyncService
    SyncService.handle_price_changes(refs)
</code></pre>
<p><strong>The key detail</strong>: we copy the set <em>before</em> clearing it. If clearing happened after processing, and processing raised an exception, stale refs would linger into future transactions. Snapshot-first is defensive.</p>
<p>This <code>clear()</code> is also what keeps requests perfectly isolated. Once a transaction commits and the flush runs, the set is empty and ready for the next request.</p>
<h3>Step 4: Resolve and dispatch</h3>
<p>The service layer resolves the raw references into business entities, groups them by destination, and dispatches one task per group. The grouping is the important part. One API call per store, regardless of how many products changed:</p>
<pre><code class="language-python"># shopping/services.py
from collections import defaultdict

class SyncService:

    @classmethod
    def _resolve_to_products(cls, refs: set[int]):
        """
        Resolve product ID references to actual Product instances
        with their Store relationship.  Fetches everything in ONE
        query using filter(id__in=...), silently ignoring stale
        IDs that no longer exist.
        """
        products = Product.objects.filter(
            id__in=refs
        ).select_related("store")

        resolved = []
        for product in products:
            resolved.append((product, product.store_id))
        return resolved

    @classmethod
    def handle_price_changes(cls, refs: set[int]):
        resolved = cls._resolve_to_products(refs)

        # Group changes by the store they belong to
        by_store = defaultdict(list)
        for product, store_id in resolved:
            by_store[store_id].append(product)

        # One API call per store, regardless of how many products changed
        for store_id, products in by_store.items():
            sync_to_marketplace.delay(store_id=store_id, products=products)
</code></pre>
<p>The <code>sync_to_marketplace</code> task is a <a href="https://docs.celeryq.dev/en/stable/userguide/tasks.html">Celery task</a> that calls the external API. It's configured with retries and backoff for transient failures:</p>
<pre><code class="language-python">@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def sync_to_marketplace(self, store_id, products):
    try:
        provider.bulk_update(store_id, products)
    except Exception as exc:
        raise self.retry(exc=exc)
</code></pre>
<hr />
<h2>Why This Works</h2>
<p><strong>Deduplication is free.</strong> A <code>set</code> naturally deduplicates, adding the same <code>object_id</code> twice is a no-op. If a price changes twice in the same request (say, a pricing rule recalculates it), the flush handler sees it once. And when it fires, it reads the latest price from the database. The most recent value always wins.</p>
<p><strong>Transaction safety is built-in.</strong> <code>transaction.on_commit</code> guarantees the flush only runs after the database confirms the change. If the transaction rolls back (a validation error, a constraint violation, a <code>raise</code> somewhere), the callback is discarded. You never sync phantom data.</p>
<p><strong>Batch resolution avoids N+1.</strong> The service resolves all references in bulk queries, not one at a time. For 50 changed products, it takes exactly 1 query regardless of count. No per-product lazy loading.</p>
<hr />
<h2>Design Decision: Avoiding the Global State Pitfall</h2>
<p>You might be wondering why we used <code>threading.local()</code> in Step 2 instead of a simple module-level variable like <code>_pending_changes = set()</code>.</p>
<p>A basic module-level set shares state across every request handled by the same worker process. If User A's request rolls back, stale refs from their failed transaction sit in the global set. When User B's request commits ten minutes later on the same worker, the flush accidentally picks up both User A's and User B's products.</p>
<p>By using <code>threading.local()</code>, we protect against this cross-request leakage. Even if a transaction rolls back, any leftover references in that thread's set will simply be cleared on its next successful commit. And in the rare event a stale reference makes it to the resolver, it evaluates to nothing and is skipped silently.</p>
<p>One thing to watch for: use <code>.filter()</code> when looking up products in your resolver, not <code>.get()</code>. <code>.filter()</code> returns an empty queryset gracefully. <code>.get()</code> throws <code>DoesNotExist</code> and tanks your flush.</p>
<hr />
<h2>The Result</h2>
<p>The user experience is deceptively simple. A user toggles "Sync product prices" on a store configuration page. From that moment on, any price change from any code path in the application syncs to the external marketplace within seconds.</p>
<p>No one has to remember to add a sync call to new features. No one has to track down every place a price can change. No one monitors a queue for failures (Celery retries handle that). The system just works.</p>
<p>How are you handling external API syncs in your Django apps? Are you using signals, or do you prefer a different pattern? Drop your approach in the comments.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[MLSA Bootcamp 2024 Postmortem: Insights and Reflections]]></title><description><![CDATA[Introduction


Brief Overview of the MLSA Bootcamp
The MLSA UNILAG (Microsoft Learn Student Ambassadors, University of Lagos) Bootcamp is an annual initiative aimed at equipping students with essential skills to thrive in the tech industry. The progr...]]></description><link>https://blog.acel.dev/mlsa-bootcamp-2024-postmortem-insights-and-reflections</link><guid isPermaLink="true">https://blog.acel.dev/mlsa-bootcamp-2024-postmortem-insights-and-reflections</guid><category><![CDATA[bootcamp]]></category><category><![CDATA[coding]]></category><category><![CDATA[tech ]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Mon, 30 Dec 2024 23:00:00 GMT</pubDate><content:encoded><![CDATA[<ol>
<li><h2 id="heading-introduction">Introduction</h2>
</li>
</ol>
<h3 id="heading-brief-overview-of-the-mlsa-bootcamp">Brief Overview of the MLSA Bootcamp</h3>
<p>The <a target="_blank" href="https://www.mlsaunilag.com.ng/">MLSA UNILAG</a> (Microsoft Learn Student Ambassadors, University of Lagos) Bootcamp is an annual initiative aimed at equipping students with essential skills to thrive in the tech industry. The program primarily focuses on software engineering, offering tracks such as UI/UX Design, Frontend Development, Backend Development, Cybersecurity, Cloud Computing, and more. Remarkably, this bootcamp is entirely free, driven by the dedication of volunteers—organizers, tutors, and facilitators—who generously contribute their time to this impactful cause.</p>
<h3 id="heading-my-role-and-involvement-as-an-organizer">My Role and Involvement as an Organizer</h3>
<p>As the leader of the tutor team and coordinator for the MLSA UNILAG Bootcamp, the responsibility for its success rested heavily on my shoulders. My role encompassed a wide array of tasks, essentially ensuring that all stakeholders—students, tutors, and organizers—were happy and satisfied throughout the bootcamp.</p>
<h3 id="heading-objective-of-the-postmortem">Objective of the Postmortem</h3>
<p>This postmortem aims to share insights, lessons learned, and strategies for improving future editions of the MLSA Bootcamp.</p>
<ol start="2">
<li><h2 id="heading-pre-planning-phase">Pre-Planning Phase</h2>
</li>
</ol>
<h3 id="heading-how-the-idea-for-the-bootcamp-came-about">How the Idea for the Bootcamp Came About</h3>
<p>The MLSA Bootcamp is a yearly tradition. The previous edition had significant challenges, but the 2024 edition was envisioned as a comeback, driven by the goal to start well and end well.</p>
<h3 id="heading-setting-goals-and-objectives-for-the-bootcamp">Setting Goals and Objectives for the Bootcamp</h3>
<p>The program was scheduled to begin in August, coinciding with the end of the second semester exams to maximize participation during the summer break. Key goals included seamless execution, robust planning, and an enhanced learning experience for all participants.</p>
<p>A physical planning meeting on July 13 set the stage for discussions on start and end dates, program duration, tutor selection, offered tracks, tools for video conferencing, and the creation of a comprehensive <a target="_blank" href="https://www.preceden.com/timelines/1139807-mlsa-unilag-bootcamp-2024/4e371257b150f5cd">timeline of events</a>.</p>
<p>One significant challenge was selecting an appropriate tool for classes. We needed a solution that supported recordings natively and was either free or cost-effective. As asking tutors to record with a separate app just wouldn't cut it, a lesson learnt from the previous edition. After exploring various tools like <a target="_blank" href="https://meet.google.com/landing">Google Meet</a>, <a target="_blank" href="https://www.microsoft.com/en-us/microsoft-teams/group-chat-software">Microsoft Teams</a>, <a target="_blank" href="https://www.zoom.com/">Zoom</a>, <a target="_blank" href="https://streamyard.com/">StreamYard</a> (my $5 in the bin 🥲) we decided to leverage Microsoft Teams’ premium features, available to UNILAG students and Microsoft Student ambassadors.</p>
<p>If you are wondering why that thought did not come to my head first, it is because bulk of the tutors were not students, neither were they student ambassadors. We had two people generally in charge of scheduling the classes on Teams. Start the recordings, then upload it to YouTube for all the tracks. Though managing recordings manually was cumbersome, it proved to be our best option.</p>
<h3 id="heading-initial-planning-organizing-a-team-defining-roles-and-establishing-timelines">Initial Planning: Organizing a Team, Defining Roles, and Establishing Timelines</h3>
<p>A “Call for Tutors” was initiated a month before the bootcamp, which generated some fascinating responses. The selection process was intended to be in two stages: reviewing applications and then scheduling interviews with those who passed the initial review. However, due to time constraints (with school still in session, exams and my final year project to deal with), we weren't able to conduct interviews. Despite that, the review process was enjoyable.</p>
<p>I'm fairly strict when it comes to reviewing applications, so to balance it out, <a target="_blank" href="https://www.linkedin.com/in/ademola-thompson">Ademola</a> and I both participated in the reviews. We rated each application on a scale of 10, and the final score was the average of our ratings. Once again, I was reminded of the challenges faced by hiring managers. At the same time, I was delighted by some of the applications. I remember telling Ademola, “Guy, the people applying to tutor this track—where did they even see the form? Shey na me go lead these ones like this? God abeg.” Their applications were so impressive that I found myself questioning whether I was truly suited to work with them. A bit of imposter syndrome, ha-ha!</p>
<ol start="3">
<li><h2 id="heading-bootcamp-preparation">Bootcamp Preparation</h2>
</li>
</ol>
<h3 id="heading-curriculum-design-amp-content-development">Curriculum Design &amp; Content Development</h3>
<p>The availability of tutors influenced the final list of tracks. Tutors were tasked with developing curriculum, learning materials, and projects, which were reviewed for quality assurance. This resulted in 14 tracks initially, although tutor withdrawals later reduced the count to 10 active tracks.</p>
<h3 id="heading-marketing-amp-outreach">Marketing &amp; Outreach</h3>
<p>The Design and Content teams created promotional materials and leveraged social media and group chats to drive registrations. Over 360 applications were received, far exceeding expectations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735638053032/ca1fdc3f-2ad7-4866-8d4c-33c4c5160f55.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li><h2 id="heading-execution-bootcamp-period">Execution (Bootcamp Period)</h2>
</li>
</ol>
<h3 id="heading-day-to-day-operations">Day-to-Day Operations</h3>
<p>Day 1 was a mess! 😂 It was Monday, August 26th, and participants were still joining the group chats. Let's rewind a bit. The first batch of invites went out to students on Saturday, just two days before the 26th. Initially, we planned to offer 14 tracks, but we had some tutors back out. It’s not ideal, but I’m grateful it happened before the program started—could’ve been worse.</p>
<p>As a result, we had to scrap a track, it could’ve been five. And we were fortunate to get last-minute replacements just in time for the other tracks. The UI/UX track also started a week late. Why did all this bother me so much? Because students had already applied, and I really didn’t want to have to say, "Sorry, we messed up."</p>
<p>Now, back to Day 1. Two classes were held that day—Data Analysis and Machine Learning—via Microsoft Teams. The recordings worked, and we uploaded them to our <a target="_blank" href="https://youtube.com/@mlsaunilag?si=QMQOedQgjGZ79Ish">YouTube channel</a>. That was really the only positive of the day for me.</p>
<p>The next day, we had our <a target="_blank" href="https://youtu.be/Q-tqGMKeWiU?si=MtReiHHl0xXuBfSv">on-boarding call</a>. Why was it a day late? My excuse was that we wanted more students to join the group chats, but honestly, I just didn’t feel up to it. I was exhausted and didn’t see it as essential. But Paul pushed for it, and some tutors asked about it, so I did it—stressed, but it went well. I gave an overview of the program, shared some advice and encouragement, and we played a quick game of <a target="_blank" href="https://kahoot.com/">Kahoot</a>.</p>
<p>The rest of the week went as expected: classes for different tracks, and our first game night that Friday. It became a weekly tradition throughout the bootcamp. Friday, 8PM—get online, good internet connection, and have fun on the random channel. I did have a little fun teasing the students once by claiming the winner would get a Pixel 9 Pro, he-he.</p>
<p>By the second week, we had a fixed schedule for most of the tracks, which made setting up Teams calls and monitoring everything much easier. We also introduced something Confidence suggested—<strong>Student of the Week</strong>. Every week, we highlighted a student who stood out, providing a motivational boost for everyone.</p>
<h3 id="heading-challenges-and-solutions">Challenges and Solutions</h3>
<p>By the fourth week, issues such as poor class attendance, incomplete assignments, and lack of enthusiasm arose. Addressing these challenges involved:</p>
<ul>
<li><p>Removing inactive students from track groups.</p>
</li>
<li><p>Modifying class schedules and teaching methods based on student feedback.</p>
</li>
<li><p>Conducting one-on-one sessions with students to understand and address their concerns.</p>
</li>
</ul>
<p>These adjustments helped boost participation and ensured steady progress across all tracks. One important conversation with a struggling student highlighted the importance of personalized support in building resilience and motivation.</p>
<ol start="5">
<li><h2 id="heading-post-bootcamp-activities">Post-Bootcamp Activities</h2>
</li>
</ol>
<h3 id="heading-graduation-amp-wrap-up">Graduation &amp; Wrap-Up</h3>
<p>The graduation ceremony on October 26 was streamed <a target="_blank" href="https://www.youtube.com/live/lxAWE7fCla0?si=vk-jsPJXBYvX029r">live on YouTube</a> and featured talks from esteemed speakers; <a target="_blank" href="https://www.linkedin.com/in/afeez-lekan-3275b0164/">Afeez Lekan</a> and <a target="_blank" href="https://www.linkedin.com/in/olayinkathannah/">Olayinka T. Hannah</a>, project demonstration by <a target="_blank" href="https://www.linkedin.com/in/olamide-soyebo-537212235/">Olamide Soyebo</a>, and awards for top-performing students. Key statistics included:</p>
<ul>
<li><p>373 applicants</p>
</li>
<li><p>37 graduates</p>
</li>
<li><p>14 tracks offered, with graduates from 10 tracks</p>
</li>
</ul>
<p>Certificates were issued a week later to all graduates. As I watched the ceremony unfold, I couldn’t help but feel proud of how far we had come as a team and as a community. It wasn’t just about the projects or presentations—it was about the journey that brought us here, a reflection of everyone’s effort and commitment. Easily one of my highlights of the year.</p>
<h3 id="heading-feedback-collection-and-follow-up-initiatives">Feedback Collection and Follow-Up Initiatives</h3>
<p>Although formal feedback collection was deferred due to exhaustion, participants were encouraged to join the <a target="_blank" href="https://forms.office.com/pages/responsepage.aspx?id=oBzDhDusrk6tEVGdgCM-b2rhIZyiDIRMq6jycZEfjHlUQUVUU0REQTFCSE40WlFKVjlKU0JaWUxMRi4u">MLSA UNILAG community</a> for continued collaboration and growth. Plans for an alumni group remain under consideration for future editions.</p>
<ol start="6">
<li><h2 id="heading-key-takeaways-amp-lessons-learned">Key Takeaways &amp; Lessons Learned</h2>
<ul>
<li><p>Coordinating a bootcamp requires unwavering commitment and resilience.</p>
</li>
<li><p>Financial resources are crucial for enhancing the participant experience through giveaways, games, and premium services.</p>
</li>
<li><p>Ensure at least two tutors per track to mitigate the risk of last-minute withdrawals.</p>
</li>
<li><p>Patience and people-management skills are essential for navigating diverse challenges.</p>
</li>
<li><p>Investing in enterprise-level tools and an LMS (Learning Management System) can significantly streamline operations.</p>
</li>
<li><p>Gamification and personalized recognition (e.g., “Student of the Week”) boost engagement and morale.</p>
</li>
</ul>
</li>
</ol>
<p>    It’s important to have someone you can turn to when things get tough. I hit a major roadblock in the middle of the bootcamp, feeling completely lost. I shared my worries and concerns with Geektutor, and he was very helpful in guiding me on how to navigate them.</p>
<hr />
<ol start="6">
<li><h2 id="heading-acknowledgments">Acknowledgments</h2>
</li>
</ol>
<p>The MLSA Bootcamp 2024 was a collective effort, and its success would not have been possible without the remarkable contributions of the following individuals:</p>
<h4 id="heading-tutors"><strong>Tutors</strong></h4>
<ul>
<li><p><a target="_blank" href="https://www.linkedin.com/in/connectmatthew">Matthew Oluwaniyi</a> - Cybersecurity</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/azeez-aweda-35908517b">Azeez Aweda</a>- Cybersecurity</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/victoria-robinson-13610919a">Victoria Robinson</a> - Cybersecurity</p>
</li>
<li><p><a target="_blank" href="http://www.linkedin.com/in/motunrayo3156">Motunrayo Sanusi</a> - Cloud Computing (AWS), Cybersecurity</p>
</li>
<li><p><a target="_blank" href="http://www.linkedin.com/in/israel-oluwasegun">Segun Isreal Makinde</a> - Data Analysis, Data science/Machine learning</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/demilade-kolawole-jacobs">Kolawole-Jacobs Demilade</a> - Data science/Machine learning</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/evergreenodeh">Evergreen Odeh</a> - UI/UX Design (Figma)</p>
</li>
<li><p><a target="_blank" href="http://www.linkedin.com/in/sulaimontaofik">Sulaimon Taofik</a> - Frontend Development (Vue)</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/tanitoluwa-ifegbesan-3614b6234/">Tanitoluwa Ifegbesan</a> - Frontend Development(React)</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/abdulmalik-alayande-b49814250/">Abdulmalik Alayande</a> - Backend Development (Django)</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/adewole-akorede">Adewole Akorede</a> - Backend Development (NodeJs)</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/adebesin-adewunmi-334897220/">Adebesin Adewunmi</a> - Backend Development (Laravel)</p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/divine-favour-chinedu-b464121b5">Divine-Favour Chinedu</a> - Smart contract development (solidity)</p>
</li>
</ul>
<h4 id="heading-facilitators"><strong>Facilitators</strong></h4>
<ul>
<li><p>Ademola Thompson</p>
</li>
<li><p>Confidence Ufuoma</p>
</li>
<li><p>Daniel Ebabhi</p>
</li>
<li><p>Olatunbosun Ayinla &amp; the Content/Community Management team</p>
</li>
<li><p>Abdulbasit Adesokan &amp; the Design team</p>
</li>
<li><p>Debola Oyeniran &amp; the Events team</p>
</li>
<li><p>Paul Asalu</p>
</li>
</ul>
<p>Each of you played an indispensable role in making this vision a reality. Thank you for being a part of this incredible journey.</p>
<ol start="7">
<li><h2 id="heading-conclusion">Conclusion</h2>
</li>
</ol>
<p>The MLSA Bootcamp 2024 was more than an initiative—it was a testament to the power of collaboration, resilience, and shared vision. I am immensely grateful to everyone who made this possible: the dedicated tutors who went above and beyond, the facilitators who kept the gears turning, and the students whose determination inspired us all.</p>
<p>Looking back, this bootcamp wasn’t just about teaching tech skills—it was about building a community, igniting passions, and fostering growth. As I reflect on the journey, I am filled with pride and excitement for what lies ahead. Here’s to many more impactful editions of the MLSA Bootcamp!</p>
<p><strong>¡Hasta Luego!</strong></p>
]]></content:encoded></item><item><title><![CDATA[How to Get Free SSL Certificates with Docker & LetsEncrypt]]></title><description><![CDATA["An SSL certificate is a digital certificate that authenticates a website's identity and enables an encrypted connection. SSL stands for Secure Sockets Layer, a security protocol that creates an encry]]></description><link>https://blog.acel.dev/how-to-get-free-ssl-certificates-with-docker-letsencrypt</link><guid isPermaLink="true">https://blog.acel.dev/how-to-get-free-ssl-certificates-with-docker-letsencrypt</guid><category><![CDATA[Docker]]></category><category><![CDATA[Let's Encrypt]]></category><category><![CDATA[SSL]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Tue, 23 Jul 2024 20:45:00 GMT</pubDate><content:encoded><![CDATA[<p>"An SSL certificate is a digital certificate that authenticates a website's identity and enables an encrypted connection. SSL stands for Secure Sockets Layer, a security protocol that creates an encrypted link between a web server and a web browser." - kaspersky</p>
<p>Majority of websites built today use SSL certificates, and if you are building a website in 2024, you will need to learn how to get one too!</p>
<p>There are several approaches to getting an SSL certificate for your domain. But in this article, we will take a look at generating SSL certificates with <a href="https://letsencrypt.org/">Let's Encrypt</a> - a nonprofit certificate authority (CA) that provides free SSL/TLS certificates for enabling HTTPS (secure HTTP) on websites. In addition to this, automating the renewal process with <a href="https://certbot.eff.org/">Certbot</a> and using a Docker image that was built on top of the official Nginx Docker images.</p>
<h2>Prerequisites</h2>
<p>To successfully complete this guide, you should be familiar with the following:</p>
<ul>
<li><p>Docker and Docker Compose</p>
</li>
<li><p>Virtual machines from cloud providers, e.g. Azure VMs, AWS EC2 etc.</p>
</li>
<li><p>Linux</p>
</li>
</ul>
<p>It is a good idea to containerize your app with Docker, but if you don't want to, you can still follow along just fine!</p>
<h2>Configuring Certbot and Nginx</h2>
<p>In the root directory of your project, please create a docker-compose file.</p>
<pre><code class="language-bash">touch docker-compose.yml
</code></pre>
<p>Then paste the following code in the docker-compose.yml file.</p>
<pre><code class="language-yaml">version: '3'

services:
  nginx:
    image: jonasal/nginx-certbot:latest
    restart: unless-stopped
    environment:
      - CERTBOT_EMAIL
    env_file:
      - ./nginx-certbot.env
    ports:
      - 80:80
      - 443:443
    volumes:
      - nginx_secrets:/etc/letsencrypt
      - ./user_conf.d:/etc/nginx/user_conf.d

volumes:
  nginx_secrets:
</code></pre>
<p>The code we just pasted above is a YAML configuration for our Nginx docker image. It makes use of the <code>jonasal/nginx-certbot:latest</code> image that has certbot built on top of Nginx.</p>
<p>Now, let us create a <code>nginx-certbot.env</code> file to store the variables needed for our Nginx image.</p>
<pre><code class="language-bash">touch nginx-certbot.env
</code></pre>
<p>Then paste this into your <code>nginx-certbot.env</code> file</p>
<pre><code class="language-text"># Required
CERTBOT_EMAIL=your@email.com

# Optional (Defaults)
DHPARAM_SIZE=2048
RSA_KEY_SIZE=2048
ELLIPTIC_CURVE=secp256r1
RENEWAL_INTERVAL=8d
USE_ECDSA=1
STAGING=1
</code></pre>
<p>Next, we create a server configuration file to configure our Nginx server.</p>
<pre><code class="language-bash">touch server.conf
</code></pre>
<pre><code class="language-text">server {
    # Listen to port 443 on both IPv4 and IPv6.
    listen 443 ssl default_server reuseport;
    listen [::]:443 ssl default_server reuseport;

    # Domain names this server should respond to.
    server_name yourdomain.com www.yourdomain.com;

    # Load the certificate files.
    ssl_certificate         /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem;

    # Load the Diffie-Hellman parameter.
    ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;

    # Redirect non-https traffic to https
    if ($scheme != "https") {
        return 301 https://\(host\)request_uri;
    }

    return 200 'Let\'s Encrypt certificate successfully installed!';
    add_header Content-Type text/plain;
}
</code></pre>
<p>Now, all we need to do is build the docker image and run it on our server.</p>
<pre><code class="language-bash">docker compose -f docker-compose.yml build
</code></pre>
<p>Next, move the images to the server whose IP address is mapped to a domain name you own and run this command:</p>
<pre><code class="language-bash">docker compose -f docker-compose.yml up
</code></pre>
<p>The command starts the server and Certbot automatically creates new SSL certificates for us. Try navigating to the domain name mapped to the server, you should see a "not trusted" page.</p>
<p>This is because we used "test" certificates to make sure that our configuration works fine. Issuing of live certificates is rate limited, so using test/staging certificates first is a better approach to ensure that it runs fine.</p>
<p>Now, we will update our <code>nginx-certbot.env</code> file to enable us generate live SSL certificates. Change the staging value from 1 to 0.</p>
<pre><code class="language-text">...
STAGING=0
</code></pre>
<p>Stop the server with ctrl+c and run the command below to regenerate SSL certificates.</p>
<pre><code class="language-bash">docker exec -it &lt;container_name&gt; /scripts/run_certbot.sh force
</code></pre>
<p>Start up the server again</p>
<pre><code class="language-bash">docker compose -f docker-compose.yml up
</code></pre>
<p>Now, navigate to your domain name. Your site should open up just fine!</p>
<h2>Certbot and Nginx configurations for Dockerized apps</h2>
<p>Assuming you dockerized your app, you will need to make a few more changes to what we have done so far. First, we update the docker-compose.yml file so that the Nginx service depends on your app.</p>
<pre><code class="language-plaintext">    ...
    volumes:
      - nginx_secrets:/etc/letsencrypt
      - ./user_conf.d:/etc/nginx/user_conf.d
    depends_on:
      - web

  web:
    container_name: core_app
    build: .
    restart: always
    ...

volumes:
  nginx_secrets:
</code></pre>
<p>Where <code>web</code> is the name of your app service.</p>
<p>Then, we update the <code>server.conf</code> file to point to our web server.</p>
<pre><code class="language-plaintext">upstream webapp {
    server core_app:8000;
    # core_app is the container name of the web server
    # 8000 is the port for the web container
}
server {
    # Listen to port 443 on both IPv4 and IPv6.
    listen 443 ssl default_server reuseport;
    listen [::]:443 ssl default_server reuseport;

    # Domain names this server should respond to.
    server_name yourdomain.com www.yourdomain.com;

    server_tokens off;
    client_max_body_size 20M;

    # Load the certificate files.
    ssl_certificate         /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem;

    # Load the Diffie-Hellman parameter.
    ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;

    # Redirect non-https traffic to https
    if ($scheme != "https") {
        return 301 https://\(host\)request_uri;
    }

    location / {
        proxy_pass http://webapp;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
}
</code></pre>
<p>Now, Nginx should be routing traffic directly to your web server (docker container).</p>
<h2>Conclusion</h2>
<p>We have successfully created free SSL certificates for our domain name that renew automatically, so we never have to worry about it unless our server goes down.</p>
<p>In addition to this article, you can learn more about this approach here: <a href="https://github.com/JonasAlfredsson/docker-nginx-certbot/tree/master">https://github.com/JonasAlfredsson/docker-nginx-certbot/tree/master</a></p>
<p>Thanks for reading.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[Deploying your Django app on Heroku]]></title><description><![CDATA[In this article, we will be deploying our app on Heroku. Heroku is a Platform as a Service (PAAS) and they offer web server and database hosting which is sufficient to deploy our app to the web.
Before moving on with this tutorial, there is a prerequ...]]></description><link>https://blog.acel.dev/deploying-your-django-app-on-heroku</link><guid isPermaLink="true">https://blog.acel.dev/deploying-your-django-app-on-heroku</guid><category><![CDATA[Heroku]]></category><category><![CDATA[Django]]></category><category><![CDATA[django rest framework]]></category><category><![CDATA[deployment]]></category><category><![CDATA[PaaS]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Thu, 19 Jan 2023 15:29:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674141806583/9bc3794f-8587-467f-bc6e-b6010e993abe.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we will be deploying our app on <a target="_blank" href="https://www.heroku.com/">Heroku</a>. Heroku is a <a target="_blank" href="https://azure.microsoft.com/en-us/resources/cloud-computing-dictionary/what-is-paas/">Platform as a Service</a> (PAAS) and they offer web server and database hosting which is sufficient to deploy our app to the web.</p>
<p>Before moving on with this tutorial, there is a prerequisite which is detailed in <a target="_blank" href="https://blog.acel.cyou/deploying-your-django-app">this guide</a>. You can call it pre-deployment. Please go through it and come back once it's checked ☑️</p>
<p>Also, you need to create an account on Heroku(obviously).</p>
<p>Now, let's begin</p>
<h2 id="heading-procfile">Procfile</h2>
<p>This tells heroku how to use our app.</p>
<pre><code class="lang-plaintext">touch Procfile
</code></pre>
<p>The configuration varies by the app but this should do. Add the configuration below to your Procfile</p>
<pre><code class="lang-plaintext">release: python manage.py makemigrations bookgrid_app
release: python manage.py migrate

web: gunicorn bookgrid_proj.wsgi
</code></pre>
<p>Where ‘bookgrid_proj’ is the name of your django project and ‘bookgrid_app’ is the name of your Django app.</p>
<h2 id="heading-runtime-file">Runtime File</h2>
<p>This tells Heroku what version of Python to use for our app</p>
<pre><code class="lang-plaintext">touch runtime.txt
</code></pre>
<p>Open the file and paste the python version below. Or anyone most compatible with your app and heroku from this <a target="_blank" href="https://devcenter.heroku.com/articles/python-support#supported-runtimes">list</a></p>
<pre><code class="lang-plaintext">python-3.8.12
</code></pre>
<h2 id="heading-django-heroku">Django-Heroku</h2>
<p>This is a Django library for Heroku applications that ensures a seamless deployment and development experience. It is very much needed if you are deploying your app on Heroku</p>
<p>To install the package run</p>
<pre><code class="lang-plaintext">pip install django-heroku
</code></pre>
<p>In your settings.py file, at the very bottom add</p>
<pre><code class="lang-plaintext">import django_heroku
django_heroku.settings(locals())
</code></pre>
<h2 id="heading-heroku-deployment">Heroku Deployment</h2>
<p>You need to have an account with heroku. If you don't, create one <a target="_blank" href="https://signup.heroku.com/">here</a>.</p>
<p>Login via the command line or terminal with</p>
<pre><code class="lang-plaintext">heroku login
</code></pre>
<p>Then create a new heroku app, where acel-app is the name you wish to give your app</p>
<pre><code class="lang-plaintext">heroku create acel-app
</code></pre>
<p>Now configure git so that when you push to Heroku, it goes to your new app name</p>
<pre><code class="lang-plaintext">heroku git:remote -a acel-app
</code></pre>
<p>Then create a PostgresSQL database on heroku for our app</p>
<pre><code class="lang-plaintext">heroku addons:create heroku-postgresql:hobby-dev
</code></pre>
<p>And manually add our environment variables except DEBUG and DATABASE_URL</p>
<pre><code class="lang-plaintext">heroku config:set SECRET_KEY='(-e%3z3yp5qsirfl6_+9=ko#!r6%0am8=^x9a))p2)3y-24g%*'
</code></pre>
<p>If we had other things in our .env file like email host or cloud storage configuration for images and videos, we’d have to manually add it to heroku as well</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635277922545/KDCJq3tBq.png" alt="Screenshot from 2021-10-26 20-49-53.png" /></p>
<p>Using email host user and email host password in the screenshot above as an example</p>
<pre><code class="lang-plaintext">heroku config:set EMAIL_HOST_USER='mattew@gmail.com'
heroku config:set EMAIL_HOST_PASSWORD='yyuiuilo7tuy'
</code></pre>
<p>Now it’s time to push our code up to Heroku itself and start a web process so our Heroku dyno is running.</p>
<pre><code class="lang-plaintext">git push heroku main
</code></pre>
<pre><code class="lang-plaintext">heroku ps:scale web=1
</code></pre>
<p>The URL of your new app will be in the command line output or you can run heroku open to find it.</p>
<p>Now we need to migrate our PostgresSQL database and create a superuser</p>
<pre><code class="lang-plaintext">heroku run python manage.py makemigrations bookgrid_app
</code></pre>
<pre><code class="lang-plaintext">heroku run python manage.py migrate
</code></pre>
<pre><code class="lang-plaintext">heroku run python manage.py createsuperuser
</code></pre>
<p>AND WE ARE DONE AMIGOS!</p>
<p>Thanks for following to the end and I hope it was helpful.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[Deploying your Django app on Railway]]></title><description><![CDATA[In this article, we will be deploying our Django app on Railway. Railway is a Platform as a Service (PAAS) and they offer web server and database hosting which is sufficient to deploy our app to the web.
Before moving on with this tutorial, there is ...]]></description><link>https://blog.acel.dev/deploying-your-django-app-on-railway</link><guid isPermaLink="true">https://blog.acel.dev/deploying-your-django-app-on-railway</guid><category><![CDATA[railway]]></category><category><![CDATA[Django]]></category><category><![CDATA[django rest framework]]></category><category><![CDATA[railway-app]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Thu, 19 Jan 2023 15:07:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674137652121/22b79514-194d-4685-aa34-268aba621ffd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we will be deploying our Django app on <a target="_blank" href="https://railway.app/">Railway</a>. Railway is a <a target="_blank" href="https://azure.microsoft.com/en-us/resources/cloud-computing-dictionary/what-is-paas/">Platform as a Service</a> (PAAS) and they offer web server and database hosting which is sufficient to deploy our app to the web.</p>
<p>Before moving on with this tutorial, there is a prerequisite which is detailed in <a target="_blank" href="https://blog.acel.cyou/deploying-your-django-app">this guide</a>. You can call it pre-deployment. Please go through it and come back once it's checked ☑️</p>
<p>Also, you need to create an account on Railway(obviously).</p>
<p>Now, let's begin.</p>
<h3 id="heading-installing-nixpacks">Installing Nixpacks</h3>
<p>Run the command below in your terminal if you use <a target="_blank" href="https://ubuntu.com/">Ubuntu</a> or <a target="_blank" href="https://learn.microsoft.com/en-us/windows/wsl/">WSL2</a>. Else you can check for other methods <a target="_blank" href="https://nixpacks.com/docs/install">here</a>.</p>
<pre><code class="lang-plaintext">curl -LO https://github.com/railwayapp/nixpacks/releases/download/v2.0.11/nixpacks-v2.0.11-amd64.deb sudo dpkg -i nixpacks-v2.0.11-amd64.deb
</code></pre>
<h3 id="heading-installing-railway-cli">Installing Railway CLI</h3>
<p>You can install this using the shell script below if you're on macOS, Linux or Windows via WSL. Just run the command in your applicable terminal. You can check for other methods <a target="_blank" href="https://docs.railway.app/develop/cli#installation">here</a>.</p>
<pre><code class="lang-plaintext">bash &lt;(curl -fsSL cli.new)
</code></pre>
<h3 id="heading-login">Login</h3>
<pre><code class="lang-plaintext">railway login
</code></pre>
<h3 id="heading-create-a-project">Create a Project</h3>
<p>A project is like a <a target="_blank" href="https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations">Github Organization</a> or an <a target="_blank" href="https://devcenter.heroku.com/articles/pipelines">Heroku Pipeline</a> that defaults to the production environment. You can also create other environments for staging, testing, etc</p>
<pre><code class="lang-plaintext">railway init
</code></pre>
<h3 id="heading-associate-directory-to-project">Associate Directory to Project</h3>
<p>Basically, this links the current directory (folder) you're in, to your Railway project. Very useful, especially as you can have multiple projects.</p>
<pre><code class="lang-plaintext">railway link
</code></pre>
<p>Navigate with the arrow keys and hit enter to select a project.</p>
<h3 id="heading-environment-variables">Environment Variables</h3>
<p>Railway may have asked you to let them set up your environment variables from your .env file while you were deploying with <code>railway up</code>. That works but you may want to modify or add more. To see all existing environment variables, run</p>
<pre><code class="lang-plaintext">railway variables
</code></pre>
<p>To set or modify a variable</p>
<pre><code class="lang-plaintext">railway variables set {VARIABLE_NAME}:{VARIABLE_VALUE}
</code></pre>
<h3 id="heading-procfile">Procfile</h3>
<p>Procfiles are a popular format used to specify a web/release process. To use this you need to create a Procfile in your root directory.</p>
<pre><code class="lang-plaintext">touch Procfile
</code></pre>
<p>Then paste this into it</p>
<pre><code class="lang-plaintext">web: gunicorn {YOUR_DJANGO_PROJECT_NAME}.wsgi

release: python manage.py migrate
</code></pre>
<h3 id="heading-deploy">Deploy</h3>
<p>At this point, our app should be ready for a smooth deployment. You can do this with</p>
<pre><code class="lang-plaintext">railway up
</code></pre>
<p>It then builds and deploys your app on Railway 🎉</p>
<h3 id="heading-nixpacks">Nixpacks</h3>
<p>If you had a successful deployment, you can skip this. However, if you have a psycop2g dependency, you'll run into an error while trying to deploy (as at the time of writing this). The fix for this is to install a <code>postgresql</code> nix package. First, create a file called <code>nixpacks.toml</code> in your root directory <code>touch nixpacks.toml</code> Then paste this snippet into it <code>[phases.setup] nixPkgs = ["...", "postgresql"]</code></p>
<h3 id="heading-port">Port</h3>
<p>After deploying your app, you need to set up a port number as Railway doesn't do this for us out of the box. Login to Railway on your web browser. Navigate to your project, then to your web deployment, then to your settings, and then set your port to <code>8000</code> as is the default for Django. Or whatever you want to use.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674139978218/74f7a585-46b6-44f6-b1d8-48d4420e2f5f.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-shell-commands">Shell commands</h3>
<p>There are where we would need to run some commands directly in the hosted environment. With Railway, we can do this in a subshell that makes use of our hosted app's variables.</p>
<pre><code class="lang-plaintext">railway shell
</code></pre>
<p>This opens up that subshell. And for example, we can then create a superuser as we usually do with</p>
<pre><code class="lang-plaintext">python manage.py createsuperuser
</code></pre>
<h3 id="heading-domain-name">Domain name</h3>
<p>After our app is deployed, we need to generate a domain name for it so that it becomes accessible as Railway does not generate one by default. Pretty much the same process for setting up our <a class="post-section-overview" href="#heading-port">port</a> earlier.</p>
<h3 id="heading-end">End</h3>
<p>And that's all that needs to be done! Thanks for following through to the end, and let me know if you encountered any issue not mentioned here.</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[2021 Year in Review]]></title><description><![CDATA[Started this year as a Frontend Dev, ending it as a Backend Dev
January
Built my portfolio site 
Applied for a couple of jobs 
February
Applied for a couple of jobs 
I was losing interest in Frontend Development cause it didn't seem to be leading any...]]></description><link>https://blog.acel.dev/2021-year-in-review</link><guid isPermaLink="true">https://blog.acel.dev/2021-year-in-review</guid><category><![CDATA[learning]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[review]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Thu, 30 Dec 2021 22:00:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640901157298/eTXvNrmaFo.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Started this year as a Frontend Dev, ending it as a Backend Dev</p>
<h3 id="heading-january">January</h3>
<p>Built my <a target="_blank" href="https://acel.cyou">portfolio site</a> </p>
<p>Applied for a couple of jobs </p>
<h3 id="heading-february">February</h3>
<p>Applied for a couple of jobs </p>
<p>I was losing interest in Frontend Development cause it didn't seem to be leading anywhere for me. Also, I'd later find out I prefer to write C than write JavaScript</p>
<p>So I decided to dabble into Data Science</p>
<h3 id="heading-march">March</h3>
<p>Applied for and got into Data Science Nigeria’s AI Wednesday program to learn machine learning</p>
<p>Completed and got the certificate for it in April</p>
<p>After completing the AI Wednesday program, I learnt Data Science was a field I didn't want to go into. </p>
<p>At this point, <a target="_blank" href="https://internship.zuri.team/">Zuri</a> was open for internship applications so I used it as an opportunity to try out another field</p>
<h3 id="heading-april">April</h3>
<p>I delved into backend development this time, with Django. </p>
<p>Zuri began fully this month. My track started with Python (which was basically a refresher for me) before moving to Django </p>
<h3 id="heading-may">May</h3>
<p>Learning Django at Zuri</p>
<p>First semester exams in school (I think)</p>
<h3 id="heading-june">June</h3>
<p>At Zuri, we had completed the learning phase and now in the project phase </p>
<p>I was the team lead for my team. </p>
<p>Leading the team was quite tasking cause though I had some experience leading a team (this was not my first internship), it is a different ball game when you have beginner level knowledge of your stack. </p>
<p>I had not even done a “Hello World” with Django Rest Framework before then, but we were able to get some work done. Luckily, there was someone more experienced with the stack on our team who helped out a lot </p>
<h3 id="heading-july">July</h3>
<p>Finished the project phase of the internship at Zuri</p>
<p>Couldn't complete the project. And though we had several endpoints ready from the backend team, when the only thing to show for it is a working Sign up, Log in and Homepage, the endpoints don't matter. Especially when you're the overall team lead </p>
<p>Also, dey change am for designers once in a while.</p>
<h3 id="heading-august">August</h3>
<p>Was focusing on school after the four-month internship </p>
<p>Started learning cloud computing with GCP on Pluralsight</p>
<h3 id="heading-september">September</h3>
<p>Second semester exams </p>
<p>Aced the first phase exam of the cloud computing program on course(s) I was taking</p>
<p>Learned a lot about the cloud and how GCP works </p>
<p>I was actually learning with someone else's account who got access to it through a GADS scholarship</p>
<p>Considering; how time consuming the course was, the certificate wouldn't be in my name(in a field that more certificate-first), I decided to discontinue learning. Thankful for the opportunity to have learnt what I did </p>
<h3 id="heading-october">October</h3>
<p>Took a Coursera course on Django offered by the University of Michigan through AUTC</p>
<p>Applied (volunteered) for Tutor roles at ECX and GDSC UNILAG. Got both </p>
<p>Continued learning cloud computing, but this time through MTC-UNILAG on Microsoft Azure</p>
<h3 id="heading-november">November</h3>
<p>Started teaching Django at GDSC</p>
<p>Attended the UnStack Conference</p>
<p>Attended DevFest Lagos</p>
<p>Met friends I had made online </p>
<p>Made new ones too </p>
<h3 id="heading-december">December</h3>
<p>Still tutoring the Django track </p>
<p>Still learning cloud computing with Azure </p>
<p>Finally got a mentor 🥺, He doesn't know it yet. Lemme say Pseudo-mentor for now  </p>
<h3 id="heading-the-good">The Good</h3>
<ul>
<li>Ticked off 4/7 things on my todo list for 2021</li>
<li>Connected with people a lot more this year. Both online and offline</li>
<li>Developed some level of proficiency in a skill I feel at home with</li>
<li>Got a mentor</li>
<li>Didn't lose any friend, nor did any friend lose a loved one</li>
</ul>
<h3 id="heading-the-bad">The Bad</h3>
<ul>
<li>Hopped around a lot from one tech stack to the other</li>
<li>Didn't get a paid gig or job this year</li>
<li>Was too comfortable doing things on my own without getting guidance from others. Led to a lot of time wastage. Not having a mentor didn't help either</li>
</ul>
<h3 id="heading-goals-for-2022">Goals for 2022</h3>
<ul>
<li>Get a paid job/gig. Then do like 15x before the year runs out</li>
<li>Get better grades in school</li>
<li>Purchase a Real Madrid jersey from the official store</li>
<li>Network more. Build on top of existing relationships</li>
</ul>
<h3 id="heading-takeaways">Takeaways</h3>
<ul>
<li>Get a mentor. Or at least have people you can ask for help and advice</li>
<li>Pick a skill and learn it well. Don't be tech rabbit</li>
<li>Network. Make friends. Except you're exceptionally brilliant, being introverted won't do you much good</li>
<li>Write articles. I'm not an article person myself but writing articles about bugs you fix, or how you added a feature etc will help you later on. You don't wanna open multiple stackoverflow tabs to everytime to do something you've done before. Trust me on that</li>
</ul>
<p>Thanks for reading and I wish you an outstanding 2022 🎊</p>
]]></content:encoded></item><item><title><![CDATA[Setting up your SMTP Server with Mailjet]]></title><description><![CDATA[Hello there 👋. In this article, I'll be walking you through on

creating your mailjet account
creating the SMTP server on mailjet 
installing the django-mailjet package
connecting it to your django app

Though this article is gonna be focusing on Dj...]]></description><link>https://blog.acel.dev/setting-up-your-smtp-server-with-mailjet</link><guid isPermaLink="true">https://blog.acel.dev/setting-up-your-smtp-server-with-mailjet</guid><category><![CDATA[smtp]]></category><category><![CDATA[Django]]></category><category><![CDATA[email]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Wed, 29 Dec 2021 20:58:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/VybzKEUMhbw/upload/v1640810852321/Dvia0_CRv.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello there 👋. In this article, I'll be walking you through on</p>
<ul>
<li>creating your mailjet account</li>
<li>creating the SMTP server on mailjet </li>
<li>installing the django-mailjet package</li>
<li>connecting it to your django app</li>
</ul>
<p>Though this article is gonna be focusing on Django as the backend, the same process should apply to whatever backend framework you wish to use. You'll just have to check if there's a package for mailjet that works with your framework</p>
<p><strong>NB:</strong> You'll need to have knowledge of Django or whatever backend framework you use. We are simply replacing the default SMTP server provided in django with a better option. And yes, it's free! 😙</p>
<h3 id="heading-creating-your-mailjet-account">Creating your Mailjet Account</h3>
<p>Click on this  <a target="_blank" href="https://app.mailjet.com/signup">link</a>  and let's get started. That should lead you to the page below. Enter your email address and password, then click on sign up.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640803914892/bSr_5kUvI5.png" alt="Screenshot from 2021-12-29 19-50-05.png" /></p>
<p>That takes up to the page below. If you are currently unemployed or a student, you can fill in <strong>Organization name</strong> with your name, <strong>Organization URL</strong> with a link to your portfolio or blog. Then select <strong>Other</strong> under <strong>Main industry</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640804335140/oWH9CdspX7.png" alt="Screenshot from 2021-12-29 19-54-46.png" /></p>
<p>You don't need to add a payment method. Click on <strong>Complete order</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640804597982/-vsc4PvWa.png" alt="Screenshot from 2021-12-29 20-01-47.png" /></p>
<p>You should get an email from mailjet to activate your account</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640805436594/TlmIwAB_J.png" alt="Screenshot edited from 2021-12-29 20-04-21.png" /></p>
<h3 id="heading-creating-the-smtp-server-on-mailjet">Creating the SMTP server on mailjet</h3>
<p>After activating it, you'll be led to this page. Click on getting started as a developer</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640805987525/fEwL-59u0.png" alt="Screenshot from 2021-12-29 20-23-10.png" /></p>
<p>What we need is a SMTP server so click on SMTP relay and continue</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640806123471/-2xkc28IR.png" alt="Screenshot from 2021-12-29 20-27-11.png" /></p>
<p>And here it is folks. We have our SMTP server set up and begging to be used!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1640806530663/R0IcFTaF_.png" alt="Screenshot  from 2021-12-29 20-29-17.png" /></p>
<h3 id="heading-installing-the-django-mailjet-package">Installing the django-mailjet package</h3>
<p>If we just use the keys provided by Mailjet in our app, we'll run into errors. And that's where  <a target="_blank" href="https://pypi.org/project/django-mailjet/">django-mailjet</a> comes to the rescue. This package helps us configure our email backend to work seamlessly with Mailjet.</p>
<p>Install the package using </p>
<pre><code><span class="hljs-attribute">pip</span> install django-mailjet
</code></pre><h3 id="heading-connecting-it-to-your-django-app">Connecting it to your django app</h3>
<p>Now, the final step. In your settings.py file, add the following settings.</p>
<pre><code><span class="hljs-attr">EMAIL_BACKEND</span> = <span class="hljs-string">'django_mailjet.backends.MailjetBackend'</span>
<span class="hljs-attr">EMAIL_HOST</span> = <span class="hljs-string">'in-v3.mailjet.com'</span>
<span class="hljs-attr">MAILJET_API_KEY</span> = <span class="hljs-string">"your mailjet api key here"</span>
<span class="hljs-attr">MAILJET_API_SECRET</span> = <span class="hljs-string">"your mailjet secret key here"</span>
<span class="hljs-attr">EMAIL_PORT</span> = <span class="hljs-number">587</span>
<span class="hljs-attr">EMAIL_USE_TLS</span> = <span class="hljs-literal">True</span>
<span class="hljs-attr">EMAIL_USE_SSL</span> = <span class="hljs-literal">False</span>
<span class="hljs-attr">EMAIL_TIMEOUT</span> = <span class="hljs-number">30</span>
<span class="hljs-attr">DEFAULT_FROM_EMAIL</span> = <span class="hljs-string">'sender_name &lt;email_name&gt;'</span>
</code></pre><p>Where <strong>sender_name</strong> is a custom sender name and <strong>email_name</strong> is an email address you want the emails to be sent from. Note that <strong>email_name</strong> must be registered on your mailjet account.</p>
<p>And we're done! Don't forget to store your API_KEY and API_SECRET with environment variables </p>
<p>Thanks for reading to the end. A like would be appreciated 😊</p>
<p>¡Hasta luego!</p>
]]></content:encoded></item><item><title><![CDATA[Deploying your Django App]]></title><description><![CDATA[In this article, I will be walking you through how you can deploy your django and DRF(Django Rest Framework) app. A little heads up, this is more of a "pre-deployment" article. This includes steps that you'll go through when deploying on most platfor...]]></description><link>https://blog.acel.dev/deploying-your-django-app</link><guid isPermaLink="true">https://blog.acel.dev/deploying-your-django-app</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[Heroku]]></category><category><![CDATA[deployment]]></category><category><![CDATA[hosting]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Wed, 27 Oct 2021 18:00:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1634757716999/mVutR1uJl.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I will be walking you through how you can deploy your django and DRF(Django Rest Framework) app. A little heads up, this is more of a "pre-deployment" article. This includes steps that you'll go through when deploying on most platforms. There are links to deploy on specific platforms at the end of the article. Also, you should be familiar with git and Github/Gitlab.</p>
<p>Below is a list of steps to successfully set up your django project for deployment:</p>
<ol>
<li><p>Environment Variables</p>
</li>
<li><p>Gitignore</p>
</li>
<li><p>Debug and allowed hosts</p>
</li>
<li><p>Secrect key</p>
</li>
<li><p>Static files</p>
</li>
<li><p>Gunicorn</p>
</li>
<li><p>Requirements file</p>
</li>
<li><p>Git &amp; Github</p>
</li>
</ol>
<p>First things first, we'll need to open our text editor and terminal. Now navigate to your project folder and activate your virtual environment. Let's begin!</p>
<p><strong>NB:</strong> All single line code snippets are to be executed in your command line</p>
<h2 id="heading-environment-variables">Environment Variables</h2>
<p>Before pushing our code to github, we don't want every single thing in our project leaving our PC. An example of such is the secret key for our app. To curb this we'll make use of a .env file to store all of our application's vital info.</p>
<p><code>pip install environs~=8.0.0</code></p>
<p><code>touch .env</code></p>
<p>Then in the settings.py file add</p>
<pre><code class="lang-plaintext">from environs import Env # new
env = Env() # new
env.read_env
</code></pre>
<h2 id="heading-gitignore">Gitignore</h2>
<p>We do not want git to track all our files like .env and our local database. To take care of that we make use of a .gitignore file.</p>
<p><code>touch .gitignore</code></p>
<p>Open .gitignore in your text editor and add the following files</p>
<pre><code class="lang-plaintext">.env
__pycache__/
db.sqlite3
.DS_Store # Mac only
</code></pre>
<h2 id="heading-debug-and-allowed-hosts">Debug and Allowed hosts</h2>
<p>It is vital that we set debug to false when moving to production and limit allowed hosts to prevent unauthorized access to our application.</p>
<p>In the settings.py file, update</p>
<pre><code class="lang-plaintext">DEBUG = env.bool(“DEBUG”, default=false)
ALLOWED_HOSTS = [‘.herokuapp.com’, ‘localhost’, ‘127.0.0.1’]
</code></pre>
<p>In the .env file add</p>
<pre><code class="lang-plaintext">export DEBUG=True
</code></pre>
<h2 id="heading-secret-key">Secret Key</h2>
<p>This is a very important info we don't want leaving our PC. Hence, we'll add it to our .env file</p>
<p>In the settings.py file you should see something like this</p>
<pre><code class="lang-plaintext">SECRET_KEY  =  '(-e%3z3yp5qsirfl6_+9=ko#!r6%0am8=^x9a))p2)3y-24g%*'
</code></pre>
<p>In the .env file add</p>
<pre><code class="lang-plaintext">export SECRET_KEY=(-e%3z3yp5qsirfl6_+9=ko#!r6%0am8=^x9a))p2)3y-24g%*
</code></pre>
<p>Notice that there's no quotation mark and space when adding it to the .env file. Also, each secret key is unique to a django project so don't expect this secret key to match with yours.</p>
<p>Now in the settings.py file, update</p>
<pre><code class="lang-plaintext">SECRET_KEY = env.str(“SECRET_KEY”)
</code></pre>
<h2 id="heading-databases">Databases</h2>
<p>We need to configure our database because while the default(sqlite3) is okay for local development, it's not sufficient for production. When deploying to Heroku, our app is automatically assigned a free PostgresSQL database. So we just need to configure our app to use sqlite3 locally.</p>
<p>In your settings.py file, update</p>
<pre><code class="lang-plaintext">DATABASES = {
"default": env.dj_db_url("DATABASE_URL")
}
</code></pre>
<p>Add this to the .env file</p>
<pre><code class="lang-plaintext">export DATABASE_URL=sqlite:///db.sqlite3
</code></pre>
<p>Then install the dj-database-url to utilize the DATABASE_URL environment variable to configure your Django application</p>
<p><code>pip install dj-database-url</code></p>
<p>Then we need to install Psycopg, a database adapter that lets our Python app talk to the PostgreSQL database.</p>
<p><code>pip install psycopg2-binary~=2.8.5</code></p>
<h2 id="heading-static-files">Static files</h2>
<p>You can skip this if you're deploying a DRF app as it doesn't include any static file. If you're using django on the other hand, we have something extra to configure. We are doing this because django doesn't support serving static files in production itself</p>
<p><code>pip install whitenoise~=5.1.0</code></p>
<p>Then we need to add the configurations for whitenoise in our settings.py file</p>
<pre><code class="lang-plaintext">INSTALLED_APPS = [
...
'whitenoise.runserver_nostatic',         # new
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',             # new
...
]
STATIC_URL = '/static/'
STATICFILES_DIRS = [str(BASE_DIR.joinpath('static'))]         # new
STATIC_ROOT = str(BASE_DIR.joinpath('staticfiles'))            # new
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'     # new
</code></pre>
<p>Then make directories for our static files</p>
<pre><code class="lang-plaintext">#Run each line one by one in your command line/terminal
mkdir static
mkdir static/css
mkdir static/js
mkdir static/images
</code></pre>
<p>Then run the collectstatic command for the first time to compile all the static file directories and files into one self-contained unit suitable for deployment.</p>
<p><code>python manage.py collectstatic</code></p>
<p>As a final step, in order for our templates to display any static files, they must be loaded in. So add {% load static %} to the top of the base.html file.</p>
<pre><code class="lang-plaintext">{% load static %}
&lt;html&gt;
…
</code></pre>
<h2 id="heading-gunicorn">Gunicorn</h2>
<p>We need to install gunicorn as the production web server</p>
<p><code>pip install gunicorn~=19.9.0</code></p>
<h2 id="heading-requirements-file">Requirements File</h2>
<p>This lets heroku know what dependencies are needed for our app so it can be installed. First run <code>pip freeze &gt; requirments.txt</code> to create a requirements.txt file that contains all our app's dependencies.</p>
<h2 id="heading-git-andamp-github">Git &amp; Github</h2>
<p>I'm assuming you're conversant with git and github by now. Just commit your latest changes and push it to your online repo on Github or Gitlab. If you don't know how to do that you can check out this <a target="_blank" href="https://docs.github.com/en/github/importing-your-projects-to-github/importing-source-code-to-github/adding-an-existing-project-to-github-using-the-command-line#adding-a-project-to-github-without-github-cli">article</a>.</p>
<h2 id="heading-deploy-on-specific-platforms">Deploy on Specific Platforms</h2>
<p>At this point, we have covered most of the steps you'll need to deploy your app. Whatever is left is platform-specific, and I have covered a few of them. Please click on one of the links below to continue your deployment on any platform of your choice.</p>
<ul>
<li><p><a target="_blank" href="https://blog.acel.cyou/deploying-your-django-app-on-railway">Deploy on Railway</a></p>
</li>
<li><p><a target="_blank" href="https://blog.acel.cyou/deploying-your-django-app-on-heroku">Deploy on Heroku</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[My 2020 Year in review]]></title><description><![CDATA[2020 was quite a year for me. How my year went:
Resumed for my 2nd year in Unilag. Finally moved in to a hostel with my dad’s permission after he wanted me to go from home till I finish…lmaooo. I moved in on my own (off and on) in my second semester....]]></description><link>https://blog.acel.dev/my-2020-year-in-review</link><guid isPermaLink="true">https://blog.acel.dev/my-2020-year-in-review</guid><category><![CDATA[learning]]></category><category><![CDATA[Learning Journey]]></category><category><![CDATA[review]]></category><dc:creator><![CDATA[Chukwuemeka Aladimma]]></dc:creator><pubDate>Thu, 31 Dec 2020 19:46:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1609443779731/QZT-69sPg.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>2020 was quite a year for me. How my year went:</p>
<p>Resumed for my 2nd year in Unilag. Finally moved in to a hostel with my dad’s permission after he wanted me to go from home till I finish…lmaooo. I moved in on my own (off and on) in my second semester. At times its good to be stubborn/rebellious, lool. </p>
<p>Then I started attending <a target="_blank" href="https://twitter.com/ecxunilag?s=20">ECX’s</a> weekly classes for frontend late January/early February. That’s where I began learning frontend development. Signed up on <a target="_blank" href="https://github.com/">github</a>, created my first repo and my first website (It looks like Beans mixed with Amala). Was stressful having to go to the venue right after classes but it was worth it.</p>
<p>Was preparing for tests before ASUU drank Alomo bitters and went on a two weeks strike. I was low key happy so that I’ll use the time to prepare properly for my tests and complete my assignments. Then Covid-19 came, sent us into panic mode and sent us packing. The whole thing made ASUU look unserious and they now decided to go on indefinite strike <em>sighs</em></p>
<p>With the way Covid-19 was going, I kuku(immediately) knew we wouldn’t resume till next year, so I chose to improve my frontend skills. Luckily, ECX came up with a #30daysofcode coding marathon. I registered willing to complete the program. </p>
<p>I started well, finished not so–naa I didn’t finish. From day 1 to day 5 were seamless. Relatively easy tasks, but by the day each task increased in difficulty. I think I managed to get to day 8/9 and fell sick. Couldn’t code for days but I recovered did my best to catch up then broke down. I had gotten Ill twice in one month! The first thing to come to mind would have been Covid-19 but I also fell Ill multiple times while in school. I took my time to recover. At this point I was far behind and just hoped to complete the tasks up to day 15.</p>
<p> While on it, my laptop broke down. <em>sighs</em> I was convinced my village people were on my matter and decided not to stress myself anymore. Later a friend reached out and asked if I was still participating in the <a target="_blank" href="https://twitter.com/Acel_dev/status/1242525482559647746?s=20">#30daysofcode</a>. We talked and <a target="_blank" href="https://twitter.com/stephcrown06?s=20">he</a> gave me ginger to continue, I resumed. Remember, my laptop was still bad and there was a very strict lockdown (as a then). I had to become a self-taught hardware engineer. Watched one or two videos, took the risk and dismantled my laptop. Luckily, all I had to do was remove my RAM cards, CMOS battery and insert them back… It worked! I assembled it and it worked like it used to. Village people 3 – 1 Acel. At the end of it all I could only complete 14 tasks–needed 15 to at least get a certificate of participation.</p>
<p>Another #30daysofcode presented itself. I participated but approached it a bit differently. Registered for the Frontend Intermediate and Python beginner track. Along the way, day 6 or 7, I dropped the frontend track. Why? </p>
<ul>
<li>I was doing other things at home. I live with my parents so I don’t have full control over my time.  </li>
<li>I didn’t know jack about JavaScript so I had to learn it on the go. There just wasn’t time for that.</li>
<li>Plus Frontend being time consuming for even the littlest of things, I had to drop it. No time and I couldn’t afford to fall sick again.</li>
</ul>
<p>So, I continued with the Python track and completed it! Python wasn’t completely new to me as I had learnt a bit of it before. So, half of the cohort was revision and the other half learning best practices and other cool stuff in Python.</p>
<p>This was May and I had to decide between continuing with Frontend or moving to Data Science. I had gained some insight on data science from TECH SKILLS HUB headed by <a target="_blank" href="https://www.linkedin.com/in/chrystarinze">Arinze</a>  but data science, data engineering, data analysis, AI, ML etc. were all so vague that one could get lost in the learning process. So, I opted for Frontend development that had a learning path one could follow.</p>
<p>I applied for <a target="_blank" href="https://twitter.com/hnginternship?s=20">hnginternship</a> and started learning JavaScript on Udemy. I didn’t have any hiccups while learning JavaScript except for the data burnt. I also paid for a course I haven’t taken till now. A whole 14k in this terrible economy <em>sighs</em>. It’s not that I have money oo, but HNG happened.</p>
<p>Learning online and all is cool but when you’re done what you get is certificates. This was a chance to get experience in what I was learning. So, I took it and put what I was learning on hold. HNG internship was an experience that’s difficult to describe. Completing tasks before deadline, having to suddenly work with people you’ve never met on short notice  yet compelled to deliver on time. Honestly the joy of completing tasks was immense but so was the task that followed. You could be having fun one minute, debugging codes the next. Playing games one second, preparing for your tasks the next. Relived by night, sent into panic mode by morning.</p>
<p> Yet, I wanted to finish it. Coming from someone that could easily fall sick if he overstresses himself, that’s how much I enjoyed it. I made friends, bantered, had Slack crushes. During this period, I was so immersed in the program that I became a Painite on WhatsApp. Quite difficult to find there. The tasks were difficult, very difficult to complete, but I kept pushing. Till I could push no more and that was stage 5. I broke down once again. That didn’t stop me though. The fact that everyone kept trying their best and wanted to get to the next stage was enough motivation for me. </p>
<p>I worked with an overworked body and days later my will failed me. I decided to stop but this time, it was my mum who reached out. I had gotten better but I was late to get a task–well almost. I got one quite late, did it and was able to make it to the 7th stage. My enthusiasm was back. I went on, learning a new technology for the next stage and applying it as much as I could. I didn’t make it past this stage, but I was happy I made it that far. Seven whole stages, stressful as f**k, and was quite time consuming. Also visited their office at Yaba once. I used my last days to have fun on the random channel and other fun channels. I learnt how to work in teams, lead teams, how to use <a target="_blank" href="https://github.com/">GitHub</a> better, how to use <a target="_blank" href="https://twitter.com/SlackHQ?s=20">Slack</a> , new tools and made my way to Tech Twitter. I thank HNG for the experience.</p>
<p>Later on, a friend of mine became motherless. Yeah. It wasn’t my mum, I know but I questioned a lot of things. I was quite sad that week, really sad. I asked my mum how it felt to lose someone close to you and she said it’s painful and the pain never leaves. You just accept it. I visited this guy and didn’t know what exactly to say. And incase you’re wondering, I did imagine losing a loved one. It’s not like I wanted to lose someone, but I just needed a good idea of what it felt like. Sad phase for me this year.</p>
<p>Participated in <a target="_blank" href="https://twitter.com/WeJapaHQ?s=20">WeJapa’s</a> Internship, August this year. There were hiccups and it was more or less you learning on your own. Just one task given which I completed. No vibes, just Insha’Allah. I don’t even know why I wasn’t given a certificate. Hopefully they do better next year.</p>
<p>Participated in <a target="_blank" href="https://twitter.com/ecxunilag?s=20">ECX’s</a> Internship that held in September. We were given a task each week during the internship. The first task was individual based and the rest, in groups. I lead my group during the internship. Compared to other groups you could say I had active members, but none of them were designers. It’s either they weren’t online or had an issue with light or something. But there was always a significant delay from their end hence, the backend devs were left with little or no time to complete the projects. This was a time I wished I knew a bit of backend so I could assist them. Unfortunately, <a target="_blank" href="https://nodejs.org/">NodeJS</a> learning materials aren’t easy to find. We even had to outsource designs for one or two projects. My experience here taught me a lot about how to manage teams and people. </p>
<p>In October I took an in-depth <a target="_blank" href="https://getbootstrap.com/">bootstrap</a> course on <a target="_blank" href="https://www.coursera.org/">Coursera</a> . This was when I really understood the struggle with online learning/ tutorials. My learning experience on <a target="_blank" href="https://www.udemy.com/">Udemy</a> and Coursera were quite different. On Coursera, there’s always an assignment(s) at the end of every week and you can’t slack a bit cause the modules are timed. Plus, you also have to mark other students’ assignment. Quite stressful but I finished it on time. Just so you know, I’m an advocate for pure CSS, no frame works. Unless for things like carousels, animations, etc. I learnt bootstrap so I can read people’s code better. Not everyone is like me and a lot of people use bootstrap. Not to mention that some companies make its usage compulsory. So, if I’m gonna work in teams, I might as well learn it properly. </p>
<p>Completed the JavaScript course I started earlier in the year and also learnt about Progressive Web Apps and how to convert web apps to PWAs.</p>
<p>I worked on my <a target="_blank" href="https://acel.netlify.app/">Portfolio</a> and Resume. Was something I had been wanting to do but just kept postponing it cause I felt I didn’t know enough to get a job anyway. Eventually did it cause of the <a target="_blank" href="https://twitter.com/EnyataCommunity?s=20">EnyataProjectBuildup</a>. Designed (in my head) and built my portfolio. Worked on my Resume with <a target="_blank" href="https://www.canva.com/">Canva</a>. Thanks <a target="_blank" href="https://twitter.com/ugonna_t?s=20">Ugonna</a> for the Resume review.</p>
<p>Was more active in church particularly at the start of December. Months away from church really made me question and reaffirm my faith, beliefs and doctrines as a Christian. Took me a while to get back to my best.</p>
<p>27th of December, I heard that a friend/primary schoolmate lost his dad a month ago. We live on the same street. What is should I say funny, is that we got to know through a banner announcing his death on our street. <em>Sighs</em></p>
<p>29th of December, I heard that a friend/secondary school classmate lost his mum exactly a month ago. He told me himself in the DM. </p>
<p>Apparently, he posted the news of her death on his WhatsApp status. I also have class mates from Sec school I chat with yet I didn’t know, didn’t even get a clue about it. We chatted. After that, I began to wonder how detached I was from friends and family, what was I doing wrong, the possibility that big things(good/bad) had happened in the life of my friends and I have no idea about it. Am I too self-centered? Or introverted? Or me just having a very bad habit of not keeping in touch? I couldn’t pinpoint a reason but decided to try to be more active socially. Even if I’ll have to leave my comfort zone of staying at home, not checking out people’s WhatsApp status, initiate chats more, even if it’s just to say hello and be a lot more active in group chats. Especially those that include friends and family. </p>
<p>My finances really took a hit and so did my body. I don’t think I’ve ever fallen ill so many times in one year. My relationship with friends and family also took a hit. This year wasn’t my best, neither was it my worst. </p>
<p>What I want for 2021</p>
<ul>
<li>I want to be a better person next year</li>
<li>I want to do really well in school next year</li>
<li>I want to be a better programmer next year</li>
<li>I want to get gigs/jobs next year    </li>
<li>I want to be a Microsoft Learn Student Ambassador next year</li>
<li>I want to have a good social life online and offline next year</li>
<li>I want have a far better health next year</li>
</ul>
<p>And how could I forget, my phone was stolen on the 1st of January 2020 and my Ulcer resurfaced December this year. And yes #EndSars. I couldn’t leave my house but I’m happy to have participated as much as I could online.</p>
<p>I apologize for having to make you read so much, I didn’t think it would be this long.  </p>
<p>Happy New Year! </p>
]]></content:encoded></item></channel></rss>