코딩 순서는 settings.py
-> models.py
-> urls.py
-> views.py
-> templates
순으로 해왔지만,
이번에는 settings.py와 models.py는 변경사항이 없어 urls과 templates 부터 작업하고 이후 views의 작업을 어떻게할지 고민하는 작업을 진행한다.
URL 패턴 | 뷰 이름 | 템플릿 파일명 |
---|---|---|
/admin/ | (장고 기본제공) | |
/todo/vonly/ | TodoVueOnlyTV(TemplateView) | todo_vue_only.html |
/todo/create/ | TodoCV(CreateView) | todo_form.html |
/todo/list/ | TodoLV(ListView) | todo_list.html |
/todo/{number}/delete/ | TodoDelV(DeleteView) | todo_confirm_delete.html |
/ | HomeView(TemplateView) | home.html |
/todo/mixin/ | TodoMOMCV(MultipleObjectMixin, CreateView) | todo_form_list.html |
/todo/{number}/delete2/ | TodoMixinDelV(DeleteView) | 팝업 |
todo/urls.py
에 아래 코드를 추가한다.
path('mixin/', views.TodoMOMCV.as_view(), name='mixin'),
# MultipleObjectMixin, CreateView
todo/views.py
에 임시 코드를 추가한다.
class TodoMOMCV(CreateView):
template_name = 'todo/todo_form_list.html'
todo/todo_form_list.html
기존의 todo_form.html 복사해서 코드를 정리한다.
{% extends 'base.html' %}
{% block title %}
todo_form.html
{% endblock %}
{% block extra-style %}
<style>
body {
text-align: center;
background-color: #ddd;
}
.inputBox {
margin: auto;
width: 70%;
background: white;
height: 50px;
border-radius: 50px;
line-height: 50px;
}
.inputBox .name {
border-style: none;
border-bottom: 1px solid #ddd;
width: 90px;
padding-left: 20px;
line-height: 20px;
}
.inputBox .item {
border-style: none;
border-bottom: 1px solid #ddd;
width: 400px;
margin-left: 50px;
padding-left: 20px;
line-height: 20px;
}
.todoList li {
display: flex;
height: 50px;
line-height: 50px;
margin: 0.5rem 0;
padding: 0 0.9rem;
background: white;
border-radius: 5px;
}
</style>
{% endblock %}
{% block content %}
<div id='app'>
<h1>My Todo App !</h1>
<strong>서로 할 일이나 의견을 공유해 봅시다.</strong>
<br>
{# csrf 토큰 공격을 방지하는 csrf 토큰을 넣는다. 장고에서 제공해주는 템플릿 태그이다. #}
<form class="inputBox" action="." method="post"> {% csrf_token %}
<input class="name" type="text" placeholder="name ..." name="name">
<input class="item" type="text" placeholder="type anything welcomed ..."
name="todo">
<button class="btn btn-info btn-sm" type="submit">ADD</button>
</form>
<ul class="todoList">
{# v-for문 대신에, 장고에서 제공하는 템플릿 태그로 바꾼다. #}
{% for todo in object_list %}
<li>
<span>{{ todo.name }}::: {{ todo.todo }}</span>
<span class="removeBtn"><a href="{% url 'todo:delete' todo.id %}">×</a></span>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
이렇게되면 폼과 리스트가 한 화면에 같이 보여지게 된다. form은 createView가 담당하고 list는 ListView가 담당하는데 이 기능을 종합하는 Mixin이 필요하다.
Templates을 코딩하면서 또 한가지 확인해야 될 사안이 View에서 templates로 넘겨줘야 될 context 변수가 어떤것이 있는지 체크해야한다.
list에서는 object_list라는 변수를 사용한다. view에서 templates로 넘겨주어야 한다.
views.py
파이썬에서는 다중 상속을 받아야할 때 List를 먼저쓸지, Create를 먼저 쓸지 상속받는 순서도 중요하다. 이에 대한 설명은 Docs 참고하면 된다.
공식홈페이지를 봐야되지만, https://ccbv.co.uk/ 이 사이트는 아주 잘 정리된 사이트이기 때문에 알아두면 좋은 사이트이다.
Edit 메뉴에서 CreateView를 클릭한다.
바로 보이는 Hierarchy diagram 버튼을 클릭하면
CreateView에 대한 상속계층들을 볼 수 있는데, 여러가지 믹스인 기능들을 조합해서 CreateView를 만들고 있다.
똑같이 ListView의 다이어그램을 보면
ListView의 상속계층을 볼 수 있다.
이렇게 장고의 클래스형 뷰는 공통적으로 사용되는 단위 기능들을 Mixin 클래스로 만들고나서 이 Mixin 클래스 기능들을 조합을 해서 만드는 방식으로 구성되어있다.
CreateView와 비교해보면 CreateView가 훨씬 복잡한 구성을 이루고 있는데, 그 이유는 form 처리 기능을 다루고 있기 때문이다.
form 처리는 주로 process form view에서 이루어지는데 form 처리는 GET, POST 방식 요청을 한다.
CreateView를 메인으로 두고, ListView는 믹스인으로 두고 처리하는 것이 에러 가능성을 줄여주는 방법이다.
그리고 코딩을 할 때는 (mixin, main) 믹스인 클래스를 먼저 써주고, 메인 클래스를 나중에 써준다.
믹스인(Mixin)의 단어 의미
Main 재료에 추가되는 첨가물을 의미한다.
우리가 지금 필요로하는 기능은 object_list라는 context 변수를 templates에 넘겨주는 기능이다. 이 기능은 ListView의 다이어그램을 살펴보면 MultipleObjectMixin에 들어있다.
ListView의 Methods중 에서는
def get_context_data(self, **kwargs):
가 있는데, context 변수를 만들고 그 중에 object_list context변수는 queryset으로, queryset은 self.object_list로 부터 만들고 있는것을 확인할 수 있다. 즉, self.object_list 변수를 미리 준비해두고, get_context_data
메소드만 호출하면 object_list라는 context변수가 templates로 넘어가게 된다.
결국 이러한 기능들이 MultipleObjectMixin에 정의가 돼있다는 것을 알 수 있다.
정리 :
MultipleObjectMixin을 먼저 상속받고 CreateView를 상속받는다.
대부분 기능은 CreateView에서 처리하고, 없는 기능인 object_list라는 context 변수를 templates로 넘겨주는 기능은 MultipleObjectMixin에서 처리한다.
(MultipleObjectMixin, CreateView)
views 소스에 반영한다.
views.py
class TodoMOMCV(MultipleObjectMixin, CreateView):
model = Todo
fields = '__all__'
template_name = 'todo/todo_form_list.html'
success_url = reverse_lazy('todo:mixin')
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
return super().post(request, *args, **kwargs)
base.html
에 메뉴바에 링크하나를 추가한다.
<li class="nav-item mx-1 btn btn-primary">
<a class="nav-link text-white" href="{% url 'todo:mixin' %}">DjangoMixin</a></li>
테스트해서 확인해본다.
DjangoMixin 메뉴 url을 들어가서 테스트 해보면 작성을 하고나면 현재 페이지에서 처리가 이루어지고 있지만,
삭제 처리 후 리다이렉션이 list url로 가기 때문에 mixin으로 수정한다.
urls.py
path('<int:pk>/delete2/', views.TodoDelV2.as_view(), name='delete2'), # 추가
todo_form_list.html
에서 a 태그의 removeBtn 링크 역할을 해주는 곳에 url을 todo:delete2로 수정한다.
<span class="removeBtn"><a href="{% url 'todo:delete2' todo.id %}">×</a></span> <!-- 수정 -->
views.py
에 코드 추가 작업을 한다.
class TodoDelV2(DeleteView):
model = Todo
template_name = 'todo/todo_confirm_delete.html'
success_url = reverse_lazy('todo:mixin')
삭제 처리를 하면서 GET 요청이 왔을 때 폼을 보여주는 대신에 Delete처리를 하도록 변경을 해버린다.
views.py
delete 처리하도록 Override get메소드 추가하고, template_name 속성은 필요없기 때문에 지운다.
class TodoDelV2(DeleteView):
model = Todo
# template_name = 'todo/todo_confirm_delete.html'
success_url = reverse_lazy('todo:mixin')
def get(self, request, *args, **kwargs):
return self.delete(request, *args, **kwargs)
todo_form_list.html
에서 삭제버튼을 누르면 팝업창이 뜨도록 만들것이다.
Bootstrap 공식 홈페이지에서 모달 컴포넌트 메뉴를 찾는다.
https://getbootstrap.com/docs/5.1/components/modal/
Modal은 팝업을 의미한다. Alert과는 다른 스타일 기능을 하며, 프론트의 기능을 담당하기도 한다.
먼저 todo_form_list.html
에서 작업을 진행하는데
컨텐츠 블럭과 스크립트 블럭을 작업한다.
{% extends 'base.html' %}
{% block title %}
todo_form.html
{% endblock %}
{% block extra-style %}
<style>
body {
text-align: center;
background-color: #ddd;
}
.inputBox {
margin: auto;
width: 70%;
background: white;
height: 50px;
border-radius: 50px;
line-height: 50px;
}
.inputBox .name {
border-style: none;
border-bottom: 1px solid #ddd;
width: 90px;
padding-left: 20px;
line-height: 20px;
}
.inputBox .item {
border-style: none;
border-bottom: 1px solid #ddd;
width: 400px;
margin-left: 50px;
padding-left: 20px;
line-height: 20px;
}
.todoList li {
display: flex;
height: 50px;
line-height: 50px;
margin: 0.5rem 0;
padding: 0 0.9rem;
background: white;
border-radius: 5px;
}
.removeBtn {
margin-left: auto;
font-size: 20px;
}
.removeBtn:hover {
color: #d24444;
}
.modal-footer a {
color: #ffffff;
}
</style>
{% endblock %}
{% block content %}
<div id='app'>
<h1>My Todo App !</h1>
<strong>서로 할 일이나 의견을 공유해 봅시다.</strong>
<br>
{# csrf 토큰 공격을 방지하는 csrf 토큰을 넣는다. 장고에서 제공해주는 템플릿 태그이다. #}
<form class="inputBox" action="." method="post"> {% csrf_token %}
<input class="name" type="text" placeholder="name ..." name="name">
<input class="item" type="text" placeholder="type anything welcomed ..."
name="todo">
<button class="btn btn-info btn-sm" type="submit">ADD</button>
</form>
<ul class="todoList">
{# v-for문 대신에, 장고에서 제공하는 템플릿 태그로 바꾼다. #}
{% for todo in object_list %}
<li>
<span>{{ todo.name }}::: {{ todo.todo }}</span>
<span class="removeBtn" data-toggle="modal" data-target="#myModal"
data-id="{{ todo.id }}" data-name="{{ todo.name }}" data-todo="{{ todo.todo }}">×</span>
</li>
{% endfor %}
</ul>
<!-- Modal -->
<div class="modal" id="myModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Are you sure to delete ?</h5>
</div>
<div class="modal-body">
body-text
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-sm">
<a href="">Delete</a></button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra-script %}
<script>
$('#myModal').on('show.bs.modal', function (event) {
let button = $(event.relatedTarget)
let id = button.data('id')
let name = button.data('name')
let todo = button.data('todo')
let modal = $(this)
modal.find('.modal-body').text(name + '::: ' + todo)
{# modal-body의 텍스트로 채워질 부분이다. #}
modal.find('.modal-footer a').attr('href', '/todo/' + id + '/delete2/')
{# attr 속성의 url은 a 태크의 링크로 채워진다. #}
})
</script>
{% endblock %}
GitHub Source
👉🏻깃허브 소스