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 %} - - - - - - - - - - - - {% for ticket in ticket_list %} - - - - - - - {% endfor %} - -
id user금액결제일 취소
{{ ticket.id }}{{ ticket.payment.money }}{{ ticket.create_at }} - - -
-{% 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")