콘텐츠 편집 기능(Bookmark, Blog)

jurin·2021년 12월 21일
0

각 앱의 DB에 들어 있는 레코드들을 장고에서는 콘텐츠라고 지칭하는데 이를 Admin 사이트에서 관리자만이 콘텐츠를 생성, 변경할 수 있었다. 일반 사용자들도 콘텐츠를 생성 및 변경할 수 있도록 한다.

but, 콘텐츠 생성 및 변경하는 권한을 모든 사용자에게 부여해서는 안된다.

  • 콘텐츠에 대한 열람은 모든 사용자가 가능하다.
  • 콘텐츠를 새로 생성하는 것은 로그인한 사용자만 가능하다. - [Add] 메뉴
  • 콘텐츠를 수정, 삭제하는 작업은 그 콘텐츠를 생성한 사용자만 가능하다. - [Change] 메뉴

테이블 설계

콘텐츠에 대한 소유자를 확인해야 하므로 각 콘텐츠 테이블별로 소유자 필드가 필요하다.

Bookmark 및 Post 테이블에 owner 필드(User 테이블에 대한 외래 키)가 추가되도록 설계한다.

필드명: owner
타입: ForeignKey(User)
제약 조건: Null

URL 설계

  • 콘텐츠 생성(add)
  • 변경(change)
  • 대상 리스트
  • 수정(update)
  • 삭제(delete)

Model 코딩하기

bookmark/models.py 수정

    owner = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)

로그인한 사용자는 여러 개의 북마크를 생성할 수 있으므로 Bookmark와 User 테이블 사이는 N:1 관계이므로 외래 키로 표현한다.

기존 레코드가 존재하는 상태에서 owner 필드를 추가하는 것이기 때문에 owner 필드는 Null 값을 가질 수 있도록 해야 한다. 폼에서도 owner 필드는 입력하지 않아도 되도록 blank=True로 설정했다.

blog/models.py 수정

모델의 필드가 추가되었으므로 변경 사항을 DB에 저장해준다.

URLconf 코딩

bookmark/urls.py

    # /bookmark/add/
    path('add/', views.BookmarkCreateView.as_view(), name="add"),

    # /bookmark/change/
    path('change/', views.BookmarkChangeLV.as_view(), name="change"),

    # /bookmark/99/update
    path('<int:pk>/update/', views.BookmarkUpdateView.as_view(), name="update"),

    # /bookmark/99/delete
    path('<int:pk>/delete/', views.BookmarkDeleteView.as_view(), name="delete"),

blog/urls.py

    # /blog/add/
    path('add/', views.PostCreateView.as_view(), name="add"),

    # /blog/change/
    path('change/', views.PostChangeLV.as_view(), name="change"),

    # /blog/99/update
    path('<int:pk>/update/', views.PostUpdateView.as_view(), name="update"),

    # /blog/99/delete
    path('<int:pk>/delete/', views.PostDeleteView.as_view(), name="delete"),

View 코딩

URLconf에서 지정한 뷰를 코딩해준다.

bookmark/views.py

blog/views.py

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    fields = ['title', 'slug', 'description', 'content', 'tags']
    initial = {'slug': 'auto-filling-do-not-input'}
    # fields = ['title', 'description', 'content', 'tags']
    success_url = reverse_lazy('blog:index')

    def form_valid(self, form):
        form.instance.owner = self.request.user
        return super().form_valid(form)


class PostChangeLV(LoginRequiredMixin, ListView):
    template_name = 'blog/post_change_list.html'

    def get_queryset(self):
        return Post.objects.filter(owner=self.request.user)


class PostUpdateView(OwnerOnlyMixin, UpdateView):
    model = Post
    fields = ['title', 'slug', 'description', 'content', 'tags']
    success_url = reverse_lazy('blog:index')


class PostDeleteView(OwnerOnlyMixin, DeleteView):
    model = Post
    success_url = reverse_lazy('blog:index')

PostCreateView 클래스의 initual로 폼의 slug 입력 항목에 대한 초기값을 지정한다. slug 필드는 title 필드로부터 자동으로 채워지는 필드로 models.py 파일의 Post 모델 정의에 있는 save() 함수에서 수행된다.

slug 필드를 처리하는 또 다른 방법은 fields 속성에서 제외해 폼에 나타나지 않도록 하는 방법으로 폼에는 보이지 않지만 Post 모델의 save() 함수에 의해 테이블의 레코드에 자동으로 채워진다.

BookMarkApp/views.py

OwnerOnlyMixin 클래스: 콘텐츠의 소유자인지를 판별

템플릿 코딩

편집용 제네릭 뷰를 상속받아 뷰를 작성할 경우 template_name 속성으로 템플릿명을 지정하지 않으면 장고에서 정의한 디폴트 템플릿 명을 사용한다.

base.html

  • [Add]와 [Change] 메뉴 수정

                    <li class="nav-item dropdown mx-1 btn btn-primary">
                        <a class="nav-link dropdown-toggle text-white" href="#" data-bs-toggle="dropdown">Add</a>
                        <div class="dropdown-menu">
                            <a class="dropdown-item" href="{% url 'bookmark:add' %}">Bookmark</a>
                            <a class="dropdown-item" href="{% url 'blog:add' %}">Post</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" href="">Album</a>
                            <a class="dropdown-item" href="">Photo</a>
                        </div>
                    </li>

                    <li class="nav-item dropdown mx-1 btn btn-primary">
                        <a class="nav-link dropdown-toggle text-white" href="#" data-bs-toggle="dropdown">Change</a>
                        <div class="dropdown-menu">
                            <a class="dropdown-item" href="{% url 'bookmark:change' %}">Bookmark</a>
                            <a class="dropdown-item" href="{% url 'blog:change' %}">Post</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" href="">Album</a>
                            <a class="dropdown-item" href="">Photo</a>
                        </div>
                    </li>

bookmark/bookmark_form.html

{% extends 'base.html' %}

{% block title %}bookmark_detail.html{% endblock %}

{% block content %}
    <h1>
        Bookmark Create/Update - {{ user }}
    </h1>
    <p class="font-italic">This is a creation or update form for your bookmark.</p>

    {% if form.errors %}
    <div class="alert alert-danger">
        <div class="font-weight-bold">Wrong! Please correct the error(s) below.</div>
        {{ form.errors }}
    </div>
    {% endif %}

    <form action="." method="post" class="card pt-3">
        {% csrf_token %}

        <div class="col-sm-5">
            {{ form.title|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}

            <div class="col-sm-5">
                {{ form.title|add_class:"form-control"|attr:"auto:"autofocus" }}
            </div>
        </div>
        
        <div class="form-group row">
            {{ form.url|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
            <div class="col-sm-5">
                {{ form.url|add_class:"form-control" }}
            </div>
        </div>

        <div class="form-group">
            <div class="offset-sm-2 col-sm-5">
                <input type="submit" value="Submit" class="btn btn-info"/>
            </div>
        </div>
    </form>
{% endblock %}

login.html과 거의 동일하다. 중요한 차이점은 login.html 파일의 경우 AuthenticationForm을 사용했지만 bookmark_form.html 파일의 form 변수는 Bookmark 모델을 사용해 장고가 내부적으로 만든 폼 객체라는 점이다.

즉, CreateView와 UpdateView는 Bookmark 모델에 대한 ModelForm을 스스로 만들고 사용한다는 점을 유의해야 한다.

bookmark/bookmark_change_list.html

Bookmark 테이블의 레코드를 변경하기 위해 기존 레코드의 리스트를 보여주는 템플릿이다.

  • 테이블에 부트스트랩 클래스 적용 ( 테이블 셀에 대한 테두리, 줄 간격, 번갈아 가면서 줄마다 음영 표시)
  • 테이블 제목은 primary 색으로
  • object_list의 각 항목을 순회하면 테이블의 데이터 행 추가
{% extends 'base.html' %}

{% block title %}bookmark_change_list.html{% endblock %}

{% block content %}
    <h1>
        Bookmark Change - {{ user }}
    </h1>
    <br>

    <table class="table table-bordered table-condensed table-striped">
        
        <thead>
            <tr class="table-primary">
                <th>Title</th>
                <th>Url</th>
                <th>Owner</th>
                <th>Update</th>
                <th>Delete</th>
            </tr>
        </thead>
        
        <tbody>
            {% for item in object_list %}
            <tr>
                <td>{{ item.title }}</td>
                <td>{{ item.url }}</td>
                <td>{{ item.owner }}</td>
                <td><a href="{% url 'bookmark:update' item.id %}">Update</a></td>
                <td><a href="{% url 'bookmark:delete' item.id %}">Delete</a>{</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>

{% endblock %}

bookmark_confirm_delete

레코드를 삭제하기 전 확인하는 화면

{% extends 'base.html' %}

{% block title %}bookmark_confirm_delete.html{% endblock %}

{% block content %}
    <h1>
        Bookmark Delete
    </h1>
    <br>

    <form action="." method="post">
        {% csrf_token %}
        <p>Are you sure you want to delete "{{ object }}" ?</p>
        <input type="submit" value="Confirm" class="btn btn-danger btn-sm" />
    </form>

</div>
{% endblock %}

blog/post_form.html

Post 레코드를 생성 or 수정하기 위한 폼을 보여주는 화면

  • slug 필드에 readonly 속성을 지정해서 사용자가 입력할 수 없도록 지정 -> blog 앱이 title 필드에 입력된 단어를 사용해서 자동으로 만들어주기 때문
  • slug, descripton, tags 필드의 help_text 옵션 문구(도움말)를 출력
{% extends 'base.html' %}
{% load widget_tweaks %}

{% block title %}post_form.html{% endblock %}

{% block content %}
    <h1>
        Post Create/Update - {{ user }}
    </h1>
    <p class="font-italic">This is a creation or update form for your post.</p>

    {% if form.errors %}
    <div class="alert alert-danger">
        <div class="font-weight-bold">Wrong! Please correct the error(s) below.</div>
        {{ form.errors }}
    </div>
    {% endif %}

    <form action="." method="post" class="card pt-3">
        {% csrf_token %}

        <div class="col-sm-5">
            {{ form.title|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}

            <div class="col-sm-5">
                {{ form.title|add_class:"form-control"|attr:"autofocus" }}
            </div>
        </div>

        <div class="form-group row">
            {{ form.slug|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
            <div class="col-sm-5">
                {{ form.slug|add_class:"form-control"|attr:"readonly" }}
            </div>
            <small class="font-text text-muted">{{ form.slug.help_text }}</small>
        </div>
        
        <div class="form-group row">
            {{ form.description|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
            <div class="col-sm-5">
                {{ form.description|add_class:"form-control" }}
            </div>
            <small class="font-text text-muted">{{ form.description.help_text }}</small>
        </div>
        
        <div class="form-group row">
            {{ form.content|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
            <div class="col-sm-">
                {{ form.content|add_class:"form-control" }}
            </div>
        </div>
        
        <div class="form-group row">
            {{ form.tags|add_label_class:"col-form-label col-sm-2 ml-3 font-weight-bold" }}
            <div class="col-sm-5">
                {{ form.tags|add_class:"form-control" }}
            </div>
            <small class="font-text text-muted">{{ form.tags.help_text }}</small>
        </div>

        <div class="form-group">
            <div class="offset-sm-2 col-sm-5">
                <input type="submit" value="Submit" class="btn btn-info"/>
            </div>
        </div>
    </form>
{% endblock %}

blog/post_change_list.html

{% extends 'base.html' %}

{% block title %}post_change_list.html{% endblock %}

{% block content %}
    <h1>
        Post Change - {{ user }}
    </h1>
    <br>

    <table class="table table-bordered table-sm table-striped">

        <thead>
            <tr class="table-primary">
                <th>Title</th>
                <th>Description</th>
                <th>Owner</th>
                <th>Update</th>
                <th>Delete</th>
            </tr>
        </thead>

        <tbody>
            {% for item in object_list %}
            <tr>
                <td>{{ item.title }}</td>
                <td>{{ item.description }}</td>
                <td>{{ item.owner }}</td>
                <td><a href="{% url 'blog:update' item.id %}">Update</a></td>
                <td><a href="{% url 'blog:delete' item.id %}">Delete</a>{</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>

{% endblock %}

blog/post_confirm_delete.html

{% extends 'base.html' %}

{% block title %}post_confirm_delete.html{% endblock %}

{% block content %}
    <h1>
        Post Delete
    </h1>
    <br>

    <form action="." method="post">
        {% csrf_token %}
        <p>Are you sure you want to delete "{{ object }}" ?</p>
        <input type="submit" value="Confirm" class="btn btn-danger btn-sm" />
    </form>

</div>
{% endblock %}

templates/403.html

OwnerOnlyMixin 클래스에서 사용하는 템플릿 파일로 403 익셉션을 발생시키면 장고의 디폴트 핸들러 중 하나인 permission_denied() 함수에서 403.html을 렌더링해서 클라이언트에게 403 응답을 보낸다.

{% extends "base.html" %}

{% block title %}403.html{% endblock %}

{% block content %}

    <h1>Permission Denied</h1>
    <br>

    <div class="alert alert-danger">
        <div class="font-weight-bold">
            {{ exception }}
        </div>
    </div>

{% endblock %}

{{ exception }} 컨텍스트 변수는 장고의 permission_denied() 핸들러에서 넘겨주는 템플릿 변수로 우리가 지정한 permission_denied_message 문구에 해당한다.





출처: Django로 배우는 파이썬 웹 프로그래밍(실전편) - 김석훈님

profile
anaooauc1236@naver.com

0개의 댓글