Hyobeom Han

February 12, 1994

github github

Wooridong-rep

1. Design ERD

wooridong_erd
Based on client's request, I designed an ERD(Entity Relationship Diagram). It's a blueprint of DB that would be built later. It can never be perfect. There will always be modifications since change of business logic can occur anytime.

2. Setup docker-compose.yml

version: "3.9"

services:
  postgres:
    container_name: postgres_service
    image: postgres:14.5

    ports:
      - "5432:5432"
    volumes:
      - ./data/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_NAME=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_HOST_AUTH_METHOD=trust

  django:
    container_name: django_service
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/server
    ports:
      - "8000:8000"
    environment:
      - POSTGRES_NAME=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - GOOGLE_APPLICATION_CREDENTIALS="/server/wooridong-rep-firebase-adminsdk.json"
      - BASE_URL=192.168.0.84:8000
      - PROTOCOL=http

    depends_on:
      - "postgres"
      
Let the project begin. I prefer setting docker up from scratch. I don't want to create another venv that will be obsolete as soon as docker image is built. I always forget to remove it, so don't create in the first place.

3. Build DB by defining django models

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin



class UserManager(BaseUserManager):
    use_in_migrations = True

    def create_user(self, identification, phone_number='', chat_name='', password=None, name=None):
        user = self.model(
            identification=identification,
            phone_number=phone_number,
            chat_name=chat_name,
            name=name
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, identification, phone_number='', password=None):
        user = self.create_user(
            identification=identification,
            phone_number=phone_number,
            password=password
        )
        user.is_admin = True
        user.is_superuser = True
        user.is_staff = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser, PermissionsMixin):
    objects = UserManager()

    identification = models.CharField(max_length=30, null=False, unique=True)
    name = models.CharField(max_length=10, null=True)
    phone_number = models.CharField(max_length=11, null=False)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)
    fcm_token = models.CharField(max_length=300, null=True)

    chat_name = models.CharField(max_length=20, null=True, default='동대표')

    USERNAME_FIELD = 'identification'
    REQUIRED_FIELDS = ['phone_number']

    def __str__(self):
        return '{}. {}'.format(self.id, self.identification)

I prefer starting with User model. It's easier User model to be defined before any migrations. Without customization of User model, django automatically create default User model, and it's quite annoying to replace the default User with customized one.

To Customize User model, we can inherit AbstractBaseUser. We need Custom Manger as well to override default create_user method.

4. Create ModelSerializer

from rest_framework import serializers
from ..models import Villa
from .building_serializer import BuildingSerializer
from .address_serializer import AddressSerializer


class VillaSerializer(serializers.ModelSerializer):

    buildings = BuildingSerializer(many=True, read_only=True)
    address = AddressSerializer(read_only=True)

    class Meta:
        model = Villa
        fields = '__all__'
        depth = 2
After define all models, make serializer for each model accordingly. Serializer parse request data to return django model and vice versa. ModelSerializer is extremely useful since it takes only model in Meta subclass and takes care of all the chores that defining fields to sync with model fields. Inheriting ModelSerializer, all we need to do is set model to django model and set fields = '__all__'

5. Create Views

from rest_framework import viewsets

from ..models import Villa
from ..serializers import VillaSerializer, VillaCreateSerializer, VillaUpdateSerializer


class VillaViewSet(viewsets.ModelViewSet):

    def get_queryset(self):
        return Villa.objects.filter(rep=self.request.user)

    def get_serializer_class(self):
        if self.action == 'create':
            return VillaCreateSerializer
        if self.request.method == 'PATCH':
            return VillaUpdateSerializer
        return VillaSerializer

    def perform_create(self, serializer):
        villa = serializer.save(rep=self.request.user)
        return villa

Now, it's time to create views where most business logic takes palace in. all we need to do is defining queryset field or override get_queryset method, and defining serializer_class or override get_serializer_class method.
class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass
ModelViewSet is just a GenericViewSet which inherit bunch of mixins and GenericViewSet. It provides create(), retrieve(), update(), partial_update(), and list() methods as default. Thus, providing queryset and serializer_class, we can use all methods inherited from mixins right away. But, usually we need to customize some methods. Then we can just override methods to write our business logic accordingly.
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework import status
from rest_framework.response import Response

from ..models import User


@api_view(['POST'])
@permission_classes([AllowAny])
def register_view(request: Request):

    data = request.data
    User.objects.create_user(**data) 
    return Response(status=status.HTTP_200_OK)

If we need only one method, we can just use api_view as class or function view. Above, I created register_view as function view with @api_view decorator that only takes POST method.
from django.urls import path,include
from rest_framework.routers import DefaultRouter

from .viewsets import VillaViewSet
from .views import ListAddress, ListAddressDetail, ChangeVillaRep, update_bank

router = DefaultRouter()
router.register('',VillaViewSet, basename='villa')

urlpatterns = [
    path('change-rep/', ChangeVillaRep.as_view()),
    path('address/',ListAddress.as_view()),
    path('address/detail/',ListAddressDetail.as_view()),
    path('update-bank/', update_bank),
    path('', include(router.urls)),
]

After creating views, we can provide those views by registering views in router. Simply we can just include the router to urlpatterns list.

6. Build backend server and run

server {
    server_name api.wooridong-rep.net;
    
    location / {
      proxy_pass http://127.0.0.1:8000;
    }
 
     listen 443 ssl;
     ssl_certificate /etc/letsencrypt/live/api.wooridong-rep.net/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/api.wooridong-rep.net/privkey.pem;
     include /etc/letsencrypt/options-ssl-nginx.conf;
     ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    }
    
server {
   if ($host = api.wooridong-rep.net) {
    return 301 https://$host$request_uri;
   } 
   server_name api.wooridong-rep.net;
   listen 80;
   return 404; 
} 
On ec2 Ubuntu server, installed nginx and configured as above. It's just a simple logic that listen to server_name and reverse proxy to localhost 8000 port on which django app run.
Now, it's time to run our app on the server.
$ sudo docker compose up -d
Let's test our server.
wooridong_erd
Done. Works smoothly.