diff --git a/accounts/templates/_base.html b/accounts/templates/_base.html
deleted file mode 100644
index b5135e6..0000000
--- a/accounts/templates/_base.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% load static %}
-
-
-
-
- {% block title %}
- {% endblock title %}
-
-
-
-
-
-
-
-
- {% block content %}
- {% endblock content %}
-
-
-
diff --git a/accounts/templates/account_mypage_payments.html b/accounts/templates/account_mypage_payments.html
deleted file mode 100644
index 8cb5d24..0000000
--- a/accounts/templates/account_mypage_payments.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends "_base.html" %}
-{% block content %}
-
-
-
- id |
- user |
- 금액 |
- 결제일 |
- 취소 |
-
-
-
- {% for ticket in ticket_list %}
-
- {{ ticket.id }} |
- {{ ticket.payment.money }} |
- {{ ticket.create_at }} |
-
-
-
- |
-
- {% endfor %}
-
-
-{% endblock content %}
diff --git a/accounts/urls.py b/accounts/urls.py
index 621645c..0b815e0 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -1,10 +1,6 @@
from django.urls import include, path
-from .views import GitHubLogin, GoogleLogin, MyPage, mypage_payments
-
-from .views import IdLogin, Logout
-
-from .views import login_api, logout_api
+from .views import GitHubLogin, GoogleLogin, IdLogin, Logout, login_api, logout_api
urlpatterns = [
path("auth/", include("dj_rest_auth.urls")),
@@ -14,13 +10,8 @@
path("accounts/signup/", IdLogin.as_view(), name="account_signup"),
path("auth/github/login/", GitHubLogin.as_view(), name="github_login"),
path("auth/google/login/", GoogleLogin.as_view(), name="google_login"),
- #path("my-page/payments", MyPage.as_view())
- path("my-page/payments/", mypage_payments),
# Endpoints for Seesion Based Login
path("api/login/", login_api, name="login-api"),
path("api/logout/", logout_api, name="logout-api"),
- # path("api/logout/", )
-
- path("api/mypage/", MyPage.as_view(), name="mypage-api"),
]
diff --git a/accounts/view_models.py b/accounts/view_models.py
deleted file mode 100644
index bf9157e..0000000
--- a/accounts/view_models.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from dataclasses import dataclass, asdict
-
-from ticket.models import Ticket
-
-
-@dataclass(init=False)
-class UserTicketInfo:
- ticket_type_name: str
- date: str
- price: int
- payment_key: str
-
- def __init__(self, ticket: Ticket):
- self.ticket_type_name = ticket.ticket_type.name
- self.date = ticket.ticket_type.day
- self.price = ticket.payment.money
- self.payment_key = ticket.payment.payment_key
-
- def to_dict(self):
- return asdict(self)
diff --git a/accounts/views.py b/accounts/views.py
index 05f4c6f..3f9eec7 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -1,23 +1,14 @@
+from allauth.account.views import LoginView, LogoutView
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.views import SocialLoginView
-
from django.conf import settings
-from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
-from django.contrib.auth import login, logout, authenticate
-from functional import seq
-
-from rest_framework.response import Response
-from rest_framework.views import APIView
+from django.contrib.auth import authenticate, login, logout
from rest_framework.decorators import api_view
+from rest_framework.response import Response
from accounts.logics import get_basic_auth_token
-from accounts.view_models import UserTicketInfo
-from ticket.models import Ticket
-
-from allauth.account.views import LoginView, LogoutView
class IdLogin(LoginView):
@@ -41,37 +32,6 @@ class GoogleLogin(SocialLoginView):
client_class = OAuth2Client
-class MyPage(APIView):
- def get(self, request):
- dto = {
- "ticket": self.get_ticket_info(request)
- # "session": None, # TODO 세션
- # "sponsor": None, # TODO 후원사
- # "user_info": None # TODO 사용자 정보
- }
-
- return Response(dto)
-
- def get_ticket_info(self, request) -> list:
- all_tickets = Ticket.objects.filter(
- user=request.user,
- is_refunded=False
- )
-
- return list(
- seq(all_tickets)
- .map(UserTicketInfo)
- .map(lambda info: info.to_dict())
- )
-
-
-@login_required
-def mypage_payments(request):
- ticket_list = Ticket.objects.filter(user=request.user)
- return render(request, 'account_mypage_payments.html',
- context={'ticket_list': ticket_list})
-
-
@api_view(["POST"])
def login_api(request):
if request.user.is_authenticated:
@@ -99,8 +59,4 @@ def logout_api(request):
return Response({"msg": "not logged in"})
logout(request)
-
- response_data = {
- "msg": "ok"
- }
- return Response(response_data)
+ return Response({"msg": "ok"})
diff --git a/payment/__init__.py b/payment/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/payment/admin.py b/payment/admin.py
deleted file mode 100644
index 72696a2..0000000
--- a/payment/admin.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from django.contrib import admin
-
-from .models import Payment, PaymentHistory
-
-# Register your models here.
-
-class PaymentAdmin(admin.ModelAdmin):
- pass
-
-
-class PaymentHistoryAdmin(admin.ModelAdmin):
- pass
-
-
-admin.site.register(Payment, PaymentAdmin)
-admin.site.register(PaymentHistory, PaymentHistoryAdmin)
diff --git a/payment/apps.py b/payment/apps.py
deleted file mode 100644
index ab29d31..0000000
--- a/payment/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class PaymentConfig(AppConfig):
- default_auto_field = "django.db.models.BigAutoField"
- name = "payment"
diff --git a/payment/clients.py b/payment/clients.py
deleted file mode 100644
index c4da648..0000000
--- a/payment/clients.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import requests
-from constance import config
-
-
-class PortOneClient:
- # TODO: 상황에 맞는 Error로 변경하기: 지금은 모두 ValueError
- def __init__(self):
- self.url = "https://api.iamport.kr"
-
- # TODO: 잘 말아서 Singleton 패턴으로 만들기
- def get_access_token(self) -> str:
- endpoint = self.url + "/users/getToken"
-
- if config.IMP_KEY is None or config.IMP_SECRET is None:
- raise ValueError("Access Token 발급 실패: imp_key 또는 imp_secret을 찾을 수 없습니다.")
-
- request_dto = {
- "imp_key": config.IMP_KEY,
- "imp_secret": config.IMP_SECRET
- }
-
- response = requests.post(
- endpoint,
- request_dto
- )
-
- if not response.ok:
- raise ValueError("Access Token 발급에 실패했습니다.")
-
- return response.json().get("access_token")
-
- def find_payment_info(self, payment_key: str):
- endpoint = self.url + "/payments/{}"
-
- if payment_key is None or payment_key == "":
- raise ValueError("payment_key (merchant_uid)는 필수값입니다.")
-
- request_header = {
- "Authorization": self.get_access_token()
- }
-
- response = requests.get(endpoint.format(payment_key), headers=request_header)
-
- if not response.ok:
- raise ValueError("결제 정보 조회에 실패했습니다.")
-
- return response.json()
-
- def req_cancel_payment(self, payment_key: str, price: int, reason: str = ""):
- endpoint = self.url + "/payments/cancel"
-
- if payment_key is None or payment_key == "":
- raise ValueError("payment_key (merchant_uid)는 필수값입니다.")
-
- if price is None or price == 0:
- raise ValueError("금액은 필수값입니다.")
-
- request_header = {
- "Authorization": self.get_access_token()
- }
-
- request_dto = {
- "merchant_uid": payment_key,
- "amount": price,
- "checksum": price # 지금은 전액환불하는 케이스만 존재함 -> 환불요청금액과 결제 건의 환불가능금액은 동일해야함
- }
-
- if reason is not None and reason != "":
- request_dto["reason"] = reason
-
- response = requests.post(
- endpoint,
- request_dto,
- headers=request_header
- )
-
- if not response.ok:
- raise ValueError("Portone에서 비정상 응답: {}".format(payment_key))
-
- response_data = response.json()
-
- if response_data.get("code") is None or response_data.get("code") != 0:
- raise ValueError("환불 처리 실패: {}".format(payment_key))
-
- return True
diff --git a/payment/enum.py b/payment/enum.py
deleted file mode 100644
index e5aa74b..0000000
--- a/payment/enum.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from enum import Enum
-
-
-class PaymentStatus(Enum):
- BEFORE_PAYMENT = 1
- PAYMENT_FAILED = 2
- PAYMENT_SUCCESS = 3
- REFUND_FAILED = 4
- REFUND_SUCCESS = 5
\ No newline at end of file
diff --git a/payment/logic.py b/payment/logic.py
deleted file mode 100644
index 32ffa75..0000000
--- a/payment/logic.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.db import transaction
-
-from payment.enum import PaymentStatus
-from payment.models import Payment, PaymentHistory
-from ticket.models import TicketType, Ticket
-
-import shortuuid
-
-User = get_user_model()
-
-
-@transaction.atomic
-def generate_payment_key(user: User, ticket_type: TicketType):
- new_payment = Payment(
- payment_key=shortuuid.uuid(),
- user=user,
- ticket_type=ticket_type,
- money=ticket_type.price,
- status=PaymentStatus.BEFORE_PAYMENT.value
- )
-
- new_payment.save()
-
- _save_history(new_payment.payment_key, PaymentStatus.BEFORE_PAYMENT.value)
- return new_payment.payment_key
-
-
-@transaction.atomic
-def proceed_payment(payment_key: str, is_succeed: bool):
- status_value = PaymentStatus.PAYMENT_SUCCESS.value if is_succeed else PaymentStatus.PAYMENT_FAILED.value
-
- target_payment = Payment.objects.get(payment_key=payment_key)
- target_payment.status = status_value
- target_payment.save()
-
- _save_history(payment_key, status_value)
-
-
-def _save_history(payment_key: str, status: int):
- new_payment_history = PaymentHistory(
- payment_key=payment_key,
- status=status
- )
-
- new_payment_history.save()
-
-
-@transaction.atomic
-def cancel_payment(payment: Payment):
- payment.status = PaymentStatus.REFUND_SUCCESS.value
- payment.save()
-
- payment_history = PaymentHistory(
- payment_key=payment.payment_key,
- status=PaymentStatus.REFUND_SUCCESS.value
- )
\ No newline at end of file
diff --git a/payment/migrations/0001_initial.py b/payment/migrations/0001_initial.py
deleted file mode 100644
index 4f5e1ed..0000000
--- a/payment/migrations/0001_initial.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-14 03:14
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name="PaymentHistory",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("payment_key", models.CharField(max_length=32)),
- ("status", models.IntegerField()),
- ("create_at", models.DateTimeField(auto_now_add=True)),
- ("update_at", models.DateTimeField(auto_now=True)),
- ],
- ),
- migrations.CreateModel(
- name="Payment",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("payment_key", models.CharField(max_length=32)),
- ("money", models.IntegerField()),
- ("create_at", models.DateTimeField(auto_now_add=True)),
- ("update_at", models.DateTimeField(auto_now=True)),
- (
- "user_id",
- models.ForeignKey(
- on_delete=django.db.models.deletion.PROTECT,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ],
- ),
- ]
diff --git a/payment/migrations/0002_rename_user_id_payment_user_payment_status_and_more.py b/payment/migrations/0002_rename_user_id_payment_user_payment_status_and_more.py
deleted file mode 100644
index 8b3c920..0000000
--- a/payment/migrations/0002_rename_user_id_payment_user_payment_status_and_more.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-15 13:40
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("ticket", "0002_rename_conferencetickettype_tickettype"),
- ("payment", "0001_initial"),
- ]
-
- operations = [
- migrations.RenameField(
- model_name="payment",
- old_name="user_id",
- new_name="user",
- ),
- migrations.AddField(
- model_name="payment",
- name="status",
- field=models.IntegerField(
- choices=[
- (1, "결제 전"),
- (2, "결제 실패"),
- (3, "결제 성공"),
- (4, "환불 실패"),
- (5, "환불 완료"),
- ],
- default=0,
- ),
- preserve_default=False,
- ),
- migrations.AddField(
- model_name="payment",
- name="ticket_type",
- field=models.ForeignKey(
- default=1,
- on_delete=django.db.models.deletion.PROTECT,
- to="ticket.tickettype",
- ),
- preserve_default=False,
- ),
- migrations.AlterField(
- model_name="paymenthistory",
- name="status",
- field=models.IntegerField(
- choices=[
- (1, "결제 전"),
- (2, "결제 실패"),
- (3, "결제 성공"),
- (4, "환불 실패"),
- (5, "환불 완료"),
- ]
- ),
- ),
- ]
diff --git a/payment/migrations/0003_paymenthistory_is_webhook.py b/payment/migrations/0003_paymenthistory_is_webhook.py
deleted file mode 100644
index 57a743a..0000000
--- a/payment/migrations/0003_paymenthistory_is_webhook.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-25 13:47
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("payment", "0002_rename_user_id_payment_user_payment_status_and_more"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="paymenthistory",
- name="is_webhook",
- field=models.BooleanField(default=False),
- ),
- ]
diff --git a/payment/migrations/__init__.py b/payment/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/payment/models.py b/payment/models.py
deleted file mode 100644
index e671939..0000000
--- a/payment/models.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from django.db import models
-from django.contrib.auth import get_user_model
-
-User = get_user_model()
-
-
-class Payment(models.Model):
- payment_key = models.CharField(max_length=32) # TODO: uuid 처리
- user = models.ForeignKey(User, on_delete=models.PROTECT)
- ticket_type = models.ForeignKey("ticket.TicketType", on_delete=models.PROTECT)
- money = models.IntegerField()
- status = models.IntegerField(
- choices=(
- (1, "결제 전"),
- (2, "결제 실패"),
- (3, "결제 성공"),
- (4, "환불 실패"),
- (5, "환불 완료"),
- )
- )
- create_at = models.DateTimeField(auto_now_add=True)
- update_at = models.DateTimeField(auto_now=True)
-
-
-class PaymentHistory(models.Model):
- payment_key = models.CharField(max_length=32)
- status = models.IntegerField(
- choices=(
- (1, "결제 전"),
- (2, "결제 실패"),
- (3, "결제 성공"),
- (4, "환불 실패"),
- (5, "환불 완료"),
- )
- )
- is_webhook = models.BooleanField(default=False)
- create_at = models.DateTimeField(auto_now_add=True)
- update_at = models.DateTimeField(auto_now=True)
diff --git a/payment/tests.py b/payment/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/payment/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/payment/urls.py b/payment/urls.py
deleted file mode 100644
index 8096b87..0000000
--- a/payment/urls.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from django.contrib import admin
-from django.urls import include, path
-
-from payment.views import PortoneWebhookApi, post__generate_payment_key, PaymentSuccessApi, post__cancel_payment
-
-urlpatterns = [
- path("portone/webhook/", PortoneWebhookApi.as_view(), name="portone-webhook"),
- path("key/", post__generate_payment_key, name="get-payment-key"),
- path("success/", PaymentSuccessApi.as_view(), name="payment-success"),
- path("failed/", PaymentSuccessApi.as_view(), name="payment-success"),
- path("cancel/", post__cancel_payment, name="cancel-payment"),
-]
diff --git a/payment/views.py b/payment/views.py
deleted file mode 100644
index a8a7ab8..0000000
--- a/payment/views.py
+++ /dev/null
@@ -1,165 +0,0 @@
-import datetime
-
-from django.db import transaction
-from rest_framework.decorators import api_view
-from rest_framework.response import Response
-from rest_framework.views import APIView
-
-from payment import enum
-from payment.clients import PortOneClient
-from ticket.models import TicketType, Ticket
-from payment.logic import generate_payment_key, cancel_payment
-from payment.models import Payment, PaymentHistory
-
-from django.conf import settings
-
-
-class PortoneWebhookApi(APIView):
- @transaction.atomic
- def post(self, request):
- portone_ips = [
- "52.78.100.19",
- "52.78.48.223",
- "52.78.5.241" # (Webhook Test Only)
- ]
-
- if settings.DEBUG is False and request.META.get("REMOTE_ADDR") not in portone_ips:
- raise ValueError("Not Allowed IP")
-
- if request.data["status"] != "paid":
- raise ValueError("결제 승인건 이외의 요청")
-
- payment_key = request.data["merchant_uid"]
-
- target_payment = Payment.objects.get(payment_key=payment_key)
- target_payment.status = enum.PaymentStatus.PAYMENT_SUCCESS.value
- target_payment.save()
-
- payment_history = PaymentHistory(
- payment_key=payment_key,
- status=enum.PaymentStatus.PAYMENT_SUCCESS.value,
- is_webhook=True
- )
- payment_history.save()
-
- ticket = Ticket.objects.create(
- ticket_type=target_payment.ticket_type,
- bought_at=datetime.datetime.now(),
- user=target_payment.user,
- payment=target_payment
- )
- ticket.save()
-
- dto = {
- "msg": "ok",
- "merchant_uid": request.data["merchant_uid"]
- }
-
- return Response(dto)
-
-
-class PaymentSuccessApi(APIView):
- def post(self, request):
- if not request.user.is_authenticated:
- return Response({"msg": "not logged in user"}, status=400)
-
- payment_key = request.data["merchant_uid"]
-
- target_payment = Payment.objects.get(payment_key=payment_key)
-
- payment_history = PaymentHistory(
- payment_key=payment_key,
- status=enum.PaymentStatus.PAYMENT_SUCCESS.value,
- is_webhook=False
- )
- payment_history.save()
-
- if not Ticket.objects.filter(payment=target_payment).exists():
- ticket = Ticket.objects.create(
- ticket_type=target_payment.ticket_type,
- bought_at=datetime.datetime.now(),
- user=target_payment.user,
- payment=target_payment
- )
- ticket.save()
-
-
- dto = {
- "msg": "ok",
- "merchant_uid": request.data["merchant_uid"]
- }
-
- return Response(dto)
-
-
-class PaymentFailedApi(APIView):
- def post(self, request):
- if not request.is_authenticated:
- return Response({"msg": "not logged in user"}, status=400)
-
- payment_key = request.data["merchant_uid"]
-
- payment = Payment.objects.get(payment_key=payment_key)
- payment.status = enum.PaymentStatus.PAYMENT_FAILED.value
- payment.save()
-
- payment_history = PaymentHistory(
- payment_key=payment_key,
- status=enum.PaymentStatus.PAYMENT_FAILED.value,
- is_webhook=False
- )
- payment_history.save()
-
- dto = {
- "msg": "ok",
- "merchant_uid": request.data["merchant_uid"]
- }
-
- return Response(dto)
-
-
-@api_view(["POST"])
-def post__generate_payment_key(request):
-
- request_ticket_type = TicketType.objects.get(id=request.data["ticket_type"])
-
- payment_key = generate_payment_key(
- user=request.user,
- ticket_type=request_ticket_type
- )
-
- response_data = {
- "msg": "ok",
- "payment_key": payment_key,
- "price": request_ticket_type.price
- }
-
- return Response(response_data)
-
-
-@api_view(["POST"])
-@transaction.atomic
-def post__cancel_payment(request):
- portone_client = PortOneClient()
-
- target_payment = Payment.objects.get(payment_key=request.data["payment_key"])
- target_ticket = Ticket.objects.get(payment=target_payment)
-
- portone_client.req_cancel_payment(
- target_payment.payment_key,
- target_payment.money,
- "구매자의 환불요청"
- )
-
- cancel_payment(target_payment)
-
- target_ticket.is_refunded = True
- target_ticket.refunded_at = datetime.datetime.now()
- target_ticket.save()
-
- dto = {
- "msg": "ok"
- }
-
- return Response(dto)
-
diff --git a/program/admin.py b/program/admin.py
index a08e498..dcc964b 100644
--- a/program/admin.py
+++ b/program/admin.py
@@ -8,6 +8,7 @@
@admin.register(Program)
class ProgramAdmin(ImportExportModelAdmin):
list_display = [
+ "year",
"id",
"host",
"profile_img",
@@ -18,8 +19,6 @@ class ProgramAdmin(ImportExportModelAdmin):
"end_at",
"program_type",
]
- list_filter = [
- "program_type",
- ]
+ list_filter = ["year", "program_type"]
search_fields = ["title", "host__username"]
resource_class = ProgramResource
diff --git a/program/migrations/0010_program_year.py b/program/migrations/0010_program_year.py
new file mode 100644
index 0000000..7cc66e5
--- /dev/null
+++ b/program/migrations/0010_program_year.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.5 on 2024-06-23 11:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('program', '0009_alter_program_program_type'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='program',
+ name='year',
+ field=models.IntegerField(default=2023),
+ ),
+ ]
diff --git a/program/models.py b/program/models.py
index 13604f8..7038cc8 100644
--- a/program/models.py
+++ b/program/models.py
@@ -14,6 +14,7 @@
class Program(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4)
host = models.CharField(max_length=100) # TODO User로?
+ year = models.IntegerField(default=2023)
title = models.CharField(max_length=100)
short_desc = models.CharField(max_length=1000)
desc = models.CharField(max_length=4000)
diff --git a/program/viewsets.py b/program/viewsets.py
index 12a70bb..1e54f34 100644
--- a/program/viewsets.py
+++ b/program/viewsets.py
@@ -1,20 +1,24 @@
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.viewsets import ReadOnlyModelViewSet
-from program.models import Program
from program import models
+from program.models import Program
from program.serializers import ProgramSerializer
class SprintListViewSet(ReadOnlyModelViewSet):
- queryset = Program.objects.filter(program_type=models.SPRINT).order_by("start_at").order_by("title")
+ queryset = Program.objects.filter(program_type=models.SPRINT).order_by("start_at", "title")
permission_classes = [IsAuthenticatedOrReadOnly]
- http_method_names = ["get"]
serializer_class = ProgramSerializer
+ def get_queryset(self):
+ return super().get_queryset().filter(year=self.request.version or 2023)
+
class TutorialListViewSet(ReadOnlyModelViewSet):
- queryset = Program.objects.filter(program_type=models.TUTORIAL).order_by("start_at").order_by("title")
+ queryset = Program.objects.filter(program_type=models.TUTORIAL).order_by("start_at", "title")
permission_classes = [IsAuthenticatedOrReadOnly]
- http_method_names = ["get"]
serializer_class = ProgramSerializer
+
+ def get_queryset(self):
+ return super().get_queryset().filter(year=self.request.version or 2023)
diff --git a/pyconkr/openapi.py b/pyconkr/openapi.py
new file mode 100644
index 0000000..d787263
--- /dev/null
+++ b/pyconkr/openapi.py
@@ -0,0 +1,7 @@
+import typing
+
+EndpointType = tuple[str, str, str, typing.Callable]
+
+
+def preprocessing_filter_spec(endpoints: typing.Iterable[EndpointType]) -> list[EndpointType]:
+ return [endpoint for endpoint in endpoints if endpoint[0].startswith("/{version}/")]
diff --git a/pyconkr/settings.py b/pyconkr/settings.py
index 4ddcc68..42f7222 100644
--- a/pyconkr/settings.py
+++ b/pyconkr/settings.py
@@ -10,10 +10,10 @@
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
import os
-from pathlib import Path
+import pathlib
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
-BASE_DIR = Path(__file__).resolve().parent.parent
+# Build paths inside the project like this: BASE_DIR / "subdir".
+BASE_DIR = pathlib.Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
@@ -54,10 +54,7 @@
"constance.backends.database",
# apps
"sponsor",
- "status",
- "ticket",
"program",
- "payment",
"accounts",
"session",
# swagger
@@ -142,18 +139,10 @@
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
- {
- "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
- },
- {
- "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
- },
+ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
@@ -186,30 +175,12 @@
# django-constance
CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
CONSTANCE_CONFIG = {
- "SLACK_SECRET": (
- "",
- "Slack 알림 전송에 사용할 Secret",
- ),
- "SPONSOR_NOTI_CHANNEL": (
- "",
- "후원사 변동사항에 대한 알림을 보낼 채널",
- ),
- "CONFERENCE_PARTICIPANT_COUNT_SAT": (
- 1700,
- "컨퍼런스(토) 참가자 수",
- ),
- "CONFERENCE_PARTICIPANT_COUNT_SUN": (
- 1700,
- "컨퍼런스(일) 참가자 수",
- ),
- "IMP_KEY": (
- "",
- "포트원 REST API 키",
- ),
- "IMP_SECRET": (
- "",
- "포트원 REST API 비밀키",
- ),
+ "SLACK_SECRET": ("", "Slack 알림 전송에 사용할 Secret"),
+ "SPONSOR_NOTI_CHANNEL": ("", "후원사 변동사항에 대한 알림을 보낼 채널"),
+ "CONFERENCE_PARTICIPANT_COUNT_SAT": (1700, "컨퍼런스(토) 참가자 수"),
+ "CONFERENCE_PARTICIPANT_COUNT_SUN": (1700, "컨퍼런스(일) 참가자 수"),
+ "IMP_KEY": ("", "포트원 REST API 키"),
+ "IMP_SECRET": ("", "포트원 REST API 비밀키"),
}
# drf-spectacular
@@ -217,13 +188,17 @@
# YOUR SETTINGS
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_AUTHENTICATION_CLASSES": (
- 'rest_framework.authentication.BasicAuthentication',
- # 'rest_framework.authentication.SessionAuthentication',
+ "rest_framework.authentication.BasicAuthentication",
+ # "rest_framework.authentication.SessionAuthentication",
"rest_framework_simplejwt.authentication.JWTAuthentication",
"dj_rest_auth.jwt_auth.JWTCookieAuthentication",
),
+ "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
+ "DEFAULT_VERSION": "2023",
+ "ALLOWED_VERSIONS": ["2023", "2024"],
}
+
SPECTACULAR_SETTINGS = {
"TITLE": "pyconkr-api-v2",
"DESCRIPTION": "파이콘 한국 웹서비스용 API (2023 ~ )",
@@ -236,8 +211,7 @@
"persistAuthorization": True,
"displayOperationId": True,
},
- # available SwaggerUI versions: https://github.com/swagger-api/swagger-ui/releases
- "SWAGGER_UI_DIST": "//unpkg.com/swagger-ui-dist@3.35.1",
+ "PREPROCESSING_HOOKS": ["pyconkr.openapi.preprocessing_filter_spec"],
}
# CORS_ALLOW_ALL_ORIGINS = True
@@ -265,6 +239,6 @@
# login_required view에 로그인 되지 않은 상태로 접속할 경우 리다이렉트할 로그인 페이지를 설정합니다.
# The URL or named URL pattern where requests are redirected for login when using the login_required() decorator
-LOGIN_URL = '/accounts/login/'
+LOGIN_URL = "/accounts/login/"
AWS_QUERYSTRING_AUTH = False
diff --git a/pyconkr/urls.py b/pyconkr/urls.py
index 2a53081..6e770df 100644
--- a/pyconkr/urls.py
+++ b/pyconkr/urls.py
@@ -15,32 +15,25 @@
"""
from django.conf import settings
from django.contrib import admin
-from django.urls import include, path
-from drf_spectacular.views import (
- SpectacularAPIView,
- SpectacularRedocView,
- SpectacularSwaggerView,
-)
-from rest_framework.routers import DefaultRouter
+from django.urls import include, path, re_path
+from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
-import payment.urls
-import sponsor.urls
-import status.urls
-import ticket.urls
+import accounts.urls
+import program.urls
import session.urls
-
+import sponsor.urls
urlpatterns = [
- path("api-auth/", include("rest_framework.urls")),
- path("summernote/", include("django_summernote.urls")),
- path("admin/", admin.site.urls),
- path("sponsors/", include(sponsor.urls)),
- path("programs/", include("program.urls")),
- path("statuses/", include(status.urls)),
- path("tickets/", include(ticket.urls)),
- path("payments/", include(payment.urls)),
- path("sessions/", include(session.urls)),
- path("", include("accounts.urls")),
+ path(kwargs={"version": "2023"}, route="sponsors/", view=include(sponsor.urls)),
+ path(kwargs={"version": "2023"}, route="programs/", view=include(program.urls)),
+ path(kwargs={"version": "2023"}, route="sessions/", view=include(session.urls)),
+ re_path(route="^(?P(2023|2024))/sponsors/", view=include(sponsor.urls)),
+ re_path(route="^(?P(2023|2024))/programs/", view=include(program.urls)),
+ re_path(route="^(?P(2023|2024))/sessions/", view=include(session.urls)),
+ path(route="summernote/", view=include("django_summernote.urls")),
+ path(route="api-auth/", view=include("rest_framework.urls")),
+ path(route="admin/", view=admin.site.urls),
+ path(route="", view=include(accounts.urls)),
]
if settings.DEBUG is True:
diff --git a/session/admin.py b/session/admin.py
index 13e7257..f09aba9 100644
--- a/session/admin.py
+++ b/session/admin.py
@@ -1,8 +1,7 @@
from django.contrib import admin
-
from import_export.admin import ImportExportModelAdmin
-from .models import Proposal, Session, Category
+from .models import Category, Proposal, Session
from .resources import SessionResource
@@ -17,9 +16,14 @@ class ProposalAdmin(admin.ModelAdmin):
"duration",
"language",
"category",
+ "get_year",
]
- list_filter = ["accepted", "difficulty", "duration", "language", "category"]
- search_fields = ["title", "user__username"]
+ list_filter = ["category__year", "accepted", "difficulty", "duration", "language", "category"]
+ search_fields = ["category__year", "title", "user__username"]
+
+ @admin.display(ordering="category__year", description="Year")
+ def get_year(self, obj: Proposal):
+ return obj.category.year
@admin.register(Session)
@@ -33,14 +37,19 @@ class SessionAdmin(ImportExportModelAdmin):
"duration",
"language",
"category",
+ "get_year",
]
- list_filter = ["difficulty", "duration", "language", "category"]
- search_fields = ["title", "user__username"]
+ list_filter = ["category__year", "difficulty", "duration", "language", "category"]
+ search_fields = ["category__year", "title", "user__username"]
resource_class = SessionResource
+ @admin.display(ordering="category__year", description="Year")
+ def get_year(self, obj: Session):
+ return obj.category.year
+
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
- list_display = ["id", "name", "visible"]
- list_filter = ["visible"]
- search_fields = ["name"]
\ No newline at end of file
+ list_display = ["year", "id", "name", "visible"]
+ list_filter = ["year", "visible"]
+ search_fields = ["year", "name"]
diff --git a/session/migrations/0006_category_year.py b/session/migrations/0006_category_year.py
new file mode 100644
index 0000000..49c8040
--- /dev/null
+++ b/session/migrations/0006_category_year.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.5 on 2024-06-23 11:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('session', '0005_session_host_introduction_session_host_profile_image'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='category',
+ name='year',
+ field=models.IntegerField(default=2023),
+ ),
+ ]
diff --git a/session/models.py b/session/models.py
index 7645ac2..32d4354 100644
--- a/session/models.py
+++ b/session/models.py
@@ -7,6 +7,7 @@
class Category(models.Model):
name = models.CharField(max_length=100, db_index=True)
visible = models.BooleanField(default=True)
+ year = models.IntegerField(default=2023)
class Meta:
verbose_name = "세션 카테고리"
@@ -151,4 +152,4 @@ class Meta:
verbose_name_plural = "세션들"
def __str__(self):
- return self.title
\ No newline at end of file
+ return self.title
diff --git a/session/viewsets.py b/session/viewsets.py
index 6a7705d..7d2ed3e 100644
--- a/session/viewsets.py
+++ b/session/viewsets.py
@@ -15,3 +15,6 @@ def get_serializer_class(self):
return SessionListSerializer
else:
return SessionSerializer
+
+ def get_queryset(self):
+ return super().get_queryset().filter(category__year=self.request.version)
diff --git a/sponsor/migrations/0006_sponsorlevel_year.py b/sponsor/migrations/0006_sponsorlevel_year.py
new file mode 100644
index 0000000..1e28f59
--- /dev/null
+++ b/sponsor/migrations/0006_sponsorlevel_year.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.5 on 2024-06-23 11:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sponsor', '0005_patron'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='sponsorlevel',
+ name='year',
+ field=models.IntegerField(default=2023),
+ ),
+ ]
diff --git a/sponsor/models.py b/sponsor/models.py
index 6547ce3..ac84386 100644
--- a/sponsor/models.py
+++ b/sponsor/models.py
@@ -25,6 +25,7 @@ class Meta:
order = models.IntegerField(default=1)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
+ year = models.IntegerField(default=2023)
objects = SponsorLevelManager()
diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py
index 95cd135..f4bde66 100644
--- a/sponsor/viewsets.py
+++ b/sponsor/viewsets.py
@@ -2,7 +2,7 @@
from django.db.transaction import atomic
from django.shortcuts import get_object_or_404
-from rest_framework import status
+from rest_framework import mixins, status, viewsets
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ViewSet
@@ -19,21 +19,25 @@
from sponsor.validators import SponsorValidater
-class SponsorViewSet(ModelViewSet):
+class SponsorViewSet(
+ mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.ListModelMixin,
+ viewsets.GenericViewSet,
+):
+ queryset = Sponsor.objects.all()
serializer_class = SponsorSerializer
permission_classes = [IsOwnerOrReadOnly] # 본인 소유만 수정 가능
- http_method_names = ["get", "post", "put"]
validator = SponsorValidater()
def get_queryset(self):
- return Sponsor.objects.all().order_by("paid_at")
+ return super().get_queryset().filter(paid_at__isnull=False, level__year=self.request.version).order_by("level__order", "paid_at")
- def list(self, request, *args, **kwargs):
- queryset = Sponsor.objects.filter(paid_at__isnull=False).order_by(
- "level", "paid_at"
- )
- serializer = SponsorListSerializer(queryset, many=True)
- return Response(serializer.data)
+ def get_serializer_class(self):
+ if self.action == "list":
+ return SponsorListSerializer
+ return SponsorSerializer
@atomic
def create(self, request, *args, **kwargs):
@@ -54,7 +58,7 @@ def create(self, request, *args, **kwargs):
def retrieve(self, request, *args, **kwargs):
pk = kwargs["id"]
- sponsor_data = get_object_or_404(Sponsor, pk=pk)
+ sponsor_data = get_object_or_404(self.get_queryset(), pk=pk)
# 본인 소유인 경우는 모든 필드
# 그렇지 않은 경우는 공개 가능한 필드만 응답
@@ -86,20 +90,6 @@ def update(self, request, *args, **kwargs):
return Response(serializer.errors, status.HTTP_400_BAD_REQUEST)
-class SponsorListViewSet(ModelViewSet):
- serializer_class = SponsorListSerializer
- http_method_names = ["get"]
-
- def get_queryset(self):
- return SponsorLevel.objects.all()
-
- def list(self, request, *args, **kwargs):
- queryset = SponsorLevel.objects.all().order_by("-price")
- serializer = SponsorListSerializer(queryset, many=True)
-
- return Response(serializer.data)
-
-
class SponsorRemainingAccountViewSet(ModelViewSet):
serializer_class = SponsorRemainingAccountSerializer
http_method_names = ["get"]
diff --git a/status/__init__.py b/status/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/status/admin.py b/status/admin.py
deleted file mode 100644
index 608954c..0000000
--- a/status/admin.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from django.contrib import admin
-
-from status.models import Status
-
-
-class StatusAdmin(admin.ModelAdmin):
- list_display = ("name", "open_at", "close_at")
- list_editable = (
- "open_at",
- "close_at",
- )
- ordering = ("open_at",)
- search_fields = ("name",)
-
-
-admin.site.register(Status, StatusAdmin)
diff --git a/status/apps.py b/status/apps.py
deleted file mode 100644
index cb23d09..0000000
--- a/status/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class StatusConfig(AppConfig):
- default_auto_field = "django.db.models.BigAutoField"
- name = "status"
diff --git a/status/migrations/0001_initial.py b/status/migrations/0001_initial.py
deleted file mode 100644
index 0ab777a..0000000
--- a/status/migrations/0001_initial.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Generated by Django 4.1.5 on 2023-02-24 17:43
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- initial = True
-
- dependencies = []
-
- operations = [
- migrations.CreateModel(
- name="Status",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("name", models.CharField(max_length=100)),
- ("open_at", models.DateTimeField()),
- ("close_at", models.DateTimeField()),
- ],
- ),
- ]
diff --git a/status/migrations/__init__.py b/status/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/status/models.py b/status/models.py
deleted file mode 100644
index 4e8c97d..0000000
--- a/status/models.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from django.db import models
-
-
-class Status(models.Model):
- name = models.CharField(max_length=100)
- open_at = models.DateTimeField()
- close_at = models.DateTimeField()
diff --git a/status/tests.py b/status/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/status/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/status/urls.py b/status/urls.py
deleted file mode 100644
index 9ba260f..0000000
--- a/status/urls.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""pyconkr URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/4.1/topics/http/urls/
-Examples:
-Function views
- 1. Add an import: from my_app import views
- 2. Add a URL to urlpatterns: path('', views.home, name='home')
-Class-based views
- 1. Add an import: from other_app.views import Home
- 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
-Including another URLconf
- 1. Import the include() function: from django.urls import include, path
- 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
-"""
-from django.contrib import admin
-from django.urls import include, path
-
-from status.views import StatusView
-
-urlpatterns = [
- path("", StatusView.as_view()),
-]
diff --git a/status/views.py b/status/views.py
deleted file mode 100644
index cc29ed3..0000000
--- a/status/views.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import datetime
-
-from django.shortcuts import get_object_or_404
-from pytz import timezone
-from rest_framework.response import Response
-from rest_framework.views import APIView
-
-from status.models import Status
-
-
-class StatusView(APIView):
- def get(self, request, name: str):
- status = get_object_or_404(Status, name=name)
- now = datetime.datetime.now(tz=timezone("Asia/Seoul"))
-
- flag = None
-
- if status.open_at < now < status.close_at:
- flag = True
- else:
- flag = False
-
- return Response({"name": name, "open": flag})
diff --git a/ticket/__init__.py b/ticket/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/ticket/admin.py b/ticket/admin.py
deleted file mode 100644
index b2bbb29..0000000
--- a/ticket/admin.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from django.contrib import admin
-
-from .models import Ticket, TicketType
-
-
-class ConferenceTicketAdmin(admin.ModelAdmin):
- list_display = (
- "ticket_code",
- "user",
- "ticket_type",
- "bought_at",
- )
- list_filter = ("ticket_type",)
-
-
-admin.site.register(Ticket, ConferenceTicketAdmin)
-
-
-class ConferenceTicketTypeAdmin(admin.ModelAdmin):
- list_display = (
- "name",
- "price",
- "min_price",
- "day",
- )
-
-
-admin.site.register(TicketType, ConferenceTicketTypeAdmin)
diff --git a/ticket/apps.py b/ticket/apps.py
deleted file mode 100644
index 99d42c3..0000000
--- a/ticket/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class TicketConfig(AppConfig):
- default_auto_field = "django.db.models.BigAutoField"
- name = "ticket"
diff --git a/ticket/migrations/0001_initial.py b/ticket/migrations/0001_initial.py
deleted file mode 100644
index 7c7fe98..0000000
--- a/ticket/migrations/0001_initial.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-14 03:14
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import ticket.models
-
-
-class Migration(migrations.Migration):
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name="ConferenceTicketType",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("code", models.CharField(max_length=50)),
- ("name", models.CharField(max_length=100)),
- ("price", models.IntegerField()),
- ("min_price", models.IntegerField(blank=True, null=True)),
- ("desc", models.TextField(max_length=1000)),
- (
- "day",
- models.CharField(
- choices=[("SAT", "토요일"), ("SUN", "일요일"), ("WEEKEND", "토/일요일")],
- max_length=10,
- ),
- ),
- ],
- ),
- migrations.CreateModel(
- name="ConferenceTicket",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("bought_at", models.DateTimeField()),
- (
- "ticket_code",
- models.CharField(
- db_index=True,
- default=ticket.models.make_ticket_code,
- max_length=25,
- unique=True,
- ),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "ticket_type",
- models.ForeignKey(
- on_delete=django.db.models.deletion.RESTRICT,
- to="ticket.conferencetickettype",
- ),
- ),
- (
- "user",
- models.ForeignKey(
- on_delete=django.db.models.deletion.RESTRICT,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ],
- ),
- ]
diff --git a/ticket/migrations/0002_rename_conferencetickettype_tickettype.py b/ticket/migrations/0002_rename_conferencetickettype_tickettype.py
deleted file mode 100644
index ef95a3b..0000000
--- a/ticket/migrations/0002_rename_conferencetickettype_tickettype.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-14 05:32
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("ticket", "0001_initial"),
- ]
-
- operations = [
- migrations.RenameModel(
- old_name="ConferenceTicketType",
- new_name="TicketType",
- ),
- ]
diff --git a/ticket/migrations/0003_ticket_remove_tickettype_code_and_more.py b/ticket/migrations/0003_ticket_remove_tickettype_code_and_more.py
deleted file mode 100644
index 09087ba..0000000
--- a/ticket/migrations/0003_ticket_remove_tickettype_code_and_more.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-15 13:40
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import ticket.models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("payment", "0002_rename_user_id_payment_user_payment_status_and_more"),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ("ticket", "0002_rename_conferencetickettype_tickettype"),
- ]
-
- operations = [
- migrations.CreateModel(
- name="Ticket",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("bought_at", models.DateTimeField()),
- (
- "ticket_code",
- models.CharField(
- db_index=True,
- default=ticket.models.make_ticket_code,
- max_length=25,
- unique=True,
- ),
- ),
- ("is_refunded", models.BooleanField(default=False)),
- ("refunded_at", models.DateTimeField(null=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "payment",
- models.ForeignKey(
- null=True,
- on_delete=django.db.models.deletion.PROTECT,
- to="payment.payment",
- ),
- ),
- ],
- ),
- migrations.RemoveField(
- model_name="tickettype",
- name="code",
- ),
- migrations.AddField(
- model_name="tickettype",
- name="is_refundable",
- field=models.BooleanField(default=True),
- ),
- migrations.DeleteModel(
- name="ConferenceTicket",
- ),
- migrations.AddField(
- model_name="ticket",
- name="ticket_type",
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.RESTRICT, to="ticket.tickettype"
- ),
- ),
- migrations.AddField(
- model_name="ticket",
- name="user",
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.RESTRICT,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ]
diff --git a/ticket/migrations/0004_alter_tickettype_id.py b/ticket/migrations/0004_alter_tickettype_id.py
deleted file mode 100644
index f4d39b4..0000000
--- a/ticket/migrations/0004_alter_tickettype_id.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-24 13:06
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('ticket', '0003_ticket_remove_tickettype_code_and_more'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='tickettype',
- name='id',
- field=models.UUIDField(primary_key=True, serialize=False),
- ),
- ]
diff --git a/ticket/migrations/0005_alter_tickettype_day.py b/ticket/migrations/0005_alter_tickettype_day.py
deleted file mode 100644
index b4d1ff5..0000000
--- a/ticket/migrations/0005_alter_tickettype_day.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-24 13:07
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('ticket', '0004_alter_tickettype_id'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='tickettype',
- name='day',
- field=models.CharField(choices=[('FRI', '금요일'), ('SAT', '토요일'), ('SUN', '일요일'), ('WEEKEND', '토/일요일')], max_length=10),
- ),
- ]
diff --git a/ticket/migrations/0006_tickettype_program.py b/ticket/migrations/0006_tickettype_program.py
deleted file mode 100644
index f9d0369..0000000
--- a/ticket/migrations/0006_tickettype_program.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-24 13:39
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('program', '0003_program'),
- ('ticket', '0005_alter_tickettype_day'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='tickettype',
- name='program',
- field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='program.program'),
- ),
- ]
diff --git a/ticket/migrations/0007_alter_tickettype_id.py b/ticket/migrations/0007_alter_tickettype_id.py
deleted file mode 100644
index 7d44dec..0000000
--- a/ticket/migrations/0007_alter_tickettype_id.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 4.1.5 on 2023-05-30 13:32
-
-from django.db import migrations, models
-import uuid
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('ticket', '0006_tickettype_program'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='tickettype',
- name='id',
- field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
- ),
- ]
diff --git a/ticket/migrations/0008_alter_ticket_refunded_at.py b/ticket/migrations/0008_alter_ticket_refunded_at.py
deleted file mode 100644
index c092bbd..0000000
--- a/ticket/migrations/0008_alter_ticket_refunded_at.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 4.1.5 on 2023-06-01 14:47
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('ticket', '0007_alter_tickettype_id'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='ticket',
- name='refunded_at',
- field=models.DateTimeField(blank=True, null=True),
- ),
- ]
diff --git a/ticket/migrations/0009_tickettype_buyable_url.py b/ticket/migrations/0009_tickettype_buyable_url.py
deleted file mode 100644
index 9d61c2c..0000000
--- a/ticket/migrations/0009_tickettype_buyable_url.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 4.1.5 on 2023-07-16 13:17
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('ticket', '0008_alter_ticket_refunded_at'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='tickettype',
- name='buyable_url',
- field=models.CharField(blank=True, max_length=255, null=True),
- ),
- ]
diff --git a/ticket/migrations/__init__.py b/ticket/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/ticket/models.py b/ticket/models.py
deleted file mode 100644
index 1d16e97..0000000
--- a/ticket/models.py
+++ /dev/null
@@ -1,102 +0,0 @@
-from __future__ import annotations
-
-from uuid import uuid4
-
-import shortuuid
-from constance import config
-from django.contrib.auth import get_user_model
-from django.db import models
-
-User = get_user_model()
-
-
-class TicketType(models.Model):
- id = models.UUIDField(primary_key=True, default=uuid4)
- name = models.CharField(max_length=100)
- price = models.IntegerField()
- min_price = models.IntegerField(null=True, blank=True)
- desc = models.TextField(max_length=1000)
- day = models.CharField(
- max_length=10,
- choices=(
- ("FRI", "금요일"),
- ("SAT", "토요일"),
- ("SUN", "일요일"),
- ("WEEKEND", "토/일요일"),
- ),
- )
- program = models.ForeignKey("program.Program", on_delete=models.PROTECT, null=True)
- is_refundable = models.BooleanField(default=True)
- buyable_url = models.CharField(max_length=255, null=True, blank=True)
-
- def __str__(self):
- return self.name
-
- @property
- def buyable(self) -> bool:
- """잔여 수량이 있는지"""
- if self.day == "FRI":
- ticket_count = Ticket.objects.filter(
- models.Q(ticket_type=self) & models.Q(is_refunded=False)
- ).count()
- if self.program.slot is not None:
- return ticket_count < self.program.slot
- return True
-
- sat_ticket_count = Ticket.objects.filter(
- models.Q(ticket_type__day="SAT") | models.Q(ticket_type__day="WEEKEND")
- ).filter(is_refunded=False).count()
- sun_ticket_count = Ticket.objects.filter(
- models.Q(ticket_type__day="SUN") | models.Q(ticket_type__day="WEEKEND")
- ).filter(is_refunded=False).count()
-
- can_buy_sat_ticket = sat_ticket_count < config.CONFERENCE_PARTICIPANT_COUNT_SAT
- can_buy_sun_ticket = sun_ticket_count < config.CONFERENCE_PARTICIPANT_COUNT_SUN
-
- if self.day == "SAT":
- return can_buy_sat_ticket
- elif self.day == "SUN":
- return can_buy_sun_ticket
- elif self.day == "WEEKEND":
- return can_buy_sat_ticket and can_buy_sun_ticket
- else:
- raise ValueError(f"{self.day} is not valid day.")
-
- def can_coexist(self, other: TicketType) -> bool:
- if self.day == "SAT" and other.day == "SUN":
- return True
- if self.day == "SUN" and other.day == "SAT":
- return True
- if self.day == "FRI" or other.day == "FRI":
- # TODO program의 시간이 겹치는지 확인?
- return True
-
- return False
-
-
-def make_ticket_code() -> str:
- return shortuuid.uuid()
-
-
-class Ticket(models.Model):
- # 구분
- ticket_type = models.ForeignKey(
- TicketType, on_delete=models.RESTRICT, db_index=True
- )
- # 구매 일자
- bought_at = models.DateTimeField()
- # 사용자
- user = models.ForeignKey(User, on_delete=models.RESTRICT, db_index=True)
- # 티켓 코드
- ticket_code = models.CharField(
- max_length=25, default=make_ticket_code, unique=True, db_index=True
- )
- # 결제 정보
- payment = models.ForeignKey("payment.Payment", on_delete=models.PROTECT, null=True)
- is_refunded = models.BooleanField(default=False)
- refunded_at = models.DateTimeField(null=True, blank=True)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- def __str__(self):
- return self.ticket_code
diff --git a/ticket/requests.py b/ticket/requests.py
deleted file mode 100644
index 87ba4a7..0000000
--- a/ticket/requests.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import json
-from dataclasses import dataclass
-from typing import Optional, Type, TypeVar
-
-import jsons
-from django.http import HttpRequest
-
-_T = TypeVar("_T", bound=Type)
-
-
-class RequestParsingException(Exception):
- ...
-
-
-def _extract_querystring(request: HttpRequest) -> dict:
- querystring: dict = dict(request.GET)
- for k, v in querystring.items():
- if (isinstance(v, list) or isinstance(v, tuple)) and len(v) == 1:
- querystring[k] = v[0]
-
- return querystring
-
-
-@dataclass(init=False)
-class GetConferenceTicketTypesRequest:
- @dataclass
- class Querystring:
- ...
-
- @dataclass
- class MatchInfo:
- ...
-
- @dataclass
- class Data:
- ...
-
- def __init__(self, request: HttpRequest, **kwargs):
- try:
- self.querystring = jsons.load(
- _extract_querystring(request),
- GetConferenceTicketTypesRequest.Querystring,
- )
- self.match_info = jsons.load(
- kwargs, GetConferenceTicketTypesRequest.MatchInfo
- )
- self.data = jsons.load(
- json.loads(request.body) if request.body else dict(),
- GetConferenceTicketTypesRequest.Data,
- )
- except Exception as e:
- raise RequestParsingException() from e
-
- querystring: Optional[Querystring] = None
- match_info: Optional[MatchInfo] = None
- data: Optional[Data] = None
-
-
-@dataclass(init=False)
-class CheckTicketTypeBuyableRequest:
- @dataclass
- class Querystring:
- username: Optional[str] = None
-
- @dataclass
- class MatchInfo:
- ticket_type_id: str
-
- @dataclass
- class Data:
- ...
-
- def __init__(self, request: HttpRequest, **kwargs):
- try:
- self.querystring = jsons.load(
- _extract_querystring(request),
- CheckTicketTypeBuyableRequest.Querystring,
- )
- self.match_info = jsons.load(
- kwargs, CheckTicketTypeBuyableRequest.MatchInfo
- )
- self.data = jsons.load(
- json.loads(request.body) if request.body else dict(),
- CheckTicketTypeBuyableRequest.Data,
- )
- except Exception as e:
- raise RequestParsingException() from e
-
- querystring: Optional[Querystring] = None
- match_info: Optional[MatchInfo] = None
- data: Optional[Data] = None
-
-
-@dataclass(init=False)
-class AddConferenceTicketRequest:
- @dataclass
- class Querystring:
- ...
-
- @dataclass
- class MatchInfo:
- ...
-
- @dataclass
- class Data:
- ticket_type: str
- bought_at: str
- username: str
-
- def __init__(self, request: HttpRequest, **kwargs):
- try:
- self.querystring = jsons.load(
- _extract_querystring(request), AddConferenceTicketRequest.Querystring
- )
- self.match_info = jsons.load(kwargs, AddConferenceTicketRequest.MatchInfo)
- self.data = jsons.load(
- json.loads(request.body) if request.body else dict(),
- AddConferenceTicketRequest.Data,
- )
- except Exception as e:
- raise RequestParsingException() from e
-
- querystring: Optional[Querystring] = None
- match_info: Optional[MatchInfo] = None
- data: Optional[Data] = None
diff --git a/ticket/templates/ticket-detail.html b/ticket/templates/ticket-detail.html
deleted file mode 100644
index 8235f88..0000000
--- a/ticket/templates/ticket-detail.html
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- 티켓 상세
-
-
- {{ ticket_type.name }}
- 상품명: {{ ticket_type.desc }}
-
-
- 전화번호:
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ticket/templates/ticket-failed.html b/ticket/templates/ticket-failed.html
deleted file mode 100644
index b3c2c86..0000000
--- a/ticket/templates/ticket-failed.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- 티켓 구매 실패
-
-
- 티켓 구매 실패
-
-
\ No newline at end of file
diff --git a/ticket/templates/ticket-list.html b/ticket/templates/ticket-list.html
deleted file mode 100644
index a6cf7e3..0000000
--- a/ticket/templates/ticket-list.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
- 티켓 종류
-
-
- {% for item in ticket_items %}
- {{ item.name }} : {{ item.price }} 구매하기
- {% endfor %}
-
-
-
\ No newline at end of file
diff --git a/ticket/templates/ticket-refund-success.html b/ticket/templates/ticket-refund-success.html
deleted file mode 100644
index de1b81d..0000000
--- a/ticket/templates/ticket-refund-success.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- 티켓 환불
-
-
- 티켓 환불 성공
-
-
\ No newline at end of file
diff --git a/ticket/templates/ticket-success.html b/ticket/templates/ticket-success.html
deleted file mode 100644
index a0faa57..0000000
--- a/ticket/templates/ticket-success.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- 티켓 구매 성공
-
-
- 티켓 구매 성공
-
-
\ No newline at end of file
diff --git a/ticket/tests.py b/ticket/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/ticket/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/ticket/urls.py b/ticket/urls.py
deleted file mode 100644
index 51bf05d..0000000
--- a/ticket/urls.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from django.urls import path, re_path
-
-from . import views
-
-urlpatterns = [
- path("ticket-types/", views.get__get_ticket_types),
- re_path(
- r"^ticket-types/(?P\w+)/check",
- views.get__check_ticket_type_buyable,
- ),
- # path("conference-tickets", views.post__add_ticket), # 티켓 생성은 payment에서
- ####################################################################################
- # 템플릿 기반 API 비활성화
- ####################################################################################
- # path("list", views.get__ticket_type_list, name="ticket-list"),
- # path("", views.TicketDetailView.as_view(), name="ticket-detail"),
- # path("success", views.ticket_success, name="page-ticket-success"),
- # path("failed", views.ticket_failed, name="page-ticket-failed"),
- ####################################################################################
- path("/refund", views.ticket_refund, name="page-ticket-refund-success"),
-]
diff --git a/ticket/view_models.py b/ticket/view_models.py
deleted file mode 100644
index e9dbbdb..0000000
--- a/ticket/view_models.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from dataclasses import asdict, dataclass
-from typing import Optional, Union
-
-from program.models import CONFERENCE, TUTORIAL, SPRINT
-from .models import TicketType
-
-
-@dataclass(init=False)
-class TicketTypeViewModel:
- @dataclass
- class Program:
- title: str
- short_desc: str
- start_at: Optional[str]
- end_at: Optional[str]
- program_type: str # type: Union[CONFERENCE, TUTORIAL, SPRINT]
-
- id: str
- name: str
- price: int
- min_price: Optional[int]
- desc: str
- day: str # choice
- program: Program
- is_refundable: bool
- # is_buyable: property # type: bool
- buyable_url: Optional[str]
-
- def __init__(self, model: TicketType):
- self.id = str(model.id)
- self.name = model.name
- self.price = model.price
- self.min_price = model.min_price
- self.desc = model.desc
- self.day = model.day
- self.program = TicketTypeViewModel.Program(
- title=model.program.title,
- short_desc=model.program.short_desc,
- start_at=model.program.start_at.strftime(
- "%Y-%m-%dT%H:%M:%S") if model.program.start_at is not None else None,
- end_at=model.program.end_at.strftime("%Y-%m-%dT%H:%M:%S") if model.program.end_at is not None else None,
- program_type=model.program.program_type,
- )
- self.is_refundable = model.is_refundable
- # self.is_buyable = model.buyable
- self.buyable_url = model.buyable_url
-
- def to_dict(self):
- return asdict(self)
diff --git a/ticket/views.py b/ticket/views.py
deleted file mode 100644
index e59b51c..0000000
--- a/ticket/views.py
+++ /dev/null
@@ -1,209 +0,0 @@
-import json
-import traceback
-from datetime import datetime
-from typing import Callable, Literal, Dict, List
-
-from django.contrib.auth import get_user_model
-from django.http import HttpRequest, HttpResponse
-from django.shortcuts import get_object_or_404, render
-from django.views import View
-from django.views.decorators.csrf import csrf_exempt
-
-import payment.logic
-from program.models import CONFERENCE, TUTORIAL, SPRINT, CHILDCARE
-from .models import Ticket, TicketType
-from .requests import (
- AddConferenceTicketRequest,
- CheckTicketTypeBuyableRequest,
- GetConferenceTicketTypesRequest,
- RequestParsingException,
-)
-from .view_models import TicketTypeViewModel
-
-User = get_user_model()
-
-METHOD = Literal["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
-
-
-def request_method(method: METHOD) -> Callable:
- def decorator(func: Callable):
- @csrf_exempt
- def wrapper(*args, **kwargs):
- if args[0].method not in method:
- return HttpResponse("Method not allowed", status=405)
- return func(*args, **kwargs)
-
- return wrapper
-
- return decorator
-
-
-def exception_wrapper(func: Callable[[HttpRequest, ...], HttpResponse]):
- def wrapper(*args, **kwargs):
- try:
- return func(*args, **kwargs)
- except RequestParsingException:
- traceback.print_exc()
- print(f"{args=}")
- print(f"{kwargs=}")
- return HttpResponse("Invalid request", status=400)
- except (Exception,):
- print(f"{args=}")
- print(f"{kwargs=}")
- traceback.print_exc()
- return HttpResponse(status=500)
-
- return wrapper
-
-
-@request_method("GET")
-@exception_wrapper
-def get__get_ticket_types(request: HttpRequest, **kwargs) -> HttpResponse:
- """티켓 종류 목록 조회"""
- request = GetConferenceTicketTypesRequest(request, **kwargs)
-
- ticket_types = TicketType.objects.all()
-
- response: Dict[str, List[dict]] = {
- "conference": [],
- "tutorial": [],
- "sprint": [],
- "childcare": [],
- }
-
- for ticket_type in ticket_types:
- if ticket_type.program.program_type == CONFERENCE:
- response["conference"].append(TicketTypeViewModel(ticket_type).to_dict())
- elif ticket_type.program.program_type == TUTORIAL:
- response["tutorial"].append(TicketTypeViewModel(ticket_type).to_dict())
- elif ticket_type.program.program_type == SPRINT:
- response["sprint"].append(TicketTypeViewModel(ticket_type).to_dict())
- elif ticket_type.program.program_type == CHILDCARE:
- response["childcare"].append(TicketTypeViewModel(ticket_type).to_dict())
-
- return HttpResponse(json.dumps(response))
-
-
-@request_method("GET")
-@exception_wrapper
-def get__check_ticket_type_buyable(
- request: HttpRequest, **kwargs
-) -> HttpResponse:
- """특정 티켓 종류 구매 가능 여부 조회"""
- request = CheckTicketTypeBuyableRequest(request, **kwargs)
-
- ticket_type = get_object_or_404(
- TicketType, id=request.match_info.ticket_type_id
- )
-
- if request.querystring.username is None:
- return HttpResponse(json.dumps(ticket_type.buyable))
-
- try:
- user = User.objects.get(username=request.querystring.username)
- except User.DoesNotExist:
- return HttpResponse(json.dumps(ticket_type.buyable))
-
- bought_tickets = Ticket.objects.filter(user=user)
-
- return HttpResponse(
- json.dumps(
- ticket_type.buyable
- and all(
- (
- bought_ticket.ticket_type.can_coexist(ticket_type)
- for bought_ticket in bought_tickets
- )
- )
- )
- )
-
-
-@request_method("POST")
-@exception_wrapper
-def post__add_ticket(request: HttpRequest, **kwargs) -> HttpResponse:
- """티켓 결제 완료, 추가 요청"""
- request = AddConferenceTicketRequest(request)
-
- data = request.data
-
- ticket_type = data.ticket_type
- if ticket_type is None:
- return HttpResponse("Invalid ticket type", status=400)
- try:
- ticket_type = TicketType.objects.get(code=ticket_type)
- except TicketType.DoesNotExist:
- return HttpResponse("Invalid ticket type", status=400)
-
- bought_at = data.bought_at
- if bought_at is None:
- return HttpResponse("Invalid bought_at", status=400)
- try:
- bought_at = datetime.strptime(bought_at, "%Y-%m-%dT%H:%M:%S")
- except ValueError:
- return HttpResponse("Invalid datetime format (bought_at)", status=400)
-
- username = data.username
- if username is None:
- return HttpResponse("Invalid username", status=400)
- try:
- user = User.objects.get(username=username)
- except User.DoesNotExist:
- return HttpResponse("Cannot find user with user_id", status=400)
-
- bought_tickets = Ticket.objects.filter(user=user)
- if any(
- (
- not bought_ticket.ticket_type.can_coexist(ticket_type)
- for bought_ticket in bought_tickets
- )
- ):
- return HttpResponse("Duplicate day", status=400)
-
- ticket = Ticket.objects.create(
- ticket_type=ticket_type,
- bought_at=bought_at,
- user=user,
- )
-
- ticket.save()
-
- return HttpResponse(ticket.id)
-
-
-def get__ticket_type_list(request):
- all_types = TicketType.objects.all()
-
- dto = {
- "ticket_items": all_types,
- }
-
- return render(request, "ticket-list.html", dto)
-
-
-# Django Template 기반 코드
-class TicketDetailView(View):
- def get(self, request, item_id: int):
- ticket_type = TicketType.objects.get(id=item_id)
- payment_key = payment.logic.generate_payment_key(request.user, ticket_type=ticket_type)
- user = request.user
-
- dto = {
- "ticket_type": ticket_type,
- "payment_key": payment_key,
- "user_name": user.last_name + user.first_name
- }
-
- return render(request, "ticket-detail.html", dto)
-
-
-def ticket_success(request):
- return render(request, "ticket-success.html")
-
-
-def ticket_failed(request):
- return render(request, "ticket-failed.html")
-
-
-def ticket_refund(request, ticket_id):
- return render(request, "ticket-refund-success.html")