API

프로젝트 세팅

$ mkdir instagram-api
$ cd instagram-api

$ pyenv virtualenv 3.5.2 instagram_api
$ pyenv local instagram_api
$ pip install django

$ django-admin startproject instagram_api
$ mv instagram_api django_app
$ cp ~/deploy_review/.gitignore .
$ pip freeze > requirements.txt
$ git init

$ py .
# Interpreter 설정
# Source Root 설정
# config로 이름 변경

.conf-secret/settings_common.json

{
  "django": {
    "secret_key": "..."
  }
}

.conf-secret/settings_local.json

{
  "django": {
    "allowed_hosts": [
      "*"
    ]
  }
}

settings.py

import json

# DEBUG = True
DEBUG = os.environ.get('MODE') == 'DEBUG'
ROOT_DIR = os.path.dirname(BASE_DIR)

CONF_DIR = os.path.join(ROOT_DIR, '.conf-secret')
CONFIG_FILE_COMMON = os.path.join(CONF_DIR, 'settings_common.json')
CONFIG_FILE = os.path.join(CONF_DIR, 'settings_local.json' if DEBUG else 'settings_deploy.json')

config_common = json.loads(open(CONFIG_FILE_COMMON).read())
config = json.loads(open(CONFIG_FILE).read())

for key, key_dict in config_common.items():
    if not config.get(key):
        config[key] = {}
    for inner_key, inner_key_dict in key_dict.items():
        config[key][inner_key] = inner_key_dict

TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates')

SECRET_KEY = config['django']['secret_key']
ALLOWED_HOSTS = config['django']['allowed_hosts']

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            TEMPLATES_DIR,
        ],
        'APP_DIRS': True,
        'OPTIONS': {
        	# ...
        },
    },
]

LANGUAGE_CODE = 'ko-kr'
TIME_ZONE = 'Asia/Seoul'
$ MODE='DEBUG' ./manage.py runserver

유저 모델 생성

$ MODE='DEBUG' ./manage.py startapp member

member/models.py

from django.contrib.auth.models import AbstractUser


class MyUser(AbstractUser):
    pass

settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'member'
]

AUTH_USER_MODEL = 'member.MyUser'
$ MODE='DEBUG' ./manage.py makemigrations
$ MODE='DEBUG' ./manage.py migrate

Post app 추가

1. 여러개 사진 올릴 수 있게 설게
	- class Post
	- class PostPhoto
$ MODE='DEBUG' ./manage.py startapp post

post/models.py

from django.conf import settings
from django.db import models


class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL)
    created_date = models.DateTimeField(auto_now_add=True)


class PostPhoto(models.Model):
    post = models.ForeignKey(Post)
    photo = models.ImageField(upload_to='post')

settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'member',
    'post',
]
$ pip install pillow
$ MODE='DEBUG' ./manage.py makemigrations
$ MODE='DEBUG' ./manage.py migrate

order_with_respect_to 적용

class PostPhoto(models.Model):
    post = models.ForeignKey(Post)
    photo = models.ImageField(upload_to='post')

    class Meta:
        order_with_respect_to = 'post'

postgresql로 변경

.conf-secret/settings_local.json

"db": {
  "engine": "django.db.backends.postgresql_psycopg2",
  "name": "instagram_api",
  "user": "hm",
  "password": "...",
  "host": "localhost",
  "port": "5432"
}

데이터 베이스 생성

$ createdb instagram_api owner=hm

settings.py

DATABASES = {
    'default': {
        'ENGINE': config['db']['engine'],
        'NAME': config['db']['name'],
        'USER': config['db']['user'],
        'PASSWORD': config['db']['password'],
        'HOST': config['db']['host'],
        'PORT': config['db']['port']
    }
}
$ pip install psycopg2
$ MODE='DEBUG' ./manage.py makemigrations
$ MODE='DEBUG' ./manage.py migrate

확인을 위해 admin 등록

post/admin.py

from django.contrib import admin

from post.models import Post, PostPhoto

admin.site.register(Post)
admin.site.register(PostPhoto)
$ MODE='DEBUG' ./manage.py createsuperuser
$ MODE='DEBUG' ./manage.py runserver

shell script 작성

#!/bin/bash

while getopts "d" opt
do
    case $opt in
        d) mode='DEBUG' ;;
    esac
done

manageCommand="MODE='$mode' ./manage.py ${@:OPTIND}"
echo $manageCommand
eval $manageCommand
$ chmod 755 ~/.scripts/manage
$ manage -d runserver

post_create API 작성

1. request.POST로 전달된 author_id를 받아 새 post를 생성
 - 이후 생성된 post의 id 값을 Httpresponse로 반환
2. 받은 author_id에 해당하는 MyUser 객체를 가져옴
 - 실패시 예외처리로 주어진 author_id에 해당하는 User는 없음을 리턴
3. url 연결

post/views.py

def post_create(request):
    """
    request.POST로 전달된 author_id를 받아 새 post를 생성
    이후 생성된 post의 id 값을 Httpresponse로 반환

    받은 author_id에 해당하는 MyUser 객체를 가져옴
    실패시 예외처리로 주어진 author_id에 해당하는 User는 없음을 리턴
    """
    if request.method == 'POST':
        try:
            author_id = request.POST['author_id']
            author = User.objects.get(id=author_id)

        except KeyError:
            return HttpResponse('key "author_id" is required field')
        except User.DoesNotExist:
            return HttpResponse('author_id {} is not exist'.format(
                request.POST['author_id']
            ))
        post = Post.objects.create(author=author)
        return HttpResponse('{}'.format(post.pk))

    else:
        return HttpResponse('Post create view')

urls.py

from post import urls as post_urls

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^post/', include(post_urls)),
]

post/urls.py

app_name = 'post'
urlpatterns = [
    url(r'^create/$', views.post_create, name='create'),
]

Postman

  • Collections 생성
    • Instagram API 이름으로 생성
    • 127.0.0.1:8000/post/post-create
    • Save
  • Send
    • Get 응답 확인 : Post create view
    • Post 응답 확인 : CSRF 검증에 실패했습니다. 요청을 중단하였습니다

API로 통신하면 csrf를 사용하기 어려우므로 다음과 같이 처리한다. (크게 신경쓰지 않아도 된다.)

@csrf_exempt
def post_create(request):

그런 후 다시 실행하면 key "author_id" is required field가 출력된다. body에 form-data author_id를 넣어다시 요청한다.

post_photo_add API 추가

- post_id, photo를 받아서
- PostPhoto 객체를 생성
- 이후 post_id 와 post에 연결된 photo 들의 id 값을 리턴

post/views.py

@csrf_exempt
def post_photo_add(request):
    if request.method == 'POST':
        try:
            post_id = request.POST['post_id']
            photo = request.FILES['photo']
            post = Post.objects.get(id=post_id)
        except KeyError:
            return HttpResponse('post_id and photo is required field')
        except Post.DoesNotExist:
            return HttpResponse('post_id {} is not exist'.format(
                request.POST['post_id']
            ))
        PostPhoto.objects.create(
            post=post,
            photo=photo
        )
        return HttpResponse('Post: {}, PhotoList: {}'.format(
            post.id,
            [photo.id for photo in post.photo_set.all()]
        ))
    else:
        return HttpResponse('Post photo add view')

post/urls.py

from django.conf.urls import url

from post import views

app_name = 'post'
urlpatterns = [
    url(r'^post/create/$', views.post_create, name='create'),
    url(r'^post/photo/add/$', views.post_photo_add, name='photo_add')
]

Media 파일 처리

settings.py

# Media path
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            TEMPLATES_DIR,
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                # MEDIA_URL 변수 사용
                'django.template.context_processors.media',
            ],
        },
    },
]

Json 응답으로 처리하기

JsonResponse를 이용해서 Post.objects.all()에 해당하는 객체 리스트를 리턴

post/views.py

def post_list(request):
    """
    JsonResponse를 이용해서 Post.objects.all()에 해당하는 객체 리스트를 리턴
    """
    # post_list = Post.objects.all()
    # 이 방법이 더 효율적임
    post_list = Post.objects.select_related('author')
    post_dict_list = []

    # 전체 Post를 loop
    for post in post_list:
        cur_post_dict = {
            'pk': post.pk,
            'photo_list': [],
            'created_date': post.created_date,
            'author': {
                'pk': post.author.pk,
                'username': post.author.username,
            }
        }
        photo_list = post.postphoto_set.all()
        for post_photo in photo_list:
            photo_dict = {
                'pk': post_photo.pk,
                'photo': post_photo.photo.url,
            }
            cur_post_dict['photo_list'].append(photo_dict)
        post_dict_list.append(cur_post_dict)

    context = {
        'post_list': post_dict_list
    }
    return JsonResponse(data=context)

모델과 뷰 분리

post/views.py

def post_list(request):
    context = {
        # 'post_list': post_dict_list
        'post_list': [post.to_dict() for post in Post.objects.select_related()]
    }
    return JsonResponse(data=context)

post/models.py

from django.conf import settings
from django.db import models


class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL)
    created_date = models.DateTimeField(auto_now_add=True)

    def to_dict(self):

        ret = {
            'pk': self.pk,
            'created_date': self.created_date,
            'author': self.author.to_dict(),
            'photo_list': [photo.to_dict() for photo in self.postphoto_set.all()]
        }
        return ret


class PostPhoto(models.Model):
    post = models.ForeignKey(Post)
    photo = models.ImageField(upload_to='post')

    class Meta:
        order_with_respect_to = 'post'

    def to_dict(self):
        ret = {
            'pk': self.pk,
            'photo': self.photo.url,
        }
        return ret

member/models.py

class MyUser(AbstractUser):
    def to_dict(self):
        ret = {
            'pk': self.pk,
            'username': self.username,
            'first_name': self.first_name,
            'last_name': self.last_name,
        }
        return ret

과제