[Django] settings 파일 & 비밀 변수 분리

Two scoops of django 책의 5장, ‘settings와 requirements 파일’을 보고 개인 프로젝트에 두 가지를 리팩토링해야겠다는 생각이 들었다.

  1. 비밀 값(e.g. OAuth 토큰) 코드에서 분리
  2. 환경별로 settings 파일 분리

제일 급한 OAuth 토큰부터 분리해보겠다.
책에선 환경변수를 이용하는 것을 추천했지만, 나는 차선책인 ‘json’파일 만드는 방법을 선택했다.
아직 shell에 익숙하지 않은 것도 있고, 다른 개발자와 json으로 공유하는게 한 눈에 보기 편할 것 같아서다.

비밀 값 코드에서 분리시키기

secret.json이란 비밀 값들을 저장하는 파일을 만든다. 나는 장고 SECRET_KEY와 facebook OAuth 키 값들을 넣었다. 이 파일은 코드 저장소에 저장되면 안되게에, .gitignore에 추가해준다.

{
  "SECRET_KEY": "m-4#rp68ffwi어쩌구",
  "SOCIAL_AUTH_FACEBOOK_KEY": "12312345678",
  "SOCIAL_AUTH_FACEBOOK_SECRET": "asdf123asdf123"
}

이는 settings파일에서 다음과 같이 불러올 수 있다.

import json

from django.core.exceptions import ImproperlyConfigured

with open("secrets.json") as f:
    secrets = json.loads(f.read())


# Keep secret keys in secrets.json
def get_secret(setting, secrets=secrets):
    try:
        return secrets[setting]
    except KeyError:
        error_msg = "Set the {0} environment variable".format(setting)
        raise ImproperlyConfigured(error_msg)

SECRET_KEY = get_secret("SECRET_KEY")

환경별로 settings 파일 분리

이것도 어렵지 않다.
프로젝트 폴더 내의 메인 앱 내부(기존에 settings.py가 있던 곳)에 /settings 폴더를 만들어준다.
나는 기본(base), 로컬(local), 운영 서버(production) 세 개로 분리했다. __init__.py도 추가해주어야 한다(settings의 app에 추가할 필요는 없다).

project_name
    settings
        __init__.py
        base.py
        local.py
        production.py

base.py에는 기존에 settings.py에 있던 것들을 옮겨주면 된다.
local.py에선 base를 상속받아준 다음, 개발 환경에서 원하는 세팅들을 추가해주면 된다.

from .base import *

DEBUG = True

실행은 다음과 같이 하면 되는데,

# settings/local.py세팅 파일로 셸 시작 
python manage.py shell --settings=twoscoops.settings.local

# settings/local.py세팅 파일로 서버 구동
python manage.py runserver --settings=twoscoops.settings.local

그럼 ImproperlyConfigured: The SECRET_KEY setting must not be empty 에러가 날 것이다. 찾아보니 이렇게 바꾼 후에는 project_name/wsgi.pymanage.py에 기본 구동 세팅을 설정해줘야 한단다.참고 링크

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings.local")

이렇게 바꾸면, 한번 migrate를 해주어야 한다. (기존 db가 날아간다 ㅠㅠ)

./manage.py migrate

Pycharm에서 settings별로 서버 실행 변수 바꾸기

1
일단 Run > Edit configurations에 좌측 +버튼으로 장고 서버를 추가해준다.
원하는 이름을 지정하고,
Environment variables 옆에 ...을 눌러 DJANGO_SETTINGS_MODULE이란 key로 원하는 세팅 파일 경로를 잡아준다. 나같은 경우엔 cart.settings.local을 추가해주었다.

virtualenv로 의존성 관리를 하고 있다면, 하단의 Python interpreter도 알맞은 것으로 바꿔주어야 한다. 원하는 것이 없으면 Pycharm > Preferences > Project Interpreter에서 추가해준다. 참고

Refer

[Django] Admin Customizing하기

Admin Form 커스터마이즈하기

class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text'] # 필드 순서 조정

admin.site.register(Question, QuestionAdmin) # 두 번째 인자로 위에 만든 model admin class를 넘긴다.
class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('날짜 정보', {'fields': ['pub_date']}),
    ]

admin.site.register(Question, QuestionAdmin)

이렇게 하면
– 헤더 없이: question_text 필드 보여짐
– ‘날짜 정보’헤더: pub_date 필드 보여짐

처럼 admin을 커스터마이즈 할 수 있다.

Related objects 추가하기

Question마다 related model인 Choices들이 있는데, admin page에선 안 보이니까 추가해준다.

from .models import Choice, Question


class ChoiceInline(admin.StackedInline): # admin.TabularInline로 하면 더 컴팩트하게 보여준다
    model = Choice
    extra = 3 # 3세트의 Choice 필드를 보여준다


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('날짜 정보', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline] # Choice오브젝트가 Question 어드민 페이지에서 edit될것이다

admin.site.register(Question, QuestionAdmin)

Admin List 커스터마이즈하기

기본적으로, 장고는 각 오브젝트의 str()를 보여준다.

class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ('question_text', 'pub_date') # 리스트에 이 필드들 추가
    list_filter = ['pub_date'] # 리스트에 필터 추가
    search_fields = ['question_text'] # 검색 필터 추가

Refer

https://docs.djangoproject.com/en/1.9/intro/tutorial07/

[Django]서버사이드 이미지 크롭/리사이징 하기

프론트엔드에서 Javascript로 온갖 삽질을 하다가, 그냥 서버사이드에서 처리하기로 했다.
훨씬 깔끔하게 끝났다(물론 이것도 삽질했지만 ^^).
HTML Canvas로 한 프론트 리사이징은 이미지 퀄리티도 안좋게 떨어진다. 웬만하면 파일 처리는 서버사이드에서 하는 것을 추천한다.

상황

내 모임 정보를 업로드 할 때, 썸네일을 올리면 자동으로 정사각형으로 크롭, 리사이징 시켜서 서버에 업로드 하고싶다.
Django Form에서 save()함수를 오버라이드 해서 전처리하는 방식으로 해결하자.

Forms.py

class MeetupEditForm(forms.ModelForm):
    desc = forms.CharField(widget=SummernoteWidget())

    class Meta:
        model = Meetup
        exclude = ('created_date', 'modified_date', )
        fields = ('title', 'desc', 'image_file', 'location', 'meetup_date', 'lat', 'lon', 'tags', )

    # (선택) 아래의 save함수에서 self.request.user를 쓰기 위해 views.py에서 전달해주었다.
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request')
        super(MeetupEditForm, self).__init__(*args, **kwargs)

    # form이 save될때 불리는 함수. 오버라이드해서 원하는 기능을 넣는다.
    def save(self, commit=True):
        instance = super(MeetupEditForm, self).save(commit=False)
        instance.author = self.request.user
        instance.published_date = timezone.now()

        # 이미지파일이 있으면, 리사이즈/크롭 한다.
        if instance.image_file:
            instance.image_file = self.rescale(self.cleaned_data.get('image_file'), 600, 600, force=True)
        if commit:
            instance.save()
        return instance

form save를 할 때, commit=False로 디비에 쓰는걸 잠시 막아두고, 전처리를 한다.
form에서 받은 이미지 파일과 원하는 width, height를 넘기면 리사이즈된 이미지파일이 넘어오는 함수를 짠다.

def rescale(self, data, width, height, force=True):
        from io import BytesIO
        from PIL import Image as pil

        max_width = width
        max_height = height

        input_file = BytesIO(data.read())
        img = pil.open(input_file)
        if not force:
            img.thumbnail((max_width, max_height), pil.ANTIALIAS)
        else:
            src_width, src_height = img.size
            src_ratio = float(src_width) / float(src_height)
            dst_width, dst_height = max_width, max_height
            dst_ratio = float(dst_width) / float(dst_height)

            if dst_ratio < src_ratio:
                crop_height = src_height
                crop_width = crop_height * dst_ratio
                x_offset = int(src_width - crop_width) // 2
                y_offset = 0
            else:
                crop_width = src_width
                crop_height = crop_width / dst_ratio
                x_offset = 0
                y_offset = int(src_height - crop_height) // 3
            img = img.crop((x_offset, y_offset, x_offset+int(crop_width), y_offset+int(crop_height)))
            img = img.resize((dst_width, dst_height), pil.ANTIALIAS)

        image_file = BytesIO()
        img.save(image_file, 'JPEG')
        data.file = image_file
        return data

MeetupEditForm클래스에 넣어준다.

https://djangosnippets.org/snippets/224/ 를 python3버전으로 바꿨다.
StringIOBytesIO로 변경
– offset계산시 나눌 때 /말고 //로 변경
– return하는 데이터 변경

force옵션을 켜면 지정한 width, height비율로 크롭되고, 끄면 max width, max height로만 적용된다.

views.py

class MeetupFormView(FormView):
    template_name = "meetup_edit.html"
    form_class = MeetupEditForm

    def get_success_url(self):
        return reverse('meetup_list')

    # form.py에 kwargs 넘기기 위함
    def get_form_kwargs(self):
        kw = super(MeetupFormView, self).get_form_kwargs()
        kw['request'] = self.request
        return kw

    def form_valid(self, form):
        form.save(self.request)
        return super(MeetupFormView, self).form_valid(form)

view에서 save가 호출되면, form에서 우리가 지정한 함수들이 실행된다.

Refer

https://djangosnippets.org/snippets/224/

[Django] Migration Conflict 해결하기

상황

근 한 달 이상 Review브랜치에서 리뷰 개발을 하고 있다.
너무 옛날 코드라 Master의 최신 코드와 Rebase했다.
바뀐 쿼리들을 적용하려고 ./manage.py migrate를 치니

./manage.py migrate
CommandError: Conflicting migrations detected (0035_merge, 0033_add-ondemand in product).
To fix them run 'python manage.py makemigrations --merge'

migrations가 충돌났다는 에러가 떴다.

여기서 python manage.py makemigrations --merge를 하면 해결 될테지만, 그러면 또 Master브랜치에서 다시 작업할 때 끊임없이 충돌이 날 수도 있다.

Master브랜치의 migrations들은 깨끗이 두고 해결하는 방법을 찾아보자.

에러1 – Conflicting migrations detected!

1

Master, Review 브랜치 두 군데에서 Point모델의 수정이 일어나, 같은 넘버로 다른 수정이 일어나 넘버링이 겹치는 문제가 생겼다.

  1. Review브랜치에서 0030_review.py부터 마지막까지 migrations 파일들을 삭제
  2. review앱의 migrations폴더에 있는 파일들도 모두 삭제
  3. Master브랜치에서 최신 상태를 Pull 땡겨온다
  4. 최신 코드로 ./manage.py makemigrations를 다시 돌려준다.

그러면 Master의 migrations 파일들 밑에 내가 새로 짠 디비 파일이 깔끔하게 붙게 된다.

에러2 – Table already exists!

그런데 만든 ORM을 디비에 migration하려고, ./manage.py migrate을 돌리니

django.db.utils.OperationalError: table "point_point" already exists

에러가 뜨는거였다.

장고 database에서 django_migrations테이블을 까보니, point앱에서 이미 0002_auto_어쩌구저쩌구로 이미 migration을 진행했던 것이다.

현재 point는 migrations파일을 다 지우고 0001_initial.py로 통합시켜놔서, 장고가 이미 0002까지 migrate를 진행한 것으로 판단해 테이블을 만들지 않은 것이다!
(영원히 고통받는다)

./manage.py dbshell #디비 쉘에 접속한다
> .tables # 테이블을 본다 -> point테이블이 있다!
> drop table point_point # 괴롭히던 테이블을 지운다

마지막으로 migrate을 한다.

./manage.py migrate

성공! 기쁘다.

[Django Models 뜯어보기 #2] Making Queries

앞으로 예시들에 사용할 모델.
글 하나에 블로그 여러 개 중 하나가 연결되어있고, author는 m2m.

class Blog(models.Model): # 블로그
name = models.CharField(max_length=100)
tagline = models.TextField()

def __str__(self):
return self.name

class Author(models.Model): # 작가
name = models.CharField(max_length=50)
email = models.EmailField()

def __str__(self):
return self.name

class Entry(models.Model): # 글
blog = models.ForeignKey(Blog)
headline = models.CharField(max_length=255)
body_text = models.TextField()
pub_date = models.DateField()
mod_date = models.DateField()
authors = models.ManyToManyField(Author)
n_comments = models.IntegerField()
n_pingbacks = models.IntegerField()
rating = models.IntegerField()

def __str__(self):
return self.headline

오브젝트 만들기

b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.') # 모델 인스턴스화
b.save() # 디비에 저장. SQL의 INSERT를 호출한다. 이거 하기 전까지는 디비를 건들지 않는다.

create()는 create하고 save까지 한다.

오브젝트 바뀐거 저장

>>> b5.name = 'New name'
>>> b5.save()

save()하면 UPDATE SQL문을 날린다.

foreignkey 필드 저장

entry = Entry.objects.get(pk=1)
yoolmoo_blog = Blog.objects.get(name='유르무르 블로그')
entry.blog = yoolmoo_blog
entry.save

ManyToManyField 저장

jay = Author.objects.create(name='Jay')
entry.authors.add(jay)

josh = Author.objects.create(name='Josh')
yoolmoo = Author.objects.create(name='Yoolmoo')
entry.authors.add(josh, yoolmoo)

add로 추가한다.

오브젝트 가져오기 (Retrieving objects)

오브젝트를 retrieve하면, Manager를 통해 Queryset을 만든다. Filters는 쿼리를 좁힌다. 모든 모델들은 하나 이상의 Manager를 가진다. Manager는 기본으로 objects라는 이름.
Queryset은 디비에서 나온 오브젝트들의 컬렉션이다.
QuerySet == SELECT
filter == WHERE, LIMIT

>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
...
AttributeError: "Manager isn't accessible via Blog instances."

Managers는 모델 클래스에서만 접근 가능한다.

모든 오브젝트 가져오기

all_entries = Entry.objects.all()

Manager의 all()메서드를 쓴다. 디비 모든 오브젝트의 쿼리셋을 반환한다.

Filter로 오브젝트 걸러서 가져오기

Entry.objects.filter(pub_date__year=2006) # all() 안써도 된다.
Entry.objects.all().filter(pub_date__year=2006)

>>> Entry.objects.filter(
... headline__startswith='What'
... ).exclude(
... pub_date__gte=datetime.date.today()
... ).filter(
... pub_date__gte=datetime(2005, 1, 30)
... )

Queryset이 evaluated되기 전까진 장고는 쿼리를 돌리지 않는다.

get()으로 싱글 오브젝트 가져오기

filter()는 싱글 오브젝트를 반환하더라도 쿼리셋으로 반환한다.
하나 오브젝트를 가져오려면 get()을써라.

one_entry = Engry.objects.get(pk=1)

get 안에 filter처럼 쓰면 된다.
쿼리 반환값이 없을 때 get()은 DoesNotExist exception을 뱉는다. 반환값이 1 이상일때는 MultipleObjectsReturned.

주석(annotate)

Queryset의 각 오브젝트마다 annotate하기.

>>> from django.db.models import Count
>>> q = Blog.objects.annotate(Count('entry'))
# 첫 번째 블로그 이름
>>> q[0].name
'율무네 블로그'
# 첫 번째 블로그의 엔트리 숫자
>>> q[0].entry__count
10

>>> q = Blog.objects.annotate(number_of_entries=Count('entry'))
# 첫 번째 블로그의 엔트리 숫자. 지정한 이름으로 가져오기
>>> q[0].number_of_entries
42

쿼리셋 제한하기

파이썬 Array-slicing 문법을 사용해서 쿼리셋의 서브셋 가져오기.
SQL의 LIMIT, OFFSET과 비슷하다.

Entry.objects.all()[:5] # 앞에서부터 5개 오브젝트
Entry.objects.all()[5:10] # 6번부터 10번까지
Entry.objects.all()[-1] # 에러난다
Entry.objects.all()[:10:2] # 10번째까지 모든 오브젝트들의 2번째 오브젝트
Entry.objects.order_by('headline')[0]

Field lookups

field이름__lookuptype=value이렇게 쓰면 된다.

Entry.objects.filter(pub_date__lte='2006-01-01')
Entry.objects.filter(blog_id=4) # foreignkey일땐 _id 서픽스를 붙여서 primary key를 찾는다.

Entry.objects.get(headline__exact="개발자 생활백서") # headline이 '개발자 생활백서'인 것을 찾는다.

Blog.objects.get(id__exact=14) # Explicit form
Blog.objects.get(id=14) # __exact is implied. 위랑 같다.

Blog.objects.get(name__iexact="beatles blog") # 대소문자 구분 X

Entry.objects.get(headline__contains='Lennon') # 포함

Span relationships의 Lookups

JOIN을 자동으로 해준다.

Entry.objects.filter(blog__name='율무 블로그') # 블로그모델의 name필드가 `율무 블로그`

Reverse relationship도 참조 가능. 모델 이름을 소문자로 쓴다.

Blog.objects.filter(entry__headline__contains='율무') # 거꾸로 참조

# 두 조건 모두 만족
Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)

Spanning multi-valued relationships

M2M필드나 reverse foreignkey를 필터링할때, 두 가지 종류의 필터가 있다.
– Blog / Entry (one-to-many)
+ 엔트리 제목이 Lennon이면서 2008년에 발행된 블로그는 1개
+ 엔트리 제목이 Lennon이면서 2008년에 발행된 블로그를 찾고싶다.

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008) # 엔트리 제목이 Lennon포함하면서 엔트리 발행일이 2008

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008) # 엔트리 제목이 Lennon포함하는 블로그를 찾고, 거기서 엔트리 발행일이 2008인 블로그를 찾는다. 위의 쿼리셋과 다를 수 있다.

모델의 필드를 참조할 수 있는 필터

특정 모델 필드랑 다른 필드 값 비교할 때도 쓸 수 있다.

>>> from django.db.models import F
>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks')) # 커멘트가 핑백보다 수가 많은 Entry 가져오기
>>> Entry.objects.filter(rating__lt=F('n_comments') + F('n_pingbacks')) # 레이팅이 커멘트+핑백보다 적은 엔트리 가져오기
>>> Entry.objects.filter(authors__name=F('blog__name')) # 관계를 span하기 위해선 __를 쓴다.
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) # 발행일보다 3일 후 날짜보다 큰 수정일을 가진 엔트리를 가져와라

pk lookup shortcut

primary key를 좀 더 편하게 참조할 수 있게 pk라고 shortcut을 만듦

>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

# pk가 1이나 4나 7인것
>>> Blog.objects.filter(pk__in=[1,4,7])

# pk가 14보다 큰 것
>>> Blog.objects.filter(pk__gt=14)

>>> Entry.objects.filter(blog__pk=3) # entry의 blog의 pk

쿼리셋 캐싱하기

>>> queryset = Entry.objects.all() # 재활용할거 할당해두기
>>> print([p.headline for p in queryset])
>>> print([p.pub_date for p in queryset])

>>> [entry for entry in queryset] # 데이터베이스 쿼리하기
>>> print queryset[5] # 캐시 쓰기
>>> print queryset[5] # 캐시 쓰기

Q 오브젝트로 복잡한 lookups 만들기

filter, exclude, get등에 쓸 수 있다.

from django.db.models import Q
Q(question__startswith='What')
Poll.objects.get(
Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)) # OR연산
)

오브젝트 삭제

e.delete()

다중 오브젝트 한번에 업데이트 하기

# pub_date가 2007인 모든 엔트리 헤드라인 업데이트하기
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

# 모든 엔트리 업데이트
>>> Entry.objects.all().update(blog=b)

update 메서드는 SQL문으로 바뀌어 바로 적용된다.

Related objects

다대일(One-to-many)

Forward
Foreignkey로 연결했으면 그냥 .으로 호출 가능

>>> e = Entry.objects.get(id=2)
>>> e.blog = some_blog
>>> e.save() #이거 해야 저장됨

>>> e = Entry.objects.get(id=2)
>>> print(e.blog) # e에서 블로그를 가져올 때 DB를 건드린다.
>>> print(e.blog) # 캐시 써서 디비를 건드리지 않는다.

>>> e = Entry.objects.select_related().get(id=2) #select_related()
>>> print(e.blog) # 캐시 써서 디비를 건드리지 않는다.
>>> print(e.blog) # 캐시 써서 디비를 건드리지 않는다.

Backward
모델이 ForeignKey를 갖고 있으면,

>>> b = Blog.objects.get(id=1)
>>> b.entry_set.all() # 거꾸로 연결되어있던 Entry를 소문자로 바꾸고, 뒤에 _set을 붙여주면 블로그랑 연결된 모든 entry의 쿼리셋 가져온다.

>>> b.entry_set.filter(headline__contains='Lennon')
>>> b.entry_set.count()

related_name파라미터를 ForeignKey만들때 쓰면 FOO_set을 오버라이드 할 수 있다.

# related_name 지정해두기
blog = ForeignKey(Blog, on_delete=models.CASCADE, related_name='entries')

# related_name으로 불러오기
>>> b = Blog.objects.get(id=1)
>>> b.entries.all()

다대다(Many-to-many)

e = Entry.objects.get(id=3)
e.authors.all() # Returns all Author objects for this Entry.
e.authors.count()
e.authors.filter(name__contains='John')

a = Author.objects.get(id=5)
a.entry_set.all() # Returns all Entry objects for this Author.

Refer

https://docs.djangoproject.com/en/1.9/topics/db/queries/

[Django Models 뜯어보기 #1] Models

Django Models

필드(Fields)

from django.db import models

class Musician(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    instrument = models.CharField(max_length=100)

class Album(models.Model):
    artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    release_date = models.DateField()
    num_stars = models.IntegerField()

class Person(models.Model):
    SHIRT_SIZES = (
        ('S', 'Small'),
        ('M', 'Medium'),
        ('L', 'Large'),
    )
    name = models.CharField(max_length=60)
    shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES)    
  • null: True면, DB의 컬럼에 NULL을 할당할 수 있게 된다. (DB의 NOT NULL)
  • blank: True면, 필드값 입력하지 않아도 된다. 디비의 제약과 관련 없음. 유효성 검증(validation)과 연관.

Verbose field names

ForeignKey, ManyToManyField, OneToOneField빼고 모든 필드는 첫 인자로(옵션) Verbose name을 받는다. 따로 지정하지 않으면 필드명으로 대신한다(언더스코어를 스페이스로 바꾸어서)

first_name = models.CharField("성", max_length=30)

ForeignKey, ManyToManyField, OneToOneField는 첫 인자가 모델 클래스이므로, verbose_name 키워드 인자를 줄 수 있다.

dogs = models.ManyToManyField(Dog, verbose_name='개 리스트')

관계(Relationships)

  1. Many-to-one
class Kind(models.Model):
    # 개 종류

class Dog(models.Model):
    kind = models.ForeignKey(Kind, on_delete=models.CASCADE) # 개마다 종류가 있음

  1. Many-to-many
class Food(models.Model):
    # 음식
    # 음식에 dogs가 있을 수도 있겠지만, Dog에 foods가 있는 것이 더 명확하므로 그렇게 한다.

class Dog(models.Model):
    foods = models.ManyToManyField(Food) # 개마다 먹을 수 있는 여러가지 음식. 복수 형태.

좀 더 복잡한 M2M관계를 표현하기 위해서는 별도의 모델을 만든다. (중간모델/intermediate model)

class Person(models.Model): #인간
    name = models.CharField

class Group(models.Model): #뮤직 그룹
    name = models.CharField
    # 그룹엔 멤버십으로 연결된 멤버들이 있다.
    members = models.ManyToManyField(Person, through='Membership') #멤버스는 멤버십을 통해 생긴다.

class Membership(models.Model): #뮤지컬 멤버십
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")

# 멤버십에 person, group 넣어서 만들면 그 그룹에 person이 들어가서 생긴다.
>>> m1 = Membership(person=ringo, group=beatles, 
...     date_joined=date(1962, 8, 16),
...     invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
[<Person: Ringo Starr>]
>>> ringo.group_set.all()
[<Group: The Beatles>]

>>> m2 = Membership.objects.create(person=paul, group=beatles,
...     date_joined=date(1960, 8, 1),
...     invite_reason="Wanted to form a band.")
>>> beatles.members.all()
[<Person: Ringo Starr>, <Person: Paul McCartney>]

# 아래 3개처럼 추가는 안된다.
>>> beatles.members.add(john)
>>> beatles.members.create(name="George Harrison")
>>> beatles.members = [john, paul, ringo, george]

# intermediate model로 만들었어도, 쿼리 아래처럼 평범하게 날릴 수 있다.
>>> Group.objects.filter(members__name__startswith='Paul')
[<Group: The Beatles>]

# intermediate model 만들때 attribute로도 쿼리 날릴 수 있다.
>>> Person.objects.filter(
...     group__name='The Beatles',
...     membership__date_joined__gt=date(1961,1,1))
  • 중간 모델을 직접 만들 때는, 관계를 가지는 두 모델에 대한 ForeignKey필드를 선언하고 추가적인 필드를 선언하면 된다.
  1. One-to-one
    다른 모델을 확장하여 새로운 모델을 만드는 경우 유용.
    가게라는 데이터베이스가 이미 있었는데, 맛집 데이터베이스를 추가적으로 만들게 되었다. 이 때 가게를 extend받아서 확장시킬 수 있다.

Meta 옵션

모델클래스 내부에 메타데이터를 추가할 수 있다.

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"] # 순서 정의
        verbose_name_plural = "oxen"

Model Attributes – objects

모델에서 가장 중요한 attribute은 Manager다. 모델 클래스 선언 기반하여 실제 디비에 대한 쿼리 인터페이스를 제공하며, 디비 레코드를 모델 객체로 인스턴스화 하는데 사용된다.
커스텀 Manager를 만들지 않으면, 기본은 ‘objects’라는 이름으로 잡힌다.

class Person(models.Model):
    people = models.Manager()

위와 같이 써주면 Person.objects는 AttributeError를 반환하지만,
Person.people.all()은 모든 Person오브젝트들을 반환한다.

모델에 메서드 추가하기

class Person(models.Model)
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    birth_date = models.DateField()

    def baby_boomer_status(self):
        "사람의 베이비 부머 status를 보여준다."
        import datetime
        if self.birth_date < datetime.date(1945, 8, 1):
            return "Pre-boomer"
        elif self.birth_date < datetime.date(1965, 1, 1):
            return "Baby boomer"
        else:
            return "Post-boomer"

    def _get_full_name(self):
        "사람의 풀네임을 반환한다."
        return '%s %s' % (self.first_name, self.last_name)
    full_name = property(_get_full_name)
  • __unicode__(): 해당 클래스의 유니코드 표현. (python2)
  • __str__(): python3에서 위와 같다. utf-8
  • get_absolute_url(): 오브젝트의 URL. 어드민이나 오브젝트에 url이 필요할 때 쓰인다.

미리 정의된 메서드 오버라이드

save()delete()를 오버라이드 할 경우가 많다.

class Blog(models.Model):
    name = models.CharField(max_length=100)

    def save(self, *args, **kwargs):
        do_something()
        super(Blog, self).save(*args, **kwargs) # 원래 save() method 부르기
        do_something_else()

superclass method를 불러서 원래 기능을 호출한다.

모델 상속(Inheritance)

Abstract base classes

공통적 정보를 다수의 모델에 넣고 싶을 때 유용.

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True #abstract = True로 해준다.

class Student(CommonInfo): #여기에 상속
    home_group = models.CharField(max_length=5)

related_name이 겹치지 않도록 unique한 reverse_name을 적어준다.

from django.db import models

class Base(models.Model):
    m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related")

    class Meta:
        abstract = True

class ChildA(Base): # related_name: common_childa_related
    pass

class ChildB(Base): # related_name: common_childb_related
    pass

Multi-table inheritance

공통 부분의 데이터는 부모모델에 저장, 자식 모델의 데이터는 자식모델 테이블에 저장.

refer

https://docs.djangoproject.com/es/1.9/topics/db/models/
http://nukggul.tistory.com/17

[Django] redirect시에 데이터 넘기고 싶어요(feat. HTTP)

상황

페이스북 로그인을 하고, 페이스북 연결 해제버튼을 눌렀을 때 비밀번호가 설정이 되어있지 않다면 비밀번호 설정 페이지로 리다이렉트 시키고 싶다.
이로 인해서 들어온 비밀번호 설정 페이지에서는 그냥 들어왔을 때와는 다르게 “페이스북 연결을 해제하려면 비밀번호를 설정해주셔야 합니다”라는 안내멘트를 보여주고 싶은데, 이를 위해 리다이렉트 시켰을 때 데이터를 넘기고 싶다.

첫 시도

Django docs에서 redirect섹션을 보고,

def my_view(request):
    ...
    return redirect('some-view-name', foo='bar')

이렇게 뒤에 foo=’bar’로 넘기면 되겠다 생각했다. (허접이 스키밍을 했을때의 폐해) (여기서 뒤에 붙는 인자는 kwargs이다.)
하지만 인자는 넘어가지 않았고, ‘django pass data with redirect’ 키워드로 구글링을 해보았다.
스택오버플로우링크의 답변을 보았는데,

redirect is merely a wrapper around HttpResponseRedirect that automatically calls reverse for you to create the URL to redirect to. As a result, the parameters you pass to it, aren’t arbitrary, they must be same you would pass to reverse and, specifically, only those required to create the URL.

Many people seem to have troubles understanding that data can’t just be arbitrarily passed to a view. HTTP is a stateless protocol: each request exists on it’s own, as if user had never been to any other page of the site. The concept of a session was created to provide a sense of “state” to a cohesive unit such as a site. With sessions, data is stored in some form of persistent storage and a “key” to look up that data is given to the client (typically the user’s browser). On the next page load, the client sends the key back to the server, and the server uses it to look up the data to give the appearance of state.

As a result, if you need data from one view available in another, you need to add it to the session, do your redirect, and look up the data in the session from the next view.

내가 찾던 답변같은데 이해하기 쉽지 않았다.
대략 HTTP의 stateless한 특성으로 reverse로는 데이터를 넘길 수 없어, 다른 방안을 찾으라는 것 같아 또 이것 저것 읽어본 결과,
1. Session을 사용하여 넘기기
2. GET으로 뒤에 Parameter달아 넘기기
3. django messages 사용하기

정도의 방법이 있는데, 지금 상황에서 어떤 방법을 써야 좀 더 세련될지 조언을 구하고자 P모님께 질문을 하였다. 결론은 역시 나의 HTTP에 대한 이해부족에서 나온 삽질이었다는 것이다.

HTTP의 Stateless성

P모님: HTTP는 왜 상태가 없죠?
나: (???)

한 초밥집이 있다. 이 초밥집은
1. 손님이 30분간 밥을 먹을동안 주방장이 계속 붙어서 손님에게 초밥을 내어줌
2. 테이크아웃
두가지 방식으로 운영될 수 있다.
1은 서로가 서로의 상태를 바로바로 알 수 있지만 한 손님이 먹는동안 주방장이 다른 손님을 응대하지 못한다는 단점이 있다.
2는 서로의 상태를 유지하고 있지 않지만, 한 주방장이 여러 손님을 순차적으로 응대할 수 있다는 장점이 있다.

보통 채팅이나 게임같은 실시간성 서비스가 1의 형태, 웹은 2의 형태를 취한다.

만약 2 방식에서 주방장이 손님의 단골메뉴를 기억해야 한다 해보자.
주방장이 손님별로 단골메뉴를 적어두거나, 손님이 각자 단골메뉴 쿠폰을 들고있다가 주방장에게 주어 기억하는 방법이 있을것이다. session을 사용하는것이 여기에 속한다. 이 방식의 특징은 서버의 디비와 사용자의 브라우저에 데이터를 저장한다는 것이다. 굳이 세션에 저장할 필요 없는 데이터를 저장하면 불필요하게 디비를 한 번 더 다녀오는 비용이 들게 된다. 그리고 django의 messages도 세션을 사용해서 구현한 것이니 위의 3가지 방법 중 1은 3을 포함하게 된다.
그리고 2번, GET으로 뒤에 파라미터를 달아 넘기는 것은, 아예 처음부터 다른 주소로 들어가는 것이다. 지금 상황처럼 이 url자체로 사람들이 많이 들어올 걱정을 안 해도 된다면 충분히 GET으로 데이터를 넘겨도 되겠다.

참고 – 쿠키와 세션 개념

render, redirect, reverse의 차이

그렇다면 왜 redirect에서 데이터를 인자로 넘기는게 안되는 것일까? 이는 HTTP의 response때문이다.
HTTP의 requestPOST혹은 GET등으로 데이터를 뭍혀서 보낸다.
그리고 온 response엔 200, 404 등 많은 종류가 있다. 여기서 200 ok같은 경우는 헤더영역에 200 등이 써있고, 그 밑에 html문서가 오던지, 혹은 요청한 데이터들이 좌라락 오던지 한다. 하지만 300번대인 redirect는, 밑에 redirect될 주소만 띡 하고 온다. 데이터를 뭍힐 곳이 없는것이다. (참고: HTTP 상태 코드)
(참고: 301은 permanent한 이동, 302는 임시 이동)

그리고 redirect, reverse의 관계는 아래와 같다.

  • redirect(‘welcome’) : 원래 redirect('/some/url/')처럼 url을 주소로 써주는데, url name을 쓰면 redirect 내부에서 자동으로 reverse를 호출하여 이름을 매칭해서 보내준다.
  • reverse(‘welcome’): url name welcome을 찾아서 보내준다.
  • redirect(reverse(‘welcome’)): 즉 이렇게 안쓰고 그냥 redirect만 써도 된다.

render는 템플릿과 컨텍스트를 합쳐서 HttpResponse 오브젝트를 리턴한다. 여기서 합친다는 말은, 예를 들어 컨텍스트에서 {"foo": "bar"}를 넘겼다고 하면, html template에서 {{ foo }}를 찾아서 bar로 치환해 섞어서 HttpResponse로 리턴해준다는 말이다.

그래서 HttpResponseRedirect를 리턴하는 redirect()는 합칠 컨텍스트를 못 받는 것이다.
참고: [Django] HttpResponse VS HttpResponseRedirect

결론

HTTP 부들부들