[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/

[git] 커밋 되돌리기

git reset HEAD^
git reset HEAD~2 # 2개 커밋 되돌리기

git push origin +master # +를 붙여주면 정보 손실 있어도 무시하고 푸쉬한다.

Refer

[Git] 아흑.. 커밋을 잘못했네;; 세상에 푸쉬까지 해버렸는데… 어쩌지…

[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

성공! 기쁘다.

일일커밋(Daily Commit) – 1년 회고

스크린샷 2016-07-02 오후 5.27.21

일일커밋(Daily Commit) – 100일 회고 글을 적은 후 시간이 흘러, 벌써 1년 회고를 쓰게 되었다. 일년 동안 어떤 변화가 생겼는지 기록해두려 한다.

습관

하루에 한 번씩은 꼭 노트북 앞에 앉아 코딩한다 (회사 일 제외).

일일커밋의 제1원칙이다. 이런 식으로 TIL, 개인 프로젝트, 오픈 소스 등에 매일 코딩을 하였다.

에잇컵스라는 IOT 텀블러가 있는데, 100일 동안 물을 꾸준히 마시면 최대 49,000원까지 환급해준다. 100일간 무언가를 꾸준히 하면 습관이 형성되고, 이를 돕기 위한 프로그램이다. 일일코딩도 이와 비슷하게 습관을 만들어준다.

작년 7월만 해도, 프로그래밍 후 공개된 장소에 올리는 것은 가끔 있을까 말까 한 일이었다. 하지만 1년 동안 습관을 들이니, 나와 비슷한 문제를 겪는 다른 사람들도 쓸 수 있도록 더 정성껏 코딩하게 되고, 이를 정리해서 올리는 것이 자연스러운 일이 되었다.

  사실 150일 후, 커밋 그래프에 처음으로 빈칸이 생긴 날 왠지 모를 희열이 느껴졌었다. 자유를 얻은 기분…? 역시 뭐든 적당히 하는 것이 좋다 🙂

커밋한 저장소

1년간 커밋한 저장소는 다음과 같다.

1. 개인 위키 – TIL(Today I Learned)

스크린샷 2016-07-02 오후 5.29.05

배운 것들을 마크다운 문법으로 정리해 올리는 개인 위키이다. 현재 240여 개의 문서들이 담겨있다.
가장 많이 참고한 문서는 파이썬 스타트킷으로, 개발 환경을 세팅할 때마다 본다(항상 까먹는다).

2. 개인 프로젝트 – 하비코딩

스크린샷 2016-07-02 오후 5.29.49

함께 코딩하기 위해 가볍게 개발자들을 모을 수 있는 밋업을 만드는 사이트이다. 개발자용 간단 온오프믹스라 생각하면 된다.
Python, Django로 만든 첫 개인 프로젝트이고, 회원가입/밋업 개최/댓글/참여/태그/검색 등을 한땀 한땀 개발해서 애착이 크다.
깃허브 Issues서머노트 달기를 적어두었는데, 지인이 그걸 보고 개발하여 Pull Request를 날려주었다. 컨트리뷰션을 받는 경험도 참 감동이라는 것을 알았다 🙂

3. 팀 프로젝트 – 9X년생 개발자 모임 블로그

스크린샷 2016-07-02 오후 5.30.54

요즘 가장 열정을 바치고 있는 ‘9X년생 개발자 모임'(페이스북 링크)의 공식 블로그다. Jykell을 이용해 Github Pages로 배포하는 Static한 사이트이며, 스태프 최현묵님이 첫 삽을 깊게 떠주셔서 편히 작업하고 있다.

4. 오픈 소스 – Angular Datatables


HTML Table을 예쁘게 보여주고 기능을 추가해주는 ‘Datatables’의 Angular.js 확장 라이브러리다. 회사 프로젝트에 사용하며 Material Design으로 스타일을 커스텀하다가, 하는 김에 컨트리뷰션도 했다. 그것에 맞추어 홈페이지 스킨도 변경해서 Pull Request를 보냈다.

사실 Star 750개짜리인 큰 프로젝트라 반려될 각오도 했는데 Merge되어 약간 어안이 벙벙하였다 🙂

많은 사람들이 내 디자인을 쓴다니… 적은 노력으로 날로 먹은 기분이다.

5. 오픈 소스 – JUI

스크린샷 2016-07-02 오후 5.33.15

제니퍼소프트에서 만든 UI 프레임워크다. 문서를 보다 Footer에 오타가 있길래 수정하여 Pull Request를 보냈다. 정말 날로 먹은 풀 리퀘스트. 이것이 내 첫 오픈소스 활동이었다. 작은 코드도 머지될 수 있다는 용기를 준 프로젝트이다.

공유

나 자신의 코딩 습관을 키워준 것 이외에도, 일일커밋은 나를 더 큰 세상으로 이끌어주었다.

1. 책

스크린샷 2016-07-02 오후 5.33.51.png

일일커밋 100일 회고 블로그 글을 보고, 한빛미디어 출판사에서 연락이 왔다. 이 인연으로 '훌륭한 프로그래머 되는 법'이란 책 말미에 작은 챕터로 '일일 커밋'글을 싣게 되었다(책 제목을 말할 때마다 부끄럽다ㅎㅎ).

2. 토크콘서트, 특강

국민대에서 '여성 개발자 토크콘서트' 패널로 신입 개발자의 삶에 대해 이야기 하고, 후에 '신입 개발자 생활백서'란 이름으로 다시 2시간 가량의 특강을 하게 되었다. 여기서도 '일일 커밋'이야기를 유용하게 써먹었다.

3. 패스트캠퍼스 강의

13490855_1183308781720031_6569680340315071918_o

슬라이드 쉐어에 공유한 신입 개발자 생활백서 발표자료를 보고, IT 실무교육 아카데미 패스트캠퍼스에서 강연 요청이 왔다. ‘더 나은 개발자가 되는 실전 팁’, ‘TIL 제작 실습’, ‘팜므어 번역기 제작 실습’으로 7시간동안 비전공자 대상으로 강연하였다.

다음 목표

일일커밋과 함께한 이 1년으로 ‘함께 개발’하는, 소셜 코딩의 첫 시작을 열게 되었다. 이로서 쪼렙인 내가 고수들과 함께 시간과 장소를 넘어 개발할 수 있게 되었다. 웹이란 참 아름다운 기술 🙂

만들고 있는 하비코딩과 9XD 사이트를 잘 다듬어, 많은 사람들과 함께 쓸 수 있도록 배포하는 것이 다음 목표이다.