<?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[Adonis SIMO's Notes]]></title><description><![CDATA[I share my knowledge, experience and article in form of stories made to provide the technical value and some context about the implementations. I talk about Web Development and Cloud Computing]]></description><link>https://blog.adonissimo.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 12:46:15 GMT</lastBuildDate><atom:link href="https://blog.adonissimo.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[BuildLog #1 I am operating a SaaS product since 2023 on the African market and I will be sharing my experiences and learning.]]></title><description><![CDATA[Hello, this is the very first article of a (hopefully) series of logs about my journey. I am from an African country named Cameroun, but I have been working remotely for a US company for the last 5 years where I am currently a Technical Product Manag...]]></description><link>https://blog.adonissimo.com/buildlog-1-i-am-operating-a-saas-product-since-2023-on-the-african-market-and-i-will-be-sharing-my-experiences-and-learning</link><guid isPermaLink="true">https://blog.adonissimo.com/buildlog-1-i-am-operating-a-saas-product-since-2023-on-the-african-market-and-i-will-be-sharing-my-experiences-and-learning</guid><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Tue, 29 Apr 2025 13:18:53 GMT</pubDate><content:encoded><![CDATA[<p>Hello, this is the very first article of a (hopefully) series of logs about my journey. I am from an African country named Cameroun, but I have been working remotely for a US company for the last 5 years where I am currently a Technical Product Manager, and I have built various stuff for clients around the world and locally. My native language is french. That's it about me.</p>
<p>I started a product targeting students in late 2023, but mostly for the African market, even though it's usable anywhere. Why that market first, because i felt like it was the easiest to reach for me. But also there is a huge demands in various type of software / SaaS, and it comes with so many challenges, there is 54 countries on the African continent, most countries has different languages (+2000) and currencies (41), the population is mainly young and younger.</p>
<p>This makes it interesting because there is a lot of challenges in such market and I believe it helps creating value at a global scale if I learn the skills to build and run a tech product in that kind of market. if I take a few examples of the challenges :</p>
<p>- Most peoples have never had a bank account or even used a credit card, so it's very hard to build subscription based tech business, unless it's <strong>VERY VERY VERY</strong> useful.</p>
<p>- The main payment system across the continent is mobile based (they call it <strong>Mobile Money</strong>), a digital wallet attached to one person phone number and used to pay various good and services.</p>
<p>- Most time online purchase are associated with poor quality product, customer service and scams, so a lot of people have a bad experiences and views about online purchases and that's not even about tech product or software.</p>
<p>- Internet coverage is very low in a lot of countries, so most users are not necessarily tech savvy.</p>
<p>- Most people prefers the human contact before purchasing something online.</p>
<p>Each of these points add a challenge on it's own, for example I discovered that I could increase the purchase rate by adding a simple button to join a Support group on WhatsApp (main communication app in most countries). Some one told me about the fact she thought she had been scammed after purchasing and she didn’t know how to get in touch with me (there was a bug on the app at that time)</p>
<p>A lot of users are not tech savvy and they don't even have a proper computer, so I had to work the mobile version of the application (responsive , light and fast) and increase the usability on mobile.</p>
<p>I had to really workout the landing page and the words to make sure the user understand very well what the app does and how it does it. I couldn’t charge a recurring fee, so I built a one time payment system, so the user pays per document, I know it's still possible to do subscriptions but i will share this story later (turn out a some users are willing to pay a subscription).</p>
<p>I operated the platform in one country for a year, received some payments, and then decided to expand internationally. This led to another round of issues and challenges, not very technical but more economics and financials. I said earlier that we have 41 currencies on the continent, and it's VERY hard to move money between countries (even neighbor countries). And stripe does not work, so i couldn’t create a stripe account and use it, PayPal works but partially (in some countries yes in some no), but it's well know. In my country for example I can use PayPal to send money but not receive.</p>
<p>Going international meant I should have register business in many countries to receive payments, but I did not have the time or the money, so I used a company from a friend (where I am also involved) to do so. For the moment most countries from where users are paying share the same currency as my country (only in the name lol, F CFA), but i can't even access those money now, But at least I am glad I see payments coming in regularly. Most of them are coming through Mobile Money payments, i was able to work with a friend in Canada to get a stripe account and another one in EU to get a PayPal account, and I've got payments in both of them from the very first day i did pushed them to online.</p>
<p><strong>let's talk about numbers a bit.</strong></p>
<p>At the moment I have:</p>
<p>- ~<strong>1130 users</strong> ,</p>
<p>- <strong>+1500</strong> documents created</p>
<p>it generated ~ <strong>$ 500</strong> till date, I know it does not seem good and it's small, but it's a lot in my currency and considering that i did not invested a lot of time and no marketing campaign on it. For example early this year I verified the numbers and tried to count the amount of revenue it would have generated during the year 2024, it was roughly ~ $2000 ( 1 000 000 <strong>F CFA</strong>) the exact number is around 850 000 <strong>F CFA</strong>, this was HUGE for many reasons: I did not really promote it the whole year, some month I did not even talk about it online, and more importantly some user where simple not able to pay because the payment during that year where not available in their countries. I was also able to receive payments from EU clients recently.</p>
<p>But the platform receives more and more visitors, users and customers. I work regularly to improve the experience and to promote it on social media.</p>
<p>So i decided to continue and share my story. The upcoming posts will be around the technical choices I made when building this, some will be about how I am promoting it.</p>
<p>Cheers.</p>
]]></content:encoded></item><item><title><![CDATA[How to Build No-Code Modal Components for Wagtail CMS Content Editors]]></title><description><![CDATA[What is a modal ?
A good way to promote something on a website is to make it appear as a modal on several pages. It can appear after a few seconds or right after the user opens the site. It’s very handy when you can do that easily from the CMS, inste...]]></description><link>https://blog.adonissimo.com/how-to-build-no-code-modal-components-for-wagtail-cms-content-editors</link><guid isPermaLink="true">https://blog.adonissimo.com/how-to-build-no-code-modal-components-for-wagtail-cms-content-editors</guid><category><![CDATA[Django]]></category><category><![CDATA[wagtail]]></category><category><![CDATA[Python]]></category><category><![CDATA[cms]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Sun, 17 Nov 2024 01:12:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731805239874/4eeaebf4-5f3f-4934-809f-b008625050fb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-what-is-a-modal">What is a modal ?</h1>
<p>A good way to promote something on a website is to make it appear as a modal on several pages. It can appear after a few seconds or right after the user opens the site. It’s very handy when you can do that easily from the CMS, instead of relying on the development team to build it every time. Wagtail does not come with this feature by default, but it's not very difficult to imagine one. During my research, I found Codered's <a target="_blank" href="https://github.com/coderedcorp/coderedcms/blob/main/coderedcms/blocks/content_blocks.py#L112">Modal Streamfield</a>, which allows the user to define a popup directly on the page. It’s good, but it does not fit my needs since I have to create many of them if I want the same popup on many pages of my site. So I designed one according to the following needs:</p>
<ul>
<li><p>I should be able to create it once.</p>
</li>
<li><p>I should be able to choose one or many pages where it will appear.</p>
</li>
<li><p>I should be able to make it appear after a certain time.</p>
</li>
<li><p>I should be able to create one but not display it.</p>
</li>
<li><p>I should allow the user to close a modal, and it doesn’t appear anymore during their session.</p>
</li>
<li><p>I should be able to edit its content using streamfields.</p>
</li>
<li><p>Not allow many popups on the same page to display on top of each other.</p>
</li>
</ul>
<h1 id="heading-storing-the-modal-data">Storing the modal data</h1>
<p>To do all these, I need a model to store the content of a popup, then I need to display it in the admin panel. So I created a Snippet and added a Snippet view to control how it displays in the admin. The following code shows how to do that.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PageModal</span>(<span class="hljs-params">models.Model</span>):</span>
    DEPTH_CHOICES = [
        (<span class="hljs-number">1</span>, _(<span class="hljs-string">'Direct children only'</span>)),
        (<span class="hljs-number">2</span>, _(<span class="hljs-string">'Children and grandchildren'</span>)),
        (<span class="hljs-number">3</span>, _(<span class="hljs-string">'Up to 3 levels deep'</span>)),
        (<span class="hljs-number">-1</span>, _(<span class="hljs-string">'All descendants (unlimited depth)'</span>)),
    ]

    title = models.CharField(
        verbose_name=_(<span class="hljs-string">'Title'</span>),
        max_length=<span class="hljs-number">255</span>
    )

    content = StreamField([
        (<span class="hljs-string">'heading'</span>, CharBlock(label=_(<span class="hljs-string">'Heading'</span>))),
        (<span class="hljs-string">'paragraph'</span>, RichTextBlock(label=_(<span class="hljs-string">'Paragraph'</span>))),
        (<span class="hljs-string">'link'</span>, URLBlock(label=_(<span class="hljs-string">'Link'</span>))),
    ], use_json_field=<span class="hljs-literal">True</span>, verbose_name=_(<span class="hljs-string">'Content'</span>))

    <span class="hljs-comment"># Modal display settings</span>
    display_delay = models.PositiveIntegerField(
        default=<span class="hljs-number">0</span>,
        verbose_name=_(<span class="hljs-string">'Display delay'</span>),
        help_text=_(<span class="hljs-string">"Delay in seconds before showing the modal"</span>)
    )

    cta_text = models.CharField(
        verbose_name=_(<span class="hljs-string">'Call to action text'</span>),
        max_length=<span class="hljs-number">50</span>,
        blank=<span class="hljs-literal">True</span>
    )

    cta_page = models.ForeignKey(
        <span class="hljs-string">'wagtailcore.Page'</span>,
        verbose_name=_(<span class="hljs-string">'Call to action page'</span>),
        null=<span class="hljs-literal">True</span>,
        blank=<span class="hljs-literal">True</span>,
        on_delete=models.SET_NULL,
        related_name=<span class="hljs-string">'+'</span>,
    )

    cta_link = models.URLField(
        verbose_name=_(<span class="hljs-string">'Call to action external link'</span>),
        blank=<span class="hljs-literal">True</span>
    )

    <span class="hljs-comment"># Page association</span>
    pages = models.ManyToManyField(
        <span class="hljs-string">'wagtailcore.Page'</span>,
        verbose_name=_(<span class="hljs-string">'Pages'</span>),
        related_name=<span class="hljs-string">'modals'</span>,
        help_text=_(<span class="hljs-string">"Select pages where this modal should appear"</span>)
    )

    include_children = models.BooleanField(
        verbose_name=_(<span class="hljs-string">'Include children'</span>),
        default=<span class="hljs-literal">False</span>,
        help_text=_(<span class="hljs-string">"Show on child pages of selected pages"</span>)
    )

    child_depth = models.IntegerField(
        verbose_name=_(<span class="hljs-string">'Child depth'</span>),
        choices=DEPTH_CHOICES,
        default=<span class="hljs-number">1</span>,
        help_text=_(<span class="hljs-string">"How many levels of child pages should this modal appear on?"</span>),
    )

    is_displayed = models.BooleanField(
        verbose_name=_(<span class="hljs-string">'Is visible'</span>),
        default=<span class="hljs-literal">True</span>,
        help_text=_(<span class="hljs-string">"If enabled, the modal will be visible"</span>)
    )

    use_session_storage = models.BooleanField(
        verbose_name=_(<span class="hljs-string">'Use session storage'</span>),
        default=<span class="hljs-literal">False</span>,
        help_text=_(<span class="hljs-string">'If enabled, the modal will only show once per session'</span>)
    )
    panels = [
        MultiFieldPanel([
            FieldPanel(<span class="hljs-string">'title'</span>),
            FieldPanel(<span class="hljs-string">'content'</span>),
        ], heading=<span class="hljs-string">"Modal Content"</span>),
        MultiFieldPanel([
            FieldPanel(<span class="hljs-string">'cta_text'</span>),
            FieldRowPanel([
                FieldPanel(<span class="hljs-string">'cta_page'</span>),
                FieldPanel(<span class="hljs-string">'cta_link'</span>),
            ], heading=<span class="hljs-string">"CTA Link (choose one)"</span>),
        ], heading=<span class="hljs-string">"Call To Action Settings"</span>),

        MultiFieldPanel([
            FieldPanel(<span class="hljs-string">'display_delay'</span>),
            FieldRowPanel([
                FieldPanel(<span class="hljs-string">'is_displayed'</span>),
                FieldPanel(<span class="hljs-string">'use_session_storage'</span>),
            ], heading=<span class="hljs-string">"Visibility Settings"</span>)
        ], heading=<span class="hljs-string">"Display Settings"</span>),
        MultiFieldPanel([
            FieldPanel(<span class="hljs-string">'pages'</span>, widget=forms.CheckboxSelectMultiple),
            FieldRowPanel([
                FieldPanel(<span class="hljs-string">'include_children'</span>),
                FieldPanel(<span class="hljs-string">'child_depth'</span>),
            ], heading=<span class="hljs-string">"Child Page Settings"</span>),
        ], heading=<span class="hljs-string">"Page Selection"</span>),
    ]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__str__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self.title

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_cta_url</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">if</span> self.cta_page:
            <span class="hljs-keyword">return</span> self.cta_page.url
        <span class="hljs-keyword">return</span> self.cta_external_url <span class="hljs-keyword">or</span> <span class="hljs-string">''</span>


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PageModalAdmin</span>(<span class="hljs-params">SnippetViewSet</span>):</span>
    model = PageModal
    menu_label = <span class="hljs-string">'Page Modals'</span>
    menu_icon = <span class="hljs-string">'placeholder'</span>
    menu_order = <span class="hljs-number">300</span>
    add_to_settings_menu = <span class="hljs-literal">True</span>
    list_display = (<span class="hljs-string">'title'</span>, <span class="hljs-string">'is_displayed'</span>, <span class="hljs-string">'display_delay'</span>, <span class="hljs-string">'include_children'</span>)
</code></pre>
<p>This gives me the base setup to start. Let’s see what this model allows us to do:</p>
<ul>
<li><p>Define the content of the popup using <code>title</code> and <code>content</code> attributes.</p>
</li>
<li><p>Store the display settings (timeout, is it displayed, etc.).</p>
</li>
<li><p>Store the page where I want it to be displayed.</p>
</li>
<li><p>Store information to know if it should be displayed in children of the selected page or not.</p>
</li>
<li><p>Store all the links and texts for the CTA (Call To Action) of the modal.</p>
</li>
</ul>
<p>The admin page looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731804336018/5a47a536-757f-4a4e-b1b6-5cbdeb2f8a72.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-sending-the-modals-to-the-frontend">Sending the modals to the frontend</h1>
<h2 id="heading-loading-the-modals-on-each-page">Loading the modals on each page</h2>
<p>The <strong>PageModalAdmin</strong> Viewset helps display it properly on the admin page. I can define which columns to show in the list of modals (popups). The next step is to load each available popup into the site frontend. We will use a template tag to inject all the modals registered to the page being rendered. The following snippet does that:</p>
<pre><code class="lang-python"><span class="hljs-comment"># modal_tags.py</span>
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">from</span> django <span class="hljs-keyword">import</span> template
<span class="hljs-keyword">from</span> django.core.serializers.json <span class="hljs-keyword">import</span> DjangoJSONEncoder


register = template.Library()


<span class="hljs-meta">@register.inclusion_tag('home/modals/page_modal.html', takes_context=True)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">page_modals</span>(<span class="hljs-params">context</span>):</span>
    current_page = context.get(<span class="hljs-string">'page'</span>)

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> current_page:
        <span class="hljs-keyword">return</span> {<span class="hljs-string">'modals'</span>: []}

    <span class="hljs-comment"># Get modals directly assigned to this page</span>
    modals = current_page.modals.filter(is_displayed=<span class="hljs-literal">True</span>)

    ancestors = current_page.get_ancestors()
    <span class="hljs-keyword">for</span> ancestor <span class="hljs-keyword">in</span> ancestors:
        <span class="hljs-comment"># Get all modals that:</span>
        <span class="hljs-comment"># 1. Are assigned to this ancestor AND</span>
        <span class="hljs-comment"># 2. Have include_children enabled AND</span>
        <span class="hljs-comment"># 3. Meet the depth requirement</span>
        ancestor_modals = ancestor.modals.filter(include_children=<span class="hljs-literal">True</span>, is_displayed=<span class="hljs-literal">True</span>)

        <span class="hljs-keyword">for</span> modal <span class="hljs-keyword">in</span> ancestor_modals:
            <span class="hljs-comment"># Calculate the distance between the ancestor and current page</span>
            distance = len(current_page.get_ancestors()) - len(ancestor.get_ancestors())

            <span class="hljs-comment"># Check if this page is within the specified depth</span>
            <span class="hljs-keyword">if</span> modal.child_depth == <span class="hljs-number">-1</span> <span class="hljs-keyword">or</span> distance &lt;= modal.child_depth:
                modals = modals | ancestor_modals.filter(pk=modal.pk)

    modal_data = []
    <span class="hljs-keyword">for</span> modal <span class="hljs-keyword">in</span> modals:
        modal_data.append({
            <span class="hljs-string">'title'</span>: modal.title,
            <span class="hljs-string">'content'</span>: list(modal.content.raw_data),
            <span class="hljs-string">'display_delay'</span>: modal.display_delay,
            <span class="hljs-string">'cta_text'</span>: modal.cta_text,
            <span class="hljs-string">'cta_link'</span>: modal.cta_link,
            <span class="hljs-string">'cta_page'</span>: modal.cta_page.url,
            <span class="hljs-string">'use_session_storage'</span>: modal.use_session_storage,
        })
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">'modals'</span>: json.dumps(modal_data, cls=DjangoJSONEncoder)
    }
</code></pre>
<p>And include the result in the base template this way.</p>
<pre><code class="lang-xml">{% load modal_tags %}
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
{% page_modals %}
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
</code></pre>
<h2 id="heading-handling-the-modals-display">Handling the modal’s display</h2>
<p>We also need JS code that will process the available popups and decide whether to display them. In the previous template tag, I added a template named <code>home/modals/page_modal.html</code>. Here is the content:</p>
<pre><code class="lang-xml">{% if modals %}
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ModalQueue</span> </span>{
        <span class="hljs-keyword">static</span> queue = [];
        <span class="hljs-keyword">static</span> currentlyShowing = <span class="hljs-literal">false</span>;

        <span class="hljs-keyword">static</span> add(modal) {
            <span class="hljs-built_in">this</span>.queue.push(modal);
            <span class="hljs-built_in">this</span>.processQueue();
        }

        <span class="hljs-keyword">static</span> processQueue() {
            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.currentlyShowing || <span class="hljs-built_in">this</span>.queue.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;

            <span class="hljs-keyword">const</span> nextModal = <span class="hljs-built_in">this</span>.queue[<span class="hljs-number">0</span>];
            <span class="hljs-built_in">this</span>.currentlyShowing = <span class="hljs-literal">true</span>;
            nextModal.display();
        }

        <span class="hljs-keyword">static</span> modalClosed() {
            <span class="hljs-built_in">this</span>.queue.shift();
            <span class="hljs-built_in">this</span>.currentlyShowing = <span class="hljs-literal">false</span>;
            <span class="hljs-built_in">this</span>.processQueue();
        }
    }

    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ModalHandler</span> </span>{
        <span class="hljs-keyword">constructor</span>(modalData) {
            <span class="hljs-built_in">this</span>.modal = modalData;
            <span class="hljs-built_in">this</span>.shown = <span class="hljs-literal">false</span>;
            <span class="hljs-built_in">this</span>.storageKey = <span class="hljs-string">`modal_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.modal.title.replace(<span class="hljs-regexp">/\s+/g</span>, <span class="hljs-string">'_'</span>).toLowerCase()}</span>`</span>;
            <span class="hljs-built_in">this</span>.init();
        }

        init() {
            <span class="hljs-comment">// Only check session storage if the feature is enabled</span>
            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.modal.use_session_storage &amp;&amp; <span class="hljs-built_in">this</span>.hasBeenShown()) {
                <span class="hljs-keyword">return</span>;
            }

            <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
                ModalQueue.add(<span class="hljs-built_in">this</span>);
            }, <span class="hljs-built_in">this</span>.modal.display_delay * <span class="hljs-number">1000</span>);
        }

        hasBeenShown() {
            <span class="hljs-keyword">return</span> sessionStorage.getItem(<span class="hljs-built_in">this</span>.storageKey) === <span class="hljs-string">'shown'</span>;
        }

        markAsShown() {
            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.modal.use_session_storage) {
                sessionStorage.setItem(<span class="hljs-built_in">this</span>.storageKey, <span class="hljs-string">'shown'</span>);
            }
        }

        display() {
            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.shown) <span class="hljs-keyword">return</span>;

            <span class="hljs-keyword">const</span> modalHtml = <span class="hljs-string">`
                &lt;div class="fixed inset-0 z-50 flex items-center justify-center modal-container"&gt;
                    &lt;!-- Backdrop --&gt;
                    &lt;div class="absolute inset-0 bg-black opacity-80"&gt;&lt;/div&gt;

                    &lt;!-- Modal content --&gt;
                    &lt;div class="relative z-10 bg-white rounded-lg max-w-lg w-2/3 mx-auto p-6"&gt;
                        &lt;h2 class="text-2xl font-bold mb-4"&gt;<span class="hljs-subst">${<span class="hljs-built_in">this</span>.modal.title}</span>&lt;/h2&gt;
                        &lt;div class="modal-content mb-6"&gt;
                            <span class="hljs-subst">${<span class="hljs-built_in">this</span>.renderContent()}</span>
                        &lt;/div&gt;
                        <span class="hljs-subst">${<span class="hljs-built_in">this</span>.renderCTA()}</span>
                        &lt;button class="close-modal absolute top-4 right-4 text-gray-500 hover:text-gray-700"&gt;
                            &lt;svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"&gt;
                                &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /&gt;
                            &lt;/svg&gt;
                        &lt;/button&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            `</span>;

            <span class="hljs-built_in">document</span>.body.insertAdjacentHTML(<span class="hljs-string">'beforeend'</span>, modalHtml);
            <span class="hljs-built_in">this</span>.shown = <span class="hljs-literal">true</span>;
            <span class="hljs-built_in">this</span>.markAsShown();

            <span class="hljs-keyword">const</span> modalElement = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'.modal-container'</span>);
            <span class="hljs-keyword">const</span> closeButton = modalElement.querySelector(<span class="hljs-string">'.close-modal'</span>);

            <span class="hljs-keyword">const</span> closeModal = <span class="hljs-function">() =&gt;</span> {
                modalElement.classList.add(<span class="hljs-string">'opacity-0'</span>);
                <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
                    modalElement.remove();
                    ModalQueue.modalClosed();
                }, <span class="hljs-number">200</span>);
            };

            closeButton.addEventListener(<span class="hljs-string">'click'</span>, closeModal);
            modalElement.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
                <span class="hljs-keyword">if</span> (e.target === modalElement) closeModal();
            });
        }

        renderContent() {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.modal.content.map(<span class="hljs-function"><span class="hljs-params">block</span> =&gt;</span> {
                <span class="hljs-keyword">switch</span> (block.type) {
                    <span class="hljs-keyword">case</span> <span class="hljs-string">'heading'</span>:
                        <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;h3 class="text-xl font-bold mb-2"&gt;<span class="hljs-subst">${block.value}</span>&lt;/h3&gt;`</span>;
                    <span class="hljs-keyword">case</span> <span class="hljs-string">'paragraph'</span>:
                        <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;div class="prose max-w-none mb-4"&gt;<span class="hljs-subst">${block.value}</span>&lt;/div&gt;`</span>;
                    <span class="hljs-keyword">case</span> <span class="hljs-string">'link'</span>:
                        <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;a href="<span class="hljs-subst">${block.value}</span>" class="text-primary-600 hover:underline block mb-2"&gt;<span class="hljs-subst">${block.value}</span>&lt;/a&gt;`</span>;
                    <span class="hljs-keyword">default</span>:
                        <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>;
                }
            }).join(<span class="hljs-string">''</span>);
        }

        renderCTA() {
            <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.modal.cta_text) <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>;

            <span class="hljs-keyword">const</span> url = <span class="hljs-built_in">this</span>.modal.cta_page || <span class="hljs-built_in">this</span>.modal.cta_url;
            <span class="hljs-keyword">const</span> target = <span class="hljs-built_in">this</span>.modal.cta_url ? <span class="hljs-string">'target="_blank" rel="noopener noreferrer"'</span> : <span class="hljs-string">''</span>;


            <span class="hljs-keyword">return</span> <span class="hljs-string">`
                &lt;a href="<span class="hljs-subst">${url}</span>" <span class="hljs-subst">${target}</span>
                   class="inline-block bg-primary-600 text-white px-6 py-2 rounded hover:bg-primary-700 transition-colors duration-200"&gt;
                    <span class="hljs-subst">${<span class="hljs-built_in">this</span>.modal.cta_text}</span>
                &lt;/a&gt;
            `</span>;
        }
    }
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'DOMContentLoaded'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">const</span> modals = {{ modals|safe }};
        modals.forEach(<span class="hljs-function"><span class="hljs-params">modalData</span> =&gt;</span> {
            <span class="hljs-keyword">new</span> ModalHandler(modalData);
        });
    });
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
{% endif %}
</code></pre>
<h3 id="heading-the-modal-handler">The modal handler</h3>
<p>The <code>ModalHandler</code> class is called as many times as there are modals to display on the page. Everything starts here:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'DOMContentLoaded'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">const</span> modals = {{ modals|safe }};
        modals.forEach(<span class="hljs-function"><span class="hljs-params">modalData</span> =&gt;</span> {
            <span class="hljs-keyword">new</span> ModalHandler(modalData);
        });
    });
</code></pre>
<p>It loads the JSON representation of a popup using <code>{{modals|safe}}</code> into an instance of the <code>ModalHandler</code> class. Let’s break down what it does.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">constructor</span>(modalData) {
            <span class="hljs-built_in">this</span>.modal = modalData;
            <span class="hljs-built_in">this</span>.shown = <span class="hljs-literal">false</span>;
            <span class="hljs-built_in">this</span>.storageKey = <span class="hljs-string">`modal_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.modal.title.replace(<span class="hljs-regexp">/\s+/g</span>, <span class="hljs-string">'_'</span>).toLowerCase()}</span>`</span>;
            <span class="hljs-built_in">this</span>.init();
        }
</code></pre>
<p>Store all the data in the instance, and it also creates a <code>storageKey</code>, which is important for later. It finishes by calling the <code>.init()</code> method.</p>
<pre><code class="lang-javascript">init() {
    <span class="hljs-comment">// Only check session storage if the feature is enabled</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.modal.use_session_storage &amp;&amp; <span class="hljs-built_in">this</span>.hasBeenShown()) {
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
      ModalQueue.add(<span class="hljs-built_in">this</span>);
    }, <span class="hljs-built_in">this</span>.modal.display_delay * <span class="hljs-number">1000</span>);
}
</code></pre>
<p>This method checks if the popup should be displayed only once per session by verifying if it can <code>use_session_storage</code> and if it has already been shown. The method (<code>hasBeenShown()</code>) does this by checking the browser’s <code>sessionStorage</code>. The <code>init()</code> method uses <code>setTimeout</code> to schedule the display of the modal by adding it to a <code>ModalQueue</code> and setting the timeout to the number of seconds specified by the user. This is important because, due to the nature of this system, the same page might need to display many popups at the same time, but we don’t want to display all of them at once. So, we enqueue them while considering the number of seconds after which they should appear.</p>
<h3 id="heading-many-modals-so-lets-enqueue-them">Many modals, so let's enqueue them !</h3>
<p>From this point on, the class <code>ModalQueue</code> handles the lifecycle of a modal. Let’s study it.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">static</span> add(modal) {
            <span class="hljs-built_in">this</span>.queue.push(modal);
            <span class="hljs-built_in">this</span>.processQueue();
        }

        <span class="hljs-keyword">static</span> processQueue() {
            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.currentlyShowing || <span class="hljs-built_in">this</span>.queue.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;

            <span class="hljs-keyword">const</span> nextModal = <span class="hljs-built_in">this</span>.queue[<span class="hljs-number">0</span>];
            <span class="hljs-built_in">this</span>.currentlyShowing = <span class="hljs-literal">true</span>;
            nextModal.display();
        }

        <span class="hljs-keyword">static</span> modalClosed() {
            <span class="hljs-built_in">this</span>.queue.shift();
            <span class="hljs-built_in">this</span>.currentlyShowing = <span class="hljs-literal">false</span>;
            <span class="hljs-built_in">this</span>.processQueue();
        }
</code></pre>
<p>The <code>.add()</code> method inserts a popup into the queue and calls <code>processQueue()</code>, which will check if there is a modal currently being displayed or if there is no popup in the queue. If not, it will get the nextModal from the queue and call its <code>.display()</code> method (remember this is in the <code>ModalHandler</code> class). This method is responsible for displaying the actual modal on the page. The <code>modalClosed</code> is called when a modal is closed so it can trigger the queue processing to check if there is a popup to display.</p>
<p>Let’s study the <code>display()</code> method from the <code>ModalHandler</code>. It contains the actual HTML code that renders the modal. It does this by calling the methods <code>renderContent</code> and <code>renderCTA</code>, whose job is to load the various parts of the modal. After displaying, it sets up the necessary actions for when the user closes it. Here is the code block.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> modalElement = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'.modal-container'</span>);
<span class="hljs-keyword">const</span> closeButton = modalElement.querySelector(<span class="hljs-string">'.close-modal'</span>);
<span class="hljs-keyword">const</span> closeModal = <span class="hljs-function">() =&gt;</span> {
     modalElement.classList.add(<span class="hljs-string">'opacity-0'</span>);
     <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        modalElement.remove();
        ModalQueue.modalClosed();
     }, <span class="hljs-number">200</span>);
};
closeButton.addEventListener(<span class="hljs-string">'click'</span>, closeModal);
modalElement.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (e.target === modalElement) closeModal();
});
</code></pre>
<p>It basically adds an event listener to the <code>click</code> event and links it to both the <code>closeButton</code> and the <code>modalElement</code>. Notice how the <code>closeModal()</code> function works: it removes the <code>modalElement</code> (which is the modal itself) and calls <code>ModalQueue.modalClosed()</code>. This last method is part of the queue handler.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">static</span> modalClosed() {
    <span class="hljs-built_in">this</span>.queue.shift();
    <span class="hljs-built_in">this</span>.currentlyShowing = <span class="hljs-literal">false</span>;
    <span class="hljs-built_in">this</span>.processQueue();
}
</code></pre>
<p>It removes the last modal from the queue and marks that there is no modal being displayed, and then it calls <code>processQueue()</code> to verify if there is another popup to be displayed.</p>
<h1 id="heading-the-end-result">The end result</h1>
<p>And the result look like this on the frontend of the site.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731804291415/1119dc75-edb0-4055-ba63-7a7597e13dd4.png" alt class="image--center mx-auto" /></p>
<p>Note this code is compatible with <a target="_blank" href="https://tailwindcss.com/">TailwindCss</a> and not bootstrap, you may need to adapt if you wan to use for your own site.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>There are still many possible improvements, like allowing different styles of modals, setting the background color, or adjusting the width from the back office page, among other things. As a Wagtail developer, I prefer to make most features editable by my clients through the admin panel. This approach significantly reduces ongoing work for simple tasks that users might request while serving visitors on the site.</p>
<p>I am Adonis SIMO, and I build web applications and websites with Django and Wagtail. Feel free to contact me if you want to discuss a project. I would be happy to help.</p>
]]></content:encoded></item><item><title><![CDATA[How to efficiently Send Emails Asynchronously in Django]]></title><description><![CDATA[Sending email in a Django project is quite a simple task. You import the send_mail function and use it. We usually send emails at critical times, like when a visitor signs up, or for more specific tasks, like sending an autogenerated invoice or when ...]]></description><link>https://blog.adonissimo.com/how-to-efficiently-sending-emails-asynchronously-in-django</link><guid isPermaLink="true">https://blog.adonissimo.com/how-to-efficiently-sending-emails-asynchronously-in-django</guid><category><![CDATA[Django]]></category><category><![CDATA[email]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Mon, 11 Nov 2024 02:11:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731302558219/9a6ec5ae-1c8a-440c-9ccd-f30f4a5f677e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sending email in a Django project is quite a simple task. You import the <code>send_mail</code> function and use it. We usually send emails at critical times, like when a visitor signs up, or for more specific tasks, like sending an autogenerated invoice or when something happens and you need to send a notification via email. When that email has to be sent from a request (HTTP), it can slow things down for several reasons. Maybe there are too many emails to be sent, or the mail server is taking time to respond, or something else, and then it becomes problematic due to errors like internal and other issues.</p>
<p>The immediate solution is to use something like Celery or Django RQ to send the email in the background. In this flow, you will probably write a task that takes some arguments such as the destination email, subject, message, and other things, and this task will then import <code>send_mail</code> to do the job. It’s great, but it has some limitations. First of all, this won’t really be possible if you are using some package that has the ability to send emails by themselves, for example, django-allauth when a user signs up or requests a password reset, and it’s far-fetched to decide to re-implement all that process yourself. At this point, the solution is to use an email backend that is designed to process email in the background by default. This way, you continue to write normal Django and everything works in the background, like <code>django-celery-email</code> or something else. This leads to another possible problem: it assumes I am always using an SMTP-based backend. I might be using a Postmark server token or SES API key or something else that is not native SMTP.</p>
<p>The solution is to write my own email backend that will get the email to be sent and call my own email backend the way it should be and send the mail asynchronously. This way, I don’t need to change the code already written. It can look something along these lines:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> logging
<span class="hljs-keyword">from</span> postmarker.django <span class="hljs-keyword">import</span> EmailBackend
<span class="hljs-keyword">from</span> home.tasks <span class="hljs-keyword">import</span> send_emails <span class="hljs-comment"># &lt;&lt; this task will send the mail in background</span>
<span class="hljs-keyword">from</span> home.utils <span class="hljs-keyword">import</span> email_to_dict


logger = logging.getLogger(__name__)


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BackgroundEmailBackend</span>(<span class="hljs-params">EmailBackend</span>):</span>
    <span class="hljs-string">"""
    Load Postmark Email backend via a job and send mails in background
    """</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, fail_silently=False, **kwargs</span>):</span>
        super(BackgroundEmailBackend, self).__init__(fail_silently)
        self.init_kwargs = kwargs

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_messages</span>(<span class="hljs-params">self, email_messages</span>):</span>
        logger.info(<span class="hljs-string">"Sending email via RQ"</span>)
        send_emails.delay([email_to_dict(msg) <span class="hljs-keyword">for</span> i, msg <span class="hljs-keyword">in</span> enumerate(email_messages)], **self.init_kwargs)
        logger.info(<span class="hljs-string">"Sending email Scheduled"</span>)
</code></pre>
<p>The content of <code>home.utils.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> logging
<span class="hljs-keyword">from</span> django_rq <span class="hljs-keyword">import</span> job
<span class="hljs-keyword">from</span> django.conf <span class="hljs-keyword">import</span> settings
<span class="hljs-keyword">from</span> django.core.mail <span class="hljs-keyword">import</span> get_connection
<span class="hljs-keyword">from</span> home.utils <span class="hljs-keyword">import</span> dict_to_email


logger = logging.getLogger(__name__)


<span class="hljs-meta">@job</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_emails</span>(<span class="hljs-params">messages, **kwargs</span>):</span>
    <span class="hljs-comment"># backward compat: handle **kwargs and missing backend_kwargs</span>

    conn = get_connection(backend=<span class="hljs-string">'postmarker.django.EmailBackend'</span>, **kwargs)
    <span class="hljs-keyword">try</span>:
        conn.open()
    <span class="hljs-keyword">except</span> Exception:
        logger.exception(<span class="hljs-string">"Cannot reach POSTMARK_EMAIL_BACKEND %s"</span>, settings.CELERY_EMAIL_BACKEND)
    conn.send_messages([dict_to_email(msg_dict) <span class="hljs-keyword">for</span> msg_dict <span class="hljs-keyword">in</span> messages])
    conn.close()
    logger.info(<span class="hljs-string">f"Send <span class="hljs-subst">{len(messages)}</span> mails."</span>)
    <span class="hljs-keyword">return</span> messages
</code></pre>
<p>In this example, I am using Postmark to send emails, and I have installed the Django email backend for Postmark. This code is fairly customized yet simple and easy to change. You can even inherit from the regular Django email backend. I’ve taken inspiration from the django-celery library to write this, and I extracted a few methods from it, namely <code>dict_to_email</code> and <code>email_to_dict</code>. Their code is:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> copy
<span class="hljs-keyword">import</span> base64
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> django.conf <span class="hljs-keyword">import</span> settings
<span class="hljs-keyword">from</span> django.template.loader <span class="hljs-keyword">import</span> render_to_string
<span class="hljs-keyword">from</span> django.utils.timezone <span class="hljs-keyword">import</span> datetime, make_aware
<span class="hljs-keyword">from</span> email.mime.base <span class="hljs-keyword">import</span> MIMEBase
<span class="hljs-keyword">from</span> django.core.mail <span class="hljs-keyword">import</span> EmailMultiAlternatives, EmailMessage


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">email_to_dict</span>(<span class="hljs-params">message</span>):</span>
    <span class="hljs-keyword">if</span> isinstance(message, dict):
        <span class="hljs-keyword">return</span> message

    message_dict = {<span class="hljs-string">'subject'</span>: message.subject,
                    <span class="hljs-string">'body'</span>: message.body,
                    <span class="hljs-string">'from_email'</span>: message.from_email,
                    <span class="hljs-string">'to'</span>: message.to,
                    <span class="hljs-string">'bcc'</span>: message.bcc,
                    <span class="hljs-comment"># ignore connection</span>
                    <span class="hljs-string">'attachments'</span>: [],
                    <span class="hljs-string">'headers'</span>: message.extra_headers,
                    <span class="hljs-string">'cc'</span>: message.cc,
                    <span class="hljs-string">'reply_to'</span>: message.reply_to}

    <span class="hljs-keyword">if</span> hasattr(message, <span class="hljs-string">'alternatives'</span>):
        message_dict[<span class="hljs-string">'alternatives'</span>] = message.alternatives
    <span class="hljs-keyword">if</span> message.content_subtype != EmailMessage.content_subtype:
        message_dict[<span class="hljs-string">"content_subtype"</span>] = message.content_subtype
    <span class="hljs-keyword">if</span> message.mixed_subtype != EmailMessage.mixed_subtype:
        message_dict[<span class="hljs-string">"mixed_subtype"</span>] = message.mixed_subtype

    attachments = message.attachments
    <span class="hljs-keyword">for</span> attachment <span class="hljs-keyword">in</span> attachments:
        <span class="hljs-keyword">if</span> isinstance(attachment, MIMEBase):
            filename = attachment.get_filename(<span class="hljs-string">''</span>)
            binary_contents = attachment.get_payload(decode=<span class="hljs-literal">True</span>)
            mimetype = attachment.get_content_type()
        <span class="hljs-keyword">else</span>:
            filename, binary_contents, mimetype = attachment
            <span class="hljs-comment"># For a mimetype starting with text/, content is expected to be a string.</span>
            <span class="hljs-keyword">if</span> isinstance(binary_contents, str):
                binary_contents = binary_contents.encode()
        contents = base64.b64encode(binary_contents).decode(<span class="hljs-string">'ascii'</span>)
        message_dict[<span class="hljs-string">'attachments'</span>].append((filename, contents, mimetype))


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


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">dict_to_email</span>(<span class="hljs-params">messagedict</span>):</span>
    message_kwargs = copy.deepcopy(messagedict)  <span class="hljs-comment"># prevents missing items on retry</span>

    <span class="hljs-comment"># remove items from message_kwargs until only valid EmailMessage/EmailMultiAlternatives kwargs are left</span>
    <span class="hljs-comment"># and save the removed items to be used as EmailMessage/EmailMultiAlternatives attributes later</span>
    message_attributes = [<span class="hljs-string">'content_subtype'</span>, <span class="hljs-string">'mixed_subtype'</span>]
    attributes_to_copy = {}
    <span class="hljs-keyword">for</span> attr <span class="hljs-keyword">in</span> message_attributes:
        <span class="hljs-keyword">if</span> attr <span class="hljs-keyword">in</span> message_kwargs:
            attributes_to_copy[attr] = message_kwargs.pop(attr)

    <span class="hljs-comment"># remove attachments from message_kwargs then reinsert after base64 decoding</span>
    attachments = message_kwargs.pop(<span class="hljs-string">'attachments'</span>)
    message_kwargs[<span class="hljs-string">'attachments'</span>] = []
    <span class="hljs-keyword">for</span> attachment <span class="hljs-keyword">in</span> attachments:
        filename, contents, mimetype = attachment
        contents = base64.b64decode(contents.encode(<span class="hljs-string">'ascii'</span>))

        <span class="hljs-comment"># For a mimetype starting with text/, content is expected to be a string.</span>
        <span class="hljs-keyword">if</span> mimetype <span class="hljs-keyword">and</span> mimetype.startswith(<span class="hljs-string">'text/'</span>):
            contents = contents.decode()

        message_kwargs[<span class="hljs-string">'attachments'</span>].append((filename, contents, mimetype))

    <span class="hljs-keyword">if</span> <span class="hljs-string">'alternatives'</span> <span class="hljs-keyword">in</span> message_kwargs:
        message = EmailMultiAlternatives(**message_kwargs)
    <span class="hljs-keyword">else</span>:
        message = EmailMessage(**message_kwargs)

    <span class="hljs-comment"># set attributes on message with items removed from message_kwargs earlier</span>
    <span class="hljs-keyword">for</span> attr, val <span class="hljs-keyword">in</span> attributes_to_copy.items():
        setattr(message, attr, val)

    <span class="hljs-keyword">return</span> message
</code></pre>
<p>These functions are important because most of the time when you send an object to a background processing tool, you need to serialize it and deserialize it when reading it from the workers (the process that runs in the background to send the actual emails). It’s better to send native Python objects. In our case, we are sending an <code>EmailMessage</code> instance, so we use <code>email_to_dict</code> when sending the email to the background job and <code>dict_to_mail</code> when reading it from the queue in the background worker. You can learn more about the original version here from the <code>django-celery-email</code> package: <a target="_blank" href="https://github.com/pmclanahan/django-celery-email/blob/d47da19c09e29eea90684692e8dfa059e026c046/djcelery_email/utils.py#L26">https://github.com/pmclanahan/django-celery-email/blob/d47da19c09e29eea90684692e8dfa059e026c046/djcelery_email/utils.py#L26</a>. I had to perform some small tweaks and remove some code since I am using <code>django-rq</code> to handle background tasks. But this method is valid regardless of the background processing you are using.</p>
<p>While writing this article, I discovered the package <code>django-mailer</code>, which does this but better because it actually lets you specify whatever email backend you want to use. Hence, you have the same outcome with more flexibility: check its usage guide here <a target="_blank" href="https://github.com/pinax/django-mailer/blob/master/docs/usage.rst#usage">https://github.com/pinax/django-mailer/blob/master/docs/usage.rst#usage</a>.</p>
<p>I build projects using Django and Wagtail CMS. If you're planning to start your next tech venture, feel free to reach out. I can help you make the right decisions, even if we don't end up working together. Just message me on X (Twitter) @adonis__simo.</p>
]]></content:encoded></item><item><title><![CDATA[How to Set Up GDPR-Compliant Analytics in Wagtail CMS: Cookie Consent with Clarity and Google Analytics]]></title><description><![CDATA[Why is it important ?
When creating a website, you include things like Google Analytics to track visits and view some insights later, but that’s called tracking, and it’s important to let the visitor know about it. In the EU, there are laws requiring...]]></description><link>https://blog.adonissimo.com/how-to-set-up-gdpr-compliant-analytics-in-wagtail-cms-cookie-consent-with-clarity-and-google-analytics</link><guid isPermaLink="true">https://blog.adonissimo.com/how-to-set-up-gdpr-compliant-analytics-in-wagtail-cms-cookie-consent-with-clarity-and-google-analytics</guid><category><![CDATA[wagtail]]></category><category><![CDATA[Django]]></category><category><![CDATA[#gdpr]]></category><category><![CDATA[cms]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Sat, 09 Nov 2024 17:38:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731173315915/d3c54cfd-0be4-4b08-851f-2cac17a7c470.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-why-is-it-important">Why is it important ?</h1>
<p>When creating a website, you include things like Google Analytics to track visits and view some insights later, but that’s called tracking, and it’s important to let the visitor know about it. In the EU, there are laws requiring website owners to add a notice that tells the user about any kind of tracking or cookie usage since those cookies are saved in their browser and are used for tracking them.</p>
<p>I’ve built a website recently using Wagtail CMS, and I wanted to track visits and visualize user actions using Microsoft Clarity. I signed up for those two services and grabbed the integration code. I also wanted to include a banner for cookie consent approval. It's quite simple to build, but there is a catch: when the user decides not to be tracked, I should not trigger Google Analytics or Clarity dynamically. This article is about how I did it on my Wagtail site.</p>
<h1 id="heading-how-to-do-it">How to do it?</h1>
<p>I created a <strong>Snippet</strong> that contains the code that can be injected into all the pages, for example, the Google Analytics code, and it has an <code>is_active</code> field. The purpose of this model is to store all the codes I want to inject into my webpage. Here is what the code looks like; I called it <code>ThirdPartyIntegration</code>.</p>
<pre><code class="lang-python"><span class="hljs-comment"># youapp.models.py</span>

<span class="hljs-meta">@register_snippet</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ThirdPartyIntegration</span>(<span class="hljs-params">models.Model</span>):</span>
    name = models.CharField(
        max_length=<span class="hljs-number">255</span>,
        help_text=<span class="hljs-string">"A descriptive name for this integration (e.g., 'Google Analytics', 'Hotjar')"</span>
    )
    html_code = models.TextField(
        help_text=<span class="hljs-string">"The HTML/JavaScript code to be inserted in the template"</span>
    )
    is_active = models.BooleanField(
        default=<span class="hljs-literal">True</span>,
        help_text=<span class="hljs-string">"Only active integrations will be included in the template"</span>
    )
    position = models.CharField(
        max_length=<span class="hljs-number">20</span>,
        choices=[
            (<span class="hljs-string">'head_start'</span>, <span class="hljs-string">'Beginning of HEAD'</span>),
            (<span class="hljs-string">'head_end'</span>, <span class="hljs-string">'End of HEAD'</span>),
            (<span class="hljs-string">'body_start'</span>, <span class="hljs-string">'Beginning of BODY'</span>),
            (<span class="hljs-string">'body_end'</span>, <span class="hljs-string">'End of BODY'</span>),
        ],
        default=<span class="hljs-string">'head_end'</span>,
        help_text=<span class="hljs-string">"Where in the template this code should be inserted"</span>
    )
    created_at = models.DateTimeField(auto_now_add=<span class="hljs-literal">True</span>)
    updated_at = models.DateTimeField(auto_now=<span class="hljs-literal">True</span>)

    panels = [
        FieldPanel(<span class="hljs-string">'name'</span>),
        FieldPanel(<span class="hljs-string">'html_code'</span>),
        FieldPanel(<span class="hljs-string">'is_active'</span>),
        FieldPanel(<span class="hljs-string">'position'</span>),
    ]

    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meta</span>:</span>
        verbose_name = <span class="hljs-string">"Third Party Integration"</span>
        verbose_name_plural = <span class="hljs-string">"Third Party Integrations"</span>
        ordering = [<span class="hljs-string">'name'</span>]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__str__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"<span class="hljs-subst">{self.name}</span> (<span class="hljs-subst">{<span class="hljs-string">'Active'</span> <span class="hljs-keyword">if</span> self.is_active <span class="hljs-keyword">else</span> <span class="hljs-string">'Inactive'</span>}</span>)"</span>
</code></pre>
<p>It also contains a <code>position</code> to specify exactly where I can include it on the webpage. There are four possible positions.</p>
<p>To render each integration on all pages, I created a Django template tag to load them, but only the active ones.</p>
<pre><code class="lang-python">
<span class="hljs-keyword">from</span> django <span class="hljs-keyword">import</span> template
<span class="hljs-keyword">from</span> django.utils.safestring <span class="hljs-keyword">import</span> mark_safe
<span class="hljs-keyword">from</span> yourapp.models <span class="hljs-keyword">import</span> ThirdPartyIntegration 

register = template.Library()


<span class="hljs-meta">@register.simple_tag</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">render_integrations</span>(<span class="hljs-params">position</span>):</span>
    <span class="hljs-string">"""
    Renders all active third-party integrations for a specific position.
    Usage: {% render_integrations 'head_end' %}
    """</span>
    integrations = ThirdPartyIntegration.objects.filter(
        is_active=<span class="hljs-literal">True</span>,
        position=position
    )
    <span class="hljs-keyword">return</span> mark_safe(<span class="hljs-string">'\n'</span>.join(integration.html_code <span class="hljs-keyword">for</span> integration <span class="hljs-keyword">in</span> integrations))
</code></pre>
<p>This will output a simple set of HTML code that will be injected directly into the template at render time.</p>
<h2 id="heading-organize-across-the-base-template">Organize across the base template</h2>
<p>Next, to inject them into the webpage, I modified the <code>base.html</code> file to call this tag at the four different positions like this:</p>
<pre><code class="lang-javascript">&lt;!DOCTYPE html&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    {% render_integrations 'head_start' %}

    <span class="hljs-comment">&lt;!--- some stuff here --&gt;</span>

    {% render_integrations 'head_end' %}
    <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    {% render_integrations 'body_start' %}

    <span class="hljs-comment">&lt;!-- the content here --&gt;</span>
    {% render_integrations 'body_end' %}
    <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
</code></pre>
<p>Obviously, I removed all the default stuff Wagtail provides in the base template for demo purposes. Now, we have called the template tag to load any third-party scripts dynamically. This is technically enough to not worry anymore about integrating these scripts manually. In my Wagtail admin, I have something that looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731170281992/3e20c5b6-30f5-410b-b2cd-b6e07c21c9b4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-fun-part-integrating-cookie-consent-properly">The fun part: integrating cookie consent properly</h2>
<h2 id="heading-the-banner">The banner</h2>
<p>The consent banner is the most important part here because, depending on what the user chooses, it will let the other scripts run or not, so it has to be written accordingly. Here is its content:</p>
<pre><code class="lang-javascript">&lt;!-- Cookie Consent Banner --&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
<span class="hljs-selector-class">.cookie-consent-banner</span> {
    <span class="hljs-attribute">display</span>: none; <span class="hljs-comment">/* Hidden by default, shown via JS */</span>
    <span class="hljs-attribute">position</span>: fixed;
    <span class="hljs-attribute">bottom</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">left</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">right</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">z-index</span>: <span class="hljs-number">9999</span>;
    <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#1e293b</span>;
    <span class="hljs-attribute">color</span>: white;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">1rem</span>;
    <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> -<span class="hljs-number">4px</span> <span class="hljs-number">6px</span> -<span class="hljs-number">1px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.1</span>);
}
<span class="hljs-selector-class">.cookie-consent-banner</span><span class="hljs-selector-class">.visible</span> {
    <span class="hljs-attribute">display</span>: block;
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"cookie-consent-banner"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"cookie-consent-banner border-t border-white"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"md:flex md:items-center md:justify-between"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1 min-w-0 mb-4 md:mb-0"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm"</span>&gt;</span>
                    THE CONSENT TEXT HERE
                <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-shrink-0 flex space-x-4"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"reject-cookies"</span> 
                    <span class="hljs-attr">class</span>=<span class="hljs-string">"inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"</span>&gt;</span>
                    Refuse All
                <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"accept-cookies"</span> 
                    <span class="hljs-attr">class</span>=<span class="hljs-string">"inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-secondary bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"margin-left: 5px"</span>&gt;</span>
                    Accept All
                <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initializeCookieConsent</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">const</span> banner = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'cookie-consent-banner'</span>);

        <span class="hljs-comment">// If user already made a choice, don't show the banner</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'cookieConsentChoice'</span>)) {
            <span class="hljs-keyword">return</span>;
        }
        <span class="hljs-comment">// Show the banner</span>
        <span class="hljs-keyword">if</span> (banner) {
            banner.classList.add(<span class="hljs-string">'visible'</span>);
        }

        <span class="hljs-comment">// Handle accept button click</span>
        <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'accept-cookies'</span>)?.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
            acceptCookies();
            hideBanner();
        });

        <span class="hljs-comment">// Handle reject button click</span>
        <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'reject-cookies'</span>)?.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
            rejectCookies();
            hideBanner();
        });
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">acceptCookies</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'cookieConsentChoice'</span>, <span class="hljs-string">'accepted'</span>);
        <span class="hljs-comment">// Dispatch custom event that other scripts can listen for</span>
        <span class="hljs-built_in">document</span>.dispatchEvent(<span class="hljs-keyword">new</span> CustomEvent(<span class="hljs-string">'cookieConsentAccepted'</span>));
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">rejectCookies</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'cookieConsentChoice'</span>, <span class="hljs-string">'rejected'</span>);
        <span class="hljs-comment">// Dispatch custom event that other scripts can listen for</span>
        <span class="hljs-built_in">document</span>.dispatchEvent(<span class="hljs-keyword">new</span> CustomEvent(<span class="hljs-string">'cookieConsentRejected'</span>));
        <span class="hljs-comment">// Disable tracking cookies if possible</span>
        disableTracking();
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hideBanner</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">const</span> banner = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'cookie-consent-banner'</span>);
        <span class="hljs-keyword">if</span> (banner) {
            banner.classList.remove(<span class="hljs-string">'visible'</span>);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">disableTracking</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// Add your GA_ID or other tracking IDs here</span>
        <span class="hljs-keyword">const</span> GA_ID = <span class="hljs-string">'G-XXXXXXXXXXX'</span>; <span class="hljs-comment">// Replace with your actual GA ID</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>[<span class="hljs-string">`ga-disable-<span class="hljs-subst">${GA_ID}</span>`</span>]) {
            <span class="hljs-built_in">window</span>[<span class="hljs-string">`ga-disable-<span class="hljs-subst">${GA_ID}</span>`</span>] = <span class="hljs-literal">true</span>;
        }
        <span class="hljs-comment">// Clear existing cookies</span>
        <span class="hljs-built_in">document</span>.cookie.split(<span class="hljs-string">";"</span>).forEach(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">c</span>) </span>{ 
            <span class="hljs-built_in">document</span>.cookie = c.replace(<span class="hljs-regexp">/^ +/</span>, <span class="hljs-string">""</span>).replace(<span class="hljs-regexp">/=.*/</span>, <span class="hljs-string">"=;expires="</span> + <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toUTCString() + <span class="hljs-string">";path=/"</span>); 
        });
    }

    <span class="hljs-comment">// Create cookie consent manager object</span>
    <span class="hljs-built_in">window</span>.CookieConsentManager = {
        <span class="hljs-attr">hasConsent</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'cookieConsentChoice'</span>) === <span class="hljs-string">'accepted'</span>;
        },
        <span class="hljs-attr">getChoice</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'cookieConsentChoice'</span>);
        },
        <span class="hljs-attr">reset</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
            <span class="hljs-built_in">localStorage</span>.removeItem(<span class="hljs-string">'cookieConsentChoice'</span>);
            initializeCookieConsent();
        }
    };

    <span class="hljs-comment">// Initialize when DOM is ready</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">document</span>.readyState === <span class="hljs-string">'loading'</span>) {
        <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'DOMContentLoaded'</span>, initializeCookieConsent);
    } <span class="hljs-keyword">else</span> {
        initializeCookieConsent();
    }
})();
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>This code assumes that you will include Google Analytics, which is why the function that runs when the user rejects consent is written this way:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">disableTracking</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// Add your GA_ID or other tracking IDs here</span>
        <span class="hljs-keyword">const</span> GA_ID = <span class="hljs-string">'G-XXXXXXXXXXX'</span>; <span class="hljs-comment">// Replace with your actual GA ID</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>[<span class="hljs-string">`ga-disable-<span class="hljs-subst">${GA_ID}</span>`</span>]) {
            <span class="hljs-built_in">window</span>[<span class="hljs-string">`ga-disable-<span class="hljs-subst">${GA_ID}</span>`</span>] = <span class="hljs-literal">true</span>;
        }
        <span class="hljs-comment">// Clear existing cookies</span>
        <span class="hljs-built_in">document</span>.cookie.split(<span class="hljs-string">";"</span>).forEach(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">c</span>) </span>{ 
            <span class="hljs-built_in">document</span>.cookie = c.replace(<span class="hljs-regexp">/^ +/</span>, <span class="hljs-string">""</span>).replace(<span class="hljs-regexp">/=.*/</span>, <span class="hljs-string">"=;expires="</span> + <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toUTCString() + <span class="hljs-string">";path=/"</span>); 
        });
    }
</code></pre>
<p>These scripts will simply check if the user has accepted or rejected the consent and enable the other scripts accordingly. They store the acceptance in a cookie and check it every time the page loads. The HTML code is written with the assumption that you use Tailwind CSS in your project, so you should modify it accordingly..</p>
<h2 id="heading-google-analytics">Google Analytics</h2>
<p>The following code will start the GA integration script or not, depending on the user’s consent.</p>
<pre><code class="lang-javascript">&lt;!-- Google Analytics <span class="hljs-keyword">with</span> Consent Check --&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Function to initialize Google Analytics</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initializeGA</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">window</span>.dataLayer = <span class="hljs-built_in">window</span>.dataLayer || [];
        <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">gtag</span>(<span class="hljs-params"></span>)</span>{dataLayer.push(<span class="hljs-built_in">arguments</span>);}
        gtag(<span class="hljs-string">'js'</span>, <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>());
        gtag(<span class="hljs-string">'config'</span>, <span class="hljs-string">'G-XXXXXX'</span>);
    }

    <span class="hljs-comment">// Function to load GA script</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loadGAScript</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">const</span> script = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'script'</span>);
        script.async = <span class="hljs-literal">true</span>;
        script.src = <span class="hljs-string">'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX'</span>;
        <span class="hljs-built_in">document</span>.head.appendChild(script);
        script.onload = initializeGA;
    }

    <span class="hljs-comment">// Check if user has already consented</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.CookieConsentManager &amp;&amp; <span class="hljs-built_in">window</span>.CookieConsentManager.hasConsent()) {
        loadGAScript();
    }

    <span class="hljs-comment">// Listen for future consent</span>
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'cookieConsentAccepted'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
        loadGAScript();
    });

    <span class="hljs-comment">// Listen for consent rejection</span>
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'cookieConsentRejected'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// Disable GA if it was previously loaded</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>[<span class="hljs-string">'ga-disable-G-XXXXXXXXXX'</span>]) {
            <span class="hljs-built_in">window</span>[<span class="hljs-string">'ga-disable-G-XXXXXXXXXX'</span>] = <span class="hljs-literal">true</span>;
        }
    });
})();
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>It does the following:</p>
<ul>
<li><p>Listens to the <code>cookieConsentRejected</code> event and disables GA if it was previously enabled.</p>
</li>
<li><p>Listens to the <code>cookieConsentAccepted</code> event and enables GA if it was disabled.</p>
</li>
</ul>
<p>The <code>loadGAScript</code> function is the one actually attaching GA to the DOM, and <code>initializeGA</code> will start the tracking process of GA. If you have already included GA integration code, you might be familiar with these two specific code blocks.</p>
<h2 id="heading-microsoft-clarity">Microsoft Clarity</h2>
<p>The following code shows how the integration of Clarity was done by following the two previous patterns for checking consent first.</p>
<pre><code class="lang-javascript">&lt;!-- Microsoft Clarity <span class="hljs-keyword">with</span> Consent Check --&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> CLARITY_ID = <span class="hljs-string">'xxxxxxxx'</span>; <span class="hljs-comment">// Replace with your actual Clarity project ID</span>

    <span class="hljs-comment">// Function to initialize Clarity</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initializeClarity</span>(<span class="hljs-params"></span>) </span>{
        (<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">c,l,a,r,i,t,y</span>)</span>{
            c[a]=c[a]||<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>)</span>{(c[a].q=c[a].q||[]).push(<span class="hljs-built_in">arguments</span>)};
            t=l.createElement(r);t.async=<span class="hljs-number">1</span>;t.src=<span class="hljs-string">"https://www.clarity.ms/tag/"</span>+i;
            y=l.getElementsByTagName(r)[<span class="hljs-number">0</span>];y.parentNode.insertBefore(t,y);
        })(<span class="hljs-built_in">window</span>, <span class="hljs-built_in">document</span>, <span class="hljs-string">"clarity"</span>, <span class="hljs-string">"script"</span>, CLARITY_ID);
    }

    <span class="hljs-comment">// Function to disable Clarity</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">disableClarity</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// Remove Clarity cookies if they exist</span>
        <span class="hljs-built_in">document</span>.cookie = <span class="hljs-string">`_clarity=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`</span>;
        <span class="hljs-built_in">document</span>.cookie = <span class="hljs-string">`_clsk=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`</span>;
        <span class="hljs-built_in">document</span>.cookie = <span class="hljs-string">`_clck=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`</span>;

        <span class="hljs-comment">// Stop any existing Clarity sessions</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.clarity) {
            <span class="hljs-built_in">window</span>.clarity(<span class="hljs-string">"stop"</span>);
        }
    }

    <span class="hljs-comment">// Check if user has already consented</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.CookieConsentManager &amp;&amp; <span class="hljs-built_in">window</span>.CookieConsentManager.hasConsent()) {
        initializeClarity();
    }

    <span class="hljs-comment">// Listen for future consent</span>
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'cookieConsentAccepted'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
        initializeClarity();
    });

    <span class="hljs-comment">// Listen for consent rejection</span>
    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'cookieConsentRejected'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
        disableClarity();
    });
})();
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>If you are familiar with Clarity integration, the function <code>initializeClarity</code> will be a reminder. This code simply listens to events related to consent status and disables or enables Clarity tracking.</p>
<p>I positioned them differently on the page:</p>
<ul>
<li><p>Clarity and GA scripts are positioned at the <code>head_end</code>.</p>
</li>
<li><p>Cookie consent is at the <code>body_end</code>.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Implementing GDPR-compliant analytics in Wagtail CMS demonstrates the platform's exceptional flexibility and extensibility. This approach to cookie consent and analytics tracking has proven robust and reusable across multiple projects, making it a valuable addition to any Wagtail website. The combination of Wagtail's structured architecture with Django's powerful framework provides an ideal foundation for building scalable, privacy-conscious web applications.</p>
<p>As a seasoned Django/Wagtail developer with years of experience building professional websites and applications, I'm available to help you implement similar solutions or develop your next web project. Whether you need assistance with analytics implementation, custom Wagtail development, or a complete website build, feel free to reach out.</p>
]]></content:encoded></item><item><title><![CDATA[How I would build a clone of canva.com]]></title><description><![CDATA[What is canva.com
Canva is a graphic design platform that provides tools for creating social media graphics, according to Wikipedia. In this article, I will share my thought process for deciding on the tech stack I would choose to build (or clone) it...]]></description><link>https://blog.adonissimo.com/how-i-would-build-a-clone-of-canvacom</link><guid isPermaLink="true">https://blog.adonissimo.com/how-i-would-build-a-clone-of-canvacom</guid><category><![CDATA[Svelte]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[B2C]]></category><category><![CDATA[b2b]]></category><category><![CDATA[Django]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Thu, 15 Aug 2024 14:18:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723731040237/638fca1e-1fae-4468-be0d-4d1bbf4b89e9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-what-is-canvacom"><strong>What is canva.com</strong></h1>
<p>Canva is <em>a graphic design platform that provides tools for creating social media graphics</em>, according to <a target="_blank" href="https://en.wikipedia.org/wiki/Canva">Wikipedia</a>. In this article, I will share my thought process for deciding on the tech stack I would choose to build (or clone) it. Let's analyze this from one part of the stack to another (frontend to database), I will also share a bit of how I decided what tools to use in order to build my own SaaS (<a target="_blank" href="https://marthaia.com">Martha AI</a>).</p>
<h1 id="heading-frontend"><strong>Frontend</strong></h1>
<p>Canva is a media manipulation project where most of the work happens on the frontend. But there is also a lot going on in the backend, so I need to choose something not too low-level like jQuery, Turbo, or HTMX. I also want it to be performant with not too much overhead. I would choose to build the frontend with <strong>Svelte</strong> because it has a whole compilation step where the code I write is copiled and optimized into a simple and light JavaScript bundle. I don’t have to learn a new syntax like JSX, and it provides much more tooling to control how I render the UI (conditions, loops, etc.). It also offers very good reactive programming tools and state management out of the box.</p>
<h1 id="heading-backend"><strong>Backend</strong></h1>
<p>Talking about the backend, I would choose something like Django, Laravel, Ruby, NestJs, or AdonisJs. On that side, various activities are happening at the same time, such as:</p>
<ul>
<li><p>emails being sent</p>
</li>
<li><p>media being processed (analysis for viruses, compression, etc.)</p>
</li>
<li><p>multiple users interacting (co-editing a design)</p>
</li>
<li><p>searching and indexing data (search images by inputting text)</p>
</li>
<li><p>account management (auth)</p>
</li>
<li><p>plugin systems</p>
</li>
<li><p>teams (groups of users sharing the same resources)</p>
</li>
<li><p>scheduling for end-users</p>
</li>
<li><p>payments integration (Stripe, PayPal, or whatever)</p>
</li>
<li><p>notifications</p>
</li>
<li><p>a lot of analytics</p>
</li>
<li><p>A lot more stuff</p>
</li>
</ul>
<p>I can certainly build something that integrates all this with SvelteKit, Next.js, or Remix.js (I personally prefer Remix over Next ), but it will quickly become VERY expensive (human, technical, and financial resources). Now, I can choose from the list of frameworks provided earlier (Laravel, Django, etc.). One important point to consider is what language I am proficient in and what about the team I have. If it's Python, then use Django; if PHP, then use Laravel, etc. I am personally good with Python and Django. But from the standpoint of the features all these frameworks offer and their ecosystems, using <strong>Laravel</strong> will be a good match. It offers a rich set of packages when it comes to stuff related to real-time communications, background processing, auth, payment infrastructure, notifications, and most of these come out of the box (job scheduling, notifications, auth &amp; permissions).</p>
<h1 id="heading-database">Database</h1>
<p>I would choose <strong>PostgreSQL</strong>. It’s very good at handling a lot of data and performing <strong>FTS</strong> (Full Text Search). It offers a wide range of capabilities for data indexing, searching, and typing (INT, Char, JSON, JSONB, Vector, etc.) in a vast amount of data while being very simple to operate and expand. For example, the whole <a target="_blank" href="https://notion.so">Notion</a> and <a target="_blank" href="https://sentry.com">Sentry</a> platforms are running on PostgreSQL.</p>
<h1 id="heading-hosting">Hosting</h1>
<p>I could host the whole thing on AWS, but I would need knowledge to handle that. If I don’t have enough knowledge, I would start on something like Digital Ocean and use <strong>S3</strong> + <strong>CloudFront</strong> (CDN) to store files and serve them at scale and low cost. Personally, I have enough experience to host this on AWS while staying cost-effective (but that is another discussion).</p>
<h1 id="heading-a-practical-and-personal-example"><strong>A practical and personal example</strong></h1>
<p>I have personally built a complete SaaS (<a target="_blank" href="https://marthaia.com">marthaia.com</a>) with Sveltekit in the B2C world, and I used a similar thought process. It's an AI-enabled platform that generates long-form documents using OpenAI models. I wanted users to be able to leave the platform while it is generating content, so I needed the ability to run code as a background process and also store data. I didn't need much more, and most of the coding was on the frontend side (building the AI Editor, displaying text while it's being generated by the AI as a stream, etc.). I chose Sveltekit and added BullMQ to handle the background processing. Since I wanted users to be able to see text as it's generated, I needed a real-time protocol between the browser and the server. I could have chosen WebSocket, but because the direction of the text is mostly from server to client (98% of the time), I chose SSE (server-sent events). I wanted to move quickly, and I knew user data would be changing a lot, so I decided to store everything in Redis (acting as my database) and went live with the whole thing. Now it's running and generating revenue. The whole thing is hosted on a VPS and automatically deployed using a script I wrote and set up via GitHub Actions. You can learn more <a target="_blank" href="https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr">in this article I wrote months back</a>.</p>
<p>At the same time, I work in a company where I lead a team of many developers. We have built a B2B SaaS platform and chose SvelteKit for the frontend and Nest, Django, and AdonisJS for our various backend services.</p>
]]></content:encoded></item><item><title><![CDATA[How to deploy a production app on a VPS and automate the process with Docker, GitHub Actions, and AWS ECR.]]></title><description><![CDATA[In this article, we will learn what it means to push an application in production and how to make it happen automatically, we will see docker and GitHub action for that. Since we are using docker I won't spend too much time on what technologies have ...]]></description><link>https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr</link><guid isPermaLink="true">https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr</guid><category><![CDATA[Docker]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[nginx]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Wed, 03 Apr 2024 23:44:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712186726683/ca1ee7fe-cbde-41bf-8344-d47c15d16e4e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we will learn what it means to push an application in production and how to make it happen automatically, we will see docker and GitHub action for that. Since we are using docker I won't spend too much time on what technologies have been used to write the application rather than what to do with the docker image itself. The premise means here a simple server, let it be a simple VPS (Virtual Private Server), a dedicated server, or any server where you have SSH to use it. We have to perform actions, prepare the server to receive and execute the app, and set up the deployment pipeline. For the upcoming part of this article, I will consider you have a Ubuntu server</p>
<h2 id="heading-lets-prepare-the-server">Let's prepare the server</h2>
<p>The very first thing to perform some one-time config on the server, the goal is to prepare it for the next steps. For this purpose, we have to consider the following topics:</p>
<h3 id="heading-docker-compose-to-run-the-application-and-its-dependencies">Docker Compose to run the application and its dependencies</h3>
<p>Docker Compose is a tool that runs many Docker applications as services and lets them communicate with each other, as well as other features like volumes for file storage. Let's first install Docker and Docker Compose on the server; you can follow the official installation guide here: https://docs.docker.com/engine/install/ubuntu/. After this, it's important to run it as a non-root user; here is what the official documentation says: https://docs.docker.com/engine/security/rootless/.</p>
<h3 id="heading-set-up-an-aws-ecr-amp-cli-credentials">Set up an AWS ECR &amp; CLI credentials.</h3>
<p><em>Do note that you can achieve the same goal with another registry if you want, I am just using AWS here because it's more simple for me.</em></p>
<p>Since we are using Docker, the images will have to be stored and retrieved from somewhere. For this purpose, I am using <strong>AWS ECR</strong> (Amazon Web Services Elastic Container Registry). It's a Docker registry within an AWS account. It's very cheap to use and easy to set up. You can also use Docker Hub to create a private repository for your images. It all starts by creating an ECR private registry in the AWS account. You will click on "<strong>Create Repository</strong>" and fill in the name of the repository.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711949227494/8d98fbc4-46bb-40c6-b32a-d98a3b54f4b2.png" alt class="image--center mx-auto" /></p>
<p>After creating a repository you can copy the repository URI and keep it for later. It has the following format <a target="_blank" href="http://284637488727.dkr.ecr.ca-central-1.amazonaws.com/myapp"><code>AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/</code></a><code>REPOSITORY_NAME</code> .</p>
<p>You will also need to set up AWS IAM credentials that have the right to pull/push from/to this repository. Let's head to the IAM service, click on new user and attach the following policy to him: <a target="_blank" href="https://us-east-1.console.aws.amazon.com/iam/home?region=ca-central-1#/policies/details/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonEC2ContainerRegistryFullAccess"><code>AmazonEC2ContainerRegistryFullAccess</code></a> , you don't need to enable access to the AWS console for him. At the end of this process, you receive 2 keys from AWS, a <code>secret key</code> and a <code>secret key id</code>, keep them aside we will need them for the upcoming work.</p>
<p>Back on our server, we need to install AWS CLI. The official way of installing it is available here. <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions">https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions</a>. After this you can test the installation by running <code>aws --version</code> the command. At this step, you will have to execute the command <code>aws configure</code> and respond to questions by providing the previous keys generated on aws, the <code>secret key</code> , and the <code>secret key id</code> . You will also be prompted to choose an output format, simple just JSON, and provide a default region, it's better to choose the region where you created the ECR registry earlier.</p>
<h3 id="heading-set-up-the-application-runner-script">Set up the application runner script.</h3>
<p>In my workflow I have written a little shell script that performs certain actions, it's the key part of this process, it login to the registry, downloads the image, and restarts the corresponding docker service, I just call it <code>redeploy.sh</code> and save it under a folder from where I want to run my app, here is the content:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># Retrieve AWS ECR login command</span>
aws ecr get-login-password --region [SWS_REGION] | docker login --username AWS --password-stdin [AWS_REGION].dkr.ecr.us-west-2.amazonaws.com

<span class="hljs-comment"># Associating repositories with identifiers</span>
<span class="hljs-built_in">declare</span> -A repositories=(
    [<span class="hljs-string">"web"</span>]=<span class="hljs-string">"[REGISTRY_NAME]:latest"</span>
)

<span class="hljs-comment"># Check if service identifier is provided as a command line argument</span>
<span class="hljs-keyword">if</span> [ -z <span class="hljs-string">"<span class="hljs-variable">$1</span>"</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Please provide a service identifier as a command line argument."</span>
    <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

service_identifier=<span class="hljs-variable">$1</span>

<span class="hljs-comment"># Check if the provided service identifier exists in the repositories array</span>
<span class="hljs-keyword">if</span> [ -z <span class="hljs-string">"<span class="hljs-variable">${repositories[$service_identifier]}</span>"</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Invalid service identifier. Available identifiers: <span class="hljs-variable">${!repositories[@]}</span>"</span>
    <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># pull the new image from the registry</span>
repository=<span class="hljs-variable">${repositories[$service_identifier]}</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Pulling [AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/<span class="hljs-variable">$repository</span>"</span>
docker pull <span class="hljs-string">"[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/<span class="hljs-variable">$repository</span>"</span>

<span class="hljs-comment"># Change directory to [APP_FOLDER] </span>
<span class="hljs-built_in">cd</span> /home/ubuntu/[APP_FOLDER] || {
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to change directory to /home/ubuntu/[APP_FOLDER]"</span>
    <span class="hljs-built_in">exit</span> 1
}

<span class="hljs-comment"># stop and restart the service, this wil force docker compose to redownload the lates image</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Re-running service <span class="hljs-variable">$service_identifier</span>"</span>
docker compose stop <span class="hljs-string">"<span class="hljs-variable">$service_identifier</span>"</span>
docker compose up --no-deps <span class="hljs-string">"<span class="hljs-variable">$service_identifier</span>"</span> -d

<span class="hljs-comment"># Remove old and un-used docker images</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Removing unused Docker images"</span>
docker image prune -fa
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Removed Dangling Images"</span>
</code></pre>
<p>The first step of this script consists of logging into the AWS account with AWS CLI to get a token that docker will use when retrieving the docker image, remember the registry is private, we can't just pull it without being authenticated.</p>
<p>Then we declare a repository list and associate them with some identifier, the specified identifier will be used as a command line arg, more on this later. After this we verify if the user has provided an argument that corresponds to an existing service identifier, we want him to type something like <code>./redeploy web</code> for example, the script will associate the argument <code>web</code> to the repository <code>web</code> as in the second step.</p>
<p>After having the service identifier we create the repository URL dynamically and perform a docker pull with it. This ensures the docker image is getting downloaded to our system.</p>
<p>The script will now cd into the application folder, <code>/home/ubuntu/[APP_FOLDER]</code> this assumes you are running everything under the user <code>ubuntu</code> and the his <code>HOME</code> folder is named <code>ubuntu</code> , <code>APP_FOLDER</code> contain the whole setup.</p>
<p>The next step consists of stopping and starting the service after which we simply remove old and unused images with the command <code>docker image prune -fa</code> you can learn more here: <a target="_blank" href="https://docs.docker.com/reference/cli/docker/system/prune/">https://docs.docker.com/reference/cli/docker/system/prune/</a>.</p>
<h3 id="heading-the-docker-compose-file">The Docker compose file</h3>
<p>Compose is the utility that runs our whole system, it needs a file named <code>docker-compose.yml</code> where you will define everything, let's assume our app needs a <code>redis</code> and a <code>postgres</code> service to run, here is what it will look like:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3.9'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">"[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/myapp:latest"</span>
    <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-number">8080</span><span class="hljs-string">:8080</span>
    <span class="hljs-attr">depends_on:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">redis</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">db</span>
    <span class="hljs-attr">env_file:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>

  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">'redis:alpine'</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'6379:6379'</span>
  <span class="hljs-attr">db:</span>    
    <span class="hljs-attr">image:</span> <span class="hljs-string">'postgres:14'</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">postgres</span>
      <span class="hljs-attr">PGDATA:</span> <span class="hljs-string">/var/lib/postgresql/data/pgdata</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [ <span class="hljs-string">"CMD"</span>, <span class="hljs-string">"pg_isready"</span>, <span class="hljs-string">"-q"</span>, <span class="hljs-string">"-d"</span>, <span class="hljs-string">"postgres"</span>, <span class="hljs-string">"-U"</span>, <span class="hljs-string">"postgres"</span> ]
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">45s</span>
      <span class="hljs-attr">interval:</span> <span class="hljs-string">10s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">10</span>
    <span class="hljs-attr">ports:</span> 
      <span class="hljs-bullet">-</span> <span class="hljs-string">'5437:5432'</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./opt/postgres/data:/var/lib/postgresql/data</span>
</code></pre>
<p>You volume <code>./opt/postgres/data:/var/lib/postgresql/data</code> will map the content of the Postgres server to the local disk so it can't get lost when the docker container stops running. Learn more about running Postgres with docker-compose here <a target="_blank" href="https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216">https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216</a>. I've used a directive name <code>env_file</code> this allows docker-compose to read a file a load its content into the docker container at runtime, i did this because usually, the docker-compose file is committed to a VCS there I don't want to keep the environment variable into it directly via the <code>environment</code> directive in the service. Note that our service is named <code>web</code> here, earlier we wrote a file <code>redeploy.sh</code> and we intend to run it like this:</p>
<pre><code class="lang-bash"> ./redeploy.sh web
</code></pre>
<p>The <code>web</code> argument is linked to the name of our service, that file is simply mapping the argument to a service name in the docker file.</p>
<h3 id="heading-setup-a-linux-service-to-keep-everything-running">Setup a Linux service to keep everything running</h3>
<p>At this step, we have to create a Linux service that will make sure to start the application every time the server starts or our application stops. The following script will help you do that:</p>
<pre><code class="lang-bash">[Unit]
Description=[APP_NAME] service executed by docker compose
PartOf=docker.service
After=docker.service
After=network.target

[Service]
Type=oneshot
RemainAfterExit=<span class="hljs-literal">true</span>
WorkingDirectory=/home/ubuntu/[APP_FOLDER]
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target
</code></pre>
<p>Let's analyze it !!!</p>
<ul>
<li><p>The <code>Unit</code> section describes our service and specifies what service our unit is part of, in this case, it's the docker service, this will make sure our service always runs when the docker service is running as well.</p>
</li>
<li><p>The <code>Service</code> section describes how to run our service, the interesting parts are <code>WorkingDirectory</code> , <code>ExecStart</code> and <code>ExecStop</code> command, they will be used according to what their name means, for example, if the service is named <code>myapp</code> when you type the command <code>systemctl start myapp</code> the command <code>ExecStart</code> will be executed. You can learn more about Linux service here <a target="_blank" href="https://www.redhat.com/sysadmin/systemd-oneshot-service">https://www.redhat.com/sysadmin/systemd-oneshot-service</a>. Learn more on how to run the docker service with <code>systemd</code> here: <a target="_blank" href="https://bootvar.com/systemd-service-for-docker-compose/">https://bootvar.com/systemd-service-for-docker-compose/</a></p>
</li>
</ul>
<p>This service needs to be installed in a way the system will run it when needed, you will have to save it in a file with a name for example: <code>myapp.service</code></p>
<pre><code class="lang-bash">touch myapp.service
<span class="hljs-comment"># open it</span>
nano myapp.service
<span class="hljs-comment"># paste the previous scrip in it</span>
cp myapp.service /etc/systemd/system/myapp.serivce
</code></pre>
<p>At this point it's recognized as a Linux service, you can run <code>systemctl start myapp</code> to start it. The following required command is</p>
<pre><code class="lang-bash">systemctl <span class="hljs-built_in">enable</span> myapp.service
</code></pre>
<p>This will make sure the service is automatically executed by the server on each reboot. You can learn more here: <a target="_blank" href="https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6">https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6</a></p>
<h3 id="heading-the-web-server">The web server</h3>
<p>I used Nginx for this task, it's small and powerful, it's widely used, and can act as a load balancer, a static file server, a reverse proxy, and much more. The first thing to do is to install it.</p>
<pre><code class="lang-bash">sudo apt-get install nginx
</code></pre>
<p>At this step the docker image is supposedly running, let's suppose it contains an app running on the port <code>8080</code> , and that port is bound to the server via the docker-compose file. We need to set up a reverse proxy configuration between Nginx and our port. Here is the configuration needed :</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">upstream</span> app_backend {
  <span class="hljs-attribute">server</span> localhost:<span class="hljs-number">8080</span>; <span class="hljs-comment"># the appliction port</span>
}

<span class="hljs-section">server</span> {
  <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
  <span class="hljs-attribute">server_name</span> [DOMAIN_NAME];

  <span class="hljs-attribute">location</span> / {
    <span class="hljs-attribute">proxy_pass</span> http://app_backend;
    <span class="hljs-attribute">proxy_http_version</span> <span class="hljs-number">1</span>.<span class="hljs-number">1</span>;
    <span class="hljs-attribute">proxy_set_header</span> Upgrade <span class="hljs-variable">$http_upgrade</span>;
    <span class="hljs-attribute">proxy_set_header</span> Connection <span class="hljs-string">'upgrade'</span>;
    <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
    <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
    <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
    <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
    <span class="hljs-attribute">proxy_cache_bypass</span> <span class="hljs-variable">$http_upgrade</span>;
  }
}
</code></pre>
<p>let's call this config <code>myapp.conf</code> and save it under a directory where nginx will find it, that folder amongst others is named <code>/etc/nginx/conf.d/</code>.</p>
<pre><code class="lang-bash">sudo touch /etc/nginx/conf.d/myapp.conf
sudo nano /etc/nginx/conf.d/myapp.conf
<span class="hljs-comment"># paste the content there</span>
</code></pre>
<p>Now all we need is to test it and restart the NGINX service with the following commands</p>
<pre><code class="lang-bash">sudo nginx -t <span class="hljs-comment"># test if the config is valid </span>
sudo nginx -s reload <span class="hljs-comment"># reload the nginx service so it will consider it</span>
</code></pre>
<p>This configuration will instruct nginx to listen to the traffic on the port <code>80</code> and with the domain name <code>[DOMAIN_NAME]</code> and send it to your app server on the port <code>8080</code> via the directive <code>proxy_pass</code> , the line <code>location / {</code> simply mean capture all the requests starting with <code>/</code> and perform the actions written under the <code>location</code> block. Learn more here <a target="_blank" href="https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example">https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example</a>.</p>
<h2 id="heading-the-build-pipeline">The Build Pipeline</h2>
<p>After having configured the server, we have to set up the build pipeline now, it mainly consists of 1 step, write a Github Action pipeline file and add it to the project, let's go.</p>
<h3 id="heading-github-actions-setup">GitHub Actions Setup</h3>
<p>GitHub action will be used to build the docker image from our source code and pushed to the registry from where the image is pulled and executed on the server. I will take a sample Dockerfile for this example, but in practice, you will have to write your own Dockerfile. For an express.js application, the docker file would be like this:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Fetching the minified node image on apline linux</span>
FROM node:slim

<span class="hljs-comment"># Declaring env</span>
ENV NODE_ENV production

<span class="hljs-comment"># Setting up the work directory</span>
WORKDIR /express-docker

<span class="hljs-comment"># Copying all the files in our project</span>
COPY . .

<span class="hljs-comment"># Installing dependencies</span>
RUN npm install

<span class="hljs-comment"># Installing pm2 globally</span>
RUN npm install pm2 -g

<span class="hljs-comment"># Exposing server port</span>
EXPOSE 8080

<span class="hljs-comment"># Starting our application</span>
CMD pm2 start process.yml &amp;&amp; tail -f /dev/null
</code></pre>
<p>Building and running this docker file will start our application on port 8000, but in our setup, we will have to run it with docker-compose.</p>
<p>The next thing is to set up the GitHub actions pipeline. For that simply create a folder <code>.github/workflows</code> in the project root and create a file named <code>docker-build.yml</code>, we will write our pipeline into it.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Build,</span> <span class="hljs-string">Push</span> <span class="hljs-string">to</span> <span class="hljs-string">ECS</span> <span class="hljs-string">and</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">Server</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> [<span class="hljs-string">'deploy/main'</span>]

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">Web</span> <span class="hljs-string">Image</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Check</span> <span class="hljs-string">out</span> <span class="hljs-string">code</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Configure</span> <span class="hljs-string">AWS</span> <span class="hljs-string">credentials</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">aws-actions/configure-aws-credentials@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">aws-access-key-id:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_ACCESS_KEY_ID</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">aws-secret-access-key:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_SECRET_ACCESS_KEY</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">aws-region:</span> [<span class="hljs-string">AWS_REGION</span>]

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Login</span> <span class="hljs-string">to</span> <span class="hljs-string">Amazon</span> <span class="hljs-string">ECR</span>
        <span class="hljs-attr">id:</span> <span class="hljs-string">login-ecr</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">aws-actions/amazon-ecr-login@v1</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build,</span> <span class="hljs-string">tag,</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">image</span> <span class="hljs-string">to</span> <span class="hljs-string">Amazon</span> <span class="hljs-string">ECR</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">ECR_REGISTRY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">steps.login-ecr.outputs.registry</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">ECR_REPOSITORY:</span> [<span class="hljs-string">REPOSITORY_NAME</span>]
          <span class="hljs-attr">IMAGE_TAG:</span> <span class="hljs-string">latest</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Restart</span> <span class="hljs-string">the</span> <span class="hljs-string">service</span> <span class="hljs-string">via</span> <span class="hljs-string">SSH</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">appleboy/ssh-action@v1.0.0</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">host:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.HOST</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">username:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.USERNAME</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">password:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.PASSWORD</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">port:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.PORT</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">script:</span> <span class="hljs-string">/home/ubntu/[APP_DIRECTORY]/redeploy.sh</span> <span class="hljs-string">web</span>
</code></pre>
<p>There are several steps to look into here:</p>
<ul>
<li><p><code>Configure AWS credentials</code>: here the system will load the previous aws key you created earlier, you will have to register them into your GitHub account's secrets</p>
</li>
<li><p><code>Build, tag, and push image to Amazon ECR</code> : this step will run the command <code>docker build</code> and <code>docker push</code> to create the docker image</p>
</li>
<li><p><code>Restart the service via SSH</code> this step will connect to the server and restart the whole application at once.</p>
</li>
</ul>
<p>This pipeline will run every time there is a pull request merged against the <code>deploy/main</code> branch.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> [<span class="hljs-string">'deploy/main'</span>]
</code></pre>
<p>At this point the whole system is in place and tied up, now it's possible to edit and apply it to your specific case. In a future article, I will share the process of building the application itself for production and running in a docker file.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This article attempts to describe the process I use to set up a VPS for automation when it comes to deployment. it describes how to set the process of application execution inside the server and the process of building the application, each part can be done with another tool, for example, you can swap nginx with Treafik if you want, and you can replace the <code>systemd</code> service with a program in <code>supervisor</code> and more. This process does not cover additional stuff like backing up the server or closing default ports on the servers, those will be explained in future articles. Feel free to ask a question if you want to adapt this to your flow. In another article I will focus on how to set up an application to be production-ready in terms of deployment, that's the part of the process that comes before the build of the Docker Image.</p>
<p>I hope you enjoyed the read.</p>
<p>I'm here to assist you in implementing this within your company or team, allowing you to concentrate on your core tasks and save money before tapping into the vast potential of Mastodon.</p>
]]></content:encoded></item><item><title><![CDATA[How to Accept Mobile Money Payments on Telegram Chatbot using NotchPay API]]></title><description><![CDATA[I built a Telegram chatbot many weeks ago and I needed to enable mobile money payment from users, to achieve that I used NotchPay (https://www.notchpay.co), a Cameroonian Payment Gateway platform that is very easy and quick to set up. In this article...]]></description><link>https://blog.adonissimo.com/how-to-accept-mobile-money-payments-on-telegram-chatbot-using-notchpay-api</link><guid isPermaLink="true">https://blog.adonissimo.com/how-to-accept-mobile-money-payments-on-telegram-chatbot-using-notchpay-api</guid><category><![CDATA[payments]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[telegram]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Tue, 04 Jul 2023 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696487848552/ce11028d-f91d-4fc3-9f35-51531bedb41c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built a Telegram chatbot many weeks ago and I needed to enable mobile money payment from users, to achieve that I used NotchPay (<a target="_blank" href="https://www.notchpay.co">https://www.notchpay.co</a>), a Cameroonian Payment Gateway platform that is very easy and quick to set up. In this article, I will go through the steps of my implementation.</p>
<p>Typically a mobile money payment happens in 3 steps, the <strong>initialization</strong>, the <strong>payment</strong> itself, and the <strong>verification</strong>. Let's dive into this using this blueprint.</p>
<h3 id="heading-the-payment-initialization">The payment initialization</h3>
<p>This is what happens when the user chooses what to pay and how much to pay, in my case it is a subscription(billing plan) the user might need to pay a weekly subscription valued at 600 FCFA (XAF) or a monthly subscription at 2000 FCFA (XAF). At this step, I would create a payment on NotchPay platform like this</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> endpointUrl = <span class="hljs-string">"https://api.notchpay.co/payments/initialize"</span>;
<span class="hljs-keyword">const</span> cb_url = process.env.BOT_REDIRECT_URL; <span class="hljs-comment">// url where the user is redirected</span>

<span class="hljs-keyword">const</span> paymentRequest = {
      email: userEmail,
      amount: paymentAmount,
      currency: <span class="hljs-string">"XAF"</span>, <span class="hljs-comment">// Replace with the appropriate currency code</span>
      description: <span class="hljs-string">"Payment for Subscription a subscription"</span>, 
      reference: transactionId,
      callback: <span class="hljs-string">`<span class="hljs-subst">${cb_url}</span>?tid=<span class="hljs-subst">${transactionId}</span>`</span>, <span class="hljs-comment">// a callback url</span>
      customer_meta: meta,
    };

    <span class="hljs-keyword">let</span> response;

    <span class="hljs-comment">// Send a POST request to the NotchPay API using Axios</span>
    <span class="hljs-keyword">try</span> {
      response = <span class="hljs-keyword">await</span> axios.post(endpointUrl, paymentRequest, {
        headers: {
          Authorization: <span class="hljs-string">`<span class="hljs-subst">${apiKey}</span>`</span>,
          <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        },
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-keyword">if</span> (error.response) {

        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"HTTP ERROR"</span>);
        <span class="hljs-built_in">console</span>.log(error.response.data);
        <span class="hljs-built_in">console</span>.log(error.response.status);
        <span class="hljs-built_in">console</span>.log(error.response.headers);
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error.request) {

        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"REQUEST ERROR"</span>);
        <span class="hljs-built_in">console</span>.log(error.request);
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Something happened in setting up the request that triggered an Error</span>
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"UNKNOWN ERROR"</span>);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Error"</span>, error.message);
      }

      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"[ERROR_CAUTH]"</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

<span class="hljs-keyword">if</span> (response.status &gt;= <span class="hljs-number">200</span> &amp;&amp; response.status &lt; <span class="hljs-number">300</span>) {

      <span class="hljs-keyword">const</span> paymentLink = response.data.transaction.reference;
      <span class="hljs-keyword">if</span> (paymentLink) {
        <span class="hljs-comment">// Return the payment link</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">`https://pay.notchpay.co/<span class="hljs-subst">${paymentLink}</span>`</span>;
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">console</span>.error(
          <span class="hljs-string">`[PAYMENT] - error while initiating the transaction on [NOTCHPAY] - 1 <span class="hljs-subst">${response.data}</span>`</span>
        );
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
      }
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">`[PAYMENT] - error while initiating the transaction on [NOTCHPAY] - 2 <span class="hljs-subst">${response.status}</span>`</span>
      );
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
</code></pre>
<p>At this point, I need to show the <code>paymentLink</code> to the user, after clicking he will end up on a checkout page branded by notchpay, where the user has to choose a payment method and input his number. Do note this line</p>
<pre><code class="lang-typescript">...
callback: <span class="hljs-string">`<span class="hljs-subst">${cb_url}</span>?tid=<span class="hljs-subst">${transactionId}</span>`</span>, 
...
</code></pre>
<p>This is the link where the user is redirected after a payment, you should set a page on your site informing the user that the payment in been successfully processed (not done, since it might still occur an issue). Here is a screenshot of how the bot will display it</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696519579456/871bb26d-0bda-4592-af44-0265f18b5b77.jpeg" alt class="image--center mx-auto" /></p>
<p>As you can see, the user have choosed a subscription and the bot displayed the payment link.</p>
<h3 id="heading-the-payment-itself">The payment itself</h3>
<p>The payment happend on the next screen shows what the user see upon click the link</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696520142623/cfde1a37-e9fb-43e3-9d65-c0119c6b402f.jpeg" alt class="image--center mx-auto" /></p>
<p>As NotchPay user we don't have much to do here, all happen on their platform, the client will input his phone number or credit card information after choosing the country where he belongs and click on "<strong>pay X F.CFA</strong>", if it's a mobile money transaction the user will receive a prompt on his phone requesting to validate a payment on his mobile wallet, otherwise an error will be displayed on NotchPay page itself. After this, if the payment is successful the user is redirected to the <code>callback</code> URL (the one you specified while creating the payment link), note that payment will usually take under <strong>5 minutes</strong> to be confirmed by the client.</p>
<h3 id="heading-the-verification">The verification</h3>
<p>When a payment is made on NotchPay, your platform will receive a notification via what is called a <a target="_blank" href="https://sendgrid.com/blog/whats-webhook/"><strong>webhook</strong></a>, it will contain information about a specific payment that has been updated, What you have to do here is to verify the state of said payment, you should never update your database right away with the information contained into the webhook (it might have been created and sent to you by a third party other than NotchPay), you should only retrieve the transaction reference and do the following:</p>
<ul>
<li><p>Check for the notch pay signature</p>
</li>
<li><p>Check if the payment exists in your database (via his reference)</p>
</li>
<li><p>Check the status: if it is still ACTIVE or PENDING COMPLETED etc.</p>
</li>
</ul>
<p>If these checks are done and successful you have to call the verification API on NotchPay to retrieve the status of this specific payment on their platform before updating yours. Here is how I did it.</p>
<p>I used a little express server that exposes a route in POST, where the webhook is sent, like this.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifyPaymentStatus</span>(<span class="hljs-params">
  transactionId: <span class="hljs-built_in">string</span>
</span>): <span class="hljs-title">Promise</span>&lt;</span>{ status: <span class="hljs-built_in">any</span>; transaction?: <span class="hljs-built_in">any</span> }&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> notchPayApiUrl = <span class="hljs-string">"https://api.notchpay.co"</span>;
    <span class="hljs-keyword">const</span> endpoint = <span class="hljs-string">`/payments/<span class="hljs-subst">${transactionId}</span>`</span>;
    <span class="hljs-keyword">let</span> response;
    <span class="hljs-keyword">try</span> {
      response = <span class="hljs-keyword">await</span> axios.get(<span class="hljs-string">`<span class="hljs-subst">${notchPayApiUrl}</span><span class="hljs-subst">${endpoint}</span>`</span>, {
        headers: {
          Authorization: <span class="hljs-string">`<span class="hljs-subst">${apiKey}</span>`</span>,
        },
      });
    } <span class="hljs-keyword">catch</span> (apiError: <span class="hljs-built_in">any</span>) {
      <span class="hljs-keyword">if</span> (apiError.response) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"HTTP ERROR"</span>);
        <span class="hljs-built_in">console</span>.log(apiError.response.data);
        <span class="hljs-built_in">console</span>.log(apiError.response.status);
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (apiError.request) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"REQUEST ERROR"</span>);
        <span class="hljs-built_in">console</span>.log(apiError.request);
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"UNKNOWN ERROR"</span>);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Error"</span>, apiError.message);
      }
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"[ERROR_CAUTH]"</span>);
      <span class="hljs-keyword">return</span> { status: <span class="hljs-literal">null</span>, transaction: apiError.response.data };
    }
    <span class="hljs-keyword">if</span> (response.status &gt;= <span class="hljs-number">200</span> &amp;&amp; response.status &lt; <span class="hljs-number">300</span>) {
      <span class="hljs-keyword">return</span> { status: <span class="hljs-literal">true</span>, transaction: response.data };
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">"NotchPay API request failed with status:"</span>,
        response.status
      );
      <span class="hljs-keyword">return</span> { status: <span class="hljs-literal">null</span> };
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error verifying payment status:"</span>, error);
    <span class="hljs-keyword">return</span> { status: <span class="hljs-literal">null</span> };
  }
}

app.get(<span class="hljs-string">"/[MY_CALLBACK_URL]"</span>, <span class="hljs-keyword">async</span> (req: Request, res: Response) =&gt; {
  <span class="hljs-keyword">const</span> hash = crypto
    .createHmac(<span class="hljs-string">"sha256"</span>, apiKey)
    .update(<span class="hljs-built_in">JSON</span>.stringify(req.body))
    .digest(<span class="hljs-string">"hex"</span>);

  <span class="hljs-keyword">if</span> (hash != req.headers[<span class="hljs-string">"x-notch-signature"</span>]) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">403</span>);
  }
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Extract data from the request </span>
    <span class="hljs-keyword">const</span> { tid, reference } = req.query <span class="hljs-keyword">as</span> {
      tid: <span class="hljs-built_in">number</span>;
      reference: <span class="hljs-built_in">string</span>;
    };

    <span class="hljs-comment">// extracting the transation from my database (supabase)</span>
    <span class="hljs-keyword">let</span> { data: payment_transaction, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"payment_transaction"</span>)
      .select(<span class="hljs-string">"*"</span>)
      .eq(<span class="hljs-string">"transaction_id"</span>, tid)

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error retrieving transaction:"</span>, error.message);
      res.status(<span class="hljs-number">400</span>).send(<span class="hljs-built_in">JSON</span>.stringify(error));
    }

    <span class="hljs-keyword">if</span> (payment_transaction === <span class="hljs-literal">null</span>) {
      res.status(<span class="hljs-number">404</span>).send(<span class="hljs-built_in">JSON</span>.stringify({ error: <span class="hljs-string">"Transaction Not Found"</span> }));
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">if</span> (
      payment_transaction[<span class="hljs-number">0</span>].status === <span class="hljs-string">"complete"</span> ||
      payment_transaction[<span class="hljs-number">0</span>].status === <span class="hljs-string">"canceled"</span> ||
      payment_transaction[<span class="hljs-number">0</span>].status === <span class="hljs-string">"failed"</span>
    ) {
      res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">"Webhook received and processed successfully"</span>);
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-comment">// actively verify the payment status on notchpay platform</span>
    <span class="hljs-keyword">let</span> paymentObject = <span class="hljs-keyword">await</span> verifyPaymentStatus(reference);

    <span class="hljs-keyword">if</span> (paymentObject.status == <span class="hljs-literal">null</span>) {
      res.status(<span class="hljs-number">503</span>).send(
        <span class="hljs-built_in">JSON</span>.stringify({
          error: <span class="hljs-string">"Provider server not available"</span>,
          data: paymentObject.transaction,
        })
      );
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">const</span> transaction = paymentObject.transaction.transaction;
    <span class="hljs-keyword">const</span> { error: tError } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"payment_transaction"</span>)
      .update({
        status: transaction[<span class="hljs-string">"status"</span>],
        api_response: [...payment_transaction[<span class="hljs-number">0</span>].api_response, transaction],
      })
      .eq(<span class="hljs-string">"transaction_id"</span>, tid)
      .select();
    <span class="hljs-keyword">if</span> (tError) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[webhook] An error occurred: <span class="hljs-subst">${tError.message}</span>`</span>);
      <span class="hljs-built_in">console</span>.table(error);
      res.status(<span class="hljs-number">500</span>).send(<span class="hljs-string">"Internal Server Error"</span>);
    }


    <span class="hljs-comment">// ... update the billing plan and the subscription code</span>
    <span class="hljs-comment">// diable existing subscription and enabling others</span>
    res.redirect(<span class="hljs-string">`https://t.me/<span class="hljs-subst">${BOT_TELEGRAM_USERNAME}</span>`</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error handling webhook:"</span>, error);
    res.status(<span class="hljs-number">500</span>).send(<span class="hljs-string">"Internal Server Error"</span>);
  }
});
</code></pre>
<p>The section that verifies the NotchPay signature is this one, this ensures that the webhook is coming from NotchPay and is related to your account.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> hash = crypto
    .createHmac(<span class="hljs-string">"sha256"</span>, apiKey)
    .update(<span class="hljs-built_in">JSON</span>.stringify(req.body))
    .digest(<span class="hljs-string">"hex"</span>);

  <span class="hljs-keyword">if</span> (hash != req.headers[<span class="hljs-string">"x-notch-signature"</span>]) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">403</span>);
  }
</code></pre>
<p>In this section, I retrieve the payment transaction from my database and check his current known status, NotchPay will recall this URL and send the webhook if the response is different than <strong>HTTP 200 OK</strong>, the reason being the fact that it doesn't know the state of your system, it might be unavailable or an error or processing has happened, this is why you should keep a real logic while returning a response from the request.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> { data: payment_transaction, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"payment_transaction"</span>)
      .select(<span class="hljs-string">"*"</span>)
      .eq(<span class="hljs-string">"transaction_id"</span>, tid)

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error retrieving transaction:"</span>, error.message);
      res.status(<span class="hljs-number">400</span>).send(<span class="hljs-built_in">JSON</span>.stringify(error));
    }

    <span class="hljs-keyword">if</span> (payment_transaction === <span class="hljs-literal">null</span>) {
      res.status(<span class="hljs-number">404</span>).send(<span class="hljs-built_in">JSON</span>.stringify({ error: <span class="hljs-string">"Transaction Not Found"</span> }));
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">if</span> (
      payment_transaction[<span class="hljs-number">0</span>].status === <span class="hljs-string">"complete"</span> ||
      payment_transaction[<span class="hljs-number">0</span>].status === <span class="hljs-string">"canceled"</span> ||
      payment_transaction[<span class="hljs-number">0</span>].status === <span class="hljs-string">"failed"</span>
    ) {
      res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">"Webhook received and processed successfully"</span>);
      <span class="hljs-keyword">return</span>;
    }
</code></pre>
<p>In this section it will get the payment object on NotchPay and update the transaction status in the database, it will also save the complete JSON object received from NotchPay, I always do this in order to make sure I can audit this information later if I need to know what happened during the lifetime of a transaction. it's saved on the field <code>api_response</code> as an array (kind of history)</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> paymentObject = <span class="hljs-keyword">await</span> verifyPaymentStatus(reference);

    <span class="hljs-keyword">if</span> (paymentObject.status == <span class="hljs-literal">null</span>) {
      res.status(<span class="hljs-number">503</span>).send(
        <span class="hljs-built_in">JSON</span>.stringify({
          error: <span class="hljs-string">"Provider server not available"</span>,
          data: paymentObject.transaction,
        })
      );
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">const</span> transaction = paymentObject.transaction.transaction;
    <span class="hljs-keyword">const</span> { error: tError } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">"payment_transaction"</span>)
      .update({
        status: transaction[<span class="hljs-string">"status"</span>],
        api_response: [...payment_transaction[<span class="hljs-number">0</span>].api_response, transaction],
      })
      .eq(<span class="hljs-string">"transaction_id"</span>, tid)
      .select();
    <span class="hljs-keyword">if</span> (tError) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[webhook] An error occurred: <span class="hljs-subst">${tError.message}</span>`</span>);
      <span class="hljs-built_in">console</span>.table(error);
      res.status(<span class="hljs-number">500</span>).send(<span class="hljs-string">"Internal Server Error"</span>);
    }
</code></pre>
<p>After this I can resume the actions that need to be taken after a payment happens, namely disabling existing subscriptions and enabling new one that corresponds to what the user paid for. In the end, I will just redirect the user to the bot URL</p>
<pre><code class="lang-typescript">res.redirect(<span class="hljs-string">`https://t.me/<span class="hljs-subst">${BOT_TELEGRAM_USERNAME}</span>`</span>);
</code></pre>
<p>This is because my system is a Telegram Chatbot, so it's necessary to resend him back to the chat UI.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>I shared with you how I used NotchPay to accept payment (mobile money) for my telegram Chatbot, this happens via 3 steps <strong>Initiating the payment, the payment itself</strong> by the user, and the <strong>verification of the transaction.</strong> In this article I didn't cover our the subscription plan logic is made on the chatbot, it will be done in another piece of content itself, the process explained here can be used on a chatbot or website or anywhere to process a payment, it's doesn't need to be a subscription like here. Not that this process is roughly the same even for a payment with Paypal or stripe and most of the payment gateway out there.</p>
]]></content:encoded></item><item><title><![CDATA[How We implemented Audit in our SaaS Django Platform]]></title><description><![CDATA[Context: what is Audit?
A while back with my team when working on a project we needed to add some audit features on the platform to be able to trace what happened in the app and show it to the end users and use it also for customer support requests, ...]]></description><link>https://blog.adonissimo.com/how-we-implemented-audit-in-our-saas-django-platform</link><guid isPermaLink="true">https://blog.adonissimo.com/how-we-implemented-audit-in-our-saas-django-platform</guid><category><![CDATA[Django]]></category><category><![CDATA[audit]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Tue, 10 Jan 2023 00:17:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1673309413820/da1a9683-41c6-4781-8e7d-3a83004068e5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-context-what-is-audit">Context: what is Audit?</h1>
<p>A while back with my team when working on a project we needed to add some audit features on the platform to be able to trace what happened in the app and show it to the end users and use it also for customer support requests, we were receiving at that time a lot of requests requiring us to know what change has happened on various objects in the database, so we need a system to help us achieve that. And we knew that when building a web application, especially when it has some business value it's important to provide the ability for the end user to know who did what action, or the changes that occurred on a specific object in the database and who did that change.</p>
<p>For example, if an invoice has been created, processed and validated, and later on we need to display all the history of changes on it, we need to build an <strong>audit trail</strong>, a system of record that can hold the history of changes like logs; this is called <strong>maintaining an audit trail.</strong></p>
<h1 id="heading-the-basic-principle">The basic Principle</h1>
<p>regardless of what framework you are using the principle remains the same, you need to find a way to listen to everything that happens in the application and log those somewhere, it could be a file, a database table or anything else, as long as it can keep all the data you send to it.</p>
<p>With Django Framework the simple way to do that is by connecting to various signals like <code>post_save</code> or <code>m2m_changed</code> on all the models at once (it's possible) and process the signal's data to save them somewhere as events, this should ideally be done in a dedicated thread or asynchronously to avoid slowing the application.</p>
<p>Therefore it will be possible as well to choose what type of event should be logged or what models should be tracked, you got the idea ;).</p>
<h1 id="heading-the-available-solutions-in-django-packages">The available solutions in Django packages</h1>
<p>There are several packages to achieve this but will showcase only 2 of them here because is actually used them both and they work differently internally.</p>
<h2 id="heading-the-django-easy-audit-package">The django-easy-audit package</h2>
<p>https://github.com/soynatan/django-easy-audit</p>
<p>This package is installed via the command <code>pip install django-easy-audit</code> and added to the project's settings like this:</p>
<pre><code class="lang-python">INSTALLED_APPS = [
    <span class="hljs-comment">#...</span>
    <span class="hljs-string">'easyaudit'</span>,
]

MIDDLEWARE = (
    <span class="hljs-comment">#...</span>
    <span class="hljs-string">'easyaudit.middleware.easyaudit.EasyAuditMiddleware'</span>,
)
</code></pre>
<p>It provides the ability to watch a lot of events such as <code>login</code> , <code>crud</code>, <code>HTTP request</code> in the project and save them into a dedicated set of database tables (models): <code>CRUDEvent</code> , <code>LoginEvent</code> and <code>RequestEvent</code> .</p>
<p>There is a set of settings to change how it works or what models it tracks. such as:</p>
<ul>
<li><p><code>DJANGO_EASY_AUDIT_WATCH_MODEL_EVENTS</code></p>
</li>
<li><p><code>DJANGO_EASY_AUDIT_WATCH_AUTH_EVENTS</code></p>
</li>
<li><p><code>DJANGO_EASY_AUDIT_WATCH_REQUEST_EVENTS</code></p>
</li>
</ul>
<p>These settings are used to disable/enable the specific type of event tracking.</p>
<h2 id="heading-the-django-simple-history-package">The django-simple-history package</h2>
<p>This package provides the same result but it behaves slightly differently, It mirrors all the table in the database and store each object changes in the mirror table related to the model. And provide an attribute that you can add to each model to access each instance's history. To add this package to the project you need to run : <code>pip install django-simple-history</code> and set it up in the project like this:</p>
<pre><code class="lang-python">INSTALLED_APPS = [
    <span class="hljs-comment"># ...</span>
    <span class="hljs-string">'simple_history'</span>,
]
MIDDLEWARE = [
    <span class="hljs-comment"># ...</span>
    <span class="hljs-string">'simple_history.middleware.HistoryRequestMiddleware'</span>,
]
</code></pre>
<p>And add the history attribute to all the models you need to track</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> simple_history.models <span class="hljs-keyword">import</span> HistoricalRecords
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SomeModel</span>(<span class="hljs-params">models.Model</span>):</span>
    history = HistoricalRecords()
</code></pre>
<p>And create a migration before running it.</p>
<pre><code class="lang-bash">python manage.py makemigrations
python manage.py migrate
</code></pre>
<p>It will add the history attribute to the <code>SomeModel</code> class and create a <code>app_historicalsomemodel</code> , the table where all the changes happening on the model will appear. It's well explained here: https://django-simple-history.readthedocs.io/en/latest/quick_start.html#what-is-django-simple-history-doing-behind-the-scenes.</p>
<p>This means the number of tables in the database will probably get multiplied by 2, at least all the models you wrote if you desire to track them all. To access the history (audit trail) of a specific model's instance it's done by using <code>someModel.history.all()</code>, It returns a QuerySet of <code>SomeModel</code> with the various version of the object over time (since his creation) and you can use queryset filters to get whatever version you want.</p>
<h1 id="heading-summary-what-to-remember">Summary, what to remember</h1>
<p>We did test these two and choose <code>django-easy-audit</code> for some reason, I will give you some pros and cons of the 2 libs.</p>
<h2 id="heading-django-easy-audit">django-easy-audit</h2>
<h3 id="heading-pros">Pros:</h3>
<ul>
<li><p>Very simple to install in the project</p>
</li>
<li><p>Add only 3 tables to the database</p>
</li>
<li><p>Requires no changes to the models or the project</p>
</li>
</ul>
<h3 id="heading-cons">Cons:</h3>
<ul>
<li><p>It saves all the model's data in the same table, which make the number of rows grow fast (according to the number of the model being monitored)</p>
</li>
<li><p>Provide no functions or utility to browse an object's history easily</p>
</li>
<li><p>Provide a really simple admin integration, just a regular list of events containing JSON objects that need to be processed by the person using the admin</p>
</li>
</ul>
<h2 id="heading-django-simple-history">django-simple-history</h2>
<h3 id="heading-pros-1">Pros:</h3>
<ul>
<li><p>Provide a simple API to navigate the model's history via <code>.history.all()</code></p>
</li>
<li><p>Avoid storing too much data in the same table</p>
</li>
<li><p>Provide a good admin integration to navigate the object's history</p>
</li>
</ul>
<h3 id="heading-cons-1">Cons</h3>
<ul>
<li><p>Create a copy of each table, which can almost duplicate the number of tables if you track all the data</p>
</li>
<li><p>In some cases migrations were not applied or well applied, the package seems to be the cause since it stopped happening when we removed it from the project.</p>
</li>
</ul>
<h2 id="heading-what-i-think">What I think</h2>
<p>I don't like the idea of having too many tables in a database, so I will go most of the time with <code>django-easy-audit</code>, but I also think audits (model's history) don't need to be stored in the database, since they are anyway not used too much most of the time, it makes more sense to me to store them in another system, file, object storage, stream processing system etc. Just like you would do with logs and logs files.</p>
<p>An improvement I think could be done on these packages is to send the generated data into an external system like a stream of data and avoid putting too much data into the database and retrieving them on demand.</p>
<p><strong>Edit:</strong> after a comment by <a class="user-mention" href="https://hashnode.com/@tomdyson">Tom Dyson</a>, it turns out there is a way to achieve this in <code>django-easy-audit</code> , simply by using the <code>DJANGO_EASY_AUDIT_LOGGING_BACKEND</code> setting s and implementing a method to send the data into another logging system as in the example:</p>
<pre><code class="lang-python">  <span class="hljs-keyword">import</span> logging

  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PythonLoggerBackend</span>:</span>
      logging.basicConfig()
      logger = logging.getLogger(<span class="hljs-string">'your-kibana-logger'</span>)
      logger.setLevel(logging.DEBUG)

      <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">request</span>(<span class="hljs-params">self, request_info</span>):</span>
          <span class="hljs-keyword">return</span> request_info <span class="hljs-comment"># if you don't need it</span>

      <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">login</span>(<span class="hljs-params">self, login_info</span>):</span>
          self.logger.info(msg=<span class="hljs-string">'your message'</span>, extra=login_info)
          <span class="hljs-keyword">return</span> login_info

      <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">crud</span>(<span class="hljs-params">self, crud_info</span>):</span>
          self.logger.info(msg=<span class="hljs-string">'your message'</span>, extra=crud_info)
          <span class="hljs-keyword">return</span> crud_info
</code></pre>
<p>learn more here: <a target="_blank" href="https://github.com/soynatan/django-easy-audit#settings">https://github.com/soynatan/django-easy-audit#settings</a></p>
<h1 id="heading-resources">Resources</h1>
<p><a target="_blank" href="https://github.com/soynatan/django-easy-audit">https://github.com/soynatan/django-easy-audit</a></p>
<p><a target="_blank" href="https://django-simple-history.readthedocs.io/en/latest/index.html">https://django-simple-history.readthedocs.io/en/latest/index.html</a></p>
]]></content:encoded></item><item><title><![CDATA[How We migrate a Joomla based website to a Django Backend with 90 Gb worth of data]]></title><description><![CDATA[Context
A few years ago while I was still running my Django Consultancy Agency, We worked with a client who built a website over the years with a Content Management System (CMS) named Joomla and he was facing some issues such as adding new features, ...]]></description><link>https://blog.adonissimo.com/how-we-migrate-a-joomla-based-website-to-a-django-backend-with-90-gb-worth-of-data</link><guid isPermaLink="true">https://blog.adonissimo.com/how-we-migrate-a-joomla-based-website-to-a-django-backend-with-90-gb-worth-of-data</guid><category><![CDATA[Django]]></category><category><![CDATA[joomla]]></category><category><![CDATA[elasticsearch]]></category><category><![CDATA[AWS]]></category><category><![CDATA[AWS s3]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Wed, 04 Jan 2023 23:28:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/4c493790f236382021a2b1cb8abe091b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-context">Context</h1>
<p>A few years ago while I was still running my Django Consultancy Agency, We worked with a client who built a website over the years with a Content Management System (CMS) named Joomla and he was facing some issues such as adding new features, many hassles with managing the content, editing the various webpages and more importantly receiving payments in local currency (XAF, XOF) via local payment processors (Mobile Money) in addition to Paypal. The whole installation was a huge set of plugins and many patches here and there made over the years with all the incompatibility coming with it, all running on MySQL server, everything was installed within a VPS where all the uploaded files were hosted.</p>
<p>The website is an educational platform where visitors could create their account and download content (Books, Software, Watch Videos, etc), now the owner wanted it to have more features such as Audio Listening of PDF books, an improved Full-Text Search engine capable of indexing the content of each PDF book uploaded to the platform and monthly subscription to access the content and various other features. We did follow some steps to properly build a new Back-end with Django and migrate the data (database and files) in it. We also choose to host it using Amazon Web Service. There was a separate team working on a React Based Frontend, so we also had to produce many REST endpoints to be consumed.</p>
<h1 id="heading-designing-and-building-the-django-back-end">Designing and Building the Django Back-end.</h1>
<p>The first step for us was to design a system with Django that could accept the old data from Joomla and enable us to develop more features easily. We decided to do a design around some OOP concept such as inheritance, so we had many models (video, document, etc) which inherited from a main model (which hold most of the attributes shared by all the content available on the site), and then we added more models according to the new features (bookmarks, likes, billing plan, subscription, payments transactions, etc).</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Resource</span>(<span class="hljs-params">models.Model</span>):</span>
    <span class="hljs-comment"># common attribute</span>
    <span class="hljs-keyword">pass</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Video</span>(<span class="hljs-params">Resource</span>):</span>
    <span class="hljs-comment"># Custom attributes and method</span>
    <span class="hljs-keyword">pass</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Document</span>(<span class="hljs-params">Resource</span>):</span>
    <span class="hljs-comment"># Custom attributes and method</span>
    <span class="hljs-keyword">pass</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Software</span>(<span class="hljs-params">Resource</span>):</span>
    <span class="hljs-comment"># Custom attributes and method</span>
    <span class="hljs-keyword">pass</span>

<span class="hljs-comment">#... more models</span>
</code></pre>
<p>We also exported the data into a CSV form to identify all the currently existing attributes per resource available on the platform and we designed our models based on those, we used them as the base for our work.</p>
<p>After all this, we made heavy usage of the Django Admin UI to avoid rebuilding a new UI from scratch, so we wrote some ModelAdmin Classes using a lot of inheritance and some optimized Queryset to fetch data from within the admin UI.</p>
<p>The PDF document was related to a model and it needed to be indexed for the full-text search engine, we chose Elasticsearch and used the <code>post_create</code> signals to process each uploaded PDF and extract all the text from it using <code>PyPDF</code> lib, once done the text is sent to Elasticsearch alongside the object serialized as a simple elastic search Document.</p>
<h1 id="heading-database-data-migration">Database Data migration</h1>
<p>After having designed how the data were saved in the database and indexed it was time to put in place some tools to migrate data from the old system and make sure they fit easily, since we were able to extract data in CSV form we used <code>django-import-export</code> in the admin UI to import each model's data from the CSV file and also we wrote some Django commands to make sure everything was properly in sync with our settings such as the static URLs configuration; For example, some paths were absolute so we needed some script to normalize them, some other CSV file where not straightforwardly usable, so we had to write some command to make sure they are transformed into a proper object and all the relationship were rebuilt, We also need to import the users, we had made the user import process to work via Django command that will load the CSV, create the user and set an unusable password, so the users were prompted to change their password on new login.</p>
<h1 id="heading-uploaded-file-migration-to-aws-s3">Uploaded File Migration to AWS S3</h1>
<p>At this point we had the data available in the database as well as their file path, we had to copy data from the VPS to an S3 bucket following a few steps:</p>
<ol>
<li><p>We installed the AWS CLI on the server</p>
</li>
<li><p>Start a copy process with <code>aws s3 cp</code> command to upload from the server to <code>S3</code></p>
</li>
<li><p>Update the missing file path in the S3 bucket or the database.</p>
</li>
</ol>
<p>Once the data are available on S3 we could do whatever we wanted and we even added a <code>CloudFront distribution</code> to serve the data efficiently.</p>
<p>From a security standpoint, all the files are private so we added some functions to get a pre-signed URL when a file was requested.</p>
<h1 id="heading-links-amp-resources">Links &amp; Resources</h1>
<p>Here is a little set of links to the various tools we used in the process.</p>
<p><a target="_blank" href="https://books.agiliq.com/projects/django-admin-cookbook/en/latest/optimize_queries.html">Optimizing Django Admin Queries</a></p>
<p><a target="_blank" href="https://dev.to/bawa_geek/how-to-transfer-data-from-your-existing-server-to-aws-s3-4p8g">How to transfert data from a server to S3</a></p>
<p><a target="_blank" href="https://medium.com/geekculture/how-to-use-elasticsearch-with-django-ff49fe02b58d">Use Elastic Search with Django</a></p>
<p><a target="_blank" href="https://django-import-export.readthedocs.io/en/latest/">Django Import Export Documentation</a></p>
<hr />
<p><strong>Thank You for reading ;)</strong></p>
]]></content:encoded></item><item><title><![CDATA[Slack isn't a Great tool to create
communities, it seems to be a fact.
How should be the perfect one?]]></title><description><![CDATA[I stumbled across this post: https://www.indiehackers.com/post/slack-isnt-a-good-community-building-tool-a3fd726a7f made a few years back.
https://www.indiehackers.com/post/slack-isnt-a-good-community-building-tool-a3fd726a7f
 
The trends have not ch...]]></description><link>https://blog.adonissimo.com/slack-isnt-a-great-tool-to-create-communities-it-seems-to-be-a-fact-how-should-be-the-perfect-one</link><guid isPermaLink="true">https://blog.adonissimo.com/slack-isnt-a-great-tool-to-create-communities-it-seems-to-be-a-fact-how-should-be-the-perfect-one</guid><category><![CDATA[slack]]></category><category><![CDATA[community]]></category><category><![CDATA[communication]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Thu, 15 Sep 2022 18:42:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1673462445243/981148fd-2e06-409a-a537-d0f77a6a4c9d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I stumbled across this post: <a target="_blank" href="https://www.indiehackers.com/post/slack-isnt-a-good-community-building-tool-a3fd726a7f">https://www.indiehackers.com/post/slack-isnt-a-good-community-building-tool-a3fd726a7f</a> made a few years back.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.indiehackers.com/post/slack-isnt-a-good-community-building-tool-a3fd726a7f">https://www.indiehackers.com/post/slack-isnt-a-good-community-building-tool-a3fd726a7f</a></div>
<p> </p>
<p>The trends have not changed since that time. We are trying an attempt with my team, to provide a proper community-building platform for companies.<br />The ideas are to enable companies to drive engagement between employees with the usage of purposeful communities, for example, communities around photography, climbing and much more fun stuff.</p>
<p><strong>A community should be a whole universe by itself.</strong></p>
<p>I used the expression "<strong>Community Building Platform</strong>" because it allows not to create of one huge community like it's done on slack, but to create many dedicated communities and enables various features in each of them.</p>
<p>A list of initial features is:</p>
<ul>
<li><p>Feed system</p>
</li>
<li><p>Live Streaming</p>
</li>
<li><p>Events</p>
</li>
<li><p>Video Conference (Webinar tool more like Zoom)</p>
</li>
<li><p>Forums</p>
</li>
<li><p>Content moderation capabilities</p>
</li>
<li><p>Company (or organization-wide) announcement board</p>
</li>
</ul>
<p>We plan on adding more to the platform as:</p>
<ul>
<li><p>Audio chat room</p>
</li>
<li><p>Text Chat</p>
</li>
<li><p>Groups within communities</p>
</li>
<li><p>Community Goals (Leader-board)</p>
</li>
<li><p>Fun Games Platform (Word, number, and more fun games)</p>
</li>
</ul>
<p>Unlike most of existing or widely used tools (Slack, Discord, etc.) which are primarily communication tools, our community platform is more a Community as a service platform where an organization has the possibility to create as many communities as needed and each community is a whole universe by himself.</p>
<p>Check us out at: <a target="_blank" href="https://www.workbud.com">https://www.workbud.com</a></p>
<p>Please feel free to comment, I am available to respond any question on the topic or the platform.<br />A feedback would be appreciated as well, thank you.</p>
<p><strong><em>Note:</em></strong> <em>I published this article here the first time:</em> <a target="_blank" href="https://www.indiehackers.com/post/slack-isnt-a-great-tool-to-create-communities-it-seems-to-be-a-fact-how-should-be-the-perfect-one-4b817d7353"><em>https://www.indiehackers.com/post/slack-isnt-a-great-tool-to-create-communities-it-seems-to-be-a-fact-how-should-be-the-perfect-one-4b817d7353</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Startup thinking has almost killed my “doer” spirit.]]></title><description><![CDATA[this have been reposted from my  medium post  
Do it just for fun
A Few years ago in 2015 (~2016) I had built a really small project during the summer, a web application to allow a car driving school to track and manage all the trainees, it was simpl...]]></description><link>https://blog.adonissimo.com/startup-thinking-has-almost-killed-my-doer-spirit</link><guid isPermaLink="true">https://blog.adonissimo.com/startup-thinking-has-almost-killed-my-doer-spirit</guid><category><![CDATA[Startups]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Thu, 01 Apr 2021 23:55:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1619826490271/Tx9_pZtoY.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>this have been reposted from my  <a target="_blank" href="[https://simoadonis.medium.com/startup-thinking-has-almost-killed-my-doer-spirit-e518d1fdcafe]">medium post </a> </em></p>
<h3 id="do-it-just-for-fun">Do it just for fun</h3>
<p>A Few years ago in 2015 (~2016) I had built a really small project during the summer, a web application to allow a car driving school to track and manage all the trainees, it was simple enough to be useful, since I was working on it with an owner of a driving school (my mother had bought a new car and she was re-learning to drive). He was pretty happy with it, i learned a lot with that little experience (how to deploy in a small company, speaking with end user, etc), back in school the next year we had a project contest in school and the goal was to show something working, useful and sellable, I was in a enginering school where one of the things we learned was to present our project as entrepreneurs or marketer, so one of the requirements was to present it in the way it could be sold to someone. At the first glance i didn’t wanted to be a part of that, but my friend convinced me to go and present my project, since he knew it and i said yes. I made the presentation for about 10 minutes. I was mainly showing the project and it’s features, at the end the judge asked me:</p>
<blockquote>
<p>“is it all ? How can you make money with this ?”</p>
</blockquote>
<p>I felt empty at that moment, i had no idea of how to do it, where to start, i was just thinking about my friend who convinced me in the beginning to be a part of this. I hangry at him for that bad moment i was having infront of everyone.</p>
<h3 id="mind-refractoring">Mind refractoring</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1619826248852/VQ20PNZj4.jpeg" alt="sticky.jpg" /></p>
<p>After that he gave me a lot of insights, he explained me a whole process of how I could reshape the product and search for stakeholders to make it more sellable. And I liked what he said, it stayed in my empty mind and it was like a reference example when I was planning to write a monetization plan for something else (even now i still think with that approach). I didn’t know what was coming with this knowledge. Then I started all my projects (even smallest one) with a plan of monetization, I always had that in mind, more important, in our school we could ended up in a project contest at any moment without planning ahead, so we always had to have projects ready to be pitched during an improvised contest (even student in networking class had to stay prepared with projects in their fields).</p>
<h3 id="mindset-shift">Mindset shift</h3>
<p>That habit took slowly a great pleace in all my mental process to choose, design and realise projects, even personnal projects. With a real lack of business knowledge, I mostly ended up doing nothing most of the time. The I shifted from the mindset of a “doer” to the one of a “thinker”. I was always planning stuff ahead and trying to see at least one way of monetization. My goal was now to have at least one monetization channel, so I could happily show it to someone who may be ready to take the project to a further step(investing, startup incubation, etc.).</p>
<p>Let’s be clear, it’s not a bad idea to think like that, but it can be a pain point at some point, especially if you are an engineer. I was in a mindset of stopping to imagine great think just because I was not able to figure out how to make money out of it at some point in the future (even potentially). This is what I think is bad at all this, I am trying to remove that mindset and start doing things just for the sake if doing them, and this come with a lot of procrastination.</p>
<p>It haven’t been that bad for me since i am working as a Tech Product manager in a early stage tech startup in HR Tech (Workerly) and most of my work is to figure out the feautres to build and make a lot of planning ahead. Now i am trying to regain my “doer” mindset.</p>
<p>Thank for reading, I just wanted to write this down like a personnal therapy.</p>
]]></content:encoded></item><item><title><![CDATA[Handling SEO with Wagtail CMS]]></title><description><![CDATA[One of the most important things to do when building a website is to handle SEO (Search Engine Optimization), it allows the web pages to be well referenced by the various search engines over the internet. It follows a set of specifics rules and those...]]></description><link>https://blog.adonissimo.com/handling-seo-with-wagtail-cms</link><guid isPermaLink="true">https://blog.adonissimo.com/handling-seo-with-wagtail-cms</guid><category><![CDATA[SEO]]></category><category><![CDATA[Django]]></category><category><![CDATA[cms]]></category><category><![CDATA[Meta]]></category><category><![CDATA[wagtail]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Sun, 24 May 2020 21:00:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1590354017948/BH4jxGVVJ.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the most important things to do when building a website is to handle SEO (Search Engine Optimization), it allows the web pages to be well referenced by the various search engines over the internet. It follows a set of specifics rules and those rules has to be used by content editor, writer or who ever is in charge of producing content for the website. But the point is Wagtail as a content management system comes with the barely minimum for to address this feature, fortunately some clever developers has written down some package to handle this.</p>
<h1 id="heading-what-is-available">What is available ?</h1>
<p>A quick tour  <a target="_blank" href="https://github.com/springload/awesome-wagtail#seo-and-smo">here</a>  shows us a list of them. We have :</p>
<ul>
<li><a target="_blank" href="https://github.com/takeflight/wagtail-metadata">wagtail-metadata</a> </li>
<li><a target="_blank" href="https://github.com/bashu/wagtail-metadata-mixin">wagtail-metadata-mixin</a> </li>
<li><a target="_blank" href="https://github.com/takeflight/wagtail-schema.org">wagtail-schema.org</a> </li>
<li><a target="_blank" href="https://github.com/candylabshq/wagtail-opengraph-image-generator">wagtail-opengraph-image-generator </a> </li>
<li><a target="_blank" href="https://github.com/Frojd/wagtail-redirect-importer">wagtail-redirect-importer</a>  </li>
</ul>
<h1 id="heading-how-to-install">How to install ?</h1>
<p>In this post we will focus on wagtail-metadata which is quite easy to install (like every others wagtail plugin);</p>
<pre><code class="lang-sh">pip install wagtail-metadata
</code></pre>
<p>Add the application to <code>INSTALLED_APPS</code> in django settings.</p>
<pre><code class="lang-python">INSTALLED_APPS = [
    ...
    <span class="hljs-string">'wagtailmetadata'</span>,
]
</code></pre>
<p>and it's done. No migrations to apply (at this level at least). </p>
<h1 id="heading-how-does-it-work">How does it work ?</h1>
<p>To properly add SEO support on a web page you should follow a set of rules and add some meta tags to the HTML document, these are named Opengraph meta tag, in the <code>&lt;head&gt;</code> tag. These tags are basically: </p>
<ul>
<li>Meta content type<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Type"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"text/html; charset=utf-8"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Type"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"text/html; charset=ISO-8859-1"</span>&gt;</span>
</code></pre>
</li>
<li>Meta title<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"title"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"page title here"</span>&gt;</span>
</code></pre>
</li>
<li><p>Meta description</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"some description goes here"</span>&gt;</span>
</code></pre>
</li>
<li><p>Meta author</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"author"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"name of person who wrote the content"</span>&gt;</span>
</code></pre>
</li>
</ul>
<p>It's more explained here on  <a target="_blank" href="https://moz.com/blog/the-ultimate-guide-to-seo-meta-tags">https://moz.com/</a>.</p>
<p>Our goal is to easily add and update those tags and their values on the site's pages and it's the goal of that lib. To do that as in wagtail every page is based on a type defined in models we will need to extends <code>MetadataPageMixin</code> from <code>from wagtailmetadata.models import MetadataPageMixin</code> and we will have a code looking like this : </p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> wagtail.core.models <span class="hljs-keyword">import</span> Page
<span class="hljs-keyword">from</span> wagtailmetadata.models <span class="hljs-keyword">import</span> MetadataPageMixin

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomePage</span>(<span class="hljs-params">MetadataPageMixin, Page</span>):</span>
    <span class="hljs-keyword">pass</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AboutPage</span>(<span class="hljs-params">MetadataPageMixin, Page</span>):</span>
    <span class="hljs-keyword">pass</span>
</code></pre>
<p>Because of our inheritance works in python and this lib implementation <code>MetadataPageMixin</code> should be put before <code>Page</code> class. 
At this point you can makemigrations on you apps and go to the site admin and try to create or update a page to see some changes. </p>
<pre><code class="lang-python">python manage.py makemigrations
python manage.py migrate
python manage.py runserver
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1590352915239/ihaNzg5_A.png" alt="Capture d’écran 2020-05-24 à 21.41.57.png" /></p>
<p>Now we have more fields on the page admin where we can edit SEO related field under the <code>promotion</code> panel, now let write some tag into the template to display those informations at the right place. 
As it need to appear on all the site's pages we will have to put it into the base template of the site, by default is named <code>base.html</code> and is found in you main project folder, if your project is named <code>mysite</code> you will find it out inside <code>mysite/templates/base.html</code>. The point is now to write useful Django tag to return this into the <code>head</code> element like in this snippet:</p>
<pre><code class="lang-html">{% load wagtailmetadata_tags %}
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
{% meta_tags %}
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>This is enough for a basic SEO support, it will handle the meta field generation process and will output meta tag for social media (facebook meta, Twitter meta, etc). At this point you can checkout the rendered HTML and see the result. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1590353104556/Xey1tuGtT.png" alt="Capture d’écran 2020-05-24 à 21.44.47.png" /></p>
<p>As you see, by default this provide meta tags for twitter and OpenGraph (which is more common), it also provide simple meta description content for HTML itself.</p>
<p>To test the render you can open a OpenGraph checker like <a target="_blank" href="https://cards-dev.twitter.com/validator">twitter card validator</a> to see how links to you pages will be interpreted and displayed on twitter. Here is the result of validation on one website i've build using Wagtail CMS. There is a lot of other tools out there to do this kind of tests.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1590353445557/8hpy1-5H5.png" alt="Capture d’écran 2020-05-24 à 21.50.12.png" /></p>
<p>There is a lot more stuff achievable with this lib, the purpose of this post was to introduce it and the concept. You can do more by checking it documentation which is well written with a lot of examples.</p>
<p>Thank you for reading, you can hit me up on  <a target="_blank" href="https://twitter.com/adonis__simo">twitter</a>  if you want to know more.</p>
]]></content:encoded></item><item><title><![CDATA[Building a URL shortening service series, Introduction.]]></title><description><![CDATA[I Was mostly working on clients project since couples of months now, mostly as backend developer with Django framework, but in past i was writing Js code with React (before hooks comes out). One of my latest contract leads me to work with AWS and it'...]]></description><link>https://blog.adonissimo.com/building-a-url-shortening-service-series-introduction</link><guid isPermaLink="true">https://blog.adonissimo.com/building-a-url-shortening-service-series-introduction</guid><category><![CDATA[Python 3]]></category><category><![CDATA[React]]></category><category><![CDATA[Flask Framework]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Fri, 17 Jan 2020 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1590355937929/5CuvGIHO6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I Was mostly working on clients project since couples of months now, mostly as backend developer with Django framework, but in past i was writing Js code with React (before hooks comes out). One of my latest contract leads me to work with AWS and it&#39;s quite large, so i decided to train by practicing and obtain 2 certifications this year. My plan for this consist more in building various tools and host them using various AWS services.</p>
<h1 id="what-tool-am-i-building-now-">What tool am i building now ?</h1>
<p>My first tool is a URL shortener service like bitly. It is small enough to allow me to add new features at every working session and avoid discouragement and stop working.</p>
<h1 id="current-tech-stack-">Current tech stack:</h1>
<ul>
<li>Frontend: React.js (JavaScript)</li>
<li>Backend: Flask (Python)</li>
<li>Database: Redis &amp; PostgreSQL (or another one)</li>
</ul>
<h1 id="what-am-i-expecting-to-learn-">What am i expecting to learn ?</h1>
<ul>
<li>React&#39;s hooks</li>
<li>React&#39;s new dev practice (it&#39;s been a long time i didn&#39;t touch that)</li>
<li>Flask, yes it feel like a shame to be a python backend dev without experience in Flask, but it&#39;s never too late to learn it.</li>
<li>Some basics UX design principles to build a usable UI</li>
<li>AWS cache service.</li>
<li>Small app deployment service with AWS (will need advices here)</li>
</ul>
<h1 id="how-will-i-progress-">How will i progress ?</h1>
<p>As my plan is to work every night on one feature, i will have to made a small blog post about the feature in this series to show my progress and explain what i&#39;ve learned, where i was stuck and what to do next.</p>
<h1 id="what-i-can-expect-from-you">What i can expect from you</h1>
<p>Am open to new ideas about the project and even advices about everything you thing is necessary (from hosting service to project design).</p>
<p>Thank you, hope we will discover great things.</p>
]]></content:encoded></item><item><title><![CDATA[Le commerce électronique en Afrique... Et s'il devenait Africain ?]]></title><description><![CDATA[Un peu de contexte
Quand on dit commerce électronique on vois directement les géants comme Amazon, Ebay, Alibaba etc, ils sont tous different sur plusieurs point mais il y'a des standard qui les caractérisent et qui sont des points communs pour eux t...]]></description><link>https://blog.adonissimo.com/le-commerce-electronique-en-afrique-et-sil-devenait-africain</link><guid isPermaLink="true">https://blog.adonissimo.com/le-commerce-electronique-en-afrique-et-sil-devenait-africain</guid><category><![CDATA[ecommerce]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Wed, 03 Jul 2019 20:16:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1580684516614/jUMKsH-5O.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="un-peu-de-contexte">Un peu de contexte</h3>
<p>Quand on dit commerce électronique on vois directement les géants comme Amazon, Ebay, Alibaba etc, ils sont tous different sur plusieurs point mais il y&#39;a des standard qui les caractérisent et qui sont des points communs pour eux tous, je citerais : le processus d&#39;achat (checking out process) le processus de vente (le système de panier etc) le système de commande et de livraison, il en existe plusieurs autres éléments qui sont recurrent a ces plateformes. Le fait est que en Afrique les plateformes de commerce électronique ne dérogent pas vraiment a cette manière de fonctionner et c&#39;est peut-être à ce niveau qu&#39;elles pèchent, vu qu&#39;il ne semble pas réellement décoller le domaine du commerce électronique sur notre continent.</p>
<h5 id="-attendez-pourquoi-je-dis-qu-il-peine-a-d-coller-"><em>Attendez pourquoi je dis qu&#39;il peine a décoller ?</em></h5>
<p>Deja a titre personnel je n&#39;ai jamais fait un achat en ligne (je ne compte pas l&#39;achat des services tel que les espaces d&#39;hébergement, nom de domain, etc) et ce n&#39;est pas l&#39;envie qui me manque mais le fait de voir certain catalogues pas très fournies (pas beaucoup de choix a mon gout ou a mon niveau) et surtout les prix qui me m&#39;arrangent pas toujours. Ceux de mon entourage qui en font payent généralement a la livraison, et c&#39;est le plus souvent a cause d&#39;un manque de confiance par rapport au site de vente. Les délais de livraison souvent très long et ce malgré le fait de livrer dans la meme ville.</p>
<p><strong>Les standards existant.</strong></p>
<p>Ceci dit, quand je regarde les sites de commerce électronique dans l&#39;écosystème africain ils reprennent tous (ou pour la plupart ) les standards venue d&#39;ailleurs( Europe, USA, Asie, etc) et jusqu&#39;ici, sa stagne, alors que le besoin est présent. Si on regarde bien, ce style de commerce est tout sauf Africain car en Afrique :</p>
<ul>
<li>On ne paye presque pas les produits avec des cartes de credit</li>
<li>On aime marchander (discuter) les prix des articles avant de prendre</li>
<li>On aime le contact physique (meme seulement vocale) avec le vendeur, on préfère le contact humain chaud plutôt que le contact digital froid a travers les écrans de telephones et autre périphériques digitaux.</li>
<li>On deviens &quot;asso&#39;o&quot; avec le vendeur</li>
<li>On demande conseille a une connaissance qui a deja consommer un produit et on achète de préférence la ou ce dernier a acheter et éventuellement au meme prix (ou moins chère)</li>
</ul>
<p>Ceci résume un peu l&#39;ADN du commerce en Afrique, son originalité, ses spécificités, si elle étaient traduite sur un site de commerce électronique veritable avec tout ces codes alors je pense qu&#39;on parlerais réellement de &quot;commerce électronique Africain&quot;(quelques aspects du commerce électronique en Afrique  <a target='_blank' rel='noopener noreferrer'  href="https://www.journaldunet.com/ebusiness/expert/66362/les-specificites-du-e-commerce-en-afrique.shtml">ici</a> ). Sa caractéristique principale serait donc son coté &quot;social&quot; car comme  <a target='_blank' rel='noopener noreferrer'  href="https://twitter.com/africatechie">Rebecca Enonchong </a> le disait dans ce  <a target='_blank' rel='noopener noreferrer'  href="https://twitter.com/africatechie/status/1139825094887464960">tweet</a>  :&quot;<strong>I believe that social e-commerce is what will work in Africa. Not shopping cart e-commerce</strong>&quot; (Je crois que le commerce électronique social est ce qui fonctionnera en Afrique. Pas le e-commerce de panier). En fait si on suis le raisonnement qu&#39;elle décrit on vois rapidement la différence entre le commerce en Afrique et ailleurs, en Europe par exemple les gens:</p>
<ul>
<li>Vont dans une boutique (super-marche ou autre)</li>
<li>Parcourent les rayons</li>
<li>Ajoutent les articles dans leur panier.</li>
<li><p>Passent a la caisse avec leur carte de credit (ou des especes)</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" data-card-width="600px" data-card-key="2e4d628b39a64b99917c73956a16b477" href="https://twitter.com/adonis__simo/status/1139820199090249728" data-card-controls="0" data-card-theme="light">https://twitter.com/adonis__simo/status/1139820199090249728</a></div>
</li>
</ul>
<p>Le processus est alors terminer, On se rend très vite compte que les sites de commerce électronique occidentaux suivent ce processus a la lettre, en fait il a simplement été digitalisé, mais en Afrique sa n&#39;a rien a voir, on préfère:</p>
<ul>
<li>Allez au marché avec une liste de course (souvent)</li>
<li>Parcourir les différents comptoirs et voir ceux qui proposent ce que nous voulons</li>
<li>&quot;Discuter&quot; (marchander) les prix avec plusieurs vendeurs à la fois pour le meme article (ma mère est très forte a sa), éventuellement devenir &quot;asso&#39;o&quot; avec le vendeur qui nous propose un &quot;bon&quot; prix</li>
<li>Mettre le tout dans un &quot;pousse&quot; qui va nous aider a transporter les marchandises hors du marcher ou meme jusqu&#39;a la maison (la partie logistique)</li>
</ul>
<p>Le processus est alors terminer, voila je pense ce qui résume l&#39;essence du commerce en Afrique, si on réussissais a digitaliser sa ce serais un très grand pas.</p>
<h3 id="qui-s-approche-le-plus-du-standard-africain-">Qui s&#39;approche le plus du standard Africain ?</h3>
<p>En regardant donc le commerce électronique sous cet angle, le seul endroit sur internet à ma connaissance qui suivent ces codes c&#39;est  <a target='_blank' rel='noopener noreferrer'  href="https://www.facebook.com/business/industries/retail-and-ecommerce">Facebook</a>  (et les autres réseaux sociaux), a travers la fonctionnalité de &quot;groupe de vente&quot;, en effet dans ces groupes les utilisateurs ont la possibilité de:</p>
<ul>
<li>Lister des articles a vendre dans plusieurs groupe a la fois</li>
<li>Au lieu d&#39;ajouter au panier, les utilisateurs (acheteurs) vont simplement &quot;discuter&quot; avec le vendeur</li>
<li>Apres que le deal sois conclu les protagonistes s&#39;échangent leurs contacts et se donnent rendez-vous physiquement pour le paiement et l&#39;échange de l&#39;article, quelques fois ils feront appel à des services de livraison comme  <a target='_blank' rel='noopener noreferrer'  href="https://twitter.com/CoursiersXpress">Coursier express</a>  si ils ne sont pas disponible dans sur le champ</li>
</ul>
<p><img src="https://media-exp1.licdn.com/dms/image/C4D12AQGIP-vAxkou3g/article-inline_image-shrink_1000_1488/0?e=1586390400&amp;v=beta&amp;t=agBuNdKd1TQyZ_extSdzfUM_7ZUEjzPGI75oYTwo5lo" alt="facebok logo"></p>
<p>C&#39;est vrai ce processus ne garantie pas la qualité du produit mais au moins le vendeur et l&#39;acheteur peuvent se rencontrer et discuter face a face, remarquez aussi que les boutiques physique (dans les marchés en général) créent leur compte sur Facebook pour faire la meme chose tel que décrit plus haut. Notons que le contact humain est si important pour nous Africain que quand je pense au fait que  <a target='_blank' rel='noopener noreferrer'  href="https://twitter.com/IngridNgoune">Ingrid NGOUNE(</a> CEO de  <a target='_blank' rel='noopener noreferrer'  href="https://www.d-fit.fr/">DFIT</a>  Delivery) m&#39;a dit tout récemment que malgré le fait que ses articles soient lister dans un site de commerce électronique les visiteurs préfèrent l&#39;appeler avant de passer a l&#39;achat et les raisons sont diverses, je pense que sa montre ce cote social a suffisance.</p>
<p>Le fait que Facebook sois plus utiliser en matière de commerce électronique que n&#39;importe quel autre site de vente en Afrique et au Cameroun en particulier (vu que c&#39;est de la que je suis) montre a suffisance que ce modèle peu réussir en Afrique, alors la on parlerais de &quot;commerce électronique Africain&quot;, bon si on regarde encore le fait que  <a target='_blank' rel='noopener noreferrer'  href="https://www.france24.com/fr/20190618-facebook-libra-nouvelle-cryptomonnaie-bitcoin-blockchain-internet">Facebook lance une monnaie électronique sur sa plateforme</a>  (et celle ci semble  <a target='_blank' rel='noopener noreferrer'  href="https://information.tv5monde.com/afrique/avec-sa-cryptomonnaie-libra-facebook-lorgne-un-peu-plus-l-afrique-307031">cibler l&#39;Afrique</a> ) sa ouvre un autre débat, mais celui ci fera eut-être le sujet d&#39;un autre article.</p>
<p>En guise de conclusion
Comme vous l&#39;avez surement compris depuis le debut cet article est très inspirer des analyses et commentaires de Rébecca ENONCHONG et d&#39;autres personnes via twitter et LinkedIn, je me suis juste contenter de faire quelques recherches pour comprendre un peu plus le sujet et compiler sous la forme de cet article, bah je dirais donc que pour faire un produit en Afrique il est très important (voire meme vitale) de comprendre la psychologie des Africains, leurs habitudes etc. Une fois de plus merci infiniment d&#39;avoir pris quelques minutes de votre précieux temps pour lire ceci, j&#39;espères avoir votre avis dans les commentaires et discuter avec vous sur le sujet. Twitter :  <a target='_blank' rel='noopener noreferrer'  href="https://www.twitter.com/adonis__simo">@adonis__simo</a> </p>
]]></content:encoded></item><item><title><![CDATA[Mon épopée dans la vente de solutions informatique de gestion d'entreprise au Cameroun]]></title><description><![CDATA[Qui suis-je ?
Ingénieur de travaux informatique en génie logiciel, je suis sur le marché du travail son fait bientôt 3 ans, je me suis spécialisé dans les applications web et les applications de gestion d'entreprises, quelques fois je touche aux site...]]></description><link>https://blog.adonissimo.com/mon-epopee-dans-la-vente-de-solutions-informatique-de-gestion-dentreprise-au-cameroun</link><guid isPermaLink="true">https://blog.adonissimo.com/mon-epopee-dans-la-vente-de-solutions-informatique-de-gestion-dentreprise-au-cameroun</guid><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Thu, 06 Jun 2019 20:14:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1580685272854/RruseBvAK.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h5 id="heading-qui-suis-je">Qui suis-je ?</h5>
<p>Ingénieur de travaux informatique en génie logiciel, je suis sur le marché du travail son fait bientôt 3 ans, je me suis spécialisé dans les applications web et les applications de gestion d'entreprises, quelques fois je touche aux sites web et applications mobiles. J'ai déjà eu à bosser dans 2 entreprises (entre autres) au même poste, celui de développeur d'applications web destinées à un usage en entreprise, et ça m'est très souvent arrivé de travailler avec l'ERP Odoo et me retrouver quelques fois dans la peau d'un commercial (ou technico-commercial) pour les rencontres client et les démos.</p>
<h5 id="heading-mon-experience">Mon expérience</h5>
<p>Au fil des mes aventures le constat a été relativement le même, malgré le fait que les cibles et le discours était relativement différent. Grosso modo il a presque toujours été question de fournir une solution logiciel de gestion totalement en ligne pour les entreprises de petite et moyenne taille, avec un système de facturation mensuelle. Le processus a été en général le même quand il s'agit de faire découvrir le logiciel au client jusqu’à son usage (ou pas) dudit logiciel, ce processus se résume en général comme suit :</p>
<ul>
<li>Discuter avec le client pour faire connaitre l'offre, les avantages etc. (faites en général par un agent commercial)</li>
<li>Présenter une démonstration de la solution (par le commercial et un technicien)</li>
<li>Installation et formation et suivis du client (si achat celui-ci fut intéressé par la solution)</li>
</ul>
<p>Ces étapes sont un résumer du processus et varient selon l'entreprise, l'approche et bien d'autres éléments. Bah C'est à ce niveau que sa devient intéressant par ce que les résultats sont relativement les mêmes peu importe ou j'ai été.</p>
<h5 id="heading-mes-constats">Mes constats</h5>
<p>À l'étape de l'adoption de la solution informatique par une entreprise il a généralement été question de discuter avec les membres de l'entreprise en question et plus précisément le(s) chargé de la cellule informatique et le(s) comptable(s) s'il y'en a. Les comportements observés ont été relativement les mêmes. Essayons de voir les potentiels points de "blocage" au sein du processus de vente d'une solution informatique.</p>
<h6 id="heading-les-informaticiens">Les informaticiens</h6>
<p>Et bien à ce niveau se trouve le premier blocage, cela dépend en fait de deux éventualités. <strong>Sois le responsable est OK pour vous laisser une chance de présenter votre application</strong> et l'adopter si tout est fonctionnelle ou alors <strong>il décide de vous mettre les bâtons dans les roues</strong>. Le deuxième cas est le plus récurrent. Malheureusement, il s'est avéré que les IT de par leurs positions ont un total contrôle sur l'entreprise et certains ont carrément de gros avantages et ne veulent pas être dérangés par l'intervention d'un tiers. Les méthodes pour outrepasser ce souci sont variées et dépendent souvent de l'agent commercial, sa peut aller du "lobbying" (pas politique cette fois ;) ) a un système de pourcentage de commission entre les IT et l'entreprise qui propose le logiciel (nous en l'occurrence), et même jusque-là ça n'est pas forcément assurer que le deal va être conclu. À mes débuts je me demandais comment un informaticien (comme moi) pouvais refuser d'utiliser des solutions informatiques innovantes pour son entreprise mais au fil du temps j'ai compris que c'est surtout une question d’intérêt et d'habitudes (<strong>pour la petite histoire</strong>, un ami a moi qui est certifié CEH et maintenant boulanger est très réticent quand je lui propose une application pour gérer ses boulangeries, il sait de quoi il est question pourtant). Après le département informatique, on est souvent amené à discuter avec le département de la compta (le nerf de la guerre).</p>
<h6 id="heading-les-comptables">Les comptables.</h6>
<p>Ceux-ci représentent le nerf de la guerre dans notre cas parce qu'ils ont un très gros pouvoir décisionnel dans l'entreprise, en effet on ne pourrait parler de gestion d'entreprise sans parler des comptables. Donc même si les IT ont déjà validé l'usage du logiciel en entreprise ceux-ci peuvent simplement invalider cette décision. En fait il se pose généralement plusieurs soucis, de manière générale <strong>les comptables ont leurs vieilles habitudes et automatismes</strong>, pour ce qui est des comptables expérimentés (pour éviter de dire "vieux") certains ont déjà en leur possession des outils comme des <strong>feuilles de calcul Excel monstre</strong> contenant carrément toutes les formules de calcul (de vrai mini ERP) et ne sont pas souvent déçus de celles-ci, vu qu'ils en sont les auteurs en général et par conséquent capables dès les changer à volonté, OK. A côté de ceci il faut aussi noter que les comptables sont toujours formés à l'usage d'application comme SAGE à l’école et par conséquent sont souvent réticent à l'usage de nouvelles solutions en matière de comptabilités ou de gestion, mais le constat général est que si vous montrez à un comptable comment générer ses bilans avec votre solution ou encore comment ressortit ses balances de comptes et autres rapports, pour ne citer que ceux-ci, ils peuvent être prêt à adopter votre solution, en fait ceci c'est dans les meilleurs des cas, au pire si le comptable n'est pas vraiment pour la transparence dans la gestion que votre application va offrir et préféré gérer la comptabilité comme une "boîte noir" sa sera encore plus compliqué. Bon une solution à ce problème est de rendre la solution informatique totalement modulable et la proposer en supprimant le module comptabilité que vous offrez et enfin permettre l'export complet de données sous un format Excel (par exemple) ainsi le comptable utilise ces données avec ses outils et tout le monde est content, bon c'est un constat que j'ai fait a ce niveau.</p>
<h6 id="heading-les-directeurs-generaux-les-fameux-ceo">Les directeurs généraux, les fameux "CEO"</h6>
<p>À ce niveau la difficulté dépend de l'approche de l'entreprise par le commercial, en fait j'ai eu à voir 2 types d'approche, disons l'approche montante et l'approche descendante(je pense que ceux qui ont fait marketing connaîtrons son mieux que moi):</p>
<ul>
<li><em>L'approche montante:</em></li>
</ul>
<p>Ici le commercial a rencontré l'entreprise en venant aux portes de celle-ci en commençant par l’accueil et q présenter son offre pour obtenir un rendez-vous avec une personne qui aurait un pouvoir décisionnel, selon l'entreprise il peut s'agir d'un comptable, un informaticien, le DG lui-même ou autres personnes. Dans cette approche les choses vont souvent lentement parce que ce n'est pas tous ceux que la commerciale rencontre qui veulent la solution (s'ils la comprennent) et par conséquent vont freiner l'avancer du commercial dans l'entreprise.</p>
<ul>
<li><em>L'approche descendante</em></li>
</ul>
<p>Ici le commercial est entré en contact avec l'entreprise directement en communiquant avec le directeur général (CEO), là les choses vont généralement plus vite, surtout quand celui-ci a compris de quoi il est question et comment sa peu l'aider. Vous l'aurez compris n'est pas vraiment accru, en fait elle dépend du niveau de compréhension du CEO de l'offre que le commercial lui fait, en outre sa dépend alors de la capacité à convaincre du commercial. Mais en général plus le directeur général est jeune mieux on a de change de présenter notre solution et de se faire entendre, voire même espérer avoir son entreprise comme cliente.</p>
<p>Vous l'aurez compris le schéma général à ce niveau j'espère (sinon les commentaires sont attendus avec toute question), je ne suis pas commercial, donc je suppose que ceux qui ont fait des études dans ce domaine en savent bien mieux que moi sur ces deux approches que j'ai décris ci-haut (uniquement de mon observation) je ne sais même pas s'il s'agit du nom exact de ces approches.</p>
<h6 id="heading-les-employes-les-utilisateurs-finaux">Les employés (les utilisateurs finaux)</h6>
<p>Les blocages ne sont pas seulement observés dans le processus de vente ou d’intégration de la solution mais aussi dans l'usage au quotidien, le but de l'application est de permettre aux employés (les utilisateurs) de rester productif dans leurs travaux quotidiens, mais si ceux-ci sont ralentis par votre solution il est normal que l'entreprise annule le contrat. Le souci ici est le fait qu'en général dans les entreprises si les employés se font des bénéfices sur l'activité opaque et non suivie de l'entreprise alors ils seront réticents et même souvent contre l'usage de telles solutions (ça leur coupe le "gombo"), ceci a très souvent été observer dans le cadre des entreprises de commerce de produits (boulangeries, etc.) où il arrive que les vendeurs se fassent des bénéfices extra sur les ventes au détriment du chiffre d'affaires de l'entreprise. alors à la moindre défaillance ils vont signaler le fait que le logiciel "ne marche pas" et doit être impérativement annulé pour revenir aux anciennes méthodes, gestion avec la paperasse en général.</p>
<h5 id="heading-ce-que-jen-dis-ou-que-jen-pense">Ce que j'en dis (ou que j'en pense)</h5>
<p>À mon humble avis toutes ces situations sont généralement issues du fait que:</p>
<ul>
<li><p><strong>Le Cameroun et l’Afrique en général sont encore vierge</strong> et ce n'est pas idéalement dans nos mœurs d'utiliser l'informatique pour autre chose que le fun (réseaux sociaux, ordinateur, etc.)</p>
</li>
<li><p><strong>Il est important de faire vendre ce genre de solution pour le moment en utilisant l'effet de réseaux et ne pas attendre des gains faramineux d'un seul coup</strong> (ce qui était le but des dirigeants des entreprises dans lesquelles j'ai bossé sur ce genre de projet). Je parle de réseau ici dans la mesure où il est question de recommandation, beaucoup d'entreprises sont prêtes à utiliser une solution informatique si elles voient d'autres les utiliser, on va alors se retrouver avec du "bouche-à-oreille" et des recommandations de la part de personnes ou d'entreprises satisfaites. Un facteur non négligeable en ressort ici alors, la confiance (a travers une bonne relation humaine), il est d'autant plus facile de vendre quelque chose quand l'acheteur est confiant et voit en vous pas seulement un type (ou une entreprise) qui veut lui faire dépenser mais quelqu'un qui vient l'aider à grandir, à résoudre ses problèmes, et cette dimension-là est très négligé dans les stratégies de ventes et acquisition de clients.</p>
</li>
<li><p><strong>Le marketing n'a pas forcement été bien fait</strong>, de manière générale le paysage numérique est vierge, par conséquent un petit investit dans une stratégie de marketing de contenus, de la promotion sur les réseaux sociaux, un excellent ciblage, une bonne segmentation des clients, un peu de dépense en matière de prospection sur le terrain (incontournable ici) et un peu d'investissement sûr de la publicité en ligne pourrait bien fonctionner. J'ai remarqué (et je pense que c'est vrai) que c'est plus facile de faire comprendre à une personne lambda que votre application est conçue pour les restaurants plutôt que lui dire que votre application est conçue pour "toutes les entreprises", même si c'est possible, genre all-in-one.</p>
</li>
<li><p><strong>La hiérarchie interne de l'entreprise qui produit et vend la solution de gestion</strong>, en fait quand une entreprise a une grosse hiérarchie, sa allonge le temps de prise de décision et le transfert de l'information entre les acteurs de l'entreprise est relativement biaisé (elle est éventuellement modifiée au cours de son chemin) et par conséquent tout le monde n'a pas forcément la même information au même moment. Je parlerais aussi et surtout de vision, en effet pour vendre des solutions informatiques "innovantes" sur un terrain par forcément éduquer et vierge comme le Cameroun, il est important que tous les acteurs aient la même vision, que l'objectif soit clair ainsi tout le monde serais capable de comprendre les actions des autres et de prendre des risques et même de faire des sacrifices à un moment ou un autre sans être inquiété. En fait j'ai souvent observé que les promoteurs des entreprises ont juste une vision grandeur nature de ce qu'ils veulent, et en général il s'agit de pâles copies de ce qu'ils ont vu en Occident, et ils refusent souvent de confier certaines tâches (comme le marketing) aux personnes qui s'y connaissent.</p>
</li>
</ul>
<h6 id="heading-mes-propositions">Mes propositions</h6>
<ul>
<li><p><strong>Une formation professionnelle de commercial axée sur la vente de solution informatique</strong>. La plupart sont juste formés au marketing opérationnel traditionnel avec des programmes qui existent depuis des années (de ce que j'ai pu observer).</p>
</li>
<li><p><strong>Vendre des solutions informatiques spécialisée et même unique</strong>, je prendrai l'exemple de l’intégrateur ERP,  <a target="_blank" href="https://twitter.com/abdounasser202">Abdou Nasser</a>  qui offre une solution unique dédier à l’édition de DSF au Cameroun via son entreprise  <a target="_blank" href="https://proxima.cm/">Proxima Technologie</a> , ainsi que des conseils et actualités de ce domaine-là, voyez par vous-même sur  <a target="_blank" href="https://twitter.com/proxima_cm">twitter</a>  :  <a target="_blank" href="https://twitter.com/proxima_cm">https://twitter.com/proxima_cm</a> . Là c'est facile de connaitre qui est la cible du produit et la cible elle-même peut savoir très rapidement si oui ou non elle est intéressée par la solution.</p>
</li>
<li><p>Il ne faut pas seulement vendre une solution informatique mais une expérience, un écosystème dans lequel le client se sent en confiance et écouté, a la manière d'Apple Inc.</p>
</li>
</ul>
<h5 id="heading-conclusion">Conclusion</h5>
<p>Dans cet article j'ai essayé de vous montrer mon aventure et l’état des lieux de ce commerce d'un nouveau genre dans notre environnement, ceci est très généralisé, les articles suivants traiteront d’éléments beaucoup plus précis, comme la vente de solution informatique dans de très petites entreprises a l'instar des quincailleries, commerce générale etc. J'espère que vous avez aimé mon histoire, je suis ouvert aux interrogations, appréciations et critiques.</p>
<p><em>Si vous trouvez cet article intéressant ou utile veuillez s'il vos plats laisser une like ou un commentaire, ça pourra aider à son référencement et aider quelqu'un d'autre. 
Merci</em></p>
]]></content:encoded></item><item><title><![CDATA[E-commerce platforms in Django ecosystem.]]></title><description><![CDATA[Another tough subject, when it come to e-commerce people use to think WooCommerce, Prestashop, Magento etc... There is also some good solutions for that into the django ecosystem and as usual those solutions are very versatiles a give a great customi...]]></description><link>https://blog.adonissimo.com/e-commerce-platforms-in-django-ecosystem</link><guid isPermaLink="true">https://blog.adonissimo.com/e-commerce-platforms-in-django-ecosystem</guid><category><![CDATA[Django]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[cms]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Sat, 16 Mar 2019 20:31:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1580688246490/PaXsOof1k.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Another tough subject, when it come to e-commerce people use to think WooCommerce, Prestashop, Magento etc... There is also some good solutions for that into the django ecosystem and as usual those solutions are very versatiles a give a great customizations possibilities to the developer. Let see 3 of them, those are the one i have worked with.</p>
<h2 id="shuup">Shuup</h2>
<p>Ready for use ecommerce solution, it is built to be used in a case of multi/single-vendor ecommerce platform, you can create the next amazon or the next shopify with shuup. It is open source (of course) and there is 2 main way to use it, cloud version or self-hosted version. As you guest you will pay for the cloud version and it allow you to do whatever shuup allow you to do without worrying about server uptime and others, the second version is easy to set up on you own servers like a normal django project, in this case you are responsible of keeping servers up and running with maintainance cost it include etc...</p>
<p>Personnally i would compare it to a platform like Magento in term of what can get done with that. (from what i have seen about the two platform)</p>
<p><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mLLxy3pS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/shuup/shuup/master/doc/_static/admin_shop_product.png" alt="shuup admin"></p>
<h3 id="checklist">Checklist</h3>
<ul>
<li><p>Project website &amp; repo :https://shuup.com/ &amp; https://github.com/shuup/shuup</p>
</li>
<li><p>Github stars : 890</p>
</li>
<li>Multi-vendor : YES</li>
<li>CMS : Yes</li>
<li><p>Default website HTML template : It is shipped with a modern HTML 5 template yep complete and good looking. Sufficient to demonstrate all the default features of the thing.</p>
</li>
<li><p>Customizability: It came with a documentation to allow you design and develop your ecommerce website.</p>
</li>
<li><p>Plugins: They (creators) have created a plugin system (addons) easy to use, you can define them within the django project or upload them within the admin UI</p>
</li>
<li>Themes : There is a way to create themes for the platform, they are created as django apps and it use Jinja templating system, which is more advance than the default django&#39;s one.</li>
<li>Community : N/A</li>
<li>Cloud version : Yes</li>
<li>Commercial support : Yes</li>
<li>REST API : yes</li>
<li>Activity reports : Yes</li>
<li>Payment and Shipping&#39;s solution integrations capabilities : Yes</li>
<li>Dashboard : Advanced</li>
</ul>
<h2 id="saleor">Saleor</h2>
<p>Claimming to be &quot;A GRAPHQL-FIRST ECOMMERCE PLATFORM FOR PERFECTIONISTS&quot;, this is a really advanced ecommerce platform in this ecosystem in terms of shipped technologies. It storefront is build as a PWA with React.js and the whole system work on GraphQL.</p>
<p><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JbplzeNs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://user-images.githubusercontent.com/5421321/47799917-8afd7a00-dd2b-11e8-88c7-63588e25bcea.png" alt="salero ui"></p>
<h3 id="checklist">Checklist</h3>
<ul>
<li>Project website &amp; repo: https://getsaleor.com &amp; https://github.com/mirumee/saleor</li>
<li>Github stars : 4050</li>
<li>Multi-vendor : NO</li>
<li>CMS : Yes</li>
<li>Default website HTML template : The default storefront is a PWA built with react.js</li>
<li>Customizability: Easy to customize.</li>
<li>Plugins: Plugins are built following some specs well defined within the documentation</li>
<li>Themes : Can be easilly create following the documentation</li>
<li>Community : N/A</li>
<li>Cloud version : No</li>
<li>Commercial support : NO</li>
<li>REST API : yes</li>
<li>Activity reports : No</li>
<li>Payment and Shipping&#39;s solution integrations capabilities : Yes</li>
<li>Dashboard : Low</li>
</ul>
<h2 id="oscar">Oscar</h2>
<p>A domain driven e-commerce solution. Oscar have been created to be modified as the developer want, it easily allow you to add your business logic into the e-commerce workflow. It is separated into several apps with a dashboard to handle products, reviews, inventory etc.</p>
<p><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9mapzZMP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://raw.githubusercontent.com/django-oscar/django-oscar/master/docs/images/screenshots/dashboard.png" alt="oscar ui"></p>
<h3 id="checklist-">Checklist:</h3>
<ul>
<li>Project website &amp; repo : http://oscarcommerce.com/ &amp; https://github.com/django-oscar/django-oscar</li>
<li>Github stars : 3601</li>
<li>Multi-vendor : NO</li>
<li>Default website HTML template : It come with a simple bootstrap template used to showcase default features. The most of work will be to override it (change it) a part from changing business logic of the store.</li>
<li>Customizability: 100 % it has been build to be customize, the code provide a lot of generic code and a lot of class designed to be inherited and overriden. AbstractProduct for example inherited by Product. It even provide a command to fork and override its apps.
Plugins: There is some plug in for different purpose availble out there, from payments gateway to REST api enabler.</li>
<li>Community : https://slack.oscarcommerce.com/</li>
<li>Cloud version : No</li>
<li>Commercial support : yes</li>
<li>API : yes</li>
<li>Activity reports : Yes</li>
<li>Payment and Shipping&#39;s solution integrations capabilities : Yes</li>
<li>Dashboard : Medium
Those are not the only e-commerce solutions within this ecosystem but those are the most complete ones (from what i&#39;ve see) and there is still a lot of good solutions out there depending on what you are going to build or your budget, here is the Django package grid for the e-commerce :  <a target='_blank' rel='noopener noreferrer'  href="https://djangopackages.org/grids/g/ecommerce/">Django Packages</a> </li>
</ul>
<p>Thanks for reading this, I am a Django Developer and a writing enthusiast (starting out), if you found any error in this article or anything that deserve a review, pleas leave it in comments; Have a great times.</p>
]]></content:encoded></item><item><title><![CDATA[Some Content Management Systems in the Django ecosystem.]]></title><description><![CDATA[In the web development world we have a lot of things that we need to use to build websites (Django, Symfony, etc.) but sometime because of lack of knowledge some Django developer prefer to use a Wordpress or Drupal to build website. But there is a lo...]]></description><link>https://blog.adonissimo.com/some-content-management-systems-in-the-django-ecosystem</link><guid isPermaLink="true">https://blog.adonissimo.com/some-content-management-systems-in-the-django-ecosystem</guid><category><![CDATA[Django]]></category><category><![CDATA[cms]]></category><category><![CDATA[headless cms]]></category><category><![CDATA[website]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Fri, 01 Mar 2019 17:27:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1590356129602/EjGAts8KB.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the web development world we have a lot of things that we need to use to build websites (Django, Symfony, etc.) but sometime because of lack of knowledge some Django developer prefer to use a Wordpress or Drupal to build website. But there is a lot of good content management system in the Django world, here is one of the most visible, i have used some of them.</p>
<p>Each of them provide really different way of managing content and the developer are all of them, i mean they really do it. The development is not driven by the behavior of plugin and especially themes.</p>
<h2 id="django-cms">Django CMS</h2>
<p>Maybe the first one, it has been done to fit the development process of Django itself so it is also easy to get started with, especially with his WYSIWYG feature.</p>
<p><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cXxl3K0M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://django-cms-2016-69800ad.aldryn-media.io/filer_public_thumbnails/filer_public/cc/3e/cc3eda7e-3ccb-4448-be9a-4e88347502eb/preview-1.png__1170x0_q90_subsampling-2.png" alt="Django cms"></p>
<h3 id="checklist">Checklist</h3>
<ul>
<li>Github star : 6569</li>
<li>Release date : ‎may 2007</li>
<li>Main maintainer : Divio, https://www.divio.com/</li>
<li>Plugins : yes, a plugin marketplace</li>
<li>Content Management Method : WYSIWYG with Drag-n-Drop on the front end</li>
<li>Administration : Yes, customised version of Django admin Easy to handle, it work like a normal Django application with a WYSIWYG feature.</li>
<li>Publication workflow : No</li>
</ul>
<h2 id="wagtail-cms">Wagtail CMS</h2>
<p>Recent but powerful, a lot of folks out there are saying that it does well some things Django CMS wasn&#39;t, i personally agree with that; for example there is a possibility to manage a CMS&#39;s page context, just magic. Can be use as a headless CMS. Note that it is data driven. (like Odoo ERP)</p>
<p><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pKRIMj-1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://media.wagtail.io/images/explorer1_HK2WdeO.width-1000.jpg" alt="wagtail cms"></p>
<h3 id="checklist">Checklist</h3>
<ul>
<li>Github star : 6772</li>
<li>Release date : February 2014</li>
<li>Main maintainer : Torchbox, https://wagtail.io/</li>
<li>Plugins : yes, there is a list of awesome list of plugins on Github
Content Management Method : Each page is define as a Django model and each instance of the page class represent a page in the site. Content are defined within the administration.</li>
<li>Administration: Yes, A totally new one build from scratch to meet some requirements.</li>
<li>Publication workflow : Yes, there is a logic of Editor, Moderators and publication review between them embed directly into the CMS.</li>
<li>Slack channel : https://wagtailcms.slack.com
Can be used as a Headless CMS since it provide a full REST API to access page and content.</li>
</ul>
<h2 id="mezzanine">Mezzanine</h2>
<p>Not a lot of thing to say, i didn&#39;t work a lot with that but i know that it is very integrated to the Django admin.</p>
<p><img src="https://camo.githubusercontent.com/2ad3a4037109bbf18b2a7ed57beb63da23fb88b4/687474703a2f2f6d657a7a616e696e652e6a75706f2e6f72672f646f63732f5f696d616765732f64617368626f6172642e706e67" alt="mezanine cms"></p>
<h3 id="cheklist">Cheklist</h3>
<ul>
<li>Github star: 3718</li>
<li>Release date : 2012</li>
<li>Maintainer : Stephen McDonald (@stephenmcd, Github)</li>
<li>Theme : There is a Theme marketplace https://mezzathe.me/</li>
<li>Administration : Django Admin</li>
<li>Content Editing Method : Directly into the Django admin UI</li>
</ul>
<h2 id="django-widgy">django-widgy</h2>
<p><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pSPDtuwA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://wid.gy/static/image/feature-drag-drop.jpg" alt="django-widgy"></p>
<h3 id="checklist">Checklist</h3>
<ul>
<li>Github star : 310</li>
<li>Release date : n/a</li>
<li>Maintainer: fusionbax, https://wid.gy/</li>
<li>Content Management Method : WYSIWYG within the Django administration</li>
<li>Administration : Django admin</li>
<li>Theming : normal Django template system</li>
</ul>
<p>Actually there is a lot of CMS solutions out there in this Django&#39;s world, some are more mature than other, so the usage of one depend also on the developer strength.
For the story Wagtail CMS has been build by Torchbox to replace Drupal in their work.</p>
<p>You can view the Django Package grid related to CMS here : https://djangopackages.org/grids/g/cms/</p>
<p>Thank for reading this, you can .leave a comment to add a CMS to the list and even a thought about it, we could find a new unicorn.</p>
]]></content:encoded></item><item><title><![CDATA[How i learnt accountancy being a developer in 2 hours.]]></title><description><![CDATA[A bit of context
Actually i am a Python developer at Normalis Consulting, i accepted to work there just because of the fact they use python for a lot of things and they actually work with Odoo ERP, since it was on my roadmap of technologies to learn ...]]></description><link>https://blog.adonissimo.com/how-i-learnt-accountancy-being-a-developer-in-2-hours</link><guid isPermaLink="true">https://blog.adonissimo.com/how-i-learnt-accountancy-being-a-developer-in-2-hours</guid><category><![CDATA[Career]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Mon, 18 Feb 2019 18:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1590350543928/zW3G811_c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="a-bit-of-context">A bit of context</h2>
<p>Actually i am a Python developer at Normalis Consulting, i accepted to work there just because of the fact they use python for a lot of things and they actually work with Odoo ERP, since it was on my roadmap of technologies to learn as python developer, i was so excited because i was about to learn about creating modules, apps and other stuff on it, in the other hand the CTO is very good on it, so i knew that i had found a mentor.</p>
<h2 id="what-happened">What happened</h2>
<p>The company actually serve Odoo for multiple clients as SaaS and when there is a new client we just configure the software to fit the client&#39;s enterprise needs and even create new modules if needed, this week we was doing that for a financial institution, and our accountant is in the process of configuring the application, he was working on the Payroll module&#39;s configuration, and he was stuck at defining some rules in the salaries calculations ( talking here about, salary rules, categories, salaries structures, etc) so he called me ( i am the technical guy), my work was to write small python codes ( but advanced for an accountant) to do some calculations, i was happy to do it but there is a thing : i am a developer, not accountant so i had to learn those concepts. What i does ? (because by the end of day i had some knowledge on this)</p>
<h2 id="how-i-did">How i did</h2>
<p>From the time he told me was going to work on Payroll and he actually came to my desk i googled stuff like : what is used to calculate salary ?, what is a salary rule ? etc.. i started to read a lot of articles and book about this concept, i learn about allocations, indemnities, base salary, how taxes are applied to the salary and a lot of things...</p>
<p>To know what i had to learn was not so difficult, i just had to navigate through the app (in Odoo) and googled labels i was seeing on menus, eg: Salary rules on google i typed : what is a salary rules ? etc...</p>
<p>By the end i had a clear ideas of those stuff and the accountant came and explain them to me more and we was ready to start, i did the jobs he wanted me to do and its worked fine. I was very proud of me.</p>
<h2 id="what-i-have-learn">What i have learn</h2>
<p>I had never made this kind of business oriented software development, i had some friend who talked a lot about that, and in CS school we had learn some basic stuff about accountant and enterprise management but i didn&#39;t gone far into those stuff, but now i am ready to re-open my old books.</p>
<p>I&#39;ve discovered that as a developer, my ability to find something on google, learn and integrate it was very developed. Every time we, developers, learn by googling things and reading documentation, even totally new technologies, it could actually be a skill.</p>
<p>Now i know how a salary is calculate in detail and what constitute it.</p>
<h2 id="in-the-future">In the future</h2>
<p>Now i am ready to learn new part of enterprise management, but i think it will be a kind of learn by doing or learn while being in the core of process... I have faced another problems but i calm, will overcome them without problems (my CTO is there ;) ).</p>
]]></content:encoded></item><item><title><![CDATA[Building a SaaS application with Django Web framework: The principle]]></title><description><![CDATA[A software which is running directly into the web browser and users have to pay for it on diverse way like per hour, or even per user, etc is called a Software as-a Service application (SaaS). Since those last years this model is very widely used by ...]]></description><link>https://blog.adonissimo.com/building-a-saas-application-with-django-web-framework-the-principle</link><guid isPermaLink="true">https://blog.adonissimo.com/building-a-saas-application-with-django-web-framework-the-principle</guid><category><![CDATA[Django]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[adonis simo]]></dc:creator><pubDate>Wed, 09 Jan 2019 20:06:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1590351000978/ygG5vnCqd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A software which is running directly into the web browser and users have to pay for it on diverse way like per hour, or even per user, etc is called a Software as-a Service application (SaaS). Since those last years this model is very widely used by startup out there to sell their services. In this first article we are going to learn about the core principles of this.</p>
<h2 id="ok-lets-start-the-principle">Ok let’s start, the principle.</h2>
<p>Actually from my small experience talking about SaaS application design and infrastructure involve talking about data management, I mean questions like <strong>how to store data in the database? how to access them? What kind of Database should be used? How to send an incoming request to the right application version?</strong> and so on. Globally the concept is to allow the existence of many versions of the same application, each instance, should only show his data and manage only his clients.</p>
<h2 id="1-so-how-are-data-actually-saved-into-the-db-what-about-accessing-them">1. So, How are data actually saved into the DB, what about accessing them?</h2>
<p>In SaaS application each instance of the application is called a tenant, so the deal here is to choose a data separation technique, properly:</p>
<p><em>A way to store tenant’s data by ensuring that they are saved without being mixed.</em></p>
<p>Ok, actually this can be done directly into the database, usually there are 3 main ways to separate stored data, they can be called data isolation level within the database. So we have: Low isolation, semi-isolation and high isolation. Let dive into them before start coding.</p>
<ul>
<li><strong>Low isolation of data</strong>
The principle here is to store tenant’s data within the same database and same tables. So you are in a case where each table has a column name for example, as tenant_uuid this column hold a unique identifier for a tenant, and there is a table in your database named for example as: tenants, it should store each tenant’s information like: name, uuid, registration_date, status, pricing_plan_id etc… In this case for all queries to the database, you will have to add a statement to ensure that you are retrieving right tenant’s data like:</li>
</ul>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> <span class="hljs-string">'table_name'</span> <span class="hljs-keyword">WHERE</span> tenant_uuid = <span class="hljs-number">5</span>
</code></pre>
<p>or with the Django's ORM</p>
<pre><code class="lang-python">TableName.objects.filter(tenant_uuid=<span class="hljs-number">5</span>)
</code></pre>
<ul>
<li><strong>Semi-isolation of data</strong></li>
</ul>
<p>The principle here is to save data within the <strong>same database, same table BUT different schemas. </strong>A schemas can be seen as a database into a database, it holds tables from as a normal database and you can create many schemas as you DBMS allow you, for those who use PostgreSQL, they use to work in the default schema named: public and SQL queries to table look sometimes like</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">from</span> public.table_name
</code></pre>
<p>With that said, semi isolation method consists in creating new schema for new tenants (your clients, the people who use your SaaS application), and all the created schemas contain the same tables. In this case the users’ data are not saved in the same place. They are technically in a separated table (or databases). So retrieving data with ORM is like:</p>
<pre><code class="lang-python3">TableName.objects.all()
# We assume that there is a tool ( a db router ) which is a charge of selecting the right schema to execute the query
</code></pre>
<p>we assume that there is a database router which is in charge to select the right schemas for the query’s execution. We’ll set it up in the next part of this article’s serie.</p>
<ul>
<li><strong>High data isolation level</strong></li>
</ul>
<p>The principle here is to save tenant’s data within <strong>different databases</strong>. In this case when you have a new client you will just create a new empty database to store data. This is why we talk about high isolation, database can be even destroyed, but only one client will be affected, but it can be very heavy in terms of resource consumption. Accessing data with Django ORM can be done like:</p>
<pre><code class="lang-python3">TableName.objects.all().using('DB_NAME')
tableNameOject.save(using='DB_NAME')
</code></pre>
<p>the <em>using()</em> method (or parameter) is directly available in Django, by default its value is ‘default’; where to find it ? In the settings.py file, at the database information credentials :</p>
<pre><code>DATABASES = {
    <span class="hljs-string">'default'</span>: {},
    <span class="hljs-string">'users'</span>: {
        <span class="hljs-string">'NAME'</span>: <span class="hljs-string">'user_data'</span>,
        <span class="hljs-string">'ENGINE'</span>: <span class="hljs-string">'django.db.backends.mysql'</span>,
        <span class="hljs-string">'USER'</span>: <span class="hljs-string">'mysql_user'</span>,
        <span class="hljs-string">'PASSWORD'</span>: <span class="hljs-string">'superS3cret'</span>
    },
    <span class="hljs-string">'customers'</span>: {
        <span class="hljs-string">'NAME'</span>: <span class="hljs-string">'customer_data'</span>,
        <span class="hljs-string">'ENGINE'</span>: <span class="hljs-string">'django.db.backends.mysql'</span>,
        <span class="hljs-string">'USER'</span>: <span class="hljs-string">'mysql_cust'</span>,
        <span class="hljs-string">'PASSWORD'</span>: <span class="hljs-string">'veryPriv@ate'</span>
    }
}
</code></pre><p>In the example above, there are 3 databases actually registered by their aliases :default, users, customers, so by default ( if you don’t specify using()) Django will choose default.</p>
<h2 id="2-how-to-know-which-tenant-is-required-for-a-specific-incoming-request">2. <strong>How to know which tenant is required for a specific incoming request ?</strong></h2>
<p>Incoming request represents here a user of your application who is connected via the browser and is about to use the application. The point here is to define what is called a <strong>tenant detection strategy</strong>;</p>
<p>It is a proper way to know how to interact with data stored in the database, it is not really related to the isolation level you have implemented in your application, the result of this operation is the name of the <strong>schema</strong> (in case of mid-isolation level) or the <strong>tenant uuid</strong> (in case of low isolation level) or <strong>database</strong> (in case of high isolation level) to use for data retrieval.</p>
<p>Usually to do that, developers use URL, by adding some indications within it, one of the most simplest and elegant way to do that IMO is by defining a sub-domain for each <strong>tenant</strong>; Let's assume that you application is a blog engine named www.bloghost.com each client will have a sub-domain and a url pointing directly to his blog like: client1.bloghost.com, another way is by defining url parameter as follow: www.bloghost.com/85DE5148WFE848 and this token can be a hash used to identify the <strong>schema | tenant’s uuid | database </strong> related to the actual client’s data. This will totally depend on you.
Now when you have identified which tenant is aimed by a request you will just need to use the appropriate query to the database. You can even achieve this by using cookies. </p>
<p>I won’t go too far in this article about that because the Django package we are going to use will handle this process for us, so I just tried to explain the concept.</p>
<p>Thank for reading this first part, it was just about knowing how it works and general terms used in this world. The next article will be more practical.</p>
]]></content:encoded></item></channel></rss>