Django Case Study: JustDjango
Intro
This case study is about JustDjango. JustDjango is a website and YouTube channel that provides courses that teach web development with a focus on Django. The website that powers the project is also built with Django.
The project started in January 2018 and since then the website has gone from a typical Django project, to a React app, to a NextJS app, and then eventually split into two projects - the landing page (justdjango.com) and the learning app (learn.justdjango.com). This setup seems to have been the best, as it still works like that today.
Building the JustDjango project has been a rollercoaster of learning. I became familiar with a lot of technologies and was constantly looking for better ways to architect the project.
In this case study, I will go through aspects of this project. I'll discuss the details of the project, it's architecture, and technologies being used.
Project Description
The aim of the project is to enable people to purchase courses. The setup of the courses is a little bit different from typical course selling. The website does not support purchasing individual courses. Instead, it works as follows:
- There are two roadmaps - the basic, and the advanced roadmap.
- A roadmap consists of multiple courses
- Users purchase access to a membership. There are two memberships - the Basic, and the Pro membership.
- The basic membership gives access to the basic roadmap
- The pro membership gives access to both the basic and the advanced roadmap.
The current setup of the website is as follows:
- The justdjango.com domain is for the landing page. It holds information about the project, has a blog, and presents other aspects of the project itself. It also contains an API.
- The learn.justdjango.com domain is for the learning app. This app is where students will log in, purchase access to memberships and watch courses.
Technologies used
Because the project is split across two domains, I'll explain the tech stack for each domain.
justdjango.com
This project has two purposes. It acts as the landing page and the API.
The landing page is a typical Django project using HTML templates. The pages are styled using TailwindCSS and there is also a small amount of JavaScript being used through AlpineJS.
The aim of this tech stack is to make page load-time as quick as possible. TailwindCSS helps a lot in that regard because you can build a production CSS file that includes only the class names that your templates are using. AlpineJS is also a really small library and helps a lot in handling simple transitions and state management.
The API is built using the Django Rest Framework, the SimpleJWT package and the dj-rest-auth package to provide JWT cookie authentication.
learn.justdjango.com
This project is a React app. The app also uses TailwindCSS. It uses many libraries to handle things like payment processing with Stripe and video playing with Vimeo. It's not using any kind of component library, although I would use the official TailwindCSS-React library once it's released. The project uses react-router-dom for navigation. It doesn't use any kind of state management library - React Context does a great job at that.
The aim of this tech stack is to make the user experience look good, feel good, easy to navigate across courses and easy to manage their membership. I also like to minimize it's dependencies so that I can maintain a smaller JavaScript build size, as it can get quite large if left unchecked.
Architecture
The biggest challenge is to allow the React app to communicate with the Django project. Hence the Django project has an API for the React app to authenticate with and request resources from. Resources include things like video URLs and membership details.
As mentioned earlier, the Django project uses the JWT cookie authentication settings from the dj-rest-auth package. When a user logs into the React app, their access token and refresh token are stored as separate cookies. When the access token expires it is automatically refreshed using the refresh token. This was the most challenging part of the architecture.
The Django project is deployed on render.com. You can read more about the switch to them over here. The React app is deployed on Netlify. One recent addition was a CDN powered by Digital Ocean. The CDN is for static and media files that are accessed in both projects. For a long time, the project was using AWS S3, but Digital Ocean Spaces has proven to be more user friendly and more understandable.
Technical Aspects
The most technical aspect, other than authentication, is the flow of users paying for a membership via Stripe and gaining access to it on the site. Stripe's documentation has tutorials on how to set up payments for subscriptions. Following the documentation, you can get the majority of the system setup. What's left is to connect the payment logic to actually giving access to a user.
Let's start with a basic representation of the database models in the project:
from django.db import models
class Subscription(models.Model):
STATUS_CHOICES = (
('active', 'active'),
('canceled', 'canceled'),
('past_due', 'past_due'),
('trialing', 'trialing'),
)
stripe_sub_id = models.CharField(max_length=100)
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
plan = models.ForeignKey('Plan', on_delete=models.CASCADE)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='trialing')
def is_active(self):
return self.status == "trialing" or self.status == "active"
class Plan(models.Model):
name = models.CharField(max_length=100)
stripe_plan_id = models.CharField(max_length=100)
class Roadmap(models.Model):
name = models.CharField(max_length=100)
plans = models.ManyToManyField('Plan', on_delete=models.CASCADE)
class Course(models.Model):
name = models.CharField(max_length=100)
is_free = models.BooleanField(default=False)
roadmap = models.ForeignKey('Roadmap', on_delete=models.CASCADE)
class Video(models.Model):
name = models.CharField(max_length=100)
is_free = models.BooleanField(default=False)
course = models.ForeignKey('Course', on_delete=models.CASCADE)
Here we have a Subscription
model that represents the membership a user has. The Plan
model corresponds to the plans we have on Stripe, which can either be the Basic Plan or the Pro Plan. The Roadmap
model is simply to manage the collection of courses, and the plans that have access to it. The Course
model belongs to a single roadmap. And the Video
model belongs to a single course. There are many ways to structure it but this is how we went with it.
Access to videos
The most important part is the logic for determining whether a user has access to a video. Every request for a video checks the users subscription with logic similar to the following:
@login_required
def video_detail(request, pk):
video = get_object_or_404(Video, id=pk)
roadmap = video.course.roadmap
sub = request.user.subscription
has_active_sub = sub.is_active()
has_access = sub.plan in roadmap.plans.all()
if has_active_sub and has_access:
return render(request, "video_player.html")
return render(request, "upgrade_membership.html")
This view grabs the Video object, checks that the user has an active Stripe subscription and that the user's plan is a plan that the roadmap gives access to. Both of these checks are important. You could imagine that if a user on the Basic Plan requests a video from a course in the Advanced Roadmap, it should detect that the user's plan is not part of the selected plans, hence rendering the template for the user to upgrade their membership to gain access.
Up-to-date Subscription Status
You might have noticed that we're storing the status of the Stripe subscription. This is so that when the user requests the video we can use the status of the Subscription
model instead of calling the Stripe API on each request.
To handle this correctly we need to make sure the subscription status is up-to-date. There are a few ways to handle this. A good method would be to call the Stripe API every time the user logs in. You can do this in an asynchronous worker so that you don't keep the user waiting for a second before logging in. Of course, this depends on how often the refresh token expires. If the token lasts one week then you might want to schedule a task to query the API every day.
Updating membership details
The Stripe API encourages using webhooks as a confirmation for when payments have been cleared. Webhooks are notifications for events that have occurred. You can configure Stripe to send webhooks for events such as successful payments and subscription cancellations. You can leverage these events to update the user's subscription when receiving an event. For example, when a user changes their membership plan from Basic to Pro. It's important to update that in our system so that the user can now access videos part of the Advanced Roadmap.
Conclusion
While this case study is a high overview of the system and its architecture, I hope it's provided some insight into how the models were designed, how the video membership logic works, and how a somewhat established project has been deployed.