결과물


‘리뷰 작성하기’ 버튼을 누르면 form 페이지로 이동


맛집 리뷰 form


맛집 form





forms.py

앱 폴더에 forms.py 파일을 만들고 아래 코드 추가


from django import forms 
from django.db import models

from .models import Review, Restaurant

class RestaurantForm(forms.ModelForm):
    class Meta:
        model = Restaurant
        fields = ['name', ...]

class ReviewForm(forms.ModelForm):
    class Meta:
        model = Review
        fields = ['restaurant', 'score', ...]





CreateView - views.py


간단한 뷰


from django.views.generic import CreateView
from .forms import ReviewForm

class ReviewCreateView(CreateView):
    template_name = 'reviewBoard/review_new.html'
    success_url = '/' #1
    form_class = ReviewForm #2


  • #1 : 데이터 생성에 성공한 경우 리다이렉트할 url. 위에서는 index 페이지로 이동한다.
  • #2 : CreateView가 사용할 form_class를 지정한다.



urls.py

urlpatterns = [
    path('review/new/', views.ReviewCreateView.as_view(), name='review_new'),

]





한 템플릿에 두 개의 폼 넣기

리뷰를 작성하려면 Restaurant 필드에서 식당을 선택해야 한다. 하지만 원하는 식당이 데이터에 없다면, 리뷰를 추가할 수 없는 상황이 온다.

그래서 Review Form을 먼저 보여준 뒤, 원하는 식당이 없다면 Restaurant Form 을 보여주고, Review Form은 숨긴다.

기존 CreateView로는 하나의 폼만 사용할 수 있기 때문에 믹스인, 클래스 상속으로 여러 폼을 넣을 수 있는 클래스뷰를 만든다.

아래 코드들을 참고했으나, validator가 제대로 작동하지 않는 오류가 있어 내 코드에 맞게 일부분 바꿨다.


참고 코드

참고 코드2





mixin


# code source : https://gist.github.com/michelts/1029336

from django.shortcuts import render, render_to_response
from .forms import ReviewForm, RestaurantForm

#################### Mixins ####################
from django.views.generic.base import View, TemplateResponseMixin
from django.views.generic.edit import FormMixin, ProcessFormView

class MultipleFormsMixin(FormMixin):
    """
    A mixin that provides a way to show and handle several forms in a
    request.
    """
    form_classes = {} 

    def get_form_classes(self):
        return self.form_classes

    def get_forms(self, form_classes):
        return dict([(key, klass(**self.get_form_kwargs())) \
            for key, klass in form_classes.items()])

    def forms_valid(self, forms):
        return super(MultipleFormsMixin, self).form_valid(forms)

    def forms_invalid(self, forms):
        return self.render_to_response(self.get_context_data(forms=forms))


class ProcessMultipleFormsView(ProcessFormView):
    """
    A mixin that processes multiple forms on POST. Every form must be
    valid.
    """
    def get(self, request, *args, **kwargs):
        form_classes = self.get_form_classes()
        forms = self.get_forms(form_classes)
        return self.render_to_response(self.get_context_data(forms=forms))

    #1
    def post(self, request, *args, **kwargs):
        form_classes = self.get_form_classes()
        forms = self.get_forms(form_classes)

        if 'ReviewForm' in request.POST:
            form_class = self.form_classes['ReviewForm']
            form_name = 'ReviewForm'
        else:
            form_class = self.form_classes['RestaurantForm']
            form_name = 'RestaurantForm'

        #2
        form = self.get_form(form_class)

        if form.is_valid(): #3
            return self.form_valid(form)
        else:
            #4 
            return self.render_to_response (self.get_context_data(forms=forms))


class BaseMultipleFormsView(MultipleFormsMixin, ProcessMultipleFormsView):
    """
    A base view for displaying several forms.
    """

class MultipleFormsView(TemplateResponseMixin, BaseMultipleFormsView):
    """
    A view for displaing several forms, and rendering a template response.
    """



form 마다 나눠서 POST하기


class ProcessMultipleFormsView(ProcessFormView):

    ...(생략)...

    def post(self, request, *args, **kwargs):
        form_classes = self.get_form_classes()
        forms = self.get_forms(form_classes)

        #1
        if 'ReviewForm' in request.POST:
            form_class = self.form_classes['ReviewForm']
            form_name = 'ReviewForm'
        else:
            form_class = self.form_classes['RestaurantForm']
            form_name = 'RestaurantForm'

        form = self.get_form(form_class)

        if form.is_valid(): #2
            return self.form_valid(form)
        else:
            #3
            return self.render_to_response (self.get_context_data(forms=forms))


  • #1 : form_classes에서 key 값이 ‘ReviewForm’인 것의 value를 ReviewForm 객체이다. 만약 request.POST을 통해 전달된 form이 ReviewForm이라면 form_class 변수에 ReviewForm 객체를 담고, 그렇지 않으면 RestaurantForm 객체를 담는다.

  • #2 : form 변수에 담긴 form이 유효한지 확인하고, 유효하다면 MultipleFormsMixin에 담긴 form_valid를 실행.

  • #3 : 유효하지 않다면, form에 담긴 데이터를 그대로 다시 돌려주고, 현재 페이지에 다시 리다이렉트 된다.





views.py


ReviewCreateView


from django.shortcuts import render, render_to_response
from django.views.generic import CreateView

from django.utils.decorators import method_decorator
from django.contrib.auth import views, models, login, decorators 
from django.contrib.auth.decorators import login_required 

from .models import Restaurant, Review
from .forms import ReviewForm, RestaurantForm


# New Review
class ReviewCreateView(CreateView, MultipleFormsView):
    model = Review
    fields = ('writer','restaurant', 'score', 'title', 'review', 'photo')
    template_name = 'reviewBoard/review_new.html'
    success_url = '/'
    #1
    form_classes = {'ReviewForm': ReviewForm,
                    'RestaurantForm': RestaurantForm}

    #2
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(ReviewCreateView, self).dispatch(*args, **kwargs)

    #3
    def get_review_initial(self):
        return {'title':'...',}

    def get_restaurant_initial(self):
        return {'name':'...',}


  • #1 : form_classes 속성에 dictionary로 여러 폼을 전달할 수 있다.

  • #2 : 로그인을 한 사람에게만 리뷰를 작성할 수 있도록, 로그인을 요구하는 데코레이터이다.

  • #3 : 각 필드의 초기값 설정



urls.py


from django.contrib.auth import views as auth_views
from django.urls import path
from . import views

urlpatterns = [
    path('review/new/', views.ReviewCreateView.as_view(), name='review_new'),
]





Template



<!-- #1 -->
<form id="ReviewForm"
    class="jumbotron collapse show multi-collapse" 
    method="POST" 
    enctype="multipart/form-data"
    action="{% url 'reviewBoard:review_new' %}">{% csrf_token %}

    <h3>리뷰 추가하기</h3><hr>

    {% for field in forms.ReviewForm %}
    
    <div class="form-group row ">
        <label for="colFormLabel" class="col-sm-2 col-form-label">{{ field.label_tag }}</label>
        {# error message 출력 #}
        <div class="col-sm-10 ">
            {{ field }}
            
            {% if field.errors %}
                {% for e in field.errors %}
                    <p style="color:palevioletred;">{{ e | escape }}</p>
                {% endfor %}
            {% endif %}

            <!-- #2 -->
            {% if field.label == "Restaurant" %}
                <button type="button" 
                data-toggle="collapse" 
                data-target=".multi-collapse" 
                aria-expanded="false" 
                aria-controls="ReviewForm RestaurantForm"
                >맛집이 리스트에 없나요?</button>
            {% endif %}
        </div>
    </div>
{% endfor %}
<button value='action' name='ReviewForm' type="submit">리뷰 추가하기</button>
</form>

<!-- #3 -->
<form id="RestaurantForm" 
    class="jumbotron collapse multi-collapse"
    method="POST" 
    action="{% url 'reviewBoard:review_new' %}">{% csrf_token %}
    
    <h3>맛집 추가하기</h3>
    <hr class="mb-5">

    {{forms.RestaurantForm.as_p}}

    <button value='action' name='RestaurantForm' type="submit">식당 추가하기</button>
</form>



  • #1 : form class의 collapse show multi-collapse는 부트스트랩에 내장된 클래스이다. multi-collapse는 여러 개의 elements에 속한다는 것을 알려주는 크래스. show는 element를 보여주는 클래스이다. #2의 버튼에 있는 collapse cotrol에 따라 없어지고, 나타난다.

  • #2 : data-target=".multi-collapse" aria-controls="ReviewForm RestaurantForm" data-target을 통해 .multi-collapse 클래스를 가진 엘리먼트를 동시에 컨트롤한다. aria-controls는 id 속성 값이 ReviewForm, RestaurantForm인 엘리먼트를 컨트롤한다.

  • #3 : 토글 버튼을 누르면 ReviewForm이 사라지고, RestaurantForm이 나타난다.