티스토리 뷰

2020/08/15 - [Django] - localLibrary 홈페이지 만들기 : index 페이지

 

localLibrary 홈페이지 만들기 : index 페이지

2020/08/13 - [Django] - localLibrary 관리자 페이지 localLibrary 관리자 페이지 2020/08/04 - [Django] - localLibrary 모델(models.py) 편집 localLibrary 모델(models.py) 편집 2020/08/03 - [Django] - loca..

editor752.tistory.com

1. 책 목록 페이지

책 목록 페이지에는 모든 사용 가능한 책 레코드들의 목록이 나열되어야 한다.

그리고 이 페이지 URL은 catalog/books/로 할 것이다. 이 페이지는 각 레코드마다 제목과 저자를 나타낼 것이며, 제목은 관련된 책 페이지로 이동하는 하이퍼링크 처리된다. 이 페이지는 사이트의 다른 페이지들과 같은 구조로 만들 것이기에 이전 튜토리얼에서 만들었던 템플릿(base_generic.html)을 확장하여 사용할 것이다.

1-1. url 매핑

catalog/urls.py 파일을 열고 아래 코드를 추가한다.

urlpatterns = [
    path('books/', views.BookListView.as_view(), name='books'),
]

인덱스 페이지처럼 path() 함수는 URL(books/)과 매치되는 패턴, URL이 매치될 때 호출되는 view 함수(views.BookListView.as_view()), 그리고 이 특정 매핑에 대한 이름을 정의하였다.

View함수는 이전과 다른 형태를 띈다. 왜냐면 이 view는 사실 클래스로 구현이 되기 때문입니다. 우리는 이 view를 직접 구현하지 않고, 이미 존재하는 generic view 함수를 상속받아 view 함수를 구현할 것이다. 이 generic view 함수는 우리가 구현하고 싶은 기능들을 거의 다 가지고 있다.

Django의 클래스 기반 view에서는, as_view() 클래스 메소드를 호출해 적절한 view 함수에 접근할 수 있다. 이건 클래스의 인스턴스를 생성하는 작업과 모든 HTTP 요청을 처리하는 핸들러 메소드의 동작을 처리한다.

1-2. 뷰

표준 function으로 쉽게 book list view를 만들 수 있는데 모든 책에 대한 데이터베이스 쿼리를 만들어서 render() 함수를 불러 특정 템플릿에 리스트를 보낸다. 대신 우리는 class-based generic list view (ListView)를 사용한다. 그런데 존재하는 뷰로부터 상속받아온 클래스이다. generic view가 이미 우리가 필요한 대부분의 기능성을 실행하면서 동시에 Django best-practice이기 때문에, 우리는 코드의 양과 반복을 줄이고 궁극적으로 유지보수에 수고가 덜 드는 견고한 리스트 뷰를 만들 수 있다.

catalog/views.py 파일을 열고, 아래의 코드를 가장 아래에 추가하자.

from django.views import generic

class BookListView(generic.ListView):
    model = Book  

이게 전부입니다! Generic view는 명시된 모델(Book)의 모든 레코드를 가져오기 위해 데이터베이스를 쿼리할 것이고, /locallibrary/catalog/templates/catalog/book_list.html(만들 예정) 경로에 있는 템플릿을 렌더링한다. 템플릿 안에서 우리는 object_listbook_list라는 템플릿 변수를 사용해 도서 목록에 접근할 수 있다. (일반적으로 "the_model_name_list").

note: Generic views/application_name/the_model_name_list.html(현재는 catalog/book_list.html)에서 템플릿을 찾는다. 이 경로는 애플리케이션의 /application_name/templates/ 디렉토리 안에 있다(/catalog/templates/).

note: Built-in class-based generic views를 방문해 다양한 예제를 살펴볼 수 있다.

속성이나 디폴트 동작을 추가할 수도 있다. 예를 들어, 같은 모델을 사용하지만 여러 개의 뷰를 사용해야 되는 경우 다른 템플릿 파일을 명시할 수 있다. 혹은 book_list 템플릿 변수명이 직관적이지 않다고 생각해 다른 템플릿 변수명을 사용하고 싶을지도 모른다. 아마 가장 유용한 변용은 리턴값의 결과를 바꾸거나 필터링하는 것이다. 따라서 모든 책을 나열하는 대신, 유저가 읽은 순으로 5개의 책을 나열할 수도 있다.

class BookListView(generic.ListView):
    model = Book
    context_object_name = 'my_book_list'   # your own name for the list as a template variable
    queryset = Book.objects.filter(title__icontains='war')[:5] # Get 5 books containing the title war
    template_name = 'books/my_arbitrary_template_name_list.html'  # Specify your own template name/location

클래스 기반 뷰의 메소드 오버라이딩

클래스 메소드 오버라이딩을 할 수도 있습니다. 예를 들어, 우리는 get_queryset() 메소드를 오버라이딩해 반환되는 레코드의 리스트들을 바꿀 수 있다. 이건 이전에 했던 queryset 속성을 지정하는 방법보다 더 유연한 방법이다.

class BookListView(generic.ListView):
    model = Book

    def get_queryset(self):
        return Book.objects.filter(title__icontains='war')[:5] # Get 5 books containing the title war

템플릿에 추가적인 컨텍스트 변수들을 전달하기 위해 get_context_data()를 오버라이딩할 수도 있다. (도서 목록이 디폴트로 전달됩니다.) 아래의 코드는 some_data라는 이름의 변수를 어떻게 컨텍스트에 추가하는지를 보여준다. (이렇게 하면 템플릿 변수로 사용할 수 있다.)

class BookListView(generic.ListView):
    model = Book

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get the context
        context = super(BookListView, self).get_context_data(**kwargs)
        # Create any data and add it to the context
        context['some_data'] = 'This is just some data'
        return context

이렇게 할 때에는, 아래의 패턴에 따라야 한다:

  • 먼저 슈퍼클래스에서 기존 컨텍스트를 가져온다(context = super(BookListView, self).get_context_data(**kwargs)).
  • 그리고 새로운 컨텍스트 정보를 추가한다(context['some_data'] = 'This is just some data').
  • 마지막으로 새롭게 업데이트된 컨텍스트를 리턴한다(return context).

1-3. 리스트 뷰 템플릿 생성하기

/locallibrary/catalog/templates/catalog/book_list.html 경로에 HTML 파일을 만든 다음, 아래의 코드를 입력하자. 이전에 설명한 것처럼, 이건 제네릭 클래스 기반 리스트 뷰에서 예상되는 기본 템플릿 파일입니다. (catalog 애플리케이션 내의 Book 모델)

{% extends "base_generic.html" %}

{% block content %}
  <h1>Book List</h1>
  {% if book_list %}
  <ul>
    {% for book in book_list %}
      <li>
        <a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
      </li>
    {% endfor %}
  </ul>
  {% else %}
    <p>There are no books in the library.</p>
  {% endif %}       
{% endblock %}

뷰는 object_listbook_list라는 디폴트 aliases로 컨텍스트(도서 목록)를 전달한다. object_listbook_list 둘 중 어느 것을 적어도 상관이 없다.

이런! index 페이지에 전체 장르별 권수를 index 페이지에 넣기 위해 했던 삽질이 무의미해지는 순간이다. 텡플릿 태그로 if, for 문을 사용할 수 있을 줄이야!

조건부 실행

if, else 그리고 endif 라는 템플릿 태그들은 book_list가 정의되었는지, 그리고 존재하는지 확인한다. book_list가 없다면 else 절의 텍스트 문구가 표시될 것이며, book_list가 존재한다면 도서 목록의 갯수만큼 반복만큼 반복해서 실행한다.

{% if book_list %}
  <!-- code here to list the books -->
{% else %}
  <p>There are no books in the library.</p>
{% endif %}\

elif라는 템플릿 태그를 사용해 추가적인 조건을 걸어 테스트할 수 있다.

반복 구문

forendfor라는 템플릿 태그들은 도서 목록을 살펴보는 루프를 위해 사용한다. 각각의 반복은 book 템플릿 변수에 현재 리스트 아이템에 대한 정보를 채운다.

{% for book in book_list %}
  <li> <!-- code here get information from each book item --> </li>
{% endfor %}

여기에서는 사용되지 않지만, 반복 구문 내에서 Django는 반복을 추적할 수 있는 다른 변수들을 만들 수 있다. 예를 들어, forloop.last라는 변수로 루프의 마지막 실행에 대한 조건부 처리를 할 수 있다.

변수 접근하기

반복 구문 내에서의 코드는 각각의 책에 대한 리스트 아이템을 생성한다. 이 리스트 아이템은 타이틀(아직 작성되지 않은 상세 뷰의 링크)과 작가의 이름을 나타낸다.

<a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})

점 표기법(dot notation)을 사용해서 연관된 책의 레코드(예를 들어 book.titlebook.author)에 대한 필드에 접근이 가능하다. book 다음의 텍스트는 모델에 정의되어 있는 필드의 이름이다.

우리는 템플릿 안에 모델에서 정의한 함수를 불러올 수도 있다. 이 경우, Book.get_absolute_url() 함수를 호출해 연관된 세부 레코드를 표시하는 URL을 가져온다. 이 작업은 함수가 아무 인자를 가지지 않을 때 제공됩니다(여기에는 인자를 넘길 방법이 없습니다!).

베이스 템플릿 업데이트

베이스 템플릿(/locallibrary/catalog/templates/base_generic.html)을 열고 All books의 URL 링크 부분에 {% url 'books' %} 코드를 삽입하자. 이건 모든 페이지 링크에 적용될 것이다.

<li><a href="{% url 'index' %}">Home</a></li>
<li><a href="{% url 'books' %}">All books</a></li>
<li><a href="">All authors</a></li>

2. Book 상세 페이지

URL catalog/book/<id>(<id>는 book의 primary key)에 접근해서, Book의 상세 페이지는 특정 책의 정보를 보여줄 것이다. Book 모델의 author, summary, ISBN, language, 그리고 genre과 같은 필드에 더해 복사본(BookInstances)의 상세부분, 즉 상태와 기대하는 반납일, 기록 그리고 아이디 등을 리스트화할 것이다. 이렇게 하면 독자들이 책의 리스트를 확인할 뿐만 아니라, 언제 책을 대여할 수 있는지에 대한 여부를 확인할 수 있게 해준다.

2-1. url 매핑

/catalog/urls.py 파일을 열고 아래 book-detail URL mapper를 추가하자. path() 함수는 연관된 제네릭 클래스 기반의 상세 뷰와 이름에 대한 패턴을 정의한다.

urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
    path('books/<uuid:pk>', views.BookDetailView.as_view(), name='book-detail'),
]

book-detail URL 패턴은 우리가 원하는 책의 id를 캡처하기 위해 특별한 구문을 사용한다. 꺾쇠 괄호는 캡처하는 URL의 일부를 정의하고 뷰가 캡처된 데이터에 액세스하는 데 사용할 수 있는 변수의 이름을 지정한다. 예를 들어, <something>은 패턴을 캡처해서 something이라는 변수에 데이터를 담아 전달합니다. 우리는 선택적으로 변수 이름 앞에 데이터 형식 (int, str, slug, uuid, path)을 정의하는 converter specification을 사용할 수 있다.
여기에서 우리는 book id을 캡쳐하기 위해 <uuid:pk>라는 특별히 포맷화된 문자열을 활용할 것이다. 그리고 pk (primary key의 단축어)라는 이름의 파라미터로서 뷰로 넘겨줄 것이다.
(번역 봉사자 주: uuid를 읽지 못한다면[NoReverseMatch] int:pk로 해보십시오.)

note: 내 경우에는 실습을 하면서 이 문제로 골치를 썩었다. 위에서 간단히 언급된 바를 간과했기 때문인데 path('books/<uuid:pk>', views.BookDetailView.as_view(), name='book-detail'), 코드가 동작하지 않았다.

북 상세 페이지까지 완성한 후 All books 메뉴에 들어갔더니 위와 같은 에러가 발생하였다. 뭐가 문제인지 알지 못한 채 이제까지의 코드를 복기했다. 하지만 어디가 문제인지 알 수 없었다. 뭘 이해하지 못했던 것일까? 코드를 이래저래 바꿔보다가 좌절하고 이제까지 강의를 다시 읽는 과정에서 이 부분을 발견했다.

이 코드를 path('books/<int:pk>', views.BookDetailView.as_view(), name='book-detail'),로 변경한 후에야 제대로된 Book 목록 페이지가 아래와 같이 떴다.

만약 실슬 과정에서 나와 같은 오류를 만나게 되면 위와 같이 코드를 수정하는 것을 잊지 말자!

note: 앞에서 언급했듯이, 관련된 URL 은 실제로는 catalog/book/<digits>다.
note: 통상 class-based detail view는 pk라는 이름을 가진 파라미터로 전달한다. 만일 자체적으로 function view를 만든다면 어떤 이름이라도 사용 가능하다. 혹은 이름이 없는 argument에 정보를 넣어 전달할 수도 있다.

정규식을 이용한 고급 path matching

path()를 이용한 패턴 검색은 간단하고 일반적인 경우 - 예를 들어 단지 특정 문자열이나 숫자가 있는지 - 매우 유용하다. 만일 좀 더 세밀한 조건 - 예를 들어 특정 문자열 길이를 갖는 문자열 검색 - 으로 검색하고자 한다면. re_path()를 사용하길 바란다. re_path()는 정규식을 사용한다는 점만 빼고 path()와 똑같다. 예를 들어 앞서 서술한 path는 다음과 같이 re_path로 대체할 수 있다.

    re_path(r'^book/(?P<pk>\d+)$', views.BookDetailView.as_view(), name='book-detail'),
  • r'^book/(?P<pk>\d+)$': 이 표현식은 먼저 문자열이 book/으로 시작하는지 검사하고 (^book/) 그 다음에 한 개 이상의 숫자가 오는지(\d+), 그리고 문자열이 끝나기 전에 숫자가 아닌 문자가 들어 있지는 않는지 검사한다. 또한 이 표현식은 모든 숫자들을 변환하고(?P<pk>\d+) 변환된 값을 view에 pk라는 이름의 parameter로 넘긴다. 변환된 값은 항상 String type으로 넘어간다! 예를 들어, 이 표현식은 book/1234을 매칭하고 변수 pk='1234' 를 view에 넘긴다.

2-2. 뷰

catalog/views.py을 열고, 다음 코드를 추가하자.

class BookDetailView(generic.DetailView):
    model = Book

이제 해야될 일은 /locallibrary/catalog/templates/catalog/book_detail.html template를 만들면, view는 template에 URL mapper에 의해 찾고자 하는 데이터베이스에 있는 특정 Book 레코드의 정보를 전달할 겁니다. template 안에서 template 변수 object 또는 book(즉, 일반적으로는 "해당_모델_명")으로 책 목록에 접근할 수 있다.

만약 필요하다면, 사용하고 있는 template 또는 template 안에서 book을 참조하는 데 사용되는 context object의 이름을 바꿀 수 있다. 또한, 예를 들어 context에 정보를 추가하는 식으로, 메서드를 오버라이드할 수도 있다.

만약 해당 레코드가 존재하지 않는다면 무슨 일이 일어날까요?

만약 요청된 레코드가 존재하지 않는다면, 제네릭 클래스 기반의 detail view는 Http404 exception이 저절로 발생할 것이다. — 만들어질 때, 적절한 "resource not found" 페이지를 알아서 보여줄 것이다. 만약 원한다면 당신이 해당 페이지를 수정할 수 있다.

이런 일이 어떻게 발생하는지 원리를 조금 알려줄게요. 밑에 있는 코드는 만약 당신이 제네릭 클래스 기반의 detail view를 사용하지 않는다면, 클래스 기반의 view를 어떻게 함수 형태로 표현 할 수 있는지 보여준다.

def book_detail_view(request, primary_key):
    try:
        book = Book.objects.get(pk=primary_key)
    except Book.DoesNotExist:
        raise Http404('Book does not exist')

    return render(request, 'catalog/book_detail.html', context={'book': book})

2-3. 상세 뷰 템플릿 생성하기

/locallibrary/catalog/templates/catalog/book_detail.html 파일을 만들고, 아래 코드를 추가한다. 위에서 설명한대로, 이 파알명은 제네릭 클래스 기반 상세 뷰의 디폴트 파일명입니다. (catalog 애플리케이션의 Book 모델을 위한 상세 뷰)

{% extends "base_generic.html" %}

{% block content %}
  <h1>제목: {{ book.title }}</h1>

  <p><strong>저자:</strong> <a href="">{{ book.author }}</a></p> <!-- author detail link not yet defined -->
  <p><strong>요약:</strong> {{ book.summary }}</p>
  <p><strong>ISBN:</strong> {{ book.isbn }}</p> 
  <p><strong>언어:</strong> {{ book.language }}</p>  
  <p><strong>장르:</strong> {% for genre in book.genre.all %} {{ genre }}{% if not forloop.last %}, {% endif %}{% endfor %}</p>  

  <div style="margin-left:20px;margin-top:20px">
    <h4>Copies</h4>

    {% for copy in book.bookinstance_set.all %}
      <hr>
      <p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'm' %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
      {% if copy.status != 'a' %}
        <p><strong>Due to be returned:</strong> {{copy.due_back}}</p>
      {% endif %}
      <p><strong>Imprint:</strong> {{copy.imprint}}</p>
      <p class="text-muted"><strong>Id:</strong> {{copy.id}}</p>
    {% endfor %}
  </div>
{% endblock %}
  • content 블럭을 오버라이드 해서 우리의 기본 템플릿을 extend하였다.
  • 조건 판단 처리를 해서 특정 컨텐츠가 있을지 없는지 체크하여 표시한다.
  • for 루프를 활용하여 objects들의 리스트를 표현한다.
  • context fields를 dot notation를 활용한다(왜냐하면 우리는 detail generic view를 사용하는데, context의 이름은 book 이기에 우리는 "object"를 사용할 수 있습니다).

새롭게 사용된 부분이 book.bookinstance_set.all() 함수이다. 이 메소드는 Django에 의해 자동으로 만들어진 메소드이다. 이 메소드는 Book과 관련된 BookInstance 레코드 집합을 반환한다.

{% for copy in book.bookinstance_set.all %}
  <!-- code to iterate across each copy/instance of a book -->
{% endfor %}

이 메소드는 관계의 한쪽(one)에만 ForeignKey(one-to many) 필드를 선언했기 때문에 필요하다. 다른(many) 모델에서 아무것도 선언하지 않았기 때문에 관련 레코드 집합을 가져올 필드가 없는 것이다. 이 문제를 해결하기 위해, Django는 지금 우리가 사용하고 있는 reverse lookup이라는 적당한 이름의 함수를 만듭니다. 이 함수의 이름은 ForeignKey가 선언되어 있는 모델 이름을 소문자로 만들고, 그 뒤에 _set을 붙이면 됩니다. (따라서 Book에서 만든 함수는 bookinstance_set()가 되겠죠.)

이 템플릿의 작가 링크는 아직 비어있다. 그렇기에 링크를 클릭한다고 해도 페이지에 변화는 없을 것이다. 이는 아직 작가 상세 페이지를 만들지 않았기 때문으로 만약 페이지가 존재한다면, URL을 아래와 같이 업데이트 해야 한다.

<a href="{% url 'author-detail' book.author.pk %}">{{ book.author }}</a>

여기까지 마쳤다면 아래와 같은 화면을 볼 수 있을 것이다.

 

2020/08/23 - [Django] - localLibrary 도전과제 Author 페이지 및 상세 페이지 만들기

 

localLibrary 도전과제 Author 페이지 및 상세 페이지 만들기

이전 글에서 licalLibrary 사이트의 잭 페이지 및 상세 페이지를 만들어 보았다. 이를 이용하여 저자 및 저자 상세 페이지를 만들어 보았다. 1. Author 페이지 1-1. url 맵퍼 catalog/urls.py 를 아래와 같이 ��

editor752.tistory.com

 


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함
05-05 18:55