Back to selected work

IASRM Global

One Django backend serving four different enrolment flows.

Role
Full stack (backend and integration)
Stack
Django, PostgreSQL
Period
2025
Status
Live at iasrmglobal.org

Context

The International Association of Stem Cell & Regenerative Medicine is a non-profit medical body. Like most professional associations, it does several unrelated-looking things at once: it teaches, it admits members, it runs a conference, and it publishes. All of that sits on one website, and a doctor arriving from a search result does not care which department owns which page.

The design was delivered by an external agency. The backend, the data model and the integration were mine.

Problem

Four separate things on this site ask a person to sign up and pay: a course, a membership, a conference, a webinar. On the surface they are the same form. Underneath, they agree on almost nothing.

  • Eligibility — a Fellowship has entry requirements a Masterclass does not. A conference tier can require that you already hold a paid lifetime membership. A webinar is open to anyone with an email address.
  • Pricing — a membership is priced by which country group the registrant is in. A conference seat is priced by which tier you qualify for and which date window you book in. A course carries a single fee in two currencies.
  • Record-keeping — a course enrolment produces an academic record that matters years later. A webinar registration is an attendance list. They do not have the same lifespan.

So the real question was not "how do I share one model between these", but "where is sharing actually worth it". The answer turned out to be different in each corner of the site, and one of those answers is a decision I would now reverse.

What I built

  • Three course taxonomies — Fellowship, Diploma and Masterclass, each with its own catalog, detail page, curriculum tree, enquiry capture and fee structure.
  • Conference registration as a priced matrix: an event has tiers (the rows) and date windows (the columns), and a price is the cell where they meet, held in both INR and USD. Workshops attach to the event separately and can be sold as bundles.
  • Memberships priced by country group — every country carries an ISO alpha-3 code and a group, and each plan stores a price per group, so a registrant's country decides the fee without a branch in the view.
  • Razorpay checkout behind membership orders, with the order, payment and signature identifiers kept on the order record and billing details on a one-to-one beside it.
  • A content layer — articles, blog, podcast, webinars, media gallery, testimonials, announcements and board member listings.
  • A cross-cutting SEO layer: one metadata model attached to any other model through a generic foreign key, so a page's title, description, canonical, robots directive and OG image live in one place rather than being re-implemented per content type.
  • User accounts on a custom user model, so one person is one person across every flow.
Python
# stemapp/models.py — abbreviated
# Three course types, three parallel families, no shared base. This is the
# shape the retrospective below is about.

class Fellowship(models.Model):
    title = models.CharField(max_length=255)
    overview = models.TextField(blank=True, null=True)
    eligibility = models.TextField(blank=True, null=True)
    duration = models.CharField(max_length=100, blank=True, null=True)
    ...

class FellowshipCurriculum(models.Model):
    fellowship = models.ForeignKey(Fellowship, related_name="curriculum", ...)

class FellowshipCurriculumTopic(models.Model):
    curriculum = models.ForeignKey(FellowshipCurriculum, related_name="topics", ...)


class Diploma(models.Model):          # same fields again
    ...

class DiplomaCurriculum(models.Model):
    diploma = models.ForeignKey(Diploma, related_name="curriculum", ...)


class Masterclass(models.Model):      # and again
    ...

class MasterclassCurriculum(models.Model):
    masterclass = models.ForeignKey(Masterclass, related_name="curriculum", ...)

Technical decisions

Conference pricing as a matrix, not a rule

A conference seat's price depends on who you are and when you book. Rather than encode that as conditions, the event owns tiers (rows: "IASRM life members", "Non-members", and so on) and price windows (columns: "Early bird until 31 December", "Standard", "On-site"). A price is simply the cell where a tier meets a window, stored in both INR and USD.

Next year's conference is then configuration, not a migration. The tier that requires an existing lifetime membership carries a flag which is checked against the memberships app at registration — so the discount is enforced by data, not by a rule someone has to remember to copy forward.

Membership pricing by country group, not by country

Every country carries an ISO alpha-3 code and a group, and each plan stores one price per group. Adding a country is a row; repricing a region is two fields. Neither touches the checkout path.

SEO as a layer, not a field on every model

Title, description, canonical, robots directive and OG image attach to any model through a generic foreign key, so the editor fills the same form whether the page is a blog post, a fellowship or a podcast episode. Without that, each new content type re-implements the same seven fields and they drift.

The three course types were left parallel — and that is the one I would take back

Fellowship, Diploma and Masterclass are three separate model families. Each has its own slide images, learning points, curriculum, curriculum topics and enquiry model. They share a shape almost exactly, and they share no code at all.

It moved fast and each type could diverge freely — Diploma grew a structure and an application model, Fellowship grew a fee table, Masterclass grew features. But a change to "how a curriculum works" is now three changes, and "how many people enrolled in anything this year" is a union across three tables rather than one query.

[FILL: if there was a specific reason the three stayed separate — a deadline, a client requirement, the types genuinely diverging — say it here. It is a stronger answer than leaving it implied.]

Results

  • Enrolment paths served by one backend: four — courses, memberships, conference, webinars
  • Django apps in the project: eight, with a custom user model shared across all of them
  • Conference pricing combinations expressed without a conditional: tiers × date windows, in two currencies
  • Ten courses published across the three taxonomies — four fellowships, three diplomas, three masterclasses — carrying 54 curriculum modules and 123 topics between them
  • 92 countries mapped onto two pricing groups, so a membership fee resolves from the registrant's country without a branch in the view
  • Conference pricing expressed as 3 tiers × 3 windows = 9 price points, plus 4 workshops, none of it written as a condition
  • 142 board and faculty profiles across 9 categories, and 16 editable site pages each carrying their own SEO record

What I would do differently

I would give the three course types one abstract base and let them differ only where they genuinely differ. The shape they share — title, overview, eligibility, duration, curriculum, enquiry — is the overwhelming majority of each model, and keeping it in triplicate means every change to a course is made three times and every question about courses is a union of three tables. The parts that actually diverge, the Diploma's application flow and the Fellowship's fee table, would sit perfectly well on top of a shared base.

Next: Reporting Dashboard