- Published on
Django admin 에서 Custom Action Page 만들기
- Authors
- Name
- hongreat
- ✉️hongreat95@gmail.com
고객은 단순히 쿠폰 하나씩 발행하는 시스템만 요청했지만, 하나씩 발행 하게 되면 사용성이 너무 떨어질 것 같다고 생각했습니다.
공수시간이 부족했지만 프로젝트의 완성도를 위해, 한 번에 여러 명의 사용자에게 쿠폰을 발행할 수 있는 기능을 추가했습니다.
결과적으로 없었으면 너무 귀찮았을그리고 만족할 수 있는 기능이 되었습니다.
이번 글에서는 Django Admin에서 여러 사용자에게 쿠폰을 일괄 발행하는 커스텀 액션 페이지를 만드는 과정을 기록합니다.
1. change_list Page Custom
쿠폰(Coupon) 이라는 모델의 Admin 페이지 안에 액션을 만들기 위해서 Admin 클래스의 change_list 를 지정합니다.
이 때 템플릿의 경로는 some_app/templates/change_list.html
일때, 아래처럼 chainge_list_template을 지정합니다.
@admin.register(Coupon)
class CouponAdmin(admin.ModelAdmin):
..(생략)..
change_list_template = "change_list.html"
Django Admin의 기본 change_list
템플릿을 상속받습니다.
{% extends "admin/change_list.html" %}
{% block object-tools-items %}
{{ block.super }}
<li>
<a href="{% url 'admin:bulk_coupon' %}" class="button">쿠폰 일괄 발행</a>
</li>
{% endblock %}
- Django Admin 페이지의 오른쪽 상단에 있는 도구 모음을 의미합니다. 이 블록은 관리자에게 특정 작업을 수행할 수 있는 버튼을 추가할 수 있는 영역입니다.
- 기본적으로 이 블록에는 '추가', '수정', '삭제' 등의 기능이 포함되는데, 여기서는
block.super
로 기본 도구를 유지하면서 새로운 항목을 추가하고 있습니다. {% url 'admin:bulk_coupon' %}
:admin:bulk_coupon
이라는 URL 네임스페이스를 사용하여 일괄 발행 페이지로 이동하는 링크를 생성합니다. 이 URL은 뒤에서 나올CouponAdmin
클래스에서 추가한bulk_coupon_view
뷰와 연결됩니다.
2. get_urls
어드민에서 get_urls
메서드를 오버라이드하여 URL을 추가합니다. (사실 이번 글에서 핵심이 되는 내용 입니다.)
get_urls
메서드는 Admin 클래스에서 URL 경로를 설정하여 관리 페이지에 새로운 기능을 추가하는 데 유용한 메서드 입니다.
@admin.register(Coupon)
class CouponAdmin(admin.ModelAdmin):
...(생략)...
def get_urls(self):
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
check = [
path("bulk_coupon/", wrap(self.bulk_coupon_view), name="bulk_coupon"),
*super().get_urls(),
]
return check
이를 통해 bulk_coupon_view
라는 새로운 뷰를 관리자의 URL 패턴에 추가했습니다.
이 URL을 세팅함으로써 템플릿에 세팅해놓은 쿠폰을 일괄 발행하는 페이지로 이동하게 됩니다.
3. View
bulk_coupon_view
는 POST
요청을 처리하고, 여러 사용자에게 한 번에 쿠폰을 발행하는 로직을 담당합니다.
뒤에서 만들 페이지에서 BulkCouponForm
렌더링하고, 폼에 따라 사용자로부터 데이터를 입력받습니다.
그렇게 입력 받은 데이터를 Coupon.objects.bulk_create()
메서드를 사용해 쿠폰을 일괄 발행합니다.
@admin.register(Coupon)
class CouponAdmin(admin.ModelAdmin):
...(생략)...
def bulk_coupon_view(self, request):
form = BulkCouponForm(request.POST or None)
if request.method == "POST" and form.is_valid():
users = form.cleaned_data["users"]
name = form.cleaned_data["name"]
discount_type = form.cleaned_data["discount_type"]
value = form.cleaned_data["value"]
description = form.cleaned_data["description"]
valid_from = form.cleaned_data.get("valid_from", None)
valid_to = form.cleaned_data.get("valid_to", None)
try:
Coupon.objects.bulk_create(
[
Coupon(
user_id=user,
name=name,
discount_type=discount_type,
value=value,
description=description,
valid_from=valid_from,
valid_to=valid_to,
)
for user in users
]
)
messages.success(request, f"{len(users)}에게 쿠폰이 성공적으로 발행되었습니다.")
except Exception as e:
messages.error(request, f"쿠폰 발행에 실패했습니다.{e}")
return redirect("admin:index")
return render(request, "bulk_coupon.html", {"form": form})
만약 로직이 실패하는 경우 어드민에서 알럿을 발행하기 위해, 쿠폰 발행로직 성공 여부에 따라 Django의 messages
모듈을 사용해 관리자에게 결과를 알립니다.
BulkCouponForm(request.POST or None)
는POST
데이터가 있으면 그 데이터를 사용해 폼을 생성하고, 없으면 빈 폼을 생성합니다.render()
함수의 세 번째 인자로 템플릿에 전달할 데이터를 담은 딕셔너리(context
)를 넘깁니다. 여기에서{"form": form}
으로 폼 객체가 전달됩니다.
4. Form
from django import forms
from app.coupon.models import CouponNameChoice, DiscountTypeChoice
from app.user.models import User
class BulkCouponForm(forms.Form):
users = forms.MultipleChoiceField(label="쿠폰을 발행할 유저", widget=forms.CheckboxSelectMultiple)
name = forms.ChoiceField(label="쿠폰 타입")
discount_type = forms.ChoiceField(label="발행할 쿠폰 할인 유형", help_text="(정가는 정해진 금액이 할인, 정률은 %로 할인 됩니다.)")
value = forms.IntegerField(
label="할인 값", widget=forms.NumberInput(), help_text="(ex: 50 1.정가인 경우:50원 할인 2.정률인 경우:50 % 할인)"
)
description = forms.CharField(max_length=256, label="쿠폰 설명(256자 제한)", widget=forms.Textarea)
valid_from = forms.DateField(
label="유효 시작 기간",
required=False,
widget=forms.DateInput(attrs={"type": "date"}),
help_text="(날짜를 선택하지 않으면 무기한 쿠폰입니다.)",
)
valid_to = forms.DateField(label="유효 종료 기간", required=False, widget=forms.DateInput(attrs={"type": "date"}))
def __init__(self, *args, **kwargs):
super(BulkCouponForm, self).__init__(*args, **kwargs)
self.fields["users"].choices = [
(user.pk, f"이름 : {user.name} / 닉네임 : {user.nick_name}") for user in User.objects.all()
]
self.fields["name"].choices = CouponNameChoice.choices
self.fields["discount_type"].choices = DiscountTypeChoice.choices
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get("value", 0) < 0:
self.add_error("value", "0 이상의 값을 입력하세요.")
if cleaned_data.get("discount_type") == DiscountTypeChoice.PERCENTAGE and cleaned_data.get("value", 0) > 100:
self.add_error("value", "100이하의 퍼센트 값을 입력하세요.")
if (cleaned_data.get("valid_from") and cleaned_data.get("valid_to")) and (
cleaned_data.get("valid_from") > cleaned_data.get("valid_to")
):
self.add_error("valid_to", "유효 종료 시간이 유효 시작 시간 보다 늦도록 입력하세요.")
return cleaned_data
4.1 Form Render
마지막으로, 관리자가 쿠폰을 일괄 발행할 수 있는 페이지의 UI를 정의하는 템플릿을 작성합니다.
자바스크립트로 전체 선택(select-all 부분) 체크박스 를 구현해 관리자의 편의성을 극대화했습니다!
{% extends "admin/base_site.html" %}
{% block content %}
<style>
/* 스타일은 생략 */
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('select-all').addEventListener('change', function () {
var isChecked = this.checked;
document.querySelectorAll('input[name="users"]').forEach(function (checkbox) {
checkbox.checked = isChecked;
});
});
});
</script>
<div class="admin-form-container">
<div class="admin-form">
<h1>쿠폰 일괄 발행</h1>
<form method="post">
{% csrf_token %}
<div>
<label><input type="checkbox" id="select-all"> 전체 선택</label>
</div>
{{ form.as_p }}
<button type="submit" class="custom-submit-button">발행하기</button>
<button type="button" style="background-color: #888; margin-top: 25px;" onclick="history.back();">뒤로 가기</button>
</form>
</div>
</div>
{% endblock %}
4.2 form.as_p
- 이 부분이
BulkCouponForm
폼을 HTML로 렌더링하는 부분입니다. form.as_p
는 Django의 폼을 HTML<p>
태그로 묶어 자동으로 렌더링해주는 Django 의 헬퍼 메서드입니다. (각 필드가<p>
태그로 감싸져 화면에 표시됩니다.)users
,name
,discount_type
,value
,description
,valid_from
,valid_to
같은 필드들이 이 부분에서 자동으로 렌더링됩니다.
고객이 직접 요청하지 않았지만, 바쁜 와중에도 더 나은 서비스 제공을 위한 고민이 반영된 사례입니다.
Django Admin을 커스터마이징 하는 작업을 몇번 경험했기 때문에 역으로 제안할 수 있었던 기능이었던 것 같습니다.
5. 레퍼런스 링크
폼 필드 렌더링
form.as_p
,form.as_table
,form.as_ul