Mega Tutorial: How to build a Trello code in Django (Part 2)
Home Django Mega Tutorial: Build a Trello Clone - Part 2

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.

  1. AbstractTimestampedModel
    • created (datetime)
    • modified (datetime)
  2. Board(AbstractTimestampedModel)
    • name (text)
    • owner (User)
    • archived (boolean)
  3. Column(AbstractTimestampedModel)
    • title (text)
    • order (int)
    • board (Board)
  4. Card(AbstractTimestampedModel)
    • column (Column)
    • title (text)
    • description (text)
    • due_date (date)
    • labels (M2M Label)
    • order (int)
    • archived (boolean)
    • cover (image)
  5. Label(AbstractTimestampedModel)
    • board (Board)
    • color (color)
    • name (text)
  6. 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:

Django Admin - Cards, Invites, Labels
Django Admin - Cards, Invites, Labels

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:

Django Admin - Columns
Django Admin - Columns

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:

Django Admin - Boards
Django Admin - Boards