본문 바로가기
개발/AWS

Django 앱 만들기 - 모델 테스트 데이터를 자동으로 만들기

by ny0011 2021. 2. 2.
반응형

Django 모델을 위한 테스트 데이터를 자동으로 만들어보자!

 

이걸 하기 전에 custom django-admin command를 만들어보자

docs.djangoproject.com/en/3.1/howto/custom-management-commands/

 

아무 App 폴더에 management 폴더를 만들고 아래처럼 폴더를 구성해준다.

commands 폴더에 내가 명령어로 실행할 파일을 만들고(roomseed.py)

python manage.py를 실행하면... Command가 없다고 나옴

rooms/management/
├── __init__.py
└── commands
    ├── __init__.py
    └── roomseed.py

python manage.py roomseed --times 50

AttributeError: module 'rooms.management.commands.roomseed' has no attribute 'Command'

roomseed.py에 아무것도 안해줬기 때문이다

class Command를 만들어주고 django에서 지원해주는 클래스 중 BaseCommand를 상속 받는다

from django.core.management.base import BaseCommand


class Command(BaseCommand):
    print("hello")

BaseCommand의 정의를 살펴보면 다음과 같다

1. manage.py가 command class를 가져와서 run_from_argv() 메소드를 부른다

2. run_from_argv() 메소드는 create_parser() 메소드를 불러서 argument를 해석함

3. execute() 메소드는 해석한 argument와 함께 handle()을 부른다

그래서 handle() 메소드는 subclass의 시작 포인트로 쓰임

=> handle()에 어떤 명령을 내릴 지 적으면 됨.

class BaseCommand:
    """
    The base class from which all management commands ultimately
    derive.

    Use this class if you want access to all of the mechanisms which
    parse the command-line arguments and work out what code to call in
    response; if you don't need to change any of that behavior,
    consider using one of the subclasses defined in this file.

    If you are interested in overriding/customizing various aspects of
    the command-parsing and -execution behavior, the normal flow works
    as follows:

    1. ``django-admin`` or ``manage.py`` loads the command class
       and calls its ``run_from_argv()`` method.

    2. The ``run_from_argv()`` method calls ``create_parser()`` to get
       an ``ArgumentParser`` for the arguments, parses them, performs
       any environment changes requested by options like
       ``pythonpath``, and then calls the ``execute()`` method,
       passing the parsed arguments.

    3. The ``execute()`` method attempts to carry out the command by
       calling the ``handle()`` method with the parsed arguments; any
       output produced by ``handle()`` will be printed to standard
       output and, if the command is intended to produce a block of
       SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``.

    4. If ``handle()`` or ``execute()`` raised any exception (e.g.
       ``CommandError``), ``run_from_argv()`` will  instead print an error
       message to ``stderr``.

    Thus, the ``handle()`` method is typically the starting point for
    subclasses; many built-in commands and command types either place
    all of their logic in ``handle()``, or perform some additional
    parsing work in ``handle()`` and then delegate from it to more
    specialized methods as needed.

    """
  
    def handle(self, *args, **options):
        """
        The actual logic of the command. Subclasses must implement
        this method.
        """
        raise NotImplementedError('subclasses of BaseCommand must provide a handle() method')

옵션을 추가하고 싶으면 add_arguments() 함수를 오버라이드 해서 

parser.add_argument()에 추가하면 됨.

 

필수 인자는 "--"없이 이름 설정해주고 옵션 인자는 이름 앞에 "--"를 붙여준다!

nargs="+" 를 추가하면 여러 개의 인자를 받을 수 있다

class Command(BaseCommand):
    print("hello")
    help = "This is help description"

    def add_arguments(self, parser):
        parser.add_argument(
            "args", nargs="+", type=str, help="This is argument")
        parser.add_argument(
            "--times", help="How many times do you want to tell?")

    def handle(self, *args, **options):
        times = options.get('times')
        print(args)
        for t in range(int(times)):
            self.stdout.write(self.style.SUCCESS("ee"))

 

 이제 amenities Model의 테스트 데이터를 자동으로 만들어보자

-> Amenity models를 참고했을 때 데이터를 만드려면 name field만 있으면 된다

class Command(BaseCommand):
    def handle(self, *args, **options):
         amenities = [
            "Air conditioning",
            "Alarm Clock",
            "Balcony",
            "Bathroom",
            "Bathtub",
            "Free Parking",
            "Free Wireless Internet",
            "Freezer",
            "Golf",
            "Hair Dryer",
            "Heating",
            "Shopping Mall",
            "Shower",
            "Toilet",
            "Towels",
            "TV",
        ]
        for a in amenities:
            Amenity.objects.create(name=a)
        self.stdout.write(self.style.SUCCESS("Amenities created!"))

Amenity.objects로 model의 Manager object를 불러와서 model이 갖고있는 메소드를 실행할 수 있당

docs.djangoproject.com/en/3.1/ref/models/instances/

>>> Amenity.objects
<django.db.models.manager.Manager object at 0x7f856a693370>

 

❗ room에 필요한 amenity와 facility를 만들었으니 user도 seed를 만들어보자~

user 테스트 데이터를 만들 땐 내용이 많으니 django-seed를 사용한당

github.com/Brobin/django-seed

 

Brobin/django-seed

:seedling: Seed your Django database with fake data - Brobin/django-seed

github.com

pipenv install django_seed

seeder를 만들어서 add_entity에 테스트 데이터를 만들 모델, 생성 개수를 넣어준다.

seed에서 모든 걸 해줄 것 같지만 foreignkey로 연결된 변수는 자동으로 넣어주지 못한다!

dict를 만들어서 값을 일일이 지정해 줘야함

import random
from django.core.management.base import BaseCommand
from django_seed import Seed
from rooms import models as room_models
from users import models as user_models


class Command(BaseCommand):

    help = "This command creates many rooms"

    def add_arguments(self, parser):
        parser.add_argument(
            "--number", default=1, type=int, help="How many rooms do you want to create?")

    def handle(self, *args, **options):
        number = options.get("number")
        seeder = Seed.seeder()
        all_users = user_models.User.objects.all()  # NOT Good
        room_types = room_models.RoomType.objects.all()

        seeder.add_entity(room_models.Room, number, {
            "host": lambda x: random.choice(all_users),
            "room_type": lambda x: random.choice(room_types),
            "price": lambda x:  random.randint(1, 300),
        })
        seeder.execute()
        self.stdout.write(self.style.SUCCESS(f"{number} rooms created!"))

 

room을 자동으로 만들 때 photo도 같이 만들 수 있게 해보자

seeder.execute()는 만들고 난 뒤에 class 생성자와 id를 dict로 리턴한다

{<class 'rooms.models.Room'>: [9]}

id만 필요하니까 .values()로 값만 가져오면 되는데 이중 list임

-> Django 내장 함수 중 flatten을 사용해서 단일 list로 만들어준다

 

Photo를 만들어야 하니까 Photo.objects.create()를 실행하는데,

Photo Models에 정의된 필드값 중 file 이름을 랜덤으로 정하면 끝!

room_models.Photo.objects.create(
                    caption=seeder.faker.sentence(),
                    file=f"room_photos/{random.randint(1,31)}.webp",
                    room=room
                )

room마다 photo 만드는 것도 랜덤으로 돌려버렷!

import random
from django.contrib.admin.utils import flatten
from django_seed import Seed
from rooms import models as room_models
from users import models as user_models


class Command(BaseCommand):

    def handle(self, *args, **options):
        number = options.get("number")
        seeder = Seed.seeder()
        # --- #
        created_rooms = seeder.execute()
        created_clean = flatten(list(created_rooms.values()))
        
        for pk in created_clean:
            room = room_models.Room.objects.get(pk=pk)
            for i in range(3, random.randint(5, 9)):
                room_models.Photo.objects.create(
                    caption=seeder.faker.sentence(),
                    file=f"room_photos/{random.randint(1,31)}.webp",
                    room=room
                )
            print(room)
        self.stdout.write(self.style.SUCCESS(f"{number} rooms created!"))

 

room에 ManyToManyField 관계에 있는 것을 추가하려면?

-> room.<ManyToManyField name>.add() 를 사용하자

	rules = room_models.HouseRule.objects.all()
	for r in rules:
	    magic_number = random.randint(0, 15)
	    if magic_number % 2 == 0:
        	room.house_rules.add(r)

to_add가 QuerySet의 묶음이라 QuerySet 안에 있는 rooms[x] ~ rooms[y]를 넣고 싶은거니까

*로 요소에 접근해서 add()해줌

        for pk in clean:
            list_model = List.objects.get(pk=pk)
            to_add = rooms[random.randint(0, 5):random.randint(6, 30)]
            list_model.rooms.add(*to_add)

날짜를 랜덤으로 만들고 싶을 때 timedelta를 사용하면 됨

from datetime import datetime, timedelta
{
"check_in": lambda x: datetime.now() - timedelta(days=random.randint(0, 3)),
}

댓글