본문 바로가기

PYTHON

20240408 ~ 20240412 9주차 정리

.★ 가상환경 생성
python -m venv venv

★ 가상환경 활성화
(Window) activate 폴더로 이동(cd 명령어) -> 해당 폴더 경로에서 activate 입력

pip install --upgrade pip

pip install django==4.2 (4.2는 2024년 기준 LTS 버전. 각 연도에 맞는 LTS 버전을 입력)

pip install django-extensions (기본 Django Shell 보다 더 많은 기능이 있는 shell plus를 제공)

pip install ipython (기본 Python Shell 에 여러 기능(자동 완성, 색상 강조 등)을 제공)

pip freeze > requirements.txt

------------------------------
★★★★★ Django Project

django-admin startproject <프로젝트명> -> '프로젝트명' 이름을 가진 폴더를 생성하며 프로젝트 시작
django-admin startproject <프로젝트명> . -> '프로젝트명' 이름으로 현재 폴더에서 프로젝트 시작

프로젝트 폴더로 이동 (manage.py 가 있는 폴더)

-> django-extensions 앱 등록 진행
INSTALLED_APPS = [] -> 여기에 "django_extensions", 추가 (trailing comma 주의)

python manage.py runserver -> 로컬 서버 실행

[주요 사용]
★ settings.py
★ urls.py

------------------------------
★★★★★ Django App

프로젝트 > 앱. 프로젝트는 앱을 다수 포함할 수 있음
앱은 하나의 기능 단위를 가지는 모듈
앱은 생성 -> 등록의 과정을 거침

python manage.py startapp <앱 이름> - -> '앱 이름' 이름으로 된 앱 생성

프로젝트 폴더의 settings.py -> INSTALLED_APPS 리스트 안에 앱 이름 작성 -> 앱 등록 완료 (trailing comma 주의)

+ app 폴더 안에 templates 폴더 생성 시 구조를 Templates Namespace를 지켜서 구조를 잡으면 좋다. (<app 이름>/templates/<app 이름>)
자세한 건 Templates Namespace 항목 참고

[주요 사용]
★ models.py
★ views.py
+ urls.py (직접 생성)
+ forms.py (직접 생성)

☆ 파이참 로컬 앱 import error(빨간 줄) 해결
https://devlog.jwgo.kr/2019/05/30/unresolved-reference-error-in-pycharm/

------------------------------
MVC 모델 -> Django MTV 모델

- **Model** : 데이터와 관련된 로직을 관리 -> Model
- **View** : 레이아웃과 관련된 화면을 처리 -> Template
- **Controller** : Model과 View를 연결하는 로직을 처리 -> View

------------------------------
urls.py -> Flask의 app.route와 역할이 같음
해당 주소의 요청을 받았을 때 어떤 파일(앱)을 실행할지 명시

앱의 views.py -> 함수형 뷰, 클래스형 뷰로 작성 가능
장고 초기엔 함수형 뷰로 작성하는 것을 권장 (흐름을 보기 위함)

urls.py 에 각 앱의 py 파일 import하여 함수 호출 준비를 마침
주소 요청을 받을 시 각 함수 호출을 할 수 있도록 path(경로, 실행할 함수) 형태로 작성

실행할 함수에서는 return render(request, html 파일 이름, context(데이터)) 형태로 작성하여 html 파일을 response(응답)하도록 함

render에서 html 파일 이름만 작성해도 바로 찾을 수 있는 이유는
프로젝트 폴더의 settings.py의 TEMPLATES를 확인해보면, 'APP_DIRS': True로 설정하였기 때문임.
각 앱의 templates 폴더에서 html 파일을 관리하겠다는 의미로 알아두면 된다.

------------------------------
Django Template Language (DTL)

탬플릿 상속

base.html
{% block <이름> %}
{% endblock <이름> %}

func.html
{% extends "base.html" %} -> 탬플릿 상속

{% block <이름> %}
{% endblock <이름> %} -> 해당 이름을 가진 block에 보여줄 html 작성

★ 커스텀 탬플릿 경로 설정

프로젝트 폴더의 settings.py의 TEMPLATES를 확인해보면, 'DIRS': [] 안에 경로를 작성하면 됨.
경로 형태: BASE_DIR/"templates" (BASE_DIR: settings.py에 명시되어있음. '/'를 바로 붙일 수 있는 이유는 BASE_DIR가 Path 클래스이기 때문

------------------------------
쿼리스트링 (Query String Parameter)

데이터를 URL주소에 포함시켜 전송하는 방식
'?' 뒤에 데이터가 위치, '&' 로 연결된 'key=value' 형태로 구성

dict.get("key", Something) -> dict 딕셔너리에서 "key" 라는 키를 가진 value를 가져오는데, 해당 key가 존재하지 않을 경우 Something을 리턴

------------------------------
★ URL Dispatcher

★ 트레일링 슬래시(Trailing slash): URL 뒤에 붙는 슬래시

★ Variable Routing
- URL 일부를 변수로 지정하여, 해당 부분에 들어온 값을 view로 넘겨줌
- view에서 변수를 받아서 그 부분에 맞게 처리

path("A/<str:B>", views.func), -> urls.py 에서 다음과 같이 추가
def func(request, B): -> views.py 에서 B를 인자로 받아 수행

★ Multiple Apps

각 앱마다 urls.py 생성 가능
각 앱마다 각 앱 폴더의 views를 import 하여 urlpatterns 작성 가능
프로젝트 폴더의 urls.py에서 각 앱 폴더의 urls.py를 포함하는 과정을 진행하고 싶다면

from django.urls import path, include -> include 추가

urlpatterns = [
    path("/<app 이름>", include("<app 이름>.urls")) -> '/<app 이름>' 형태의 url 패턴이 발견될 경우, 해당 앱의 urls.py에서 처리하도록 넘김
]

각 앱 폴더의 urls.py에서는, 프로젝트 폴더의 urls.py에서 처리한 앞 부분('/<app 이름>')을 제외한 뒷 부분만을 명시하여 처리하도록 함.

(예시)
urlpatterns = [
    path("", views.func1),
    path("<str:username>/", views.func2),
]

★ Naming URL Patterns

- 어떠한 URL을 작성할 때 직접 하드코딩 하지 않고 각각의 URL에 ‘이름’을 붙임
- view와 template에서 특정 경로에 대한 의존성을 제거

urlpatterns = [
    path("", views.func1, name='A'), -> 해당 path를 'A' 라는 이름으로 명명
]

이후 templates 안의 html 파일들에서 "{% url 'A' %}" 형태로 작성하여 해당 url 사용 가능

+ URL Namespace 까지 사용하여 작성하는 것이 좋다. 자세한 건 URL Namespace 항목 참고

------------------------------
DB Setting

★ 각 app 폴더 안의 models.py에서 아래와 같이 작성

from django.db import models

class <Table 이름>(models.Model): -> <Table 이름> 의 이름을 가진, models.Model 클래스를 상속받은 자식 클래스 정의
    <Field 이름> = models.<Field 타입>(<Option>=) -> field 명을 <Field 이름>으로 정의, 해당 field의 타입을 <Field 타입>으로 정의, 해당 Field의 옵션을 '<Option>=' 형태의 parameter로 지정

★ models.py에서 작성한 내용을 DB에 반영하는 과정(Migrate)

python manage.py makemigrations -> models.py에서 변경한 내용을 토대로 migration 생성 ★★★★★

python manage.py migrate -> migration 내용을 DB에 반영 ★★★★★

★ 생성일시, 수정일시 내부적으로 자동 데이터 반영

models.py에서

create_dt = models.DateTimeField(auto_now_add=True) -> row가 생성될 때 자동으로 'row가 추가된 일시'로 반영
update_dt = models.DateTimeField(auto_now=True) -> row가 수정될 때 자동으로 'row가 수정된 일시'로 반영

해당 내용으로 migration 생성하려고 할 시
-> 안내 문구 출력됨. (옵션 1: 기존 데이터 행들에 입력한 default 값을 추가함 / 옵션 2: 취소하고 default 값을 models.py에서 설정)
-> 1번 선택 후 문구 출력 -> 엔터를 바로 누르면 현재 시간으로 반영, 그렇지 않고 원하는 시간대를 입력하고 반영할 수도 있음

------------------------------
ORM (Object Relational Mapping) -> python 으로 데이터 조작을 가능하게 해줌

Django ORM Manager (기본 이름: Objects)

django shell 사용 위해 django-extensions, ipython 설치 (프로젝트 시작 시 설치)

shell로 Database 조회
-> Python 에서는 DB를 조회할 때 API의 도움을 받아 조회할 수 있음.
Django ORM Manager으로 Database API를 사용하여 DB 조작 가능

MyModel .objects .all()
Model Class  .Manager .QuerysetAPI

Model Class: 각 앱의 models.py에서 정의한 class(DB Table) 명
Manager: Django ORM Manager 호출 이름(objects로 고정)
QuerysetAPI: 특정 query 수행을 도와주는 API 명


★ Create


row = <Model Class 명>() -> Model Class 인스턴스 생성
row.<column A> = 'Data A' -> row의 'A Column'에 'Data A' 넣기
row.<column B> = 'Data B' -> row의 'B Column'에 'Data B' 넣기
row.save() -> DB에 insert (실제로 insert 됨)


row_02 = <Model Class 명>(
    column A='Data A',
    column B='Data B'
) -> Model Class 인스턴스를 생성하면서 각 column 속성에 데이터 정의
row.save() -> DB에 insert (실제로 insert 됨)


<Model Class 명>.objects.create(
    column A='Data A',
    column B='Data B'
) -> Manager를 통해 바로 insert (실제로 insert 됨)


※ magic method 사용
models.py 에서 각 Model Class 마다

def __str__(self):
    return self.<column A>

해당 메서드를 정의할 경우 Django Shell에서 .all() 등의 API를 호출하면 <<Model Class 명>: 'column A 데이터'> 형태로 print 하게 됨
python class 메서드를 정의한 것이기 때문에 DB에 변동사항은 없다. ★ 따라서 migrate를 수행할 필요가 없다.


★ Read

.all() -> 해당 Model Class(DB Table)의 모든 row 조회 (QuerySet, iterable한 자료형, List 처럼 indexing 가능)
.get() -> 해당 조건에 맞는 row 하나만 조회 (조건은 column을 매개변수로 설정 가능. ex: <column A>='Data A')
.filter() -> 해당 조건에 맞는 모든 row 조회 (조건은 lookup 이라고 부름. lookup 형태는 다양하므로 공식 문서 참고(https://docs.djangoproject.com/en/4.2/topics/db/queries/#field-lookups))
+ 해당 조건에 맞는 row가 없을 경우 빈 QuerySet을 return


★ Update

update_row = <Model Class 명>.objects.get(id=N) -> 해당 Model Class(DB Table)에 N번째 row를 조회한 것을 변수 update_row에 넣기
update_row.<column A> = 'Data C' -> update_row의 <column A> 속성(column A 데이터)을 'Data C'로 변경
update_row.save() -> DB에 update (실제로 update 됨)


★ Delete

delete_row = <Model Class 명>.objects.get(id=N) -> 해당 Model Class(DB Table)에 N번째 row를 조회한 것을 변수 delete_row에 넣기
delete_row.delete() -> DB에서 delete (실제로 delete 됨)

------------------------------
CRUD 활용

★ DB 조작을 위해 각 앱의 views.py 에서 'from <앱 이름>.models import <Class 명(DB Table)>' 을 통해 import 해준다.
(<앱 이름> 부분은 생략 가능하다. .models 의 의미는 현재 폴더(앱) 내 models.py를 가리킴)
-> 이렇게 해야 Django ORM Manager의 도움을 받아 DB 조작을 수행할 수 있다.

.order_by(<column A>) -> 조건에 맞게 가져온 QuerySet을 'column A'를 기준으로 오름차순으로 정렬한다 (내림차순일 경우 -<column A> 와 같이, '-'를 붙여준다.)
ex) <Model Class 명>.objects.all().order_by(<column A>)

★ HTTP Method

GET: R
POST: CUD

- GET 방식은 데이터를 URL에 담고, POST 방식은 Body에 담는다.
- 따라서 GET 방식은 데이터를 전송하는 데에 한계(URL 길이에 한계가 있음)가 있으나, POST 방식은 그렇지 않다.
- POST 방식으로 데이터를 전송할 때는 CSRF Token이 필요하다. (CSRF 방지)

★ 사이트간 요청 위조 (CSRF. Cross-Site-Request-Forgery)
- 정의
유저가 실수로 해커가 작성한 로직을 따라 특정 웹페이지에 C U D 요청을 보내게 되는 것
서버 입장에서는 어디서 온 요청인지 구분 필요. 즉, 정말 유저가 의도한 요청인지 검증이 필요

- 해결
CSRF Token을 사용해서 방지

- 사용법
POST 요청을 보낼 form 태그 안에 {% csrf_token %} 작성 -> 프로젝트 폴더 안의 settings.py에서 MIDDLEWARE - 'django.middleware.csrf.CsrfViewMiddleware' 에서 처리


★ 리다이렉션 (redirect)
- 정의
지정한 URL로 되돌리는 것

- 사용법
각 앱의 views.py 에서 from django.shortcuts import render, redirect -> redirect 추가
views.py에서 정의한 함수에서 return redirect("Name A") -> url Naming text를 첫번째 param 값으로 전달 (그냥 url 사용해도 되지만 하드 코딩을 피한다.)

이러한 패턴을 PRG(Post-Redirect-Get) 패턴이라고 함.

- 심화
★ Variable Routing 시 parameter 값 넘기기

views.py에서 작성 시: return redirect("Name A", <val A>) -> url Naming text를 첫번째 param 값, val A 를 두번째 param 값으로 전달
html(Template)에서 작성 시: {% url 'Name A' <val A> %} -> url Naming text 뒤에 val A 를 이어서 작성. comma로 구분하지 않는다.

------------------------------
Django Form Class

★ 데이터 유효성 검사
django는 일부 반복되는 작업과 코드를 줄일 수 있는 django form을 제공
사용해도 되고 안해도 됨. 선택 사항


★★★ Django Form 사용 방법
각 앱 폴더 하위에 forms.py 파일 생성

from django import forms -> 모듈 import

class <Form 이름>(forms.Form): -> <Form 이름> 의 이름을 가진, forms.Form 클래스를 상속받은 자식 클래스 정의
    <Input Field 이름> = forms.<Field 타입>(<Option>=) -> Input Field 명을 <Input Field 이름>으로 정의, 해당 field의 타입을 <Field 타입>으로 정의, 해당 Field의 옵션을 '<Option>=' 형태의 parameter로 지정

★ Django Form 사용을 위해 각 앱의 views.py 에서 'from <앱 이름>.forms import <Form Class 명)>' 을 통해 import 해준다.
(<앱 이름> 부분은 생략 가능하다. .forms 의 의미는 현재 폴더(앱) 내 forms.py를 가리킴)
-> 이렇게 해야 forms.py에서 작성한 Django Form을 Template에 context로 전달하여 반영할 수 있다.

import 후
return render(request, "<template A>.html", context={"form": <Form 이름>()})
을 통해 template A에 context로 작성한 Django Form을 전달할 수 있다.

이후 templates 안의 html 파일들에서 {{ form.as_p }} 등을 통해 Django Form을 보여줄 수 있다.
as_div, as_p, as_ul, as_table 등으로 Form이 보이는 형태를 변경할 수 있다.


★ Form Widget

forms.py 에서 각 input field를 정의할 때, 'widget=' parameter를 넘겨주면 해당 type으로 html에 보여지게 된다.
ex) widget=forms.Textarea -> Textarea 속성을 가진 input field가 된다.

자세한 내용: https://docs.djangoproject.com/en/4.2/ref/forms/widgets/#module-django.forms.widgets


★ Dropdown 형태 input

SOME_CHOICES = [
    ('Data A', "Show A"), -> 'Show A'로 보이고 실제 데이터로 'Data A'가 반영되는 선택지를 작성한다
    ('Data B', "Show B"),
    ('Data C', "Show C"),
]

class <Form 이름>(forms.Form):
    <Input Field 이름> = models.ChoiceField(choices=SOME_CHOICES) -> 해당 형태로 작성하면 SOME_CHOICES에서 작성한 선택지들로 보여지는 드롭다운 input을 만들 수 있다.


★★★ Django ModelForm

- 사용 방법
각 앱 폴더 하위에 forms.py 파일 생성

from django import forms -> 모듈 import
from .models import <Model Class A> -> 현재 forms.py와 동일한 폴더(앱) 내의 models.py 에서 Model Class A(DB Table)를 import

class <ModelForm 이름>(forms.ModelForm): -> <Form 이름> 의 이름을 가진, forms.ModelForm 클래스를 상속받은 자식 클래스 정의
    class Meta: -> Meta 클래스를 정의. 하위 속성값을 설정한다.
        model = <Model Class A> -> 참조하는 모델(DB Table)
        fields = "__all__" -> ("col A", "col B")와 같은 column을 담은 tuple 형태로 전달해도 되지만, "__all__" 을 통해 모든 column을 전달 
        exclude = ("exclude col A", "exclude col B") -> exclude col A, exclude col B column을 제외한다.

★ Django ModelForm 사용을 위해 각 앱의 views.py 에서 'from <앱 이름>.forms import <ModelForm Class A>' 을 통해 import 해준다.
(<앱 이름> 부분은 생략 가능하다. .forms 의 의미는 현재 폴더(앱) 내 forms.py를 가리킴)
-> 이렇게 해야 forms.py에서 작성한 Django ModelForm을 Template에 context로 전달하여 반영할 수 있다.

import 후
return render(request, "<template A>.html", context={"form": <Form Class 명)>()})
을 통해 template A에 context로 작성한 Django ModelForm을 전달할 수 있다.

이후 templates 안의 html 파일들에서 {{ form.as_p }} 등을 통해 Django ModelForm을 보여줄 수 있다.
as_div, as_p, as_ul, as_table 등으로 Form이 보이는 형태를 변경할 수 있다.


★ Django ModelForm으로 받은 데이터를 바로 DB에 반영할 수 있다.

views.py 안의 함수에서
form = <ModelForm Class A>(request.POST) -> request.POST 데이터를 바인딩한 ModelForm Class A 인스턴스를 생성
if form.is_valid(): -> is_valid()를 통해 데이터 바인딩된 form의 유효성을 확인하고, 괜찮다면
    row = form.save() -> 해당 form을 저장하고 return한 form을 article에 저장(insert row 데이터를 저장) 
    return redirect("Name A", row.id) -> Name A로 naming 된 url로 리다이렉션 하면서 row의 id 값(index 값)을 url variable로 넘긴다.
return redirect("Back") -> form이 유효하지 않을 경우 이전 페이지로 리다이렉션 한다.


★ 작성 페이지와 DB 처리를 하나의 함수 안에서 처리할 수 있다.

페이지 이동: GET
페이지 내 데이터를 전송해 DB에 CUD 처리: POST

따라서 request 방식에 따라 분기하여 처리하면 하나의 함수로 처리 가능

ex)
def create(request):
    # POST 요청인 경우 -> POST 요청인 경우
    if request.method == "POST":
        form = <ModelForm Class A>(request.POST)
        # form이 유효한 경우
        if form.is_valid(): -> form이 유효한 경우 DB에 저장
            new_article =  form.save()
            return redirect("rows", new_article.id)
        # 유효하지 않은 경우 -> 유효하지 않은 경우 작성 페이지로 리다이렉션
        else:
            return redirect("create")
    # GET 요청인 경우 -> GET 요청인 경우 (리다이렉션도 포함)
    else:
        return render(request, create.html, context={"form": ArticleModelForm()})


★ 수정 페이지도 작성 페이지와 마찬가지로 하나의 함수 안에서 처리할 수 있다.

ex)
def update(request, row_idx):
    update_row = <Model Class A>.objects.get(id=row_idx)
    
    if request.method == "POST":
        form = <ModelForm Class A>(request.POST, instance=update_row) -> 'instance=' parameter로 수정할 row를 지정할 수 있다.
수정할 row에 바인딩한 request.POST 데이터로 수정할 준비를 한다.
        if form.is_valid():
            form.save()
            return redirect("detail", row_idx)
        else:
            return redirect("update", row_idx)
    else:
        context = {
            "idx": row_idx,
            "article_form": <ModelForm Class A>(instance=update_row),
        }
        return render(request, "update.html", context=context)

------------------------------
★★ URL Namespace

서로 다른 앱의 urls.py에서 URL naming을 했는데, 그 name이 서로 같은 경우
template에서 해당 name의 url을 작성하면 어떤 url로 이동하는가?
-> 프로젝트 폴더 안의 settings.py에서 INSTALLED_APPS 에 작성한 앱 중 가장 먼저 작성한 앱의 url로 이동함.

이런 상황을 방지하기 위해
① 서로 다른 url naming을 할 수 있지만
② URL Namespace 를 사용하여 같은 url naming을 사용할 수 있다.

- 사용법
각 앱의 urls.py 에서

app_name = <app name A> -> app_name = <원하는 이름> 형태로 지정. 보통은 app의 이름으로 하는 것이 좋음

각 template 에서

{% url <app name A>:<url_name A> %} -> app_name:url_name 형태로 작성 (ex: articles:write, articles:delete 등)

★ 주의: URL Namespace를 사용할 경우 해당 앱의 template에서 naming된 url로 작성한 모든 부분을 app_name:url_name 형태로 수정해야 함!


★★ Templates Namespace

서로 다른 앱의 templates 폴더 안의 template를 호출하려는데, 그 파일 이름이 서로 같은 경우
해당 name의 template을 호출하면 어떤 template를 렌더링하는가?
-> 프로젝트 폴더 안의 settings.py에서 INSTALLED_APPS 에 작성한 앱 중 가장 먼저 작성한 앱의 templates 폴더 안의 template를 호출

이런 상황을 방지하기 위해
① 서로 다른 이름을 사용할 수 있지만
② Templates Namespace 를 사용하여 같은 template 파일 이름을 사용할 수 있다.

- 사용법
각 앱의 templates 폴더 안에 <앱 이름> 으로 된 폴더 하나를 더 생성한 후, 해당 폴더 안에 template 들을 넣는다.
(경로가 <앱 이름>/templates/<앱 이름> 형태라 햄버거 형태)

views.py 에서

기존에 <template A>.html 로 작성했다면, 이를 <app name>/<template A>.html 로 수정하면 된다.

------------------------------
★★★ Auth

인증(Authentication)과 권한(Authorization)을 합쳐서 Auth라고 함. (=인증 시스템)

HTTP 특징
- 비연결지향 (Connectionless): 한 번 요청에 대한 응답을 하면 연결이 끊어짐
- 무상태(Stateless): 연결이 끊어지면 통신이 끝나고 서로를 잊어버림. 모든 메세지는 독립적

HTTP 특징으로 인해 기억할 수 없다 -> 쿠키, 세션을 통해 해결

★ 쿠키 (Cookie)

- 서버에서 웹 브라우저에 전달하는 작은 데이터 조각. 유저가 웹을 방문하게 되면 서버로부터 쿠키를 전달받음
- Key-Value 형태로 데이터 저장
- 이후 동일한 서버에 보내는 모든 요청에 쿠키가 함께 전달
- 유저 로컬에 저장됨

웹 페이지에 요청을 보내면 서버가 쿠키를 함께 전달하고
이후부터는 같은 서버에 보내는 모든 요청에 쿠키를 함께 담아서 요청을 보내게 된다.

그러나 쿠키는 변조가 용이하기 때문에, 신뢰할 수 없다. -> 따라서 세션과 같이 사용

★ 세션 (Session)

- 세션과 쿠키가 쓰이는 방법
1. 클라이언트가 서버에 접속하면
2. 서버가 특정 session id를 발급하고 기억 (DB or 메모리)
3. session id 전달받아 쿠키에 저장
4. 이후 클라이언트는 해당 쿠키를 이용해서 요청
5. 서버에서는 쿠키에서 session id를 꺼내서 검증
6. 검증에 성공했다면 알맞은 로직을 처리
-> 쿠키에 민감한 정보를 저장하지 않고 session id만 저장해두면, 이를 서버에서 검증하는 방식으로 사용
-> ★★★ 로그인은 이러한 절차로 구현


★ 세션쿠키 (Session Cookie)
- 현재의 세션이 종료되면(브라우저가 닫히면) 삭제

★ 지속쿠키 (Persistent Cookie)
- 디스크에 저장되며 브라우저를 닫거나 컴퓨터를 재시작해도 존재
- Max-Age를 지정하고, 해당 기간이 지나면 삭제 가능


★★★★★ Django Authentication System

★ Authentication Form
- Django Built-in Form
- 로그인을 위한 기본적인 form 제공

★ login()
- 개발자가 직접 구현하지 않아도 login() 함수 하나만 사용하면 앞선 로그인 절차 모두 실행
- 사용자 로그인 처리, 내부적으로 session 사용 및 user 정보 저장 의 모든 과정


로그인은 세션을 생성하는 과정이므로 POST 요청이어야 함. (<form method="POST">)

- Authentication Form 사용법
views.py 에서

from django.contrib.auth.forms import AuthenticationForm -> 필요 모듈 import

def login(request):
    return render(request, "accounts/login.html", context={"form": AuthenticationForm()}) -> context에 AuthenticationForm 인스턴스를 전송

template 에서 {{ form.as_p }} 형태로 작성 (Django Form, Django ModelForm 과 유사)


★ superuser 생성 (cmd)

python manage.py createsuperuser
Username (leave blank to use 'mjchu'): admin
Email address: admin@sparta_ai_6_test.com
Error: Enter a valid email address.
Email address: admin@sparta-ai-6-test.com 
Password: admin1234 -> password 입력 시 실제로는 보이지 않음
Password (again): admin1234 -> 확인 절차도 마찬가지
The password is too similar to the username.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y -> 해당 비밀번호를 정말 사용하겠냐? Y
Superuser created successfully.


★ 로그인 구현

from django.contrib.auth import login as auth_login -> 필요 모듈 import

def login(request):
    if request.method == "POST":
        form = AuthenticationForm(data=request.POST) -> ★ 'data=' 파라미터를 꼭 지정해야 함. Django Form, Django ModelForm 사용할 때와 다름
        if form.is_valid():
            auth_login(request, form.get_user()) -> login(request, form.get_user()) 형태.
이를 진행하면 DB Table 중 'django_session' 에 세션이 저장된다.
            return redirect("<url_name A>") -> 로그인 성공 후 원하는 페이지로 리다이렉션
    else:
        return render(request, "accounts/login.html", context={"form": AuthenticationForm()}) -> GET 요청일 경우 로그인 페이지 렌더링


★ 로그아웃 구현

from django.contrib.auth import logout as auth_logout -> 필요 모듈 import

def logout(request):
    auth_logout(request) -> logout(request) 형태.
이를 진행하면 DB Table 중 'django_session' 에 저장해뒀던 세션이 삭제된다.
    return redirect("<HOME>") -> 원하는 로그아웃 페이지(홈페이지 등)로 이동


★ 로그인, 로그아웃은 base.html 에 배치해서 이 template를 상속받는 모든 페이지에서 보이도록 한다. (<div class="navbar"> 안에 넣으면 상단에 위치)


★ Django가 HTTP요청을 처리하는 방법
자세한 내용: https://docs.djangoproject.com/en/4.2/topics/http/shortcuts/#module-django.shortcuts

① Django shortcut functions
HTTP 상태 코드
2xx: 정상
4xx: 클라이언트 문제
5xx: 서버 문제

클라이언트의 문제로(잘못된 접근) DB 데이터를 조회하는 과정에서 error 발생 -> 그냥 두면 5xx 상태 코드(서버 문제)를 던져줌
이를 4xx 상태 코드(클라이언트 문제)로 던져주도록 만들어주는 함수를 Django shortcut functions 라고 함.

- get_object_or_404()
get을 호출했는데 데이터가 존재하지 않는다면 404 에러를 raise하여 404 페이지로 이동

- get_list_or_404()
filter를 호출했는데 데이터가 존재하지 않아 빈 리스트라면 404 에러를 raise하여 404페이지로 이동


② 데코레이터
데코레이터 함수로 안의 함수를 감싸준다.

사용 이유:
- 가독성
- 사전 예외 처리로 안의 함수 작동을 보호

@require_http_methods([HTTP.method]) -> require_http_methods(["GET", "POST"]) 와 같은 형태로 작성. GET, POST 요청만 처리한다.
@require_POST -> POST 요청만 처리한다.

해당 데코레이터들을 views.py 에서 사용하기 위해
from django.views.decorators.http import require_http_methods, require_POST -> 모듈을 import 한다.


★ Template with Auth

① template으로는 우리가 context를 넘기지 않아도 자동으로 넘어가는 context들이 존재 -> 항상 참조할 수 있는 context
-> request. 으로 접근

로그인 사용자 id: request.user.username
로그인에 성공한 사용자인가? (bool): request.user.is_authenticated

이것은 template, view 모두 사용 가능하다.

② 데코레이터 '@login_required'

해당 데코레이터를 작성하면 로그인이 되어있지 않은 상태로 view 함수 실행 시
프로젝트 폴더의 settings.py에 LOGIN_URL 에 작성한 url로 이동(리다이렉션)하게 된다.
-> default는 작성하지 않은 상태이며, default url은 '/accounts/login/' 이다.
-> 그래서 계정과 관련된 앱인 경우 accounts 로 앱 이름을 설정하는 것이다. 로그인 url도 login으로 하는 것이 관례.

해당 데코레이터를 views.py 에서 사용하기 위해
from django.contrib.auth.decorators import login_required -> 모듈을 import 한다.

해당 데코레이터를 통해 default url로 리다이렉션 할 때
뒤에 쿼리 스트링 '?next=로그인하지 않은 상태로 접근하려던 경로' 를 추가한다. ('?next=/articles/create/')

이 쿼리 스트링을 view 단에서 처리해주면
로그인 버튼을 눌러서 로그인 페이지로 이동했을 때와
로그인하지 않았는데 로그인 해야지만 이동할 수 있는 페이지로 접근하여 데코레이터를 통해 리다이렉션했을 때를
분기하여 처리할 수 있다.

- 적용
views.py의 login 함수에서

next_path = request.GET.get("next") or <로그인 후 이동할 url name>
return redirect(next_path)

와 같이 작성하여 분기 처리

login template에서

기존 로그인 버튼의 form 태그 속성 중 action 값을 비운다.(<form action="">)
이렇게 되면 현재 url로 다시 POST 요청을 보내게 되어 의도한 분기 처리를 수행하게 된다.
이를 수정하지 않고 진행하면, 데코레이터에서 처리해준 쿼리 스트링이 제외된 default url(login url)로 POST 요청을 보내기 때문에
views.py에서 분기 처리한 부분에서 <로그인 후 이동할 url name>이 next_path에 담기게 된다.

------------------------------
회원기능 구현

★ 회원 가입

- UserCreationForm 사용법
views.py 에서

from django.contrib.auth.forms import UserCreationForm -> 필요 모듈 import

@require_http_methods(["GET", "POST"])
def signup(request):
    if request.method == "POST":
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            """
            # 회원 가입 후 바로 로그인을 원할 경우
            user = form.save() -> 회원 가입 후 바로 로그인까지 진행할꺼면 user에 row 데이터를 저장하고
해당 row 데이터를 user 데이터로 해서 login을 진행
            auth_login(request, user)
            """
            return redirect("<HOME>")
        else:
            return redirect("accounts:signup")
    else:
        return render(request, "accounts/signup.html", context={"form": UserCreationForm()}) -> context에 UserCreationForm 인스턴스를 전송

template 에서 {{ form.as_p }} 형태로 작성 (Django Form, Django ModelForm 과 유사)


★ 회원 탈퇴

- 구현
views.py 에서

from django.contrib.auth.models import User -> 필요 모델(DB Table) import

@require_POST -> 삭제 관련은 POST 요청만 받는다.
def delete(request):
    if request.user.is_authenticated: -> 로그인한 상태일 때만 아래 코드를 수행
        user_row = get_object_or_404(User, id=request.user.id) -> 해당 user의 row를 가져온다.
        user_row.is_active = 0 -> 해당 user의 is_active를 0으로 수정한다. (비활성화)
        user_row.save() -> 수정 사항을 DB에 반영한다.
        auth_logout(request) -> 현재 세션 삭제(로그아웃)도 진행한다.
        return redirect("<HOME>")


★ 회원 정보 수정

- UserChangeForm 사용법
views.py 에서

from .forms import CustomUserChangeForm -> 필요 클래스 import

@require_http_methods(["GET", "POST"])
def update(request):
    update_user_row = get_object_or_404(User, id=request.user.id) -> 해당 유저의 row 데이터를 update_user_row에 저장
    
    if request.method == "POST":
        form = CustomUserChangeForm(request.POST, instance=update_user_row) -> 'instance=' parameter로 수정할 row를 지정할 수 있다.
수정할 row에 바인딩한 request.POST 데이터로 수정할 준비를 한다. 
        if form.is_valid():
            form.save()
            return redirect("<HOME>")
        else:
            return redirect("accounts:update")
    else:
        context = {
            "form": CustomUserChangeForm(instance=update_user_row),
        }
        return render(request, "accounts/update.html", context=context)


★★ CustomUserChangeForm 정의

앱 폴더 내에 forms.py 생성

from django.contrib.auth.forms import UserChangeForm -> 상속받을 UserChangeForm 클래스 import
from django.contrib.auth import get_user_model -> user model(DB Table)을 가져오기 위한 get_user_model 메서드 import


class CustomUserChangeForm(UserChangeForm): -> UserChangeForm 을 상속받는 자식 클래스 CustomUserChangeForm 정의
    """
    # password 안보이고 싶은 경우 -> update 페이지에서 password 안보이고 싶은 경우 주석과 같이 처리
    password = None
    """
    
    class Meta: -> class Meta Overriding
        model = get_user_model()
        fields = [ -> 수정하고 싶은 필드 작성
            "first_name",
            "last_name",
            "email",
        ]

    def __init__(self, *args, **kwargs): -> password 관련 텍스트 수정 위해 __init__ Overriding
        super().__init__(*args, **kwargs)
        if self.fields.get("password"):
            password_help_text = (
                "비밀번호 변경하러 " '<a href="{}">이동</a>'
            ).format(f"{reverse('accounts:change_password')}")
            self.fields["password"].help_text = password_help_text


★ 비밀번호 수정

@require_http_methods(["GET", "POST"])
def change_password(request):
    update_user_row = get_object_or_404(User, id=request.user.id)
    
    if request.method == "POST":
        form = PasswordChangeForm(update_user_row, request.POST)
        if form.is_valid():
            form.save()
            return redirect("<HOME>")
        else:
            return redirect("accounts:change_password")
    else:
        context = {
            "form": PasswordChangeForm(update_user_row),
        }
        return render(request, "accounts/change-password.html", context=context)

------------------------------
Django Static & Media

'PYTHON' 카테고리의 다른 글

[TIL] 20240423 49일차  (0) 2024.04.23
[TIL] 20240422 48일차  (0) 2024.04.22
[TIL] 20240412 42일차  (0) 2024.04.12
[TIL] 20240409 40일차  (0) 2024.04.09
[TIL] 20240408 39일차  (0) 2024.04.08