admin 설정

코딩 연습/Django 2019. 5. 22. 16:31

admin에서 model 등록하기

  • admin에 등록하고자하는 모델을 import한다.
  • @admin.register() 장식자(decorator)를 사용하여 등록한다.
  • 모델 클래스처럼 admin 클래스를 작성하고 별다른 변경사항이 없는 경우 pass 처리한다.
from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
	pass

admin 등록 후 Post 모델에 저장된 데이터가 노출된다

 

모델에 저장된 data를 원하는 필드에 맞게 노출시키기

  • list_display는 노출시키고자 하는 필드를 선택
  • list_display_links는 등록한 필드의 데이터에 링크를 걸어 세부내역을 확인할 수 있다
  • search_fields는 해당 필드의 데이터를 검색할 수 있게 해준다.
from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['id', 'title']
    list_display_links = ['id', 'title']
    search_fields = ['title']

ID 필드가 노출되며, 두 필드 모두 링크가 걸려있고, TITLE 검색까지 가능해졌다

 

블로그 이미지

쵸잇

,

폼 샘플 형태

 

나는 Title, Top text, Author 3가지를 입력 받아 책 표지에 제목, 부제, 저자 형태로 출력할 수 있는 폼을 만들고 싶다.

 

 

1) [forms.py]를 앱 폴더에 추가하여 폼의 이름과 필드명, 데이터타입을 지정한다.

- 책의 표지에 쓸 데이터를 입력 받으므로 [CoverForm]으로 지정

- title, top_text, author 모두 문자로 입력 받을 것이므로 [CharField()]를 사용

from django import forms

class CoverForm(forms.Form):
    title = forms.CharField()
    top_text = forms.CharField()
    author = forms.CharField() 

 

 

2) [forms.py]에서 만든 [CoverForm]을 import하여 [views.py]에서 로직을 임시로 작성한다.

- [CoverForm()]을 변수 [form]에 담아 context(ctx)를 통해 [index.html]로 render한다.

from django.shortcuts import render
from .forms import CoverForm

# Create your views here.
def index(request):
	form = CoverForm()
    ctx = {
        'form' : form,
    }
    return render(request, 'cover/index.html', ctx)

 

 

3) [index.html]에서 <form>태그를 작성한다.

- 우리가 웹브라우저에서 볼 수 있는 모양의 폼을 드디어 만든다.

- [action=""]은 입력 받은 데이터를 현재 페이지에서 처리하도록 한다.

- [method="post"]는 데이터를 저장할 수 있게 처리를 해주는 메소드이다.

- 따라서 폼을 통해 입력한 데이터를 현재 페이지에 전송하여 DB로 저장할 수 있게 기본 세팅을 하는 것이다.

<form action="" method="post">
    {% csrf_token %}
      {{ form }}
    <input type="submit" value="Generator O RLY">
</form>

 

 

짜잔, 완성된 폼이다. 

 

샘플과 비교하면 1줄로 작성된게 어색하게 느껴진다

 

<table> 태그를 활용하여 줄바꿈을 할 수 있다. 

<form action="" method="post">
    {% csrf_token %}
    <table>
      {{ form }}
    </table>
    <input type="submit" value="Generator O RLY">
form>

 

샘플과 똑같은 모습을 갖췄다

 

4) [views.py]에서 다시 로직을 구체적으로 구성한다.

- 웹브라우저에서 입력 받아 전송된 데이터를 [index 함수]를 통해 가공을 거친다.

- [CoverForm(request.POST)]는 폼과 폼에 입력된 데이터를 포함하고 있다.

- [is_valid() 메소드]는 데이터의 유효성을 검사하는데, [CharField()]에 해당하는지 검증하는 것이다.

- [cleaned_data]는 데이터를 딕셔너리 타입으로 제공하도록 해준다. 

- 완료시 cover 앱의 index 이름의 URL로 redirect시킨다. 

from django.shortcuts import render
from .forms import CoverForm

# Create your views here.
def index(request):
    if request.method == "POST":
        form = CoverForm(request.POST)
        if form.is_valid():
            form.cleaned_data
            return redirect('cover:index')
    else:
        form = CoverForm()

    ctx = {
        'form' : form,
    }
    return render(request, 'cover/index.html', ctx)
블로그 이미지

쵸잇

,

이 에러 때문에 얼마나 고생했던지 수명이 짧아졌을 정도다.
구글링을 하면 대부분이 .py파일을 .txt파일로 실행한 경우에 에러가 발생하는데,
나와 같은 경우 Django로 만든 웹사이트를 runserver하면 명령프롬프트에서는 해당 에러가 나타나고,
웹브라우저에서는 A server error occurred. Please contact the administrator.라는 문장이 노출되었다.
디버그 화면이 나와야하는데 그렇지 않아서 어떻게 손을 써야할지 몰랐다.
사실 힌트는 이미 수십번 찾았다.

open(encoding="utf-8")

템플릿의 유니코드를 utf-8로 인코딩해주는 짧은 코드를 적재적소에 작성하면 되는 것이다.
그 적재적소를 찾는데 오랜 시간이 걸린 것 같다.
[https://chibychi.blogspot.com/2019/04/django-unicodedecodeerror.html?showComment=1556001026219#c6818858330997373051\](https://chibychi.blogspot.com/2019/04/django-unicodedecodeerror.html?showComment=1556001026219#c6818858330997373051)
바로 이 분 덕분에 정확한 위치를 찾아서 코드를 집어넣었다.

가상환경 폴더에서 Lib\site-packages\django\views\debug.py를 찾아

def get_traceback_html(self):
    """Return HTML version of debug 500 HTTP error page."""
    with Path(CURRENT_DIR, 'templates', 'technical_500.html').open() as fh:
        t = DEBUG_ENGINE.from_string(fh.read())
    c = Context(self.get_traceback_data(), use_l10n=False)
    return t.render(c)

비어있던 open() 메서드에 encoding="utf-8"을 기입했다.

def get_traceback\_html(self): 
    """Return HTML version of debug 500 HTTP error page.""" 
    with Path(CURRENT_DIR, 'templates', 'technical_500.html').open(encoding="utf-8") as fh: 
        t = DEBUG\_ENGINE.from_string(fh.read()) 
    c = Context(self.get_traceback_data(), use_l10n=False) 
    return t.render(c) 

가상환경 활성화를 꼭 잊지 않고 실행해야한다.

블로그 이미지

쵸잇

,

장고 2.1버전으로 새 블로그를 만들면서 프로젝트 폴더의 urls.py 안에서 인증 처리를 하는 방법을 배웠다.


기존에 내가 해오던 방식은, 


1) app폴더 urls.py에서 login, logout url을 등록

2) login 템플릿 만들기 (로그인 폼)

3) views.py에서 login, logout 함수를 호출하여 view 함수 만들기


3단계를 거쳐서 만들었다면,


1) 프로젝트 폴더 urls.py에서 django.contrib.auth에 포함된 views를 호출. views에 포함된 LoginView와 LogoutView를 path에 담아서 그대로 사용. 

2) login 템플릿 만들기 (로그인 폼)


1
2
3
4
5
6
7
8
9
10
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
    path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
    path('accounts/logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
]
cs



1
2
path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login')
# 템플릿 경로 
cs


1
2
path('accounts/logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout')
# next_page는 리다이렉트 페이지로서 기본페이지 설정
cs



그리고 로그인 폼


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{% extends 'blog/base.html' %}
 
{% block body %}
<div class="jumbotron">
  <h2>Please Login:</h2>
  <h3>(Must be SuperUser, please check with the site admin)</h3>
 
  {% if form.errors %}
    <p>Your username and password didn't match! Please try again.</p>
  {% endif %}
 
  <form action="" method="post">
    {% csrf_token %}
      {{ form.as_p }}
      <input type="submit" class="btn btn-primary" value="Login">
      <input type="hidden" class="next" value="{{ next }}">
  </form>
</div>
{% endblock %}
cs


블로그 이미지

쵸잇

,

pip freeze  

라이브러리 목록 보여주기


pip freeze > requirements.txt

requirements.txt 파일에 라이브러리 목록 담기


pip install -r requirements.txt

requirements.txt 파일에 담긴 라이브러리 클론하기

블로그 이미지

쵸잇

,

상황:

고객용으로 회원가입시 고객 그룹인 "client"가 자동으로 추가되고, 

업체용인 경우는 "partner"가 그룹으로 추가되도록 한다.

(매개변수 활용)



1) 고객용 회원가입 페이지 접속시 "client" 값을 인자로 지정한다.


1
2
3
def signup(request):
    ctx = { "is_client" : True }
    return common_signup(request, ctx, "client")
cs



2-1) group 매개변수를 통해 "client" 인자를 넘겨 받고, user에 "client" 그룹을 추가한다.


2-2) Group 클래스는 User 클래스와 ManyToMany 관계로 user.groups로 데이터 접근이 가능하다.


1
2
3
4
5
6
7
8
9
10
11
def common_signup(request, ctx, group):
    if request.method == "GET":
        pass
    elif request.method == "POST":
        username = request.POST.get("username")
        email = request.POST.get("email")
        password = request.POST.get("password")
 
        user = User.objects.create_user(username, email, password)
        target_group = Group.objects.get(name=group)
        user.groups.add(target_group)
cs



3) client로 회원가입한 경우 고객용 로그인 페이지, partner로 가입한 경우 업체용 페이지로 리다이렉트한다.


1
2
3
4
5
6
        if group == "partner":
            return redirect("/partner/login/")
        else:
            return redirect("/login/")        
 
    return render(request, "signup.html", ctx)
cs



4) 고객용 회원가입은 인자를 "partner"로 변경하면 된다.


1
2
3
def signup(request):
    ctx = {}
    return common_signup(request, ctx, "partner")
cs



**

상황:

고객이 로그인 시도할 경우 고객 그룹 소속여부에 따라 로그인처리를 한다.

고객 그룹이 아닐시 "접근 불가" 메세지를 노출시킨다.



1)


1
2
3
def login(request):
    ctx = { "is_client" : True }
    return common_login(request, ctx, "client")
cs

 

 

2) group 매개변수로 받은 "client" 인자와 조건문을 통해 해당 유저가 속한 그룹과 대조를 한다. 실패시 에러메세지를 출력한다. 


1
2
3
4
5
6
7
8
9
10
def common_login(request, ctx, group):
    if request.method == "GET":
        pass
    elif request.method == "POST":
        username = request.POST.get("username")
        password = request.POST.get("password")
        user = authenticate(username=username, password=password)
        if user is not None:
            if group not in [group.name for group in user.groups.all()]:
                ctx.update({ "error" : "접근 권한이 없습니다." })
cs



3) 로그인에 성공시 속한 그룹에 따라 메인페이지를 달리한다. 


1
2
3
4
5
6
7
8
9
10
            else:
                auth_login(request, user)
                next_value = request.GET.get("next")
                if next_value:
                    return redirect(next_value)
                else:
                    if group == "partner":
                        return redirect("/partner/")
                    else:
                        return redirect("/")
cs


블로그 이미지

쵸잇

,


상황:

현재 고객용, 업체용 로그인 view 함수가 넘겨주는 ctx를 제외한 나머지가 동일하다.

굳이 똑같은 코드를 2번이나 사용할 필요가 없으므로 1개로 줄이고 서로 호출해서 사용하고자한다.



1-1) login 함수의 내용을 common_login 함수로 옮긴다. 


1-2) 고객용 로그인에서 업체용 링크를 배치시켜야하므로 ctx를 매개변수로 포함시킨다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def common_login(request, ctx):
    if request.method == "GET":
        pass
    elif request.method == "POST":
        username = request.POST.get("username")
        password = request.POST.get("password")
        user = authenticate(username=username, password=password)
        if user is not None:
            auth_login(request, user)
            next_value = request.GET.get("next")
            if next_value:
                return redirect(next_value)
            else:
                return redirect("/partner/")
 
        else:
            ctx.update({ "error" : "존재하지 않는 사용자입니다." })
 
    return render(request, "login.html", ctx)
cs



2-1) common_login 함수를 호출하여 사용한다. 


2-2) ctx도 같이 넘겨준다.


1
2
3
def login(request):
    ctx = { "is_client" : True }
    return common_login(request, ctx)
cs


3) 회원가입 페이지도 마찬가지로 하나로 줄여서 호출해서 사용하며, 업체용 view에서는 로그인 함수를 import하여 사용한다.

블로그 이미지

쵸잇

,


상황:

2개의 로그인 함수(업체용, 고객용)가 1개의 로그인 템플릿공유하고 있다.

기본 로그인 페이지고객용에 해당하고, 업체용고객용 페이지 하단에 링크를 만들어 링크를 통해 이동한다.

단, 해당 링크고객용에서만 노출되고, 업체용 로그인 페이지에서는 링크가 노출되지 않아야한다.




1-1) 우선 링크를 작성하면 양쪽 로그인 페이지 하단에서 링크가 노출되고 있다.


1-2) 조건문을 작성하여 "참(True)"인 경우에만 링크가 노출되도록 한다. 



1
2
3
4
5
{% if is_client %}
<div class="text-center">
  <a href="/partner/login/">업체용 로그인 페이지</a>
</div>
{% endif %}
cs

 


2) 고객용 로그인 뷰 함수에서 ctx를 통해 "True"값을 전달한다.


1
2
def login(request):
    ctx = { "is_client" : True }
cs



업체용 로그인 뷰에서는 True 값이 전달되지 않으므로 링크가 노출되지 않는다.

블로그 이미지

쵸잇

,

장고 Admin에서 각 user의 소속 그룹지정해줄 수 있다.

그리고 그룹에 따라 접속 가능한 페이지도 나눌 수 있다.



상황:

"partner" 그룹에 속한 유저만 해당 페이지에 접속 허용하고, 그 외 그룹에 속하지 않는 유저메인페이지리다이렉트한다.



1
2
3
4
def menu_add(request):
    ctx = {}
    if "partner" not in request.user.groups.all():
        return redirect("/")
cs


1) 그룹 데이터직접 접근한다.


조건문을 작성하여 해당 페이지에 접속한 유저의 그룹이 필요로하는 "partner" 값인지 유무를 따지고, 없다는 조건이 참이면 메인페이지로 리다이렉트한다.


단, 여기서도 동일한 내용의 코드를 여러 뷰 함수에 담아서 사용하는데에 발생하는 번거로움과 늘어나는 코드량이 문제가 될 수 있다.



2) 1)번과 동일한 기능은 갖춘 user_passes_test 데코레이터 활용하기


1
2
3
4
5
6
7
8
9
from django.contrib.auth.decorators import user_passes_test
 
URL_LOGIN = '/partner/login/'
 
def partner_group_check(user):
    return "partner" in user.groups.all()
 
@user_passes_test(partner_group_check, login_url=LOGIN_URL)
def menu_add(request):
cs


user_passes_test 데코레이터를 사용하기 위해서는 별도의 함수 작성이 필요하다.

함수명은 partner_group_check로서 유저의 그룹이 "partner"인지 확인하는 것으로 만약 거짓이라면 로그인 페이지리다이렉트하게 된다.


*매개변수를 user로 받는데, 이 user는 menu_add 함수의 request.user와 동일하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
URL_LOGIN = '/partner/login/'
 
def partner_group_check(user):
    return "partner" in user.groups.all()
 
@user_passes_test(partner_group_check, login_url=LOGIN_URL)
def menu_add(request):
 
@user_passes_test(partner_group_check, login_url=LOGIN_URL)
def menu_edit(request):
 
@user_passes_test(partner_group_check, login_url=LOGIN_URL)
def menu_delete(request):
cs


partner 그룹에 속한 유저만 사용해야하는 모든 경우에 데코레이터를 손쉽게 가져다 사용할 수 있다.




블로그 이미지

쵸잇

,

상황:

패스트푸드 사이트에서 메뉴페이지신메뉴를 추가시킬 수 있는 권한관리자로그인한 경우만 가능하다.

따라서 관리자가 아닌 익명의 유저URL를 직접 입력하여 관리자의 권한이 필요한 페이지로 접근을 하려고한다.



1) User 모델의 속성 중 하나인 "is_anonymous"를 활용한다.


다른 속성인 "is_authenticated"는 해당 유저가 인증이 되었는지 유무를 알려준다.

"is_anonymous"는 User와 AnonymousUser 객체의 차이를 두기 위해 사용한다.

확인을 위해서는 request.user.is_anonymous 형태로 사용된다.


1
2
if request.user.is_anonymous:
    return redirect("/partner/login/")
cs


익명의 사용자라는 것이 확인되면 로그인 페이지리다이렉트한다.



2) 1)번을 간편하게 쓸 수 있는 login_required 데코레이터활용한다.


1
2
3
4
from django.contrib.auth.decorators import login_required
 
@login_required(login_url='/accounts/login/')
def my_view(request):
cs


view 함수 위에 해당 데코레이터와 로그인 페이지를 입력하면 AnonymousUser를 즉시 로그인 페이지리다이렉트 처리한다.




**

데코레이터를 반복적으로 여러 뷰 함수에 사용하는 경우 (login_url='/accounts/login/') 부분이 중복되므로 상수처리한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
URL_LOGIN = '/accounts/login/'
 
@login_required(login_url=URL_LOGIN)
def my_view(request):
 
@login_required(login_url=URL_LOGIN)
def your_view(request):
 
@login_required(login_url=URL_LOGIN)
def his_view(request):
 
@login_required(login_url=URL_LOGIN)
def her_view(request):
cs



**

새로운 상황:

접근 권한이 필요한 페이지URL을 통해 직접 접근한 경우, 

login_required 데코레이터에 의해 로그인 페이지로 넘어가는데,

이때 login_required 데코레이터는 로그인한 경우 해당 페이지리다이렉트해주는 기능이 포함되어있다.

그러나 로그인 뷰에 의해 해당 페이지가 아닌 로그인 뷰에 지정된 페이지리다이렉트가 된다.



로그인 후 해당 페이지로 즉시 이동하려면?


URL을 살펴보면, "127.0.0.1:8000/partner/login/?next=/partner/menu/"이다.

URL 중 쿼리스트링으로 작성된 부분인 "?next=/partner/menu/"에서 "next"의 값으로 "/partner/menu/" URL를 갖고 있다.

이 쿼리스트링 부분을 login_required 데코레이터가 만들어 준 것이다.


로그인 뷰에서 GET 방식을 통해 URL에서 "next" 값을 호출하여 "next_value"라는 변수로 정의한다.

조건을 만들어서 URL에 "next" 값이 존재한 경우에는 그 값에 해당하는 URL로 리다이렉트하고,

"next" 값이 존재하지 않다면 메인페이지로 이동하도록 한다.


1
2
3
4
5
6
7
if user is not None:
    auth_login(request, user)
    next_value = request.GET.get("next")
    if next_value:
        return redirect(next_value)
    else:
        return redirect("/partner/")
cs







블로그 이미지

쵸잇

,