Log for everything - Day

Django를 이용한 건물주 평판 조회 서비스 제작 (5) - Heroku 업로드

|

진행률

모든 기능구현을 끝내고 개발/운영환경을 설정 후 여러 보안 키들을 분리했다.
Django 설정 개발/운영환경 나누기, PostgreSQL 사용하기

그리고 Heroku에 릴리즈하기 위한 설정을 했다.
Django 프로젝트 Heroku에 릴리즈하기

Heroku 설정 (환경변수)

Django의 SECRET_KEY나 여러 API키들을 숨기기 위해 환경변수를 이용한 설정을 하는 경우,
Heroku에도 동일하게 환경변수를 이용해 설정을 해줘야 한다.

SECRET_KEY 설정

heroku config:set SECRET_KEY=asdasdsad = 양쪽에 공백이 없어야 한다.

DJANGO_SETTINGS_MODULE 설정

heroku config:set DJANGO_SETTINGS_MODULE=앱이름.settings.prod
이 항목을 설정해야 Heroku에 push 할때 여러개의 설정 파일 중, 운영환경의 설정 파일을 적용한다.
이 항목을 적용하지 않으면 ModuleNotFoundError: No module named 'settings' 에러가 발생한다.

아니면 Heroku 사이트의 대시보드에서 Settings 탭의 Config Variables를 통해 직접 설정할 수도 있다.

Heroku 설정 (Local Test)

heroku conifg를 통해 환경변수 설정값을 확인하고,
heroku local web을 통해 heroku 설정을 이용해 로컬 웹서버를 띄워본다.
이때 여전히 ModuleNotFoundError: No module named 'settings' 에러가 발생하는 경우가 있다.
이는 로컬 웹서버를 띄울 시 DJANGO_SETTINGS_MODULE 환경변수 값을 Heroku에 설정한 값이 아닌
로컬에 설정되어 있는 값을 가져오기 때문이다.

env | grep DJANGO_SETTINGS_MODULE을 통해 값을 확인하고 값이 Heroku에 설정한 값과 다를 경우
export DJANGO_SETTINGS_MODULE=앱이름.settings.prod를 입력하면 해결할 수 있다.

Heroku 설정 (Debug=False 및 whitenoise)

이 설정 때문에 무난히도 삽질을 했다.
전에 Django를 이용한 스케쥴러 제작 (2) - 구현 시에는 사실 나 혼자 쓸 것이기도 하고,
귀찮기도 해서 Debug도 켜놓고 키값조차 감추지 않은 개발환경 그대로 Heroku에 업로드를 했다.
그때만 해도 Django 프로젝트 Heroku에 릴리즈하기 대로만 하면 아무런 문제없이 Heroku에서 동작했다.

문제는 Debug를 False로 놓았을 때인데… Heroku 문서부터가 삽질을 조장한다.
whitenoise 문서를 보면 settings.py의 MIDDLEWARE 설정에 whitenoise.middleware.WhiteNoiseMiddleware',를 추가하라 되어있지만,
이런 내용이 Heroku에는 쏙 빠져있다.
덕분에 500 Internal server error를 일으키며 site가 동작하지 않는다.

나의 경우는 미들웨어 설정을 추가했는데도 whitenoise가 계속 문제를 일으키며 staticfile을 제대로 제공하지 않아,
결국 whitenoise 대신 기본 Django staticfiles로 해결했다.

whitenoise 대신 default Django staticfiles 이용

STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'로 교체
이후 python manage.py collectstatic --settings=앱이름.settings.prod를 실행하고
heroku local web을 실행하면 사이트가 정상 동작한다.
heroku에서는 Profile을 통해 collectstatic이 자동으로 실행된다.

마무리

이걸로 또 하나의 프로젝트가 마무리 되었다.
우리동네집주인 바로가기

Django 설정 개발/운영환경 나누기, PostgreSQL 사용하기

|

개발/운영환경 설정파일 분리

  1. Settings 폴더를 생성하여 __init__.py 파일을 넣어 모듈로 만든다.
  2. 기존 settings.pybase.py로 변경하여 setting에 공통으로 필요한 부분만 남긴다.
  3. base.py를 import한 prod.py를 추가하여 운영 환경에 관련된 세팅을 추가한다.
  4. base.py를 import한 dev.py를 추가하여 개발환경에 관련된 세팅을 추가한다.
from .base import *


# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

키값 분리

base.py에서 SECRET_KEY나 여러 API키 등 노출되면 안되는 부분을 분리해야 한다.
가상환경의 bin/activate 파일을 수정하여 마지막 부분에 다음 줄을 넣는다.


export SECRET_KEY=`secret key`

이후 SECRET_KEY = os.environ["SECRET_KEY"]를 세팅에 추가한다.
Runserver를 동작시켜보면 잘 돌아간다.

Pycharm에서의 문제점

위와 같이 설정할 경우 Pycharm에서 서버를 실행하면 환경변수를 받아오지 못한다.
이는 Pycharm과 같은 GUI 프로그램이 가상환경을 실행한 Shell을 상속받지 않기 때문인데,

def get_env_variable(var_name, default=False):
    try:
        return os.environ[var_name]
    except KeyError:
        import io
        import configparser
        env_file = os.environ.get('PROJECT_ENV_FILE', os.getcwd() + "/.env")
    try:
        config = io.StringIO()
        config.write("[DATA]\n")
        config.write(open(env_file).read())
        config.seek(0, os.SEEK_SET)
        cp = configparser.ConfigParser()
        cp.read_file(config)
        value = dict(cp.items('DATA'))[var_name.lower()]
        if value.startswith('"') and value.endswith('"'):
            value = value[1:-1]
        elif value.startswith("'") and value.endswith("'"):
            value = value[1:-1]
        os.environ.setdefault(var_name, value)
        return value
    except (KeyError, IOError):
        if default is not False:
            return default
        from django.core.exceptions import ImproperlyConfigured
        error_msg = "Either set the env variable '{var}' or place it in your " \
                    "{env_file} file as '{var} = VALUE'"
        raise ImproperlyConfigured(error_msg.format(var=var_name, env_file=env_file))


# Make this unique, and don't share it with anybody.
SECRET_KEY = get_env_variable('SECRET_KEY')

위와 같이 환경변수를 받아오는 메소드를 설정파일에 집어넣고,
프로젝트 루트에 .env파일을 생성해서

SECRET_KEY=`secret key`

과 같이 작성하면 해결된다.

위와 같은 경우에는 .gitignore에 .env 파일을 추가해서 키값이 노출되는 것을 막아줘야 한다.

설정파일 적용

python manage.py runserver --settings=앱이름.settings.dev
위와 같이 설정파일을 각각 적용 가능하다.

PostgreSQL 사용하기

Django는 기본 DB로 SQLite를 사용한다.
운영 환경에서는 PostgreSQL을 이용할 것이므로, 개발환경에서도 PostgreSQL을 적용하기로 했다.
ORM이 많은 부분을 해결해 주더라도 개발/운영환경이 일치되는 편이 문제 발생률을 크게 줄여준다.
또한 실환경의 Data를 덤프해서 개발환경에 집어넣고 테스트가 가능하기도 하다.

  1. psycopg2 설치
    pip3 install psycopg2

  2. postgresql 설치 및 db 생성
    createdb db이름

  3. dev.py에 설정
    DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.postgresql_psycopg2',
         'NAME': 'db이름',
         'USER': '',
         'PASSWORD': '',
         'HOST': '',
         'PORT': '',
     }
    }
    
  4. migrate 하기
    python manage.py migrate --settings=앱이름.settings.dev

  5. superuser 생성
    python manage.py createsuperuser --settings=앱이름.settings.dev

  6. 서버 실행
    python manage.py runserver --settings=앱이름.settings.dev

pgAdmin4

Data 보기

db이름 → Schemas → public → Tables → 테이블에서 우클릭 → View Data

기타

SQlite GUI 관리툴로는 DB Browser for SQLite를 쓰며 만족스러웠는데,
이놈의 pgAdmin4는 왜이렇게 마음에 안드는지 모르겠다.

Django에 Rest API 추가하기 (2) - API에 인증 추가

|

지난번에 추가한 API

지난번에는 Django REST Framework의 generics.RetrieveUpdateDestroyAPIView를 이용하여,
GET/PUT/PATHCH/DELETE에 반응하는 API를 제작했다.
Django에 Rest API 추가하기 - 링크

하지만 지금 상태로는 누구나 정보를 수정/삭제가 가능하므로 인증 기능을 추가해야 한다.

API에 인증 추가

로그인 하지 않으면 Read 권한만 주고 로그인한 사용자에게는 Write/Update 권한을 주려면 간단한 방법이 있다.

from rest_framework import permissions

class ReputationDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
    queryset = Reputation.objects.all()
    serializer_class = ReputationSerializer
    lookup_fields = ('longitude', 'latitude')
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)  # 추가

하지만 현재 서비스는 다중 사용자 환경이므로, 이와 같이 작성하면 계정을 가진 모두가 정보를 삭제할 수 있게 된다.
따라서 정보 작성자의 계정과 api 접속 인증 계정이 일치할 때만 정보를 삭제할 수 있도록 해야 한다.
이를 위해서 먼저 Custom permisson을 추가한다.

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:  # GET, HEAD, OPTIONS 
            return True

    return obj.owner_id == request.user.id

위와 같이 새로운 권한을 추가하고, SAFE_METHOD(객체의 상태를 변화시키지 않는 메소드)일 경우는 True를 반환,
그렇지 않은 경우에는 정보의 모델 필드의 owner_id가 요청하는 유저의 id와 일치할때만 True를 반환하게 했다.

from reputation.permissions import IsOwnerOrReadOnly

class ReputationDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
    queryset = Reputation.objects.all()
    serializer_class = ReputationSerializer
    lookup_fields = ('longitude', 'latitude')
    permission_classes = (IsOwnerOrReadOnly,)  # 변경

그 후 API를 반환하는 View의 퍼미션 클래스에 해당 클래스를 추가했다.
마지막으로 object를 반환하는 mixin 클래스에서,

class MultipleFieldLookupMixin(object):
    def get_object(self):
        queryset = self.get_queryset()             # Get the base queryset
        queryset = self.filter_queryset(queryset)  # Apply any filter backends
        filter = {}
        for field in self.lookup_fields:
            if self.kwargs[field]:  # Ignore empty fields.
                filter[field] = self.kwargs[field]
        obj = get_object_or_404(queryset, **filter)  # Lookup the object
        self.check_object_permissions(self.request, obj)  # Check permissions
        return obj

위와 같이 obj를 반환하기 전에 퍼미션을 확인하도록 변경하였다.

API 테스트 결과

Postman을 이용해서 api를 테스트 한 결과 (GET)

실행결과 GET 명령은 인증 없이도 잘 동작한다.

Postman을 이용해서 api를 테스트 한 결과 (DELETE)

실행결과 Delete 명령은 인증이 없으니 동작하지 않는다.

Postman을 이용해서 api를 테스트 한 결과 (DELETE & wrong Auth)

실행결과 Auth를 포함할 때 잘못된 아이디/패스워드를 사용했을때는 다음과 같은 결과가 돌아온다.

Postman을 이용해서 api를 테스트 한 결과 (DELETE & Auth (not owner))

실행결과 제대로된 Auth로 인증했으나 Owner가 아닌 경우에도 역시 동작하지 않는다.

Postman을 이용해서 api를 테스트 한 결과 (DELETE & Auth (Owner))

실행결과 Owner인 경우에는 정상적으로 동작하여 해당 정보가 삭제되었다.

Django를 이용한 포트폴리오 사이트 제작 (2) - 사이트 형태 잡기, User Model 확장

|

공동 프로젝트 진행상황

1주차 : JQuery를 이용한 간단한 애니메이션
2주차 : Phaser Library를 이용한 마우스 이벤트에 반응하는 인터랙션
3주차 : 개인 포트폴리오 사이트 공동 기획

4주차 진행상황

이번주는 토요일에 작업을 진행하기 힘들것 같아 일정을 미리 앞당겨 진행했다.
먼저 지난주 목표였던 요구사항에 대해 작업을 진행했다.

지난주 요구사항

최신 작업중인 작품을 보여주는 페이지

TemplateView를 상속한 HomeView를 추가해서 최신 작품의 커버 이미지를 보여주는 페이지를 제작했다.

간단한 프로필 페이지 (Contact, Profile description, SNS 링크 등)

TemplateView를 상속해 마찬가지로 정적 페이지로 제작해 집어넣었다가 마지막에 약간 수정했다.
Django의 기본 User Model을 확장하여 SNS ID나 Biography 등을 넣을 수 있게끔 하고,
이 내용이 프로필 페이지에 반영되도록 했다.

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500)
    twitter_id = models.CharField(max_length=30, blank=True)
    instagram_id = models.CharField(max_length=30, blank=True)
    tumblr_id = models.CharField(max_length=30, blank=True)
    facebook_id = models.CharField(max_length=30, blank=True)

별도의 User model 자체에 대한 변경은 필요없고 몇개 추가적인 필드가 필요한 뿐이라,
위와 같이 OneToOneField를 이용해 유저 모델을 확장했다.

기존 작품 목록

  • 목록을 선택하면 ISSUU와 같은 책 형식으로 작품을 보여줄 수 있어야 함.
  • 아날로그적인 느낌으로 책장을 넘기는 애니메이션이 있으면 좋겠음

목록은 단순히 그리드 형태로 배치하고, 선택했을때 Modal popup을 이용해 상세정보를 띄워주도록 했다.
책장을 넘기는 애니메이션이 있는 Viewer는 JQuery Library 중 page transition 관련 하나를 골라 제작했다.

사이드 프로젝트 목록

  • 주 작업 외에 기타 작품들을 전시할 공간

아직 어떠한 작품들을 어떠한 형식으로 보여주고 싶은지 확정되지 않아 Navbar와 링크만 제작해 놓았다.

다음주 목표

  1. 작품 업로드 구현하기
    • 여러장의 이미지로 이루어진 파일을 손쉽게 올릴수 있는 방법 생각
    • 여러장의 이미지에는 Cover와 table contents가 포함되어 있음
  2. 관리자 기능 추가 및 디자인 업데이트

Django에 Rest API 추가하기

|

API 구성

기존에 만든 Django를 이용한 건물주 평판 조회 서비스에 Rest API를 추가하려 한다.
API endpoint로 /api/위도+경도의 URL을 날리면 해당 좌표에 맞는 정보가 날아오는 형태이다.

Django REST Framework 설치

pip3 install djangorestframework
Rest API를 생성할때 가장 보편적으로 쓰는 라이브러리인 Django REST Framework를 설치한다.

Serializer 클래스 정의

별도의 serializer.py 파일을 정의하였다.
모델 전체를 Serialize 할 수도 있지만, ModelSerializer를 상속받으면 더욱 간단하다.

from reputation.models import Reputation
from rest_framework import serializers


class ReputationSerializer(serializers.ModelSerializer):
class Meta:
model = Reputation
fields = ('address', 'latitude', 'longitude', 'description')

Django Shell에서 작성한 Serializer를 확인해 보았다.

>>> from reputation.serializer import ReputationSerializer
>>> ser = ReputationSerializer()
>>> print(repr(ser))
ReputationSerializer():
address = CharField(max_length=50)
latitude = DecimalField(decimal_places=7, max_digits=9)
longitude = DecimalField(decimal_places=7, max_digits=10)
description = CharField(style={'base_template': 'textarea.html'})

API URL 생성

# Endpoint URL: /api/126.575834+33.427337/
url(r'^api/(?P<longitude>\d{1,3}\.\d{1,7})\+(?P<latitude>\d{1,2}\.\d{1,7})/$',
ReputationDetail.as_view(), name='reputation_rest_api'),

위도, 경도를 매개변수로 넘겨주도록 URL을 설정했다.

API를 제공하는 View 생성

하나의 모델에 대해 read-write-delete 엔드포인트를 제공하는 RetrieveUpdateDestroyAPIView를 상속받았다.

from reputation.serializer import ReputationSerializer
from rest_framework import generics

class ReputationDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Reputation.objects.all()
    serializer_class = ReputationSerializer

DB의 리스트를 전부 출력하기 위해서는 위와 같이 generics를 적절히 상속받는 것으로 충분하나
넘겨받은 위/경도를 이용해서 DB를 검색하기 위해 조금 추가 작업이 필요하다.

여러개의 필드를 검색할 수 있는 Mixin class를 생성하여 ReputationDetail이 상속받도록 한다.

class MultipleFieldLookupMixin(object):
    def get_object(self):
        queryset = self.get_queryset()             # Get the base queryset
        queryset = self.filter_queryset(queryset)  # Apply any filter backends
        filter = {}
        for field in self.lookup_fields:
            if self.kwargs[field]: # Ignore empty fields.
                filter[field] = self.kwargs[field]

        return get_object_or_404(queryset, **filter)  # Lookup the object

이후 ReputationDetail이 MultipleFieldLookupMixin을 상속하게 하고,
lookup_fields 항목에 필터로 지정할 항목을 정해주면 된다.

class ReputationDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
    queryset = Reputation.objects.all()
    serializer_class = ReputationSerializer
    lookup_fields = ('longitude', 'latitude')

API 테스트 결과

Postman을 이용해서 api를 테스트 한 결과 실행결과

다음편 링크

Django에 Rest API 추가하기 (2) - API에 인증 추가