diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..2228666 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @pythonkr/PyConKR-2023 \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cb922f7..c2c9164 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,9 @@ ### 목표 * -### 작업내용 +### 작업 내용 * -### 유의사항 -* 리뷰어는 `PyConKR-2023`을 지정 해 주세요. +### 유의 사항 +* 리뷰어는 `PyConKR-2023`을 지정해주세요. * 작업한 내용을 상세하게 작성해주세요. \ No newline at end of file diff --git a/.github/workflows/deploy_on_dev.yml b/.github/workflows/deploy_on_dev.yml index f459ba9..5683233 100644 --- a/.github/workflows/deploy_on_dev.yml +++ b/.github/workflows/deploy_on_dev.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2 - uses: psf/black@stable with: - options: "--check --verbose --exclude migrations" + options: "--check --verbose" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 diff --git a/.github/workflows/deploy_on_prod.yml b/.github/workflows/deploy_on_prod.yml index 94fa814..d46d3ed 100644 --- a/.github/workflows/deploy_on_prod.yml +++ b/.github/workflows/deploy_on_prod.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: psf/black@stable with: - options: "--check --verbose --exclude migrations" + options: "--check --verbose" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 diff --git a/.github/workflows/pull-request-merge-precondition.yml b/.github/workflows/pull-request-merge-precondition.yml index ddad109..9d4c5c9 100644 --- a/.github/workflows/pull-request-merge-precondition.yml +++ b/.github/workflows/pull-request-merge-precondition.yml @@ -22,11 +22,11 @@ jobs: - uses: psf/black@stable with: - options: "--check --verbose --exclude migrations" + options: "--check --verbose" - uses: isort/isort-action@master with: - configuration: "--check-only --diff --profile black" + configuration: "--check-only --diff" requirementsFiles: "requirements.txt" - name: install dependencies diff --git a/program/__init__.py b/program/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/program/admin.py b/program/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/program/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/program/apps.py b/program/apps.py new file mode 100644 index 0000000..16d4ba1 --- /dev/null +++ b/program/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProgramConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "program" diff --git a/program/migrations/__init__.py b/program/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/program/models.py b/program/models.py new file mode 100644 index 0000000..ac57f65 --- /dev/null +++ b/program/models.py @@ -0,0 +1,89 @@ +from django.contrib.auth import get_user_model +from django.db import models + +User = get_user_model() + + +class ProposalCategory(models.Model): + name = models.CharField(max_length=100, db_index=True) + visible = models.BooleanField(default=True) + + def __str__(self): + return self.name + + +class Proposal(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) + + title = models.CharField(max_length=255) + brief = models.TextField(max_length=1000, help_text="리뷰용: 발표에 대한 간단한 설명.") + desc = models.TextField(max_length=4000, help_text="리뷰용: 발표에 대한 자세한 설명") + comment = models.TextField( + max_length=4000, null=True, blank=True, help_text="리뷰용: 파준위에게 전하고 싶은 말" + ) + + difficulty = models.CharField( + max_length=15, + choices=( + ("BEGINNER", "Beginner"), + ("INTERMEDIATE", "Intermediate"), + ("EXPERIENCED", "Experienced"), + ), + ) + + duration = models.CharField( + max_length=15, + choices=( + ("SHORT", "25min"), + ("LONG", "40min"), + ), + ) + + language = models.CharField( + max_length=15, + choices=( + ("", "---------"), + ("KOREAN", "Korean"), + ("ENGLISH", "English"), + ), + default="", + ) + + category = models.ForeignKey( + ProposalCategory, + on_delete=models.SET_DEFAULT, + null=True, + blank=True, + default=14, + ) + accepted = models.BooleanField(default=False) + introduction = models.TextField( + max_length=2000, + null=True, + blank=True, + help_text="발표 소개 페이지에 들어가는 내용입니다. 변경 사항은 최대 60분 이내에 적용됩니다.", + ) + video_url = models.CharField( + max_length=255, null=True, blank=True, help_text="발표 영상 URL" + ) + slide_url = models.CharField( + max_length=255, null=True, blank=True, help_text="발표 자료 URL" + ) + room_num = models.CharField( + max_length=15, + null=True, + blank=True, + help_text="발표장소", + choices=( + ("101", "101"), + ("102", "102"), + ("103", "103"), + ("104", "104"), + ("105", "105"), + ), + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title diff --git a/program/serializers.py b/program/serializers.py new file mode 100644 index 0000000..80aa88c --- /dev/null +++ b/program/serializers.py @@ -0,0 +1,45 @@ +from rest_framework.serializers import ModelSerializer + +from program.models import Proposal, ProposalCategory + + +class ProposalSerializer(ModelSerializer): + class Meta: + model = Proposal + fields = [ + "user", + "title", + "brief", + "desc", + "comment", + "difficulty", + "duration", + "language", + "category", + "accepted", + "introduction", + "video_url", + "slide_url", + "room_num", + ] + + +class ProposalListSerializer(ModelSerializer): + class Meta: + model = Proposal + fields = [ + "title", + "brief", + "difficulty", + "duration", + "language", + "category", + ] + + +class ProposalCategorySerializer(ModelSerializer): + class Meta: + model = ProposalCategory + fields = [ + "name", + ] diff --git a/program/tests.py b/program/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/program/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/program/views.py b/program/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/program/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/pyconkr/settings-dev.py b/pyconkr/settings-dev.py index 2c47248..ce98099 100644 --- a/pyconkr/settings-dev.py +++ b/pyconkr/settings-dev.py @@ -1,6 +1,7 @@ import os from pyconkr.settings import * +from pyconkr.storage import * DEBUG = True @@ -22,8 +23,12 @@ # django-storages: S3 del MEDIA_ROOT -DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" -STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage" +# DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" +# STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage" + +DEFAULT_FILE_STORAGE = "pyconkr.storage.MediaStorage" +STATICFILES_STORAGE = "pyconkr.storage.StaticStorage" + AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID") AWS_S3_SECRET_ACCESS_KEY = os.getenv("AWS_S3_SECRET_ACCESS_KEY") AWS_DEFAULT_REGION = "ap-northeast-2" diff --git a/pyconkr/settings.py b/pyconkr/settings.py index 4a756fe..ff036f1 100644 --- a/pyconkr/settings.py +++ b/pyconkr/settings.py @@ -24,6 +24,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +APPEND_SLASH = False ALLOWED_HOSTS = [] @@ -47,11 +48,14 @@ "status", # swagger "drf_spectacular", + # cors + "corsheaders", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -170,3 +174,5 @@ # available SwaggerUI versions: https://github.com/swagger-api/swagger-ui/releases "SWAGGER_UI_DIST": "//unpkg.com/swagger-ui-dist@3.35.1", } + +CORS_ALLOW_ALL_ORIGINS = True diff --git a/pyconkr/storage.py b/pyconkr/storage.py new file mode 100644 index 0000000..9633d47 --- /dev/null +++ b/pyconkr/storage.py @@ -0,0 +1,12 @@ +from django.conf import settings +from storages.backends.s3boto3 import S3Boto3Storage, S3StaticStorage + + +class MediaStorage(S3Boto3Storage): + def _get_security_token(self): + return None + + +class StaticStorage(S3StaticStorage): + def _get_security_token(self): + return None diff --git a/pyconkr/urls.py b/pyconkr/urls.py index 564933c..5428007 100644 --- a/pyconkr/urls.py +++ b/pyconkr/urls.py @@ -29,8 +29,8 @@ path("api-auth/", include("rest_framework.urls")), path("summernote/", include("django_summernote.urls")), path("admin/", admin.site.urls), - path("sponsors/", include(sponsor.routers.get_router().urls)), - path("status/", include(status.urls)), + path("sponsors", include(sponsor.routers.get_router().urls)), + path("status", include(status.urls)), ] if settings.DEBUG is True: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..88c3fba --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[tool.black] +line-length = 88 +include = '\.pyi?$' +extend-exclude = ''' +( + migrations +) +''' + +[tool.isort] +extend_skip = ["migrations"] +profile = "black" diff --git a/requirements.txt b/requirements.txt index 523fdff..737ef7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ Pillow==9.4.0 django-constance==2.9.1 django-picklefield==3.1 drf-spectacular==0.25.1 +django-cors-headers==3.14.0 \ No newline at end of file diff --git a/sponsor/models.py b/sponsor/models.py index f1ecc80..8ee52b3 100644 --- a/sponsor/models.py +++ b/sponsor/models.py @@ -17,7 +17,7 @@ class SponsorLevel(models.Model): ) visible = models.BooleanField(default=True) price = models.IntegerField(default=0) - limit = models.IntegerField(default=0, help_text="후원사 등급 별 구좌수") + limit = models.IntegerField(default=0, help_text="후원사 등급별 구좌 수") order = models.IntegerField(default=1) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -64,8 +64,8 @@ class Meta: creator = models.ForeignKey( User, - null=True, # TODO: 추루 로그인 적용 후 입력 - blank=True, # TODO: 추루 로그인 적용 후 입력 + null=True, # TODO: 추후 로그인 적용 후 입력 + blank=True, # TODO: 추후 로그인 적용 후 입력 on_delete=models.CASCADE, help_text="후원사를 등록한 유저", related_name="sponsor_creator", @@ -110,7 +110,7 @@ class Meta: max_length=100, null=True, blank=True, - help_text="후원사 사업자 등록번호입니다. 세금 계산서 발급에 사용됩니다.", + help_text="후원사 사업자 등록 번호입니다. 세금 계산서 발급에 사용됩니다.", ) business_registration_file = models.FileField( null=True, @@ -123,13 +123,13 @@ class Meta: null=True, blank=True, upload_to=bank_book_file_upload_to, - help_text="후원사 사업자 등록증 스캔본입니다. 세금 계산서 발급에 사용됩니다.", + help_text="후원사 통장 사본입니다.", ) url = models.CharField( max_length=255, null=True, blank=True, - help_text="파이콘 홈페이지에 공개되는 후원사 홈페이지 주소입니다.", + help_text="파이콘 한국 홈페이지에 공개되는 후원사 홈페이지 주소입니다.", ) logo_image = SorlImageField( upload_to=logo_image_upload_to, @@ -150,5 +150,5 @@ class Meta: created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - def __str_(self): + def __str__(self): return f"{self.name}/{self.level}" diff --git a/sponsor/routers.py b/sponsor/routers.py index 00d6291..b06b8a3 100644 --- a/sponsor/routers.py +++ b/sponsor/routers.py @@ -5,8 +5,8 @@ def get_router(): router = DefaultRouter() - router.register("remaining", SponsorRemainingAccountViewSet, basename="remaining") - router.register("prospectus", SponsorLevelViewSet, basename="prospectus") + router.register("/remaining", SponsorRemainingAccountViewSet, basename="remaining") + router.register("/prospectus", SponsorLevelViewSet, basename="prospectus") router.register("", SponsorViewSet, basename="sponsor") return router diff --git a/sponsor/serializers.py b/sponsor/serializers.py index 0fe4371..14231ac 100644 --- a/sponsor/serializers.py +++ b/sponsor/serializers.py @@ -1,10 +1,10 @@ +import rest_framework.serializers as serializers from rest_framework.fields import SerializerMethodField -from rest_framework.serializers import ModelSerializer from sponsor.models import Sponsor, SponsorLevel -class SponsorSerializer(ModelSerializer): +class SponsorSerializer(serializers.ModelSerializer): class Meta: model = Sponsor fields = [ @@ -23,7 +23,7 @@ class Meta: ] -class SponsorListSerializer(ModelSerializer): +class SponsorListSerializer(serializers.ModelSerializer): class Meta: model = Sponsor fields = [ @@ -35,7 +35,7 @@ class Meta: ] -class SponsorLevelSerializer(ModelSerializer): +class SponsorLevelSerializer(serializers.ModelSerializer): class Meta: model = SponsorLevel fields = [ @@ -47,8 +47,9 @@ class Meta: ] # TODO: Add fields to show -class SponsorRemainingAccountSerializer(ModelSerializer): +class SponsorRemainingAccountSerializer(serializers.ModelSerializer): remaining = SerializerMethodField() + available = SerializerMethodField() class Meta: model = SponsorLevel @@ -58,8 +59,13 @@ class Meta: "limit", "remaining", "id", + "available", ] @staticmethod def get_remaining(obj): return obj.current_remaining_number + + @staticmethod + def get_available(obj: SponsorLevel): + return True if obj.current_remaining_number > 0 else False diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 5cbceaf..97504ea 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -18,8 +18,8 @@ class SponsorViewSet(ModelViewSet): serializer_class = SponsorSerializer - permission_classes = [IsOwnerOrReadOnly] # 본인 소유만 수정가능 - http_method_names = ["get", "post"] # 지금은 조회/등록만 가능 TODO: 추후 수정기능 추가 + permission_classes = [IsOwnerOrReadOnly] # 본인 소유만 수정 가능 + http_method_names = ["get", "post"] # 지금은 조회/등록만 가능 TODO: 추후 수정 기능 추가 validator = SponsorValidater() def get_queryset(self): @@ -75,7 +75,7 @@ def list(self, request, *args, **kwargs): class SponsorRemainingAccountViewSet(ModelViewSet): - serializer_class = SponsorLevelSerializer + serializer_class = SponsorRemainingAccountSerializer http_method_names = ["get"] def get_queryset(self): @@ -83,6 +83,6 @@ def get_queryset(self): def list(self, request, *args, **kwargs): queryset = SponsorLevel.objects.all().order_by("-price") - serializer = SponsorRemainingAccountSerializer(queryset, many=True) + serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) diff --git a/status/views.py b/status/views.py index 7e8daad..cc29ed3 100644 --- a/status/views.py +++ b/status/views.py @@ -1,5 +1,6 @@ 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 @@ -9,7 +10,7 @@ class StatusView(APIView): def get(self, request, name: str): - status = Status.objects.get(name=name) + status = get_object_or_404(Status, name=name) now = datetime.datetime.now(tz=timezone("Asia/Seoul")) flag = None