Mais conteúdo relacionado Semelhante a Django admin site 커스텀하여 적극적으로 활용하기 (20) Django admin site 커스텀하여 적극적으로 활용하기2. 2년 조금 넘는 기간 동안,
스타트업에서 이것저것 개발하며 배운 것 중,
공유하고 싶은 것이 있어서 발표를 하게 되었습니다.
4. ‘트웬티’ 앱을 개발하던 2015년 9월쯤,
Python과 Django를 알게 됐고,
덕분에 RESTful API와 CMS를 쉽고 빠르게 만들 수 있었습니다.
7. 이번 파이콘 주제가 ‘Back to the basic’ 인 걸 보고,
경험을 공유하고 싶다는 생각이 들었습니다.
8. 이 발표에서 다루는 내용
• Django admin site의 기본적인 사용 방법
• Django admin site를 커스텀 하는 여러가지 방법과 그 결과
• Python
• Django 내부 동작, 구현
9. 들어가기 전에… Django ?
• Documentation
• Middleware
• Model (ORM)
• Form
• Class based view
• Template
Django admin site
10. 발표의 모든 내용은 여기에 있습니다.
• https://docs.djangoproject.com/en/1.11/ref/contrib/admin
• ModelAdmin, InlineModelAdmin, AdminSite, LogEntry
• Overriding admin templates, Reversing admin urls
• https://docs.djangoproject.com/en/1.11/ref/contrib/admin/actions/
• Writing actions, Advanced action techniques
• https://docs.djangoproject.com/en/1.11/ref/contrib/admin/admindocs/
• https://docs.djangoproject.com/en/1.11/ref/contrib/admin/javascript/
12. 예제를 위한 모델
MEMBER
name 이름
email 이메일
permission 권한
certification_date 인증일
is_certificated 인증상태
POST
member 작성자
category 카테고리
title 제목
subtitle 부제목
content 내용
is_deleted 삭제여부
created_at 작성일
COMMENT
member 작성자
post 원글
content 내용
report_count 신고수
created_at 작성일
CATEGORY
name 이름
* permission (관리자, 에디터, 일반)
13. 예제를 위한 모델 - Member
class Member(AbstractBaseUser):
TYPE_PERMISSIONS = (
('AD', '관리자'),
('ET', '에디터'),
('MB', '일반'),
)
email = models.EmailField('이메일', max_length=255, unique=True)
username = models.CharField('닉네임', max_length=30)
permission = models.CharField('권한', max_length=2, choices=TYPE_PERMISSIONS, default='MB')
certification_date = models.DateField('인증일', default=None, null=True, blank=True)
is_certificated = models.BooleanField('인증여부', default=False)
14. 예제를 위한 모델 - Post
class Category(models.Model):
name = models.CharField('카테고리 이름', max_length=20)
class Post(models.Model):
member = models.ForeignKey(Member, verbose_name='작성자')
category = models.ForeignKey(Category, verbose_name='카테고리')
title = models.CharField('제목', max_length=255)
content = models.TextField('내용')
is_deleted = models.BooleanField('삭제된 글', default=False)
created_at = models.DateTimeField('작성일', auto_now_add=True)
class Comment(models.Model):
member = models.ForeignKey(Member, verbose_name='작성자')
post = models.ForeignKey(Post, verbose_name=‘원본글’)
content = models.TextField()
is_blocked = models.BooleanField('노출 제한', default=False)
16. Django 설치
example $ pyenv virtualenv 3.6.2 pyconExample # 가상 환경 추가
example $ pyenv shell envExample # 가상 환경 실행
(envExample) example $ pip install django # django 설치
(envExample) example $ django-admin.py startproject example # django 프로젝트 생성
(envExample) example $ python manage.py startapp member # member, post 앱 추가
(envExample) example $ python manage.py startapp post
(envExample) example $ python manage.py createsuperuser # 관리자 추가
(envExample) example $ python manage.py runserver # 실행
http://localhost:8000 http://localhost:8000/admin/
18. Model을 admin site에 등록
# member/admin.py
from django.contrib import admin
from member.models import Member
admin.site.register(Member)
# post/amdin.py
from django.contrib import admin
from post.models import Category, Post, Comment
admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Comment)
19. Model을 admin site에 등록
# post/models.py
class Category(models.Model):
class Meta:
verbose_name_plural = "categories"
name = models.CharField(max_length=20)
21. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
22. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
23. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = (
'id', 'email', ‘username',
'permission', ‘is_certificated',
‘certification_date’, ‘post_count', )
24. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = (
'id', 'email', ‘username',
'permission', ‘is_certificated',
‘certification_date’, ‘post_count', )
list_editable = ('permission', )
25. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = (
'id', 'email', ‘username',
'permission', ‘is_certificated',
‘certification_date’, ‘post_count', )
list_editable = ('permission', )
list_filter = ('permission', )
26. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = (
'id', 'email', ‘username',
'permission', ‘is_certificated',
‘certification_date’, ‘post_count', )
list_editable = ('permission', )
list_filter = ('permission', )
search_fields = ('username', )
27. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = (
'id', 'email', ‘username',
'permission', ‘is_certificated',
‘certification_date’, ‘post_count', )
list_editable = ('permission', )
list_filter = ('permission', )
search_fields = ('username', )
ordering = ('-id', 'email', 'permission', )
28. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = (
'id', 'email', ‘username',
'permission', ‘is_certificated',
‘certification_date’, ‘post_count', )
list_editable = ('permission', )
list_filter = ('permission', )
search_fields = ('username', )
ordering = ('-id', 'email', 'permission', )
def post_count(self, obj):
return Post.objects.filter(member=obj).count()
post_count.short_description = '작성한 글 수'
29. 기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = (
'id', 'email', ‘username',
'permission', ‘is_certificated',
‘certification_date’, ‘post_count', )
list_editable = ('permission', )
list_filter = ('permission', )
search_fields = ('username', )
ordering = ('-id', 'email', 'permission', )
def post_count(self, obj):
return Post.objects.filter(member=obj).count()
post_count.short_description = '작성한 글 수'
admin.site.register(Member, MemberAdmin)
30. 기본 - Form
# post/admin.py
class PostAdmin(admin.ModelAdmin):
list_per_page = 10
list_display = (
'id', 'title', ‘member',
'is_deleted', 'created_at', )
list_editable = ('is_deleted', )
list_filter = (
‘member__permission',
'category__name', 'is_deleted', )
fields = ('member', 'category', 'title', )
admin.site.register(Category)
admin.site.register(Post, PostAdmin)
admin.site.register(Comment)
31. 기본 - Form
# post/admin.py
class PostAdmin(admin.ModelAdmin):
. . .
fieldsets = (
('기본 정보', {
'fields': (('member', 'category', ), )
}),
('제목 및 내용', {
'fields': (
'title', 'subtitle', ‘content',
)
}),
('삭제', {
'fields': ('is_deleted', 'deleted_at', )
})
)
. . .
32. 기본 - Custom Validation
# post/forms.py
class MyPostAdminForm(forms.ModelForm):
def clean_content(self): # clean_{field_name}
ModelForm Documentation
https://docs.djangoproject.com/en/1.11/topics/forms/modelforms
33. 기본 - Custom Validation
# post/forms.py
class MyPostAdminForm(forms.ModelForm):
def clean_content(self): # clean_{field_name}
content = self.cleaned_data['content']
words = ['심심하다', ‘관리자’, ‘금지어’, ]
error_message =
'[{0}] {1}'.format(', '.join(words), ‘와…’)
if any(word in content for word in words):
raise forms.ValidationError(error_message)
return content
ModelForm Documentation
https://docs.djangoproject.com/en/1.11/topics/forms/modelforms
34. 기본 - Custom Validation
# post/forms.py
class MyPostAdminForm(forms.ModelForm):
def clean_content(self): # clean_{field_name}
content = self.cleaned_data['content']
words = ['심심하다', ‘관리자’, ‘금지어’, ]
error_message =
'[{0}] {1}'.format(', '.join(words), ‘와…’)
if any(word in content for word in words):
raise forms.ValidationError(error_message)
return content
# post/admin.py
class PostAdmin(admin.ModelAdmin):
form = MyPostAdminForm
. . .
ModelForm Documentation
https://docs.djangoproject.com/en/1.11/topics/forms/modelforms
35. 기본 - Custom Validation
# post/forms.py
class MyPostAdminForm(forms.ModelForm):
def clean_content(self): # clean_{field_name}
content = self.cleaned_data['content']
words = ['심심하다', ‘관리자’, ‘금지어’, ]
error_message =
'[{0}] {1}'.format(', '.join(words), ‘와…’)
if any(word in content for word in words):
raise forms.ValidationError(error_message)
return content
# post/admin.py
class PostAdmin(admin.ModelAdmin):
form = MyPostAdminForm
. . .
ModelForm Documentation
https://docs.djangoproject.com/en/1.11/topics/forms/modelforms
[심심하다, 관리자, 금지어]와 같은 단어들은 입력하실 수 없습니다.
37. 심화 - Custom list filter
# post/filters.py
class CreatedDateFilter(admin.SimpleListFilter):
title = '작성일'
parameter_name = 'date'
def lookups(self, request, model_admin):
results = []
for i in range(-3, 6):
date = datetime.date.today() + datetime.timedelta(days=i)
display_str = '{0} [{1}개]'.format(
date,
Post.objects.filter(created_at__date=date).count()
)
display_str += ' - 오늘' if i == 0 else ''
results.append((date, display_str))
return results
def queryset(self, request, queryset):
if self.value():
return queryset.filter(created_at__date=self.value())
else:
return queryset.all()
38. 심화 - Custom list filter
# post/filters.py
class CreatedDateFilter(admin.SimpleListFilter):
title = '작성일'
parameter_name = 'date'
def lookups(self, request, model_admin):
results = []
for i in range(-3, 6):
date = datetime.date.today() + datetime.timedelta(days=i)
display_str = '{0} [{1}개]'.format(
date,
Post.objects.filter(created_at__date=date).count()
)
display_str += ' - 오늘' if i == 0 else ''
results.append((date, display_str))
return results
def queryset(self, request, queryset):
if self.value():
return queryset.filter(created_at__date=self.value())
else:
return queryset.all()
39. 심화 - Custom list filter
# post/filters.py
class CreatedDateFilter(admin.SimpleListFilter):
title = '작성일'
parameter_name = 'date'
def lookups(self, request, model_admin):
results = []
for i in range(-3, 6):
date = datetime.date.today() + datetime.timedelta(days=i)
display_str = '{0} [{1}개]'.format(
date,
Post.objects.filter(created_at__date=date).count()
)
display_str += ' - 오늘' if i == 0 else ''
results.append((date, display_str))
return results
def queryset(self, request, queryset):
if self.value():
return queryset.filter(created_at__date=self.value())
else:
return queryset.all()
40. 심화 - Custom action
# member/admin.py
from member.forms import SetCertificationDateForm
class MemberAdmin(admin.ModelAdmin):
actions = ['set_certification_date']
action_form = SetCertificationDateForm # SelectDateWidget
41. 심화 - Custom action
# member/admin.py
from member.forms import SetCertificationDateForm
class MemberAdmin(admin.ModelAdmin):
actions = ['set_certification_date']
action_form = SetCertificationDateForm # SelectDateWidget
def set_certification_date(self, request, queryset):
year, month, day = . . . # POST Request에서 값을 꺼냄
if year and month and day:
date_str = '{0}-{1}-{2}'.format(year, month, day)
date = strptime(date_str, "%Y-%d-%m").date()
for member in queryset:
Member.objects
.filter(id=member.id)
.update(is_certificated=True, certification_date=date)
messages.success(request, '{0}명의 회원을 인증했습니다.'.format(len(queryset)))
else:
messages.error(request, '날짜가 선택되지 않았습니다.')
42. 심화 - Custom action
# member/admin.py
from member.forms import SetCertificationDateForm
class MemberAdmin(admin.ModelAdmin):
actions = ['set_certification_date']
action_form = SetCertificationDateForm # SelectDateWidget
def set_certification_date(self, request, queryset):
year, month, day = . . . # POST Request에서 값을 꺼냄
if year and month and day:
date_str = '{0}-{1}-{2}'.format(year, month, day)
date = strptime(date_str, "%Y-%d-%m").date()
for member in queryset:
Member.objects
.filter(id=member.id)
.update(is_certificated=True, certification_date=date)
messages.success(request, '{0}명의 회원을 인증했습니다.'.format(len(queryset)))
else:
messages.error(request, '날짜가 선택되지 않았습니다.')
set_certification_date.short_description = '선택된 유저를 해당 날짜 기준으로 인증합니다.'
44. 페이지 추가하기
# post/admin.py
class PostAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(PostAdmin, self).get_urls()
post_urls = [
url(r'^status/$', self.admin_site.admin_view(self.post_status_view))
]
return post_urls + urls
def post_status_view(self, request):
context = dict(
self.admin_site.each_context(request),
posts=Post.objects.all(),
key1=value1,
key2=value2,
)
return TemplateResponse(request, "admin/post_status.html", context)
45. 페이지 추가하기
# post/admin.py
class PostAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(PostAdmin, self).get_urls()
post_urls = [
url(r'^status/$', self.admin_site.admin_view(self.post_status_view))
]
return post_urls + urls
def post_status_view(self, request):
context = dict(
self.admin_site.each_context(request),
posts=Post.objects.all(),
key1=value1,
key2=value2,
)
return TemplateResponse(request, "admin/post_status.html", context)
46. 페이지 추가하기
# post/admin.py
class PostAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(PostAdmin, self).get_urls()
post_urls = [
url(r'^status/$', self.admin_site.admin_view(self.post_status_view))
]
return post_urls + urls
def post_status_view(self, request):
context = dict(
self.admin_site.each_context(request),
posts=Post.objects.all(),
key1=value1,
key2=value2,
)
return TemplateResponse(request, "admin/post_status.html", context)
47. 페이지 추가하기
# templates/admin/post_status.html
{% extends "admin/base_site.html" %}
{% block content %}
<h2>Post Status</h2>
<ul>
{% for post in posts %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% endblock %}
http://localhost:8000/admin/post/post/status/
49. UI 변경하기
├── example # Project Directory
│ ├── assets
│ │ └── admin
│ │ ├── css
│ │ │ ├── custom.css # 전체 레이아웃을 수정하는 CSS
│ │ │ ├── dropdown.css # 상단 메뉴바에 드롭다운 메뉴를 적용하기 위한 CSS
• https://github.com/bbayoung/django-admin-site-custom-example/blob/master/example/assets/admin/css/custom.css
• https://github.com/bbayoung/django-admin-site-custom-example/blob/master/example/assets/admin/css/dropdown.css
# example/settings.py
. . .
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'assets'),
)
. . .
50. • https://github.com/django/django/
UI 변경하기
├── admin
│ ├── templates
│ │ ├── admin
│ │ │ ├── 404.html
│ │ │ ├── 500.html
│ │ │ ├── actions.html
│ │ │ ├── app_index.html
│ │ │ ├── auth
│ │ │ ├── base.html
│ │ │ ├── base_site.html
│ │ │ ├── change_form.html
│ │ │ └── index.html
base.html
base_site.html
app_index.htmlindex.html login.html
51. # templates/admin/base_site.html
{% extends "admin/base.html" %}
{% load static %}
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block extrastyle %}
{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django
administration') }}</a></h1>
{% endblock %}
{% block nav-global %}{% endblock %}
UI 변경하기
52. # templates/admin/base_site.html
{% extends "admin/base.html" %}
{% load static %}
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block extrastyle %}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/dropdown.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "admin/css/custom.css" %}" />
{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django
administration') }}</a></h1>
{% endblock %}
{% block nav-global %}{% endblock %}
UI 변경하기
# example/settings.py
admin.site.site_title = '파이콘 한국 2017’ # 브라우저 타이틀
admin.site.site_header = 'Back to the basic’ # 웹사이트 header 부분 타이틀
55. # example/context_processors.py
def gnb_menus(request):
menus = [
{
'name': '회원',
'sub_menus': [
{'name': '관리자', 'url': '/admin/member/member/?permission__exact=AD'},
{'name': '에디터', 'url': '/admin/member/member/?permission__exact=ET'},
{'name': '일반', 'url': '/admin/member/member/?permission__exact=MB'},
]
},
{
'name': ' 글 ',
'sub_menus': [
{'name': 'GENDER', 'url': '/admin/post/post/?category__name=GENDER'},
{'name': 'SOCIAL', 'url': '/admin/post/post/?category__name=SOCIAL'},
{'name': 'POLITICS', 'url': '/admin/post/post/?category__name=POLITICS'},
{'name': '통계', 'url': '/admin/post/post/status/'},
]
}
]
return {'gnb_menus': menus}
UI 변경하기
56. # example/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates")],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
‘example.context_processors.gnb_apps', # 장고에 추가한 기본 앱 메뉴
‘example.context_processors.gnb_menus', # 이전 페이지에서 직접 정의한 상단 메뉴
],
},
},
]
UI 변경하기
57. # templates/admin/base_site.html
{% block nav-global %}
<div id="gnb">
<div id="gnb-app-list">
<ul class="drop-down-menu">
{% for menu in gnb_menus %}
// 커스텀 메뉴 출력 - - - - - - - - - - - - - (1)
{% endfor %}
{% if gnb_apps %}
// Django 전체 모델 출력 - - - - - - - - - (2)
{% endif %}
</ul>
</div>
</div>
{% endblock %}
UI 변경하기
58. # templates/admin/base_site.html
. . . (1)
{% for menu in gnb_menus %}
<li>
<a {% if menu.url %}href="{{ menu.url }}"{% endif %}>{{ menu.name }}</a>
<ul>
{% for sub_menu in menu.sub_menus %}
<li><a href="{{ sub_menu.url }}">{{ sub_menu.name }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
. . .
UI 변경하기
59. # templates/admin/base_site.html
. . . (2)
{% if gnb_apps %}
<li><a>전체 앱</a>
<ul>
{% for app in gnb_apps %}
<li><a href="/admin/{{ app.app_url }}">{{ app.name }}</a>
<ul>
{% for model in app.models %}
<li><a href="{{ model.admin_url }}">{{ model.name }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</li>
{% endif %}
. . .
UI 변경하기
62. Django admin site 분리하기
# post/admin.py
from django.contrib.admin import AdminSite
class CommentAdminSite(AdminSite):
site_header = 'Comment administration'
comment_admin = CommentAdminSite(name='comment admin')
comment_admin.register(Comment, CommentAdmin)
63. Django admin site 분리하기
# post/admin.py
from django.contrib.admin import AdminSite
class CommentAdminSite(AdminSite):
site_header = 'Comment administration'
comment_admin = CommentAdminSite(name='comment admin')
comment_admin.register(Comment, CommentAdmin)
# example/urls.py
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^admin/comment/', comment_admin.urls),
]
65. 문서화
(envExample) example $ pip install docutils # docutils 설치
# example/urls.py
urlpatterns = [
. . .
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
. . .
]
# example/settings.py
INSTALLED_APPS = [
. . .
'django.contrib.admindocs',
. . .
]
66. 문서화
# post/models.py
class Comment(models.Model):
"""
사용들이 작성한 글에 대한 댓글입니다.
댓글은 :model:`post.Post` 와 :model:`member.Member`. 모델과 1:N 관계입니다.
"""
member = models.ForeignKey(Member, verbose_name='작성자')
post = models.ForeignKey(Post, verbose_name='원본글')
content = models.TextField(verbose_name='내용', help_text='댓글 내용입니다.')
67. • 시간이 길지 않아, 준비한 내용은 여기까지입니다.
• 이 외에도 공식 문서에 추가적인 커스텀 방법들이 소개되어 있습니다.
• 예제 코드는 아래에서 확인하실 수 있습니다.
https://github.com/bbayoung/django-admin-site-custom-example
감사합니다.