KEMBAR78
Django admin site 커스텀하여 적극적으로 활용하기 | PDF
Django admin site
커스텀하여 적극적으로 활용하기
박영우
2년 조금 넘는 기간 동안,
스타트업에서 이것저것 개발하며 배운 것 중,
공유하고 싶은 것이 있어서 발표를 하게 되었습니다.
‘캠쿠’
대학생활 개선 앱
‘트웬티’
대학생 미디어
‘ALT’
뉴미디어 스타트업
RESTful API
CMS
(Contents Management System)
‘트웬티’ 앱을 개발하던 2015년 9월쯤,
Python과 Django를 알게 됐고,
덕분에 RESTful API와 CMS를 쉽고 빠르게 만들 수 있었습니다.
그런데, 6개월이 지나서야
Django admin site의 존재를 알게 됐습니다.
CMS를 모두 다 구현하고 난 뒤에…
Django admin site를 커스텀해서 사용하는 것이,
직접 만든 CMS 보다
훨씬 쉽고, 빠르고, 안정적이었습니다.
이번 파이콘 주제가 ‘Back to the basic’ 인 걸 보고,
경험을 공유하고 싶다는 생각이 들었습니다.
이 발표에서 다루는 내용
• Django admin site의 기본적인 사용 방법
• Django admin site를 커스텀 하는 여러가지 방법과 그 결과
• Python
• Django 내부 동작, 구현
들어가기 전에… Django ?
• Documentation
• Middleware
• Model (ORM)
• Form
• Class based view
• Template
Django admin site
발표의 모든 내용은 여기에 있습니다.
• 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/
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
예제를 위한 모델
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 (관리자, 에디터, 일반)
예제를 위한 모델 - 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)
예제를 위한 모델 - 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)
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
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/
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
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)
Model을 admin site에 등록
# post/models.py
class Category(models.Model):
class Meta:
verbose_name_plural = "categories"
name = models.CharField(max_length=20)
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
기본 - List
# member/admin.py
class MemberAdmin(admin.ModelAdmin):
list_per_page = 5
기본 - 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
# 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
# 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', )
기본 - 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', )
기본 - 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', )
기본 - 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 = '작성한 글 수'
기본 - 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)
기본 - 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)
기본 - Form
# post/admin.py
class PostAdmin(admin.ModelAdmin):
. . .
fieldsets = (
('기본 정보', {
'fields': (('member', 'category', ), )
}),
('제목 및 내용', {
'fields': (
'title', 'subtitle', ‘content',
)
}),
('삭제', {
'fields': ('is_deleted', 'deleted_at', )
})
)
. . .
기본 - 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
기본 - 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
기본 - 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
기본 - 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
[심심하다, 관리자, 금지어]와 같은 단어들은 입력하실 수 없습니다.
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
심화 - 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()
심화 - 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()
심화 - 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()
심화 - Custom action
# member/admin.py
from member.forms import SetCertificationDateForm
class MemberAdmin(admin.ModelAdmin):
actions = ['set_certification_date']
action_form = SetCertificationDateForm # SelectDateWidget
심화 - 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, '날짜가 선택되지 않았습니다.')
심화 - 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 = '선택된 유저를 해당 날짜 기준으로 인증합니다.'
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
페이지 추가하기
# 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)
페이지 추가하기
# 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)
페이지 추가하기
# 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)
페이지 추가하기
# 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/
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
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'),
)
. . .
• 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
# 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 변경하기
# 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 부분 타이틀
UI 변경하기
UI 변경하기
# 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 변경하기
# 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 변경하기
# 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 변경하기
# 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 변경하기
# 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 변경하기
UI 변경하기
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
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)
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),
]
순서
1.Django 설치
2.Model을 admin site에 등록하기
3.기본적인 사용법
4.조금 더 심화된 사용법
5.커스텀 페이지 추가
6.UI 변경하기
7.Admin site 분리하기
8.마지막으로 (간단한) 문서화!
문서화
(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',
. . .
]
문서화
# 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='댓글 내용입니다.')
• 시간이 길지 않아, 준비한 내용은 여기까지입니다.
• 이 외에도 공식 문서에 추가적인 커스텀 방법들이 소개되어 있습니다.
• 예제 코드는 아래에서 확인하실 수 있습니다.

https://github.com/bbayoung/django-admin-site-custom-example
감사합니다.

Django admin site 커스텀하여 적극적으로 활용하기

  • 1.
    Django admin site 커스텀하여적극적으로 활용하기 박영우
  • 2.
    2년 조금 넘는기간 동안, 스타트업에서 이것저것 개발하며 배운 것 중, 공유하고 싶은 것이 있어서 발표를 하게 되었습니다.
  • 3.
    ‘캠쿠’ 대학생활 개선 앱 ‘트웬티’ 대학생미디어 ‘ALT’ 뉴미디어 스타트업 RESTful API CMS (Contents Management System)
  • 4.
    ‘트웬티’ 앱을 개발하던2015년 9월쯤, Python과 Django를 알게 됐고, 덕분에 RESTful API와 CMS를 쉽고 빠르게 만들 수 있었습니다.
  • 5.
    그런데, 6개월이 지나서야 Djangoadmin site의 존재를 알게 됐습니다. CMS를 모두 다 구현하고 난 뒤에…
  • 6.
    Django admin site를커스텀해서 사용하는 것이, 직접 만든 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/
  • 11.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 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)
  • 15.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 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/
  • 17.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 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)
  • 20.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 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.
    기본 - CustomValidation # 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.
    기본 - CustomValidation # 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.
    기본 - CustomValidation # 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.
    기본 - CustomValidation # 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 [심심하다, 관리자, 금지어]와 같은 단어들은 입력하실 수 없습니다.
  • 36.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 37.
    심화 - Customlist 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.
    심화 - Customlist 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.
    심화 - Customlist 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.
    심화 - Customaction # member/admin.py from member.forms import SetCertificationDateForm class MemberAdmin(admin.ModelAdmin): actions = ['set_certification_date'] action_form = SetCertificationDateForm # SelectDateWidget
  • 41.
    심화 - Customaction # 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.
    심화 - Customaction # 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 = '선택된 유저를 해당 날짜 기준으로 인증합니다.'
  • 43.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 44.
    페이지 추가하기 # post/admin.py classPostAdmin(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 classPostAdmin(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 classPostAdmin(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/
  • 48.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 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 부분 타이틀
  • 53.
  • 54.
  • 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 {% blocknav-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 변경하기
  • 60.
  • 61.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 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), ]
  • 64.
    순서 1.Django 설치 2.Model을 adminsite에 등록하기 3.기본적인 사용법 4.조금 더 심화된 사용법 5.커스텀 페이지 추가 6.UI 변경하기 7.Admin site 분리하기 8.마지막으로 (간단한) 문서화!
  • 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 감사합니다.