Mega Tutorial: Build a Trello Clone - Part 2
12 November, 2024
This is the 2nd part of the build a Trello Clone Tutorial. In this part we are going to focus on writing the foundational models of the application. We are going to see how to model Boards, Columns and Cards. We are also going to talk about how to attach timestamps to the models, how to use query managers and how to use abstract Models to make our code better.
Planning
If you have a Trello account, log in and try to figure out what are the main "entities". Some examples of "entities" or "models" are: Board and Card. Can you think of others?
Here are the main models I came up with. These might change during the development of the application. But they provide a good starting point.
-
AbstractTimestampedModel
- created (datetime)
- modified (datetime)
-
Board(AbstractTimestampedModel)
- name (text)
- owner (User)
- archived (boolean)
-
Column(AbstractTimestampedModel)
- title (text)
- order (int)
- board (Board)
-
Card(AbstractTimestampedModel)
- column (Column)
- title (text)
- description (text)
- due_date (date)
- labels (M2M Label)
- order (int)
- archived (boolean)
- cover (image)
-
Label(AbstractTimestampedModel)
- board (Board)
- color (color)
- name (text)
-
Invite(AbstractTimestampedModel)
- board (Board)
- user (User)
Create "boards" Application
Let's use the Django command to init the boards
application:
python manage.py startapp boards
Head over to settings.py
and add the boards
application to the INSTALLED_APPS
list:
1 2 3 4 5 6 7 8 9 10 | INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'accounts', 'boards', ] |
Writing the models
Head over to models.py
and let's put into code what we've discussed previously.
Here is the code for our application's data models:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | from django.db import models from django.contrib.auth import get_user_model User = get_user_model() class TimeStampedModel(models.Model): created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) class Meta: abstract = True class Board(TimeStampedModel): title = models.CharField(max_length=128) owner = models.ForeignKey(User, on_delete=models.CASCADE) order = models.IntegerField(null=True, blank=True) def __str__(self): return f"{self.owner} > {self.title}" def save(self, *args, **kwargs): if self.order is None: max_order = Board.objects.filter( owner=self.owner).aggregate( models.Max('order'))['order__max'] if max_order is None: max_order = 0 self.order = max_order super().save(*args, **kwargs) class Column(TimeStampedModel): board = models.ForeignKey(Board, on_delete=models.CASCADE) title = models.CharField(max_length=64) order = models.IntegerField(null=True, blank=True) def __str__(self): return f"{self.board.owner} > {self.board.title} > {self.title}" def save(self, *args, **kwargs): if self.order is None: max_order = Column.objects.filter( board=self.board).aggregate( models.Max('order'))['order__max'] if max_order is None: max_order = 0 self.order = max_order super().save(*args, **kwargs) class Label(TimeStampedModel): board = models.ForeignKey(Board, on_delete=models.CASCADE) color = models.CharField(max_length=10) title = models.CharField(max_length=32) def __str__(self): return f"{self.board.title} > {self.title}" class Card(TimeStampedModel): column = models.ForeignKey(Column, on_delete=models.CASCADE) title = models.CharField(max_length=128) description = models.TextField(blank=True) due_date = models.DateField(null=True, blank=True) labels = models.ManyToManyField(Label, related_name="cards", blank=True) order = models.IntegerField(null=True, blank=True) archived = models.BooleanField(default=False) cover = models.ImageField(null=True, blank=True) def __str__(self): return f"{self.column.board.owner} > {self.column.board.title} > {self.column.title} > {self.title}" def save(self, *args, **kwargs): if self.order is None: max_order = Card.objects.filter( column=self.column).aggregate( models.Max('order'))['order__max'] if max_order is None: max_order = 0 self.order = max_order super().save(*args, **kwargs) class Invite(TimeStampedModel): board = models.ForeignKey(Board, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self): return f"{self.board.owner} > {self.board.title} > {self.user}" |
Database Migrations
Django migrations are essentially version control for your database. They help you manage changes to your database schema, so it aligns with your Django models.
Migrations typically are automatically (but you can also write your onw migrations) generated Python files
stored in a migrations
folder within each Django app.
The main job of the migrations is to make sure the Django models you define are in sync with the schema of the database.
Let's create the migration for our new models:
python manage.py makemigrations
And now apply it:
python manage.py migrate
Integrating Models with Django Admin
We want to make it easy to edit everything from the Django Admin. If you are currently not super familiar with all the tricks the Django Admin can do, I did a roundup called Mastering the Django Admin .
Django Admin
Django Admin automatically creates CRUD (Create/Read/Update/Delete) interfaces for your models.
These interfaces can be customized to a great extent but are not the solution to every problem. The Django Admin can have its limitations.
Let's go to admin.py
in our boards
application and write the code for the admin console.
We will start with the simplest ones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | from django.contrib import admin from .models import Board, Label, Card, Column, Invite @admin.register(Invite) class InviteAdmin(admin.ModelAdmin): list_display = ('__str__', ) list_filter = ( 'created', ) list_per_page = 100 ordering = ("-created", ) @admin.register(Label) class LabelAdmin(admin.ModelAdmin): list_display = ('__str__', ) list_filter = ( 'created', ) list_per_page = 100 ordering = ("-created", ) @admin.register(Card) class CardAdmin(admin.ModelAdmin): list_display = ('__str__', ) list_filter = ( 'created', ) list_per_page = 100 ordering = ("-created", ) |
I considered these models to be the simplest because they have no foreign keys to other models. Here's how they look in our admin:
Let's now look at the Column
model.
Since each Column
can contain several Card
instances, let's add the Card
as a
TabularInline
model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class CardTabularInlineAdmin(admin.TabularInline): fields = ( 'title', 'due_date', 'order', 'archived', ) model = Card show_change_link = True extra = 1 @admin.register(Column) class ColumnAdmin(admin.ModelAdmin): list_display = ('__str__', ) list_filter = ( 'created', ) list_per_page = 100 ordering = ("-created", ) inlines = ( CardTabularInlineAdmin, ) |
Here's how the create Column
form looks like:
And finally, this is the admin code for the Board
model.
Note that we embedded all its foreign keys as TabularInlineAdmin
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class InviteTabularInlineAdmin(admin.TabularInline): fields = ( 'user', ) model = Invite show_change_link = True extra = 1 class ColumnTabularInlineAdmin(admin.TabularInline): fields = ( 'title', 'order', ) model = Column show_change_link = True extra = 1 class LabelTabularInlineAdmin(admin.TabularInline): fields = ( 'title', 'color', ) model = Label show_change_link = True extra = 1 @admin.register(Board) class BoardAdmin(admin.ModelAdmin): list_display = ('__str__', 'owner', 'created') list_filter = ( 'created', ) list_per_page = 100 ordering = ("-created", ) inlines = ( InviteTabularInlineAdmin, ColumnTabularInlineAdmin, LabelTabularInlineAdmin ) |
Here's how the form looks like: