티스토리 뷰

  ● Order 주문 시스템   

 

 

 

order/models.py

from django.db import models

class Order(models.Model):
    user = models.ForeignKey('users.Users', verbose_name = "사용자", on_delete = models.CASCADE)
    product = models.ForeignKey('product.Product',verbose_name = "상품", on_delete = models.CASCADE)
    registered_date = models.DateTimeField(auto_now_add=True, verbose_name="등록시간")
    quantity = models.IntegerField(verbose_name="수량")

    def __str__(self):
        return str(self.user) + ' ' + str(self.product)
    
    class Meta:
        db_table = "Shoppingmall_Order"
        verbose_name = "주문"
        verbose_name_plural = "주문"

 

 

 

 

order/admin.py

from django.contrib import admin
from .models import Order

class OrderAdmin(admin.ModelAdmin):
    list_display = ('user', 'product', 'quantity',)

admin.site.register(Order, OrderAdmin)

 

 

 

 

 

order/forms.py

 

- 주문서에는 상품 정보(Product), 수량(Order), 주문자 정보(Users) 의 정보가 필요하다.

- 상품 정보와 수량은 Product와 Order 객체를 통해서 알아낼 수 있지만 주문자 정보의 경우에는 단순하게 Users 객체에서 가져오는 것이 아니라 현재 로그인한 사용자의 정보를 가져와야한다.

- 주문 버튼을 누르는 로그인한 사용자의 정보는 session에 들어있고 session을 알기 위해서는 request가 필요하다. 그렇기 때문에 request 값을 전달 받아야하므로 __init__ 을 상속받아 OrderForm의 self.request 값에 로그인한 사용자 정보인 request를 저장하는 것이다. 

 

- 상품명은 입력하는 것이 아니라, 해당 상품 상세 페이지에서 상품 id만 가져오고 수량만 입력하는 것이므로 product 필드에는 widget = forms.HiddenInput 인자를 추가한다.

 

- clean() 을 통해서 폼에 입력된 데이터의 유효성을 검사한다.

 

- 필요한 세 가지 정보가 다 입력되지 않으면 에러메세지를 출력하게끔 한다. 

from django import forms
from .models import Order
from users.models import Users
from product.models import Product

class OrderForm(forms.ModelForm):
    def __init__(self, request, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.request = request
    
    quantity = forms.IntegerField(
        error_messages={'required':"수량을 입력하세요."},
        label = "수량"
    )
    product = forms.IntegerField(
        error_messages={'required':"상품을 입력하세요."},
        label = "상품", widget = forms.HiddenInput
    )

    def clean(self):
        cleaned_data = super().clean()
        quantity = cleaned_data.get('quantity')
        product = cleaned_data.get('product')
        user = self.request.session.get('user')

    
    if not(quantity and product and user):
        self.add_error('quantity', "수량이 없습니다.")
        self.add_error('product', "상품이 없습니다.")

 

 

 

 

 

users/decorators.py login_required

 

- 주문하는 데에 있어서 무조건 로그인 되어있는 상태여야한다. 그래야 세션 정보를 받아올 수 있고 주문자의 정보를 얻을 수 있기때문이다. 앞서 구현한 admin_required 와 같은 방식이다. 사용자 세션 값을 받아오고 아무것도 없거나 사용자가 아니라면 로그인 페이지로 넘어가도록 한다.

def login_required(func):
    def wrap(request, *args, **kwargs):
        user = request.session.get('user')
        if user is None or not user:
            return redirect('/login/')
        return func(request, *args, **kwargs)
    return wrap

 

 

 

 

order/views.py OrderCreate(FormView) with transacrion.atomic

 

- 주문 객체를 만드는 로직. product_detail.html 에서 원하는 수량을 입력하고 주문 버튼을 누르면 주문 객체가 생성되는 것이므로 템플릿이 따로 필요없고 주문서 정보를 받아올 OrderForm과 주문서 객체로 만들 OrderCreateView만 있으면 된다. 

 

- OrderForm에서 유효성 검사를 마친 데이터(cleaned_data)로 구성한 폼이 유효한 폼인지 form_valid 를 통해서 검사한다. 

    - 수량은 OrderForm에서 가져온 그대로 quantity 값 그대로 저장한다. 

    - 상품 정보는 OrderForm에서 가져온 상품번호 'product'와 일치하는 상품을 Product 객체 내에서 찾아서 prod에 저장한다. 

    - 주문자 정보는 OrderForm에서 __init__ 함수를 통해 저장한 self.request 값을 가져와서 Users 객체의 email 값과 일치하는 사용자 객체를 저장한다. 왜냐면 사용자 객체 Users 를 생성할 때, session 값에 email을 저장했기 때문에 email 값으로 비교할 수 있다. 

    - 세 가지의 필드값을 order 주문서 객체에 저장하고 save() 함수를 통해서 order 객체를 DB에 저장한다.

 

- 주문서가 완성되면 주문이 들어온 수량만큼 해당 상품의 재고 값을 변경해야한다. 해당 상품 객체 prod 값을 변경하고 다시 DB에 저장한다.

 

- 이 때, 가장 중요한 것이 주문이 들어오고 해당 상품에 대해서 수량을 재정의하는 동작들이 동시에 수행되어야한다. 이렇게 일관적으로 동작을 수행하도록 하는 것을 트랜잭션 transaction이라고 한다. 이러한 기능을 django DB에서 제공한다. DB 값에 관여하는 코드들을 with transaction.atomic()으로 감싸면 with 구문 아래에 있는 다수의 동작들이 동시에 수행될 수 있도록 보장받는다. 

@method_decorator(login_required, name = 'dispatch')
class OrderCreate(FormView):
    form_class = OrderForm
    success_url = '/product/list'

    def form_valid(self, form):
        with transaction.atomic():
            prod = Product.objects.get(pk=form.data.get('product'))
            order = Order(
                quantity = form.data.get('quantity'),
                product = prod,
                user = Users.objects.get(email = self.request.session.get('user'))
            )
            order.save()

            prod.stock -= int(form.data.get('quantity'))
            prod.save()
        return super().form_valid(form)

 

 

 

- OrderCreate는 화면에 보여주는 용도가 아니라 주문서 객체를 생성하는 용도이기 때문에 template_name을 설정할 필요가 없다. 그래서 success_url만 설정하면 되는데, 간혹 에러가 발생하는 경우 어떤 페이지를 보여줘야 하는지 모르기 때문에 template_name 을 입력하라고

TemplateResponseMixin requires either a definition of 'template_name' or an implementation of 'get_template_names()'

오류가 날 수 있다. 그렇기 때문에 예외처리로 form_invalid 함수로 작성한다.

 

- 장고 공식문서에서 다음과 같이 나와있다. 기본적으로 FormView에서 제공해주는 인자값 kw에다가 주문서에 필요한 self.request 값을 추가하는 것이다. 

     def form_invalid(self, form):
        return redirect('/product/'+str(form.data.get('product')))
 
     def get_form_kwargs(self, **kwargs):
        kw = super().get_form_kwargs(**kwargs)
        kw.update({
            'request':self.request
        })
        return kw

 

 

 

templates/product_detail.html

- 그러면 이제는 제품 상세보기 페이지 product_detail.html 에 수량을 정하고 주문서를 넣는 부분을 추가한다.

- <form> 부분이 새롭게 추가된 부분이다. 해당 폼이 "/order/create/" 으로 제출되도록 action 값으로 지정한다.

- {% ifnotequal field.name 'product' %} 를 추가한 이유는 상품 product 는 HiddenInput인데 아래 사진처럼 굳이 출력될 필요가 없기 때문에 ifnotequal을 사용하여 숨긴다. 

{% extends 'base.html' %}
{% load humanize %}
{% block contents %}

<div class = "row mt-5">
    <div class = "col-12">
        <div class = "card" style = "width:100%">
            <div class = "card-body">
                <h5 class = "card-title">{{product.name}}</h5>
            </div>
            <ul class = "list-group list-group-flush">
                <li class = "list-group-item">
                    <form method = "POST" action = "/order/create/">
                    {% csrf_token %}
                    {% for field in form %}
                        <div class = "form-group">
                            {% ifnotequal field.name 'product' %}
                            <label for = "{{field.id_for_label}}">{{field.label}}</label>
                            {% endifnotequal %}
                            <input type = "{{field.field.widget.input_type}}" class = "form-control" id = "{{field.id_for_label}}"
                            placeholder="{{field.label}}" name = "{{field.name}}"
                            value = "{% ifequal field.name 'product' %} {{product.id}} {% endifequal %}">
                        </div>
                        {% if field.errors %}
                        <span style = "color:red">{{field.errors}}</span>
                        {% endif %}
                        {% endfor %}
                        <button type = "submit" class = "btn btn-primary">주문</button>
                    </form>
                </li>
                <li class = "list-group-item">상품 가격 : {{ product.price | intcomma }} 원</li>
                <li class = "list-group-item">남은 수량 : {{ product.stock}}</li>
                <li class = "list-group-item">등록 날짜 : {{ product.registered_date|date:'Y-m-d H:i'}}</li>
                <li class = "list-group-item">{{ product.description|safe }} </li>
            </ul>
        </div>
    </div>
</div>

{% endblock %}

 

 

 

 

 

product/views.py ProductDetailView

 

- 제품 상세보기 페이지의 로직을 담당하는 ProductDetailView 의 기존 코드에 self.request를 함께 전달하기 위해 get_context_data 함수를 사용한다.

class ProductDetailView(DetailView):
    template_name = "product_detail.html"
    queryset = Product.objects.all()
    context_object_name = 'product'
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = OrderForm(self.request)
        return context

** OrderForm 은 product_detail.html 에서 form 태그 안에서 전달되는거 아닌가? 그러면 context는 언제 호출되고 어디로 반환되는건지.. self.request 정보를 함께 전달하기 위해서 get_context_data 함수를 사용하는 것은 알겠는데 context가 어디로 가는건지 궁금하다. 

 

 

Error handling

__init__() takes 1 positional argument but 2 were given

=> urls.py 에서 path('order/create/', OrderCreate) 으로 되어있었다. 그런데 view를 url로 연결할 때에는 as_view()를 함께 써야한다.   path('order/create/', OrderCreate.as_view()) 에러 해결!

 

 

 

 

실행 결과

- 마카롱에 10개의 수량을 입력하고 주문을 누르면 OrderCreate의 success_url에 의해서 /product/list/의 경로로 이동한다. 

 

 

- 이후에 다시 확인해보면 10개가 주문되어서 남은수량의 개수가 10만큼 마이너스된 것을 알 수 있다. 이것으로 보아 with transaction.atomic이 제대로 진행되고 있다는 것을 확인할 수 있다.

 

 

 

 

- 그리고 이제 admin 페이지에서 주문이 제대로 들어왔는지 확인해볼 수 있다. 

 

 

 

댓글