[django] TODO LIST 웹 앱 만들기 실습
realpython의 tutorial을 보고 개인학습용으로 정리한 내용임.
Manage Your To-Do Lists Using Python and Django – Real Python
Use Django to build a to-do list manager app. This step-by-step project will teach you how to use Django's class-based views to build a powerful app while dramatically reducing your development time.
realpython.com
실습
기본 설정 및 가상환경 설정
todo_list 폴더 생성 후 가상환경 설정
$ python -m venv venv # venv 명령어를 통해 venv라는 이름의 가상환경 생성
위 명령어를 통해 todo_list 프로젝트 폴더 상에 venv라는 이름의 폴더가 생성되게 됨.
실습에 맞게 진행하기 위해 원래 내가 가진 Django version 4.1.5 대신 실습 tutorial에서 설정한 버전에 맞게 가상환경 activate하고 진행함.
가상환경 활성화
# 내 실습환경 등에 맞게 코드 수정하여 실행함
$ source venv/Scripts/activate # 해당 폴더 안 activate 파일 통해 가상환경 활성화
위와 같이 실행하면 아래와 같이 가상환경이 활성화됨.
비활성화를 원한다면 deactivate만 입력하면 됨
▶▶▶ 이를 통해 Django를 포함하여 지금부터 설치되는 모든 라이브러리는 위 venv라는 가상환경 상에 설치되고 해당 가상환경에서만 독립적으로 사용할 수 있게 되는 것. 이후에 가상환경에서의 작업을 마쳤을 때 deactivate만 입력하면 모든 것이 원래의 환경 상태로 돌아감. 가상환경 활성화가 필요한 경우 위의 단계를 반복하여 언제든지 다시 가상환경을 활성화할 수 있음.
가상환경 상에 Django 설치
$ python -m pip install django=="3.2.9"
설치하던 중에 다음과 같은 warning 메세지가 떠서 pip 업그레이드 후에 다시 django 설치함.
현재 Django version 확인
$ python
>>> import django
>>> django.get_version() # '3.2.9'
>>> exit()
모든 package의 version 관련하여 텍스트로 저장
$ python -m pip freeze > requirements.txt
APP 생성 및 설정하기
Django는 project와 app을 구분함. project는 하나 이상의 app 기능을 관리하고 가질 수 있음. 이 실습에서는 하나의 app만 생성.
project 생성하기
settings.py, urls.py 등의 파일을 가지는 project 폴더 생성.
$ django-admin startproject todo_project .
이때 위 명령어에서 마지막 점(.)을 빼도 생성되긴 하지만 todo_project라는 폴더가 이중으로 생겨버림(todo_project 안에 todo_project가 있는 형태). SO 위에 점을 추가해주면 폴더를 추가적으로 생성하는 것을 중지함.
app 생성하기
$ django-admin startapp todo_app
cf) todo_app/migrations/ : 향후 데이터베이스의 변경 사항에 대한 정보가 저장됨.
app 배열에 추가하기(todo_list/todo_project/settings.py)
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"todo_app", # 코드 추가
]
그 외 또 다른 요소들
cf) SECRET_KEY
: 현재로선 무시해도 되지만 public server에 앱을 배포하려는 경우 중요한 key임. Django는 모든 새 project에 대해 새로운 임의의 SECRET_KEY를 생성해줌
DEBUG
: 앱을 개발하는 동안엔 True로 설정해두는게 개발 시 유용하지만 web에 공개되게 될 때는 DEBUG가 켜져 있을 경우 코드 작동에 대해 너무 많은 정보가 노출될 수 있기 때문에 꼭 False로 설정하여 꺼줘야 함
url 링크 추가하기(todo_list/todo_project/urls.py)
from django.contrib import admin
from django.urls import include, path # 코드 추가
urlpatterns = [
path("admin/", admin.site.urls),
# 이미 존재하는 app name이 없는 경우와 같을 때에 빈 문자열을 처리할 수 있는 요소 추가한 것
# project에 또 다른 app 기능이 존재한다면 이름을 붙여줬겠지만
# 이 실습의 경우 app이 하나밖에 없어서 굳이 이름 붙이지 않은 것 같음
path("", include("todo_app.urls")), # 코드 추가
]
Django의 URL dispatcher(URL 발송자)가 들어오는 요청을 발송하는 방법을 결정하기 위해 urls.py 파일의 urlpatterns 배열에 있는 요소를 사용함. 링크를 새롭게 하나 추가해줌으로써 URL dispatcher가 수신한 URL traffic을 새 todo_app이라는 app으로 redirect하게 됨.
이전 실습에서 한 것과 달리 이 실습에서는 APP 하에 urls.py 파일을 또 따로 생성하여 거기서 더 자세하게 url들을 관리할 예정인 것으로 보임.
cf) website traffic = website 방문자가 보내고 받은 데이터의 양 의미[1]
해당 App과 관련된 URL 구성 파일 만들기(todo_list/todo_app/urls.py)
urlpatterns = [
]
일단을 비워두자. 곧 실제 경로를 추가해줄 예정.
어느 정도의 틀은 잡혔으므로 이 상태에서 runserver를 해보면 다음과 같은 화면이 잘 나오는 것을 확인할 수 있음(migration 추가하라 어쩌구 메세지가 뜰테지만 일단은 나중에 추가할 예정이니 무시하기)
Database 설정하기
Data Model 정의하기(models.py)
★ 이 실습(todo list app)에서 필요한 두 가지 타입의 데이터
- 각 title로 구성된 todotitle
- 또다른 특정한 list로 연결되는 todoitem
- todoitem의 title
- description
- created date(cdate)
- due date
위와 같은 데이터 모델은 해당 app의 backbone(중추)를 형성하게 될 것임.
# 코드 전체 추가
from django.utils import timezone
from django.db import models
from django.urls import reverse
def one_week_hence():
return timezone.now() + timezone.timedelta(days=7)
class ToDoList(models.Model):
title = models.CharField(max_length=100, unique=True) # unique=True - 고유한 field여야 함
def get_absolute_url(self):
return reverse("list", args=[self.id])
def __str__(self):
return self.title
class ToDoItem(models.Model):
title = models.CharField(max_length=100)
description = models.TextField(null=True, blank=True) # blank=True - 비어있는 field 선언
created_date = models.DateTimeField(auto_now_add=True)
due_date = models.DateTimeField(default=one_week_hence) # 기본 마감 기한이 일주일 후로 설정되도록 함 - 함수를 변경하면 기한 변경 가능
todo_list = models.ForeignKey(ToDoList, on_delete=models.CASCADE) # 외래키로 선언
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.todo_list.id), str(self.id)]
)
def __str__(self):
return f"{self.title}: due {self.due_date}"
class Meta:
ordering = ["due_date"] # due_data에 따라 ToDoItem들을 ordering시키도록 default ordering 설정
위 models.py는 전체 Data Model을 정의함. 1개의 함수와 2개의 data model class를 정의함.
one_week_hence()
: ToDoItem의 기본 마감 기한을 설정하는데 있어 유용하게 쓰일 stand-alone utility function(독립 실행형 유틸리티 함수)
todo_list = models.ForeignKey()
: ToDoItem 본인이 속한 ToDoList를 정확히 하나 가져옴 - 일(list)대다(item) 관계
on_delete=models.CASCADE
: todo_list 하나를 삭제할 경우 종속(cascade)된 모든 todo_item들도 삭제되도록 함.
def __str__(self)
: object의 readable한 representation을 만들어주는 표준 파이썬 방식임. 이 함수를 반드시 작성할 필요는 없지만 debugging에 도움이 될 수 있음.
def get_absolute_url(self)
: data model을 위한 Django에서의 관습적인 표현. 특정 데이터 항목의 URL을 반환해줌. 이를 통해 코드에서 URL을 편리하고 강력하게 참조할 수 있음 URL과 해당 매개 변수의 하드 코딩을 피하기 위해 reverse()를 사용함.
cf) hard-coding = 파일의 경로를 직접 넣어준 경우 하드코딩에 해당됨. 주로 파일 경로, URL 또는 IP 주소, 비밀번호, 화면에 출력될 문자열 등이 대상이 됨.[2]
cf) Object-Relational Mapping(ORM) [3]
: 객체(Object)와 관계형 데이터베이스(Relational)을 연결(Mapping)해 주는 것을 의미. 즉, 데이터베이스의 테이블을 객체(Object)와 연결함으로써 table에 CRUD를 할 때, SQL 쿼리를 사용하지 않고도 가능하게 하는 것을 말함.
Database 생성하기
migration 생성하고 activate시키자.
$ python manage.py makemigrations todo_app
$ python manage.py migrate
makemigrations = app에 모델의 변경을 알리고 변경 사항을 기록하기 원한다는 것을 알림
migrate = 변경 사항을 적용함
이를 통해 data model이 database에 mirroring되었음.
App의 기능 test하기
superuser 계정 생성하기
이미 만들어져 있는 admin 인터페이스를 이용해서 간단하게 데이터를 추가하면서 간단하게 test를 진행해보자.
$ ./manage.py createsuperuser
admin에 모델 등록하기(todo_list/todo_app/admin.py)
from django.contrib import admin
from todo_app.models import ToDoItem, ToDoList # 코드 추가
admin.site.register(ToDoItem) # 코드 추가
admin.site.register(ToDoList) # 코드 추가
이를 통해 admin 관리자 계정을 통해 실제로 data model에 접근하여 간단하게 데이터와 관련된 test를 할 수 있게 됨.
runserver로 서버 실행하고 간단하게 데이터 추가 등의 test를 진행해보자.
ADD로 간단하게 list, item 추가함.
이처럼 간단하게 test함으로써 data model 등이 정상적으로 설정되었음을 확인할 수 있음. 다만 실제 user가 사용하기에는 적절하지 않으므로 public user interface → View와 Templates를 이제 만들어보자.
View 생성하기
view와 template 꾸미기
cf) View
: HttpRequest를 input으로 받고, HttpResponse를 return함. 따라서 Django에서는 view에 의해서 request-response cycle이 제어됨.
🔥 View를 코딩하는 두 가지 basic한 approaches
- 함수를 만들어서 작동시키기(☆ 앞선 게시판 실습에서 사용한 방식 write(), list() 등)
- Class를 만들어 Class 내부의 method가 request를 처리하도록 작동시키기(★ 이번 실습에서 사용할 방식)
- Class의 method를 사용할 때의 장점
- Consistency(일관성)
- 처리하는 request(GET, POST, HEAD 등)의 목적 등에 맞게 고유한 method를 가질 수 있음 ex) HTTP GET request를 처리하는 method의 이름은 .get()임.
- Inheritance(상속)
- View에 필요한 작업의 대부분을 이미 수행하고 있는 기존 Class를 확장(extend)하여 상속 기능을 사용할 수 있음.
- Consistency(일관성)
- Class의 method를 사용할 때의 장점
→ 위 두 방식 모두 특정한 타입의(지정한 타입의) request를 처리하기 위해 작동함
🔥 CRUD
: 다양한 database와 data model이 있지만 기본적으로 모두 record라는 정보 단위와 이에 대해 수행할 수 있는 네 가지 기본 작업(Create Read Update Delete)를 기반으로 함. 이미 이러한 기반이 존재하므로 개발자인 우리가 해야 할 일은 data model과 application에 맞게 customize하고, 사용자가 특정하게 수정할 때마다 여기저기서 동작을 조정하는 것임.
View 작성하기(todo_list/todo_app/views.py)
# 코드 추가
from django.views.generic import ListView
from .models import ToDoList
class ListListView(ListView):
model = ToDoList
template_name = "todo_app/index.html"
함수 기반 방식이 아니라 Class의 method를 이용한 방식을 사용할 것이기 때문에 기존에 import되어있던 render를 사용하지 않는 듯.
ListListView 클래스는 todo list의 title list를 표시함. django.views.generic.ListView라는 이미 Django에 존재하는 코드를 사용하기 때문에 많은 코드 작성이 필요하지 않음. database에서 object의 list를 검색하는 방법을 이미 알고 있으므로 다음 두 가지만 알려주면 됨:
- 가져올 data model class (이 경우 ToDoList class에 해당함)
- list의 형식을 지정할 template(template 파일)의 이름
Base Template 작성하기(todo_list/todo_app/templates/base.html)
코드를 재사용하게 될 수도 있기 때문에 모든 page에 표시할 모든 상용 HTML 코드를 포함하는 base template를 만드는 것으로 시작하겠음. 이렇게 하면 View가 기본 Class에서 대부분의 기능을 상속하는 것처럼 실제 app page는 이 base template을 상속하여 사용하게 됨.
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--Simple.css-->
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<title>Django To-do Lists</title>
</head>
<body>
<div>
<h1 onclick="location.href='{% url "index" %}'">
Django To-do Lists
</h1>
</div>
<div>
{% block content %}
This content will be replaced by different html code for each page.
{% endblock %}
</div>
</body>
</html>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
: 오픈소스인 Simple.css 라이브러리를 import하는 코드. 이후에 다른 css 코드 등을 이용해서 다르게 꾸며보기도 해보자.
<h1 onclick="location.href='{% url "index" %}'">
: template syntax {%url "index" %}을 onclick이라는 event handler의 속성으로 설정. 장고 템플릿 엔진은 URLConf 파일 to do_app/urls.py에서 "index"라는 이름의 URL 패턴 항목을 찾고 이 템플릿을 올바른 경로로 대체함. 이에 따라 todo list title을 클릭하면 브라우저가 "index"라는 이름의 URL로 redirection시켜줌.
cf) onclick 속성
: 클릭되었을시 발생되는 이벤트 의미함. [4] 아니면 event handler 함수의 이름을 onclick으로 설정한 것?
event handler
: 웹 페이지 상에서 발생하는 수많은 특정 요소에 따라 발생하는 event를 처리하기 위해 사용. 이벤트 핸들러가 연결된 특정 요소에서 지정된 타입의 이벤트가 발생하면, 웹 브라우저는 연결된 이벤트 핸들러를 실행함. [5]
▶▶▶ 위의 base template의 block 사이에 추가함으로써 template을 꾸미고 추가하면 됨.
home page에 해당하는 페이지 작성하기(todo_list/todo_app/templates/todo_app/index.html)
templates/<appname> 아래에 template html 파일들을 두는 건 Django의 관습적인 방식임.
{% extends "base.html" %} // bsae template을 상속받아 확장하여 사용
{% block content %}
<!--index.html-->
{% if object_list %}
<h3>All my lists</h3>
{% endif %}
<ul>
{% for todolist in object_list %}
<li>
<div
role="button"
onclick="location.href='{% url "list" todolist.id %}'"> // list라는 URL로
{{ todolist.title }}
</div>
</li>
{% empty %}
<h4>You have no lists!</h4>
{% endfor %}
</ul>
{% endblock %}
이미 base template으로 HTML의 틀을 만들어뒀기 때문에 새로운 부분만 작성하면 됨.
{% if object_list %} {% endif %}
: object_list가 null이거나 비어 있으면 All my lists라는 제목이 나타나지 않게 조건문 설정. ListView는 context variable인 object_list를 template에 자동으로 제공하며, 이러한 object_list에는 todo list의 list가 포함되어 있음.
cf) context variable
: context에 따라 다른 값을 가질 수 있는 변수. 주요 사용 사례는 concurrent asynchronous task에서 변수를 추적하는 것임.
{% for todolist in object_list %} {% endfor %}
: list의 각 object에 대해 for문 블럭 안에 있는 HTML을 rendering함. 추가로(선택), {% empty %} tag를 사용하면 list가 비어 있는 경우 대신 rendering할 항목을 정의할 수 있음.
{{ todolist.title }}
: mustache(템플릿 엔진) syntax임. 이중 중괄호를 사용하면 템플릿 엔진에 포함된 변수의 값을 표시하는 HTML을 내보냄(즉, 화면에 보여줌). 이 경우 loop variable 'todolist'의 title 속성을 rendering함.
▶▶▶ index.html을 작성함으로써 todo list가 존재할 경우 이를 display해주고, 없을 경우 메세지를 display해주는 todo list app의 home page를 완성함.
Request Handler build하기(todo_list/todo_app/urls.py)
from django.urls import path
from . import views
urlpatterns = [
# URL 뒷부분이 비어있다면 request을 처리하기 위해 ListListView class가 반드시 호출되어야 함을 의미
path("", views.ListListView.as_view(), name="index"),
]
name="index"
: base.html의 {% url "index" %}와 match되는 부분.
🔥 request-response cycle 과정
- server가 brower로부터 지정한 URL 형식의 GET request를 받으면, HTTPRequest object를 생성하고, ListListView(views.py)로 object를 보냄
- 이 특정한 View는 ToDoList model을 기반으로 하는 ListView이므로, database에서 모든 ToDoList record를 가져와 ToDoList Python 객체로 변환하고 기본적으로(default) object_list라는 list에 추가함
- view는 특정한 (template인 index.html을 이용하여) display하기 위해 list를 template engine에 전달함
- template engine은 index.html에서 HTML 코드를 build하여 자동으로 base.html과 결합하고, 전달된 데이터와 템플릿의 내장된 logic을 사용하여 HTML 요소를 채움.
- View는 완전히 build된 HTML을 포함하는 HttpResponse object를 생성하고 이를 Django로 반환함.
- DJango는 HttpResponse를 HTTP message로 변환하고 이를 brower로 돌려보냄.
- brower는 완전히 형성된 HTTP page를 보고, 이를 user에게 display함.
▶▶▶ 이를 통해 첫번째 end-to-end Django request handler를 완성함. BUT 현재 index.html이 존재하지 않는 URL 링크인 list를 참고하기 때문에 아직 제대로 작동하지 않음. URL을 정의하고 해당 링크로 연결되는 view를 만들기 전에 class-based view를 자세히 이해하는게 필요함.
🔥 class-based generic view의 재사용
이 실습에서 사용된 ListListView와 그 모든 다른 view들이 다 class-based view에 해당됨.
일반적으로 view로서 작동되도록 작성된 class들은 class django.views.View를 extend하고 해당 class의 method(.get(), .post() 등)를 override하여 사용함. 이때 이 method들은 HttpRequest를 받고, HttpResponse를 반환함.
class-based generic view는 reusability(재사용성)을 한 단계 더 끌어올림. 대부분의 필요하거나 기대되는 기능들은 거의 다 이미 base class에 encoding되어 있기 때문임. 따라서 generic view class는, ListView class를 예로 들자면, 2가지만 알면 됨.
- listing할 data의 type
- HTML을 rendering하는 데 사용할 template
위 정보를 사용하여 object list를 rendering할 수 있음. 물론 ListView는 다른 일반적인 view와 마찬가지로 매우 기본적인 패턴으로 제한되지 않음. base class를 마음껏 조정하고 subclass로 분류할 수 있음. 하지만 기본적인 기능은 이미 존재하며 cost는 더 들지 않는다는 장점 가짐.
▶▶▶ 결론적으로, 해당 튜토리얼에서 generic class = django.views.generic.list.ListView이고, subclass = ListListView임. 그리고 이 ListListView는 todo list의 list들을 display해주는 역할을 수행함. 이번엔 todo item의 list를 display하는 class를 작성하자.
ItemListView subclass 생성하기(todo_list/todo_app/views.py)
todo item의 list를 display해주는 subclass. 역시나 ListListView처럼 generic Django class인 ListView를 확장한 class임.
from django.views.generic import ListView
from .models import ToDoList, ToDoItem # 코드 추가
class ListListView(ListView):
model = ToDoList
template_name = "todo_app/index.html"
# 코드 추가
class ItemListView(ListView):
model = ToDoItem
template_name = "todo_app/todo_list.html"
def get_queryset(self):
return ToDoItem.objects.filter(todo_list_id=self.kwargs["list_id"])
def get_context_data(self):
context = super().get_context_data()
context["todo_list"] = ToDoList.objects.get(id=self.kwargs["list_id"])
return context
get_queryset()
: 모든 ToDoItem 나열X. 현재 list에 속한 ToDoItem만 나열O(list_id로 list 구분). ToDoItem의 method인 get_queryset()을 override한 method임.
get_queryset()의 결과는 자동으로 object_list(key) 하의 context에 포함되지만, 쿼리에 의해 반환된 항목(todoitem list)뿐만 아니라 todo_list(원래의 object)자체에도 template이 접근할 수 있도록 해야 함.
get_context_data()
: View class를 상속받는 모든 하위 클래스가 가지는 method임. 이때 해당 method가 반환하는 context는 rendering에 사용할 수 있는 데이터를 결정하는 Python dictionary인 template의 context임.
get_context_data()를 재정의하여 이 reference를 context dictionary에 추가함. superclass의 .get_context_data()를 먼저 호출함으로써 새 데이터를 기존 context와 병합할 수 있도록 하는 것이 중요.
▶▶▶ 이때 위의 두 overriden method 둘 다 self.kwargs["list_id"]을 사용했다는 것을 확인할 수 있음. 이는 class가 생성될 때 class에 전달되는 list_id라는 keyword argument가 있어야 함을 의미.
ToDoList의 item들 보여주기(todo_list/todo_app/templates/todo_app/todo_list.html)
역시나 list에 있는 모든 item들을 다 보여주는 것이므로 {% for <item> in <list> %} {% endfor %} 구성은 없어서는 안 됨.
{% extends "base.html" %}
{% block content %}
<div>
<div>
<div>
<h3>Edit list:</h3>
<h5>{{ todo_list.title | upper }}</h5>
</div>
<ul>
{% for todo in object_list %}
<li>
<div>
<div
role="button"
onclick="location.href='#'">
{{ todo.title }}
(Due {{ todo.due_date | date:"l, F j" }})
</div>
</div>
</li>
{% empty %}
<p>There are no to-do items in this list.</p>
{% endfor %}
</ul>
<p>
<input
value="Add a new item"
type="button"
onclick="location.href='#'" />
</p>
</div>
</div>
{% endblock %}
{{ 변수명 | 필터[:인자] }} [6]
: 파이프 기호(|)를 사용한 template filter라는 이름의 expression. 파이프 기호 오른쪽에 위치한 filter를 이용하여 각 왼쪽의 변수의 형식을 지정함. 값 지정이 아니라 해당 값을 저장할 형식을 지정해주는 것. 즉, template 변수를 특정 형식으로 변환해주기 위해 사용함. HTML 문서를 작성할 때 프로그래밍하듯 작성할 수 있게 해줌.
- {{ todo_list.title | upper }} = todo_list.title 변수의 값을 대문자로 filtering하여 변환
- {{ todo.due_date | date:"l, F j" }} = date라는 filter의 경우 뒤에 인자를 필요로 함. 지정한 인자에 맞게 todo.due_date 변수의 값을 변환
type="button" onclick="location.href='#'"
: 위와 같은 형태는 button-like element를 정의해줌. 현재로선 작동하지 않지만 뒤에서 수정하여 위 코드에 대한 onclick event handler가 작동할 수 있게 할 예정.
itemlist 보는 페이지 경로 추가하기(todo_list/todo_app/urls.py)
from todo_app import views
urlpatterns = [
path("", views.ListListView.as_view(), name="index"),
path("list/<int:list_id>/", views.ItemListView.as_view(), name="list"), # 코드 추가
]
path(placeholder(자리 표시자), 함수, name)
: 위 코드의 경우 placeholder를 통해 list_id 값을 지정하면 ItemListView의 객체에게 해당 값을 전달해줌. (views.py의 ItemListView 코드를 보면 해당 코드가 self.kwargs["list_id"]의 형태로 list_id를 참조하고 있다는 것을 확인할 수 있음)
여기까지 코드 작성 후 실행해보면 다음과 같음
▶▶▶ 여기까지가 CRUD의 Read 부분에만 해당됨. 아직까진 그 외에 list를 add하거나 delete하는 것 또는 item을 add하거나 edit하거나 remove하는게 불가능함.
Model Object를 Create하고 Update하기
Create과 Update를 지원하는 새로운 View(기능) class 추가하기(todo_list/todo_app/views.py)
from django.urls import reverse
from django.views.generic import (
ListView,
CreateView, # 코드 추가
UpdateView, # 코드 추가
)
from .models import ToDoItem, ToDoList
class ListListView(ListView):
model = ToDoList
template_name = "todo_app/index.html"
class ItemListView(ListView):
model = ToDoItem
template_name = "todo_app/todo_list.html"
def get_queryset(self):
return ToDoItem.objects.filter(todo_list_id=self.kwargs["list_id"])
def get_context_data(self):
context = super().get_context_data()
context["todo_list"] = ToDoList.objects.get(id=self.kwargs["list_id"])
return context
# 코드 추가
class ListCreate(CreateView):
model = ToDoList
fields = ["title"]
def get_context_data(self):
context = super(ListCreate, self).get_context_data()
context["title"] = "Add a new list"
return context
# 코드 추가
class ItemCreate(CreateView):
model = ToDoItem
fields = [
"todo_list",
"title",
"description",
"due_date",
]
def get_initial(self):
initial_data = super(ItemCreate, self).get_initial()
todo_list = ToDoList.objects.get(id=self.kwargs["list_id"])
initial_data["todo_list"] = todo_list
return initial_data
def get_context_data(self):
context = super(ItemCreate, self).get_context_data()
todo_list = ToDoList.objects.get(id=self.kwargs["list_id"])
context["todo_list"] = todo_list
context["title"] = "Create a new item"
return context
def get_success_url(self):
return reverse("list", args=[self.object.todo_list_id])
# 코드 추가
class ItemUpdate(UpdateView):
model = ToDoItem
fields = [
"todo_list",
"title",
"description",
"due_date",
]
def get_context_data(self):
context = super(ItemUpdate, self).get_context_data()
context["todo_list"] = self.object.todo_list
context["title"] = "Edit item"
return context
def get_success_url(self):
return reverse("list", args=[self.object.todo_list_id])
3개의 새로운 class 추가. 역시나 Django의 generic view class로부터의 파생 클래스임.
- ListCreate Class
- django.view.generic.CreateView 확장한 subclass
- todolist의 title을 포함하는 form을 정의함
- form 자체에도 context data에 전달되는 title이 있음
- ItemCreate Class
- django.view.generic.CreateView 확장한 subclass
- 4개의 field를 가지는 form을 생성
- get_success_url() method는 새로운 item이 create되고 난 후 보여줄 페이지를 view에 제공함
- 이 경우, form 제출이 성공한 후 list view를 호출하여 새 list가 포함된 전체 todo list를 보여줌
- ItemUpdate Class
- django.view.generic.UpdateView 확장한 subclass
- ItemCreate과 매우 유사하지만 보다 더 적절한 title을 제공함
🔥 CreateView
: 어떤 Model subclass도 사용할 수 있는 generic view. 즉, object를 create하기 위해 고안된 어떤 view에서도 사용할 수 있는 base class.
🔥 UpdateView
: CreateView와 꽤나 유사함. 주된 차이점은 UpdateView를 사용한 ItemUpdate class는 template form을 기존 ToDoItem의 데이터로 미리 채운다는 점.
route 정의하기(todo_list/todo_app/urls.py)
user가 적절한 data value들이 설정된 각 new view에 도달할 수 있도록 route 정의.
from django.urls import path
from todo_app import views
urlpatterns = [
path("", views.ListListView.as_view(), name="index"),
path("list/<int:list_id>/", views.ItemListView.as_view(), name="list"),
# 코드 추가
# CRUD patterns for ToDoLists
path("list/add/", views.ListCreate.as_view(), name="list-add"),
# CRUD patterns for ToDoItems
path(
"list/<int:list_id>/item/add/",
views.ItemCreate.as_view(),
name="item-add",
),
path(
"list/<int:list_id>/item/<int:pk>/",
views.ItemUpdate.as_view(),
name="item-update",
),
]
3개의 new route.
"list/<int:list_id>/item/add/" "list/<int:list_id>/item/<int:pk>/"
:마지막 2개의 path()의 경우 path의 URL pattern이 <int:list_id>와 같이 매개변수를 갖는다는 것을 확인할 수 있음. 새로운 item을 create하기 위해서는 view code가 parent list의 list_id를 알아야 함. 또 item을 update하기 위해서는 (여기선 pk라 불린) item 고유의 ID와 list_id 모두 view에게 알려져야 함.
new view 활성화 위해 template에 link 제공하기(index.html)
{% endblock %} tag 바로 앞에 button 추가하기
<p>
<input
value="Add a new list"
type="button"
onclick="location.href='{% url "list-add" %}'"/>
</p>
위 button을 클릭하면 "list-add" pattern의 request가 생성될 것임. 이는 URL dispatcher가 ListCreate view를 객체화(instantiate)하게 함.
2개의 dummy onclick event를 update하기(todo_list/todo_app/templates/todo_app/todo_list.html)
{% extends "base.html" %}
{% block content %}
<div>
<div>
<div>
<h3>Edit list:</h3>
<h5>{{ todo_list.title | upper }}</h5>
</div>
<ul>
{% for todo in object_list %}
<li>
<div>
<div
role="button"
onclick="location.href=
'{% url "item-update" todo_list.id todo.id %}'"> // 코드 추가
{{ todo.title }}
(Due {{ todo.due_date | date:"l, F j"}})
</div>
</div>
</li>
{% empty %}
<p>There are no to-do items in this list.</p>
{% endfor %}
</ul>
<p>
<input
value="Add a new item"
type="button"
onclick="location.href='{% url "item-add" todo_list.id %}'" // 코드 추가
/>
</p>
</div>
</div>
{% endblock %}
이에 따라 onclick event handler가 새 URL(item-update와 item-add라는 이름 가진 URL)을 호출. {% url ... %} syntax에 따라 urlpattern name은 하이퍼링크를 구성하기 위해 context의 data와 결합됨.
'{% url "item-update" todo_list.id todo.id %}'
: item의 update를 위해서는 item-update URL은 list_id와 item id 둘 다를 필요로 함. 어느 list에 속한 item인지 알아야 하고, 어떤 item을 update하려는 건지 알아야 하기 때문
'{% url "item-add" todo_list.id %}'
: 반면 item의 add를 위해서는 item-add URL은 todo_list.id만 필요로 함. 해당 item을 추가시킬 list만 알면 추가할 수 있기 때문.
이제 새로운 3개의 View들을 rendering시킬 template들을 작성해보자.
새 list를 create하기 위한 form 구성을 위한 template 작성하기(todo_list/todo_app/templates/todo_app/todolist_form.html)
{% extends "base.html" %}
{% block content %}
<h3>{{ title }}</h3>
<div>
<div>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input
value="Save"
type="submit">
<input
value="Cancel"
type="button"
onclick="location.href='{% url "index" %}';">
</form>
</div>
</div>
{% endblock %}
user가 form을 submit하였을 때 POST request를 생성하는 <form>을 작성함. 위의 경우에서는 제출해야 할 form은 오로지 list의 title만 존재.
{% csrf_token %} macro
: 오늘날의 web form에 필수적인 예방책인 Cross-Site Request Forgery token을 생성하는 macro
{{ form.as_p }}
: {{ form.as_p }} tag를 사용하여 View 클래스의 .as_p() method를 호출. 이렇게 하면 field 속성 및 model 구조에서 form 내용이 자동으로 생성됨. 즉, model class 내에서 미리 작성한 model의 field 등에 따라 form의 내용을 적절하게 자동으로 생성해줌. form은 <p> 태그 안에서 HTML로 rendering.
새 ToDoItem을 create하기 위한 form 구성을 위한 template 작성하기(todo_list/todo_app/templates/todo_app/todoitem_form.html)
새 ToDoItem을 생성하거나 이미 존재하던 ToDoItem의 detail을 수정할 수 있도록 하는 form을 위한 template을 작성해보자.
{% extends "base.html" %}
{% block content %}
<h3>{{ title }}</h3>
<form method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input
value="Submit"
type="submit">
<input
value="Cancel"
type="button"
onclick="location.href='{% url "list" todo_list.id %}'">
</form>
{% endblock %}
이번 template에서는 {{ form.as_table }}을 사용해서 form을 rendering함. 왜냐하면 list와 달리 각 item에는 다수의 field들이 존재하기 때문. 또한 Submit button을 누르게 되면 user에 의해 작성된 form의 내용에 따라 POST request를 생성해줌. Cancel button을 누를 경우 user를 "list" URL로 redirection하여 현재 list ID를 매개 변수로 전달해줌.
ToDoList와 Item Delete 기능 추가하기
user가 한 번에 한 item을 delete하거나 또는 전체 list를 delete할 수 있도록 하는 기능을 추가해보겠음. Django에서는 역시나 이러한 delete와 관련된 경우에 대해서도 다룰 수 있는 generic view를 제공함.
DeleteView import에 추가하기(todo_list/todo_app/views.py)
해당 subclass들은 django.views.generic.DeleteView를 확장한 subclass들임.
from django.urls import reverse, reverse_lazy # 코드 추가
from django.views.generic import (
ListView,
CreateView,
UpdateView,
DeleteView, # 코드 추가
)
DeleteView Subclass들 생성하기(todo_list/todo_app/views.py)
deleting 객체들을 support하는 새로운 veiw class들을 추가하자. list와 item 각각에 대해 해당 delete view class가 필요함.
# 코드 추가
class ListDelete(DeleteView):
model = ToDoList
# You have to use reverse_lazy() instead of reverse(),
# as the urls are not loaded when the file is imported.
success_url = reverse_lazy("index")
class ItemDelete(DeleteView):
model = ToDoItem
def get_success_url(self):
return reverse_lazy("list", args=[self.kwargs["list_id"]])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["todo_list"] = self.object.todo_list
return context
위 두 class 모두 django.views.generic.edit.DeleteView를 extend함.
🔥 DeleteView
: confirmation page를 표시하고 기존 object를 삭제하는 view. 지정된 object는 request method가 POST인 경우에만 삭제됨. GET을 통해 이 view를 가져온 경우 동일한 URL에 POST하는 form이 포함된 confirmation page가 표시됨.
이제 Deletion Confirmations과 URL을 define해주자.
list object delete를 위한 template 작성하기(todo_list/todo_app/templates/todo_app/todolist_confirm_delete.html)
editing page에서 user에게 delete 옵션을 제공할 것이므로 해당 confirmation page에 대한 새 template만 작성하면 됨. 이러한 confirmation template에 대해서 default name인 <modelname>_confirm_delete.html이라는 name도 존재함. 이러한 template가 있는 경우, DeleteView에서 파생된 클래스는 관련 form이 제출될 때 자동으로 template를 rendering함.
{% extends "base.html" %}
{% block content %}
<h3>Delete List</h3>
<p>Are you sure you want to delete the list <i>{{ object.title }}</i>?</p>
<form method="POST">
{% csrf_token %}
<input
value="Yes, delete."
type="submit">
</form>
<input
value="Cancel"
type="button"
onclick="location.href='{% url "index" %}';">
{% endblock %}
"Yes, delete." button을 클릭함으로써 form을 submit하게 되고, class는 database에서 list를 delete함.
"Cancel" button을 클릭한다면 아무 작업도 수행되지 않음.
위 두 경우 모두 Django는 user를 home page로 redirection함.
item object delete를 위한 template 작성하기(todo_list/todo_app/templates/todo_app/todoitem_confirm_delete.html)
{% extends "base.html" %}
{% block content %}
<h3>Delete To-do Item</h3>
<p>Are you sure you want to delete the item: <b>{{ object.title }}</b>
from the list <i>{{ todo_list.title }}</i>?</p>
<form method="POST">
{% csrf_token %}
<input
value="Yes, delete."
type="submit">
<input
value="Cancel"
type="button"
onclick="location.href='{% url "list" todo_list.id %}';">
</form>
{% endblock %}
todolist_confirm_delete.html과 정확히 동일한 logic으로 동작하지만 이번에는 "Cancel" button을 클릭하면 home page에 해당하는 index page로 가는 것이 아니라, 상위 list를 display하기 위해 user가 "list" URL로 redirection됨.
deletion URL을 위한 route define하기(todo_list/todo_app/urls.py)
from django.urls import path
from todo_app import views
urlpatterns = [
path("", views.ListListView.as_view(), name="index"),
path("list/<int:list_id>/", views.ItemListView.as_view(), name="list"),
# CRUD patterns for ToDoLists
path("list/add/", views.ListCreate.as_view(), name="list-add"),
# 코드 추가
###########################################################################
path(
"list/<int:pk>/delete/", views.ListDelete.as_view(), name="list-delete"
),
###########################################################################
# CRUD patterns for ToDoItems
path(
"list/<int:list_id>/item/add/",
views.ItemCreate.as_view(),
name="item-add",
),
path(
"list/<int:list_id>/item/<int:pk>/",
views.ItemUpdate.as_view(),
name="item-update",
),
# 코드 추가
###########################################################################
path(
"list/<int:list_id>/item/<int:pk>/delete/",
views.ItemDelete.as_view(),
name="item-delete",
),
###########################################################################
]
Django가 기본적으로 해당 request들을 처리하고 방금 추가한 template인 <modelname>_confirm_delete으로 confirmation page를 rendering하기 때문에 delete confirmation을 위해 또다른 특별한 URL을 정의할 필요가 없음.
Deletion 활성화하기(todo_list/todo_app/templates/todo_app/todoitem_form.html)
지금까지 item을 delete하기 위해 view와 URL을 만들었지만 user가 이 기능을 호출할 수 있는 메커니즘은 없는 상태. 따라서 이를 활성화하기 위해 코드 수정이 필요함. user가 현재 item을 삭제할 수 있도록 button을 추가하여 실제로 활성화시켜주자.
{% extends "base.html" %}
{% block content %}
<h3>{{ title }}</h3>
<form method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input
value="Submit"
type="submit">
<input
value="Cancel"
type="button"
onclick="location.href='{% url "list" todo_list.id %}'">
<!-- 코드 추가 -->
{% if object %}
<input
value="Delete this item"
type="button"
onclick="location.href=
'{% url "item-delete" todo_list.id object.id %}'">
{% endif %}
<!-- -->
</form>
{% endblock %}
존재하는 item에 대해서만 삭제할 수 있도록 if문 추가해줌.
이제 전체 list를 delete하기 위한 user interface 요소를 추가해야 함.
user interface element 추가하기(todo_list/todo_app/templates/todo_app/todo_list.html)
{% extends "base.html" %}
{% block content %}
<div>
<div>
<div>
<h3>Edit list:</h3>
<h5>{{ todo_list.title | upper }}</h5>
</div>
<ul>
{% for todo in object_list %}
<li>
<div>
<div
role="button"
onclick="location.href=
'{% url "item-update" todo_list.id todo.id %}'">
{{ todo.title }}
(Due {{ todo.due_date | date:"l, F j" }})
</div>
</div>
</li>
{% empty %}
<p>There are no to-do items in this list.</p>
{% endfor %}
</ul>
<p>
<input
value="Add a new item"
type="button"
onclick="location.href=
'{% url "item-add" todo_list.id %}'" />
<!-- 코드 추가 -->
<input
value="Delete this list"
type="button"
onclick="location.href=
'{% url "list-delete" todo_list.id %}'" />
<!-- -->
</p>
</div>
</div>
{% endblock %}
▶▶▶ 완성!
결론(정리)
- Django를 이용하여 Web app을 만들었음.
- 코드 재사용성을 높이고 유지 관리성을 개선하기 위해 object-oriented(객체 지향) 원리와 inheritance(상속)을 사용함.
- 일대다 관계로 data model 구조화함.
- Django admin interface를 이용하여 data model을 탐색하고 test data를 추가해봄.
- template를 통해 list display함.
- class-based view를 통해 표준 database operation 다뤄봄.
- class-based view가 유일한 방식은 아님. 기본적인 application에 대해서 function-based view(게시판 만들기 실습에서 사용한 방식)를 사용하면 더 쉽고 간단함.
- 또한 class-based view를 사용하더라도 Django의 generic view를 사용하는데 얽매이지 않고 request-response construct를 따르는 모든 function이나 method를 application에 연결할 수 있으며, 이 경우에도 Django의 인프라의 이점을 여전히 누릴 수 있음.
- Django URL dispatcher를 제어하고 적절한 view로 request를 routing하기 위해 URL configurations을 생성함.
직접 구현하면 좋을 추가 기능
- duration을 user가 조정할 수 있도록 하기
- user가 완료된 작업과 미완료된 작업을 구분할 수 있도록 field 추가하기
- user가 duration을 놓칠 경우 email로 알림 보내주기
- 팀별 공유 list 생성하고 각 팀원별로 따로 고유한 list도 가질 수 있도록 하기
- 이 경우 Multi-User database에 대해 학습해야 함
- 더 많은 class-based view 사용해보기
<reference>
[1] 참고 글1
웹사이트 트래픽이란 무엇입니까? - 방문자 분석
웹사이트 트래픽은 웹사이트 방문자가 보내고 받은 데이터의 양을 말하며, 웹사이트의 성공 또는 특정 페이지의 인기도를 보기 위해 여러 측정항목(방문자, 고유 방문자, 방문한 페이지)을 사용
www.visitor-analytics.io
[2] 참고 글2
하드코딩을 피해라.
…
tecoble.techcourse.co.kr
[3] 참고 글3
장고(django)의 ORM
장고(django)의 ORM(Object-Relational Mapping)을 사용하여 데이터를 생성하고 읽고 갱신하고 삭제하는 방법(CRUD - Create Read Update Delete)에 대해서 알아봅시다.
dev-yakuza.posstree.com
[4] 참고 글4
JavaScript 태그의 on 속성 - ofcourse
개요 HTML상에서 이벤트 발생시 실행될 코드나 함수를 바로 등록할 수 있습니다. 위는 버튼을 클릭했을 때 “Hello world” 경고창을 띄우는 코드입니다. onclick 속성은 클릭되었을시 발생되는 이벤
ofcourse.kr
[5] 참고 글5
코딩교육 티씨피스쿨
4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등
tcpschool.com
[6] 참고 글6
Template Language
💡 화면을 구성하는 Template을 작성할 때 보다 편리하게 작성할 수 있도록 도와주는 언어
velog.io