How to digitalize your wedding

When my fiancée and I dove into wedding planning, we quickly saw an opportunity to put our coding skills to good use. Amidst the whirlwind of selecting venues, choosing floral arrangements, and organizing guest lists, we discovered a way to simplify these tasks and inject some fun into the process. We decided to tackle the chaos of wedding planning by coding our way through it, leveraging our programming skills to bring our vision to life in a unique and efficient way.

The challenge

Organizing a wedding is a colossal task. Between designing and sending invitations, keeping track of RSVPs, and figuring out seating arrangements, the list of to-dos seemed endless. Naturally, I was compelled to find ways to automate some of these processes.

The beginning

With my proficiency in Django, I chose this framework to develop a straightforward web application. The beauty of Django is its user-friendly admin panel, which became indispensable to us. We kicked things off by creating a Guest model in our database, a simple yet effective way to manage our guest list:

class Guest(models.Model):
    name = models.CharField(max_length=100)
    surname = models.CharField(max_length=100)
    plus_one = models.BooleanField(default=False)

This setup allowed us to digitally organize and visualize our guest list, keeping everything neatly in order. After conducting some tests locally, we were ready to launch our project into the real world.

Setting up the cluster

A colleague introduced me to an article on setting up a free Kubernetes cluster in the cloud. With several options like AWS, Heroku, and Azure, Oracle Cloud’s free tier caught our attention, offering up to four instances with 1 CPU and 6GB of RAM each. Following the steps outlined in this e-book, I was able to set up the instances, load balancer, SSH, iptables, and more, resulting in a fully functioning Kubernetes cluster.

Here’s what the neofetch output looked like:

OS: Ubuntu 22.04.1 LTS aarch64
Host: KVM Virtual Machine virt-4.2
Kernel: 5.15.0-1045-oracle
Uptime: 178 days, 21 hours, 29 mins
Packages: 848 (dpkg), 7 (snap)
Shell: bash 5.1.16
Resolution: 1024x768
Terminal: /dev/pts/0
CPU: (1)
GPU: 00:01.0 Red Hat, Inc. Virtio GPU
Memory: 1777MiB / 5916MiB

Route53

For domain access, we opted for Route53, purchasing a domain, creating a hosted zone, and establishing an A record to point to our Oracle load balancer’s IP. This allowed us to access our app through a personalized domain.

Helm chart

Our chart included several components: deployment, an upgrade job for new releases, a PVC for PostgreSQL, services, and ingress for domain access, alongside secrets for database passwords, Django’s secret key, and Dockerhub registry credentials.

CI/CD

We stored our app’s code on GitHub and employed GitHub Actions for deployment. I set up a self-hosted GitHub Actions runner on an Oracle Cloud instance to streamline our workflow.

Whenever we updated the main branch, this setup would automatically build the Docker image and upload it to a private Docker Hub repository, taking advantage of my fiancée’s account for the free private repo. A custom GitHub Action by a colleague simplified deploying the app using this image.

Going live

With the cluster operational, the app’s local configuration verified, and the CI/CD pipeline ready, we moved forward with deployment. Despite initial challenges, we managed to launch smoothly. Our app was now accessible via a custom domain, with all data securely housed in a PostgreSQL database. Moreover, each git push triggered an automatic update, with changes going live in about a minute.

Guests list

Django’s admin panel made managing the guest list a breeze. We could easily list everyone we wanted to invite and specify if they could bring a plus one. With our guest list complete, we pondered our next steps.

Invitations

Our goal was to craft personalized PDF invitations for each guest, incorporating their names for a personal touch.

To track invitation specifics, we introduced a new model in Django:

class Invitation(models.Model):
    text = models.TextField(blank=True)
    code = models.CharField(
        max_length=6,
        unique=True,
        default=random_code,
    )

We then connected invitations to guests through a new relationship in our Guest model:

class Guest(models.Model):
    ...
    invitation = models.ForeignKey(
        Invitation,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )

After setting up our system, we populated our database with guest and invitation details, automatically assigning a unique code to each invitation. This code facilitated the generation of a QR code for each invite, directing guests to our website for RSVPs and dietary preferences.

Generating QR codes

Creating QR codes was straightforward. A “Generate QR code” button in the Invitation admin panel enabled us to download QR codes directly. For this task, we utilized the qrcode and pypdf libraries.

Given the sheer number of invitations, manually creating QR codes for each was impractical. Consequently, we automated the process.

Background tasks

To efficiently generate around 40 QR codes without browser timeouts, we employed celery with redis as the message broker. This setup allowed us to trigger a celery task for QR code creation through an admin panel button, with the codes generated in the background to avoid any timeouts. I expanded the helm chart to include additional deployments for both celery and redis.

NFS

To ensure all four nodes worked efficiently, I set up an NFS (Network File System) using my Raspberry Pi. This allowed each backend pod across the nodes to access the same files, a crucial aspect for seamless operation.

● nfs-server.service - NFS server and services
     Loaded: loaded (/lib/systemd/system/nfs-server.service; enabled; vendor preset: enabled)
     Active: active (exited) since Mon 2024-03-18 21:01:38 CET; 2 weeks 1 days ago
   Main PID: 623 (code=exited, status=0/SUCCESS)
      Tasks: 0 (limit: 3933)
        CPU: 0
     CGroup: /system.slice/nfs-server.service

After configuring the NFS server on the Raspberry Pi, I included a reference in the deployment as follows:

volumes:
...
- name: common-space
  nfs:
    server: ip.of.my.rpi
    path: /
    readOnly: false

While NFS might not be the fastest option, it met our needs. By clicking the button to generate QR codes, I could wait for the zip file with all codes to be ready for download.

Submissions

Ensuring the QR codes functioned flawlessly was essential. After testing, we were directed to the RSVP page via the /guest/<code> URL, indicating a need for the next component: a system for guests to confirm attendance and specify their preferences.

We developed a new model to manage this:

class Submission(models.Model):
    invitation = models.ForeignKey(
        "Invitation",
        on_delete=models.SET_NULL,
        related_name="submissions",
        null=True,
        blank=True,
    )
    data = models.JSONField()

This model linked each response to its corresponding invitation, allowing us to track who had replied and their preferences. The data field, utilizing Django’s JSON capabilities, enabled us to efficiently store diverse guest responses.

Next, we developed a form on the RSVP page. It was dynamically generated to capture essential details from our guests — whether they could attend, if they’re staying for the next day, if they need hotel or bus, their meal choices, preferred drinks and any special requests they might have.

First responses

We have finished making the invitation, generated the QR code, and set up the RSVP page. After some tests, we were ready to send them. We printed it out on fancy paper, put it in the envelope, and sent it out. The QR codes were being scanned, submissions started coming in.

With everything we built, we could easily enter the k8s pod’s shell and write a query like this:

$ ./manage.py shell
>>> from guest.models import Invitation
>>> sum(
        i.last_submission.data.get('needs_bus',0)
        for i in Invitation.objects.all()
        if i.last_submission is not None
    )
10

And we could see how many guests need a bus.

Summary

Our wedding planning journey is shaping up to be an extraordinary adventure, blending our passion for coding with the thrill of anticipating our big day. By embracing digital solutions early on, we’re reimagining the traditional planning process to make it more streamlined and enjoyable. From establishing a Kubernetes cluster to automating routine tasks and crafting QR codes, every step is an opportunity to learn and innovate. We’re not just planning a wedding; we’re engineering an event that captures our unique personalities and vision. For those embarking on their own wedding planning journey, we encourage you to think creatively and make use of your coding skills to add a personal touch to your celebration. Remember, your wedding day is a reflection of your individuality and love story — let it shine in every detail.