diff --git a/.gitignore b/.gitignore index 8faa4d2..f39d21a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.idea /db.sqlite3 +/sponsor/migrations diff --git a/pyconkr/settings-dev.py b/pyconkr/settings-dev.py index 343d2ed..6b4cc89 100644 --- a/pyconkr/settings-dev.py +++ b/pyconkr/settings-dev.py @@ -16,6 +16,7 @@ } # django-storages: S3 +del MEDIA_ROOT DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage" AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID") diff --git a/pyconkr/settings-prod.py b/pyconkr/settings-prod.py index 1e107f6..0c752e5 100644 --- a/pyconkr/settings-prod.py +++ b/pyconkr/settings-prod.py @@ -16,6 +16,7 @@ } # django-storages: S3 +del MEDIA_ROOT DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage" AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID") diff --git a/pyconkr/settings.py b/pyconkr/settings.py index 6b0b6b1..5b72b94 100644 --- a/pyconkr/settings.py +++ b/pyconkr/settings.py @@ -37,8 +37,13 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - # djangorestframework + + # add-on "rest_framework", + "django_summernote" + + # apps + "sponsor", ] MIDDLEWARE = [ @@ -123,3 +128,7 @@ # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# django-summernote +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') diff --git a/pyconkr/urls.py b/pyconkr/urls.py index ecbd711..8d2711b 100644 --- a/pyconkr/urls.py +++ b/pyconkr/urls.py @@ -18,5 +18,7 @@ urlpatterns = [ path("api-auth/", include("rest_framework.urls")), + path('summernote/', include("django_summernote.urls")), path("admin/", admin.site.urls), + path("sponsors/", include("sponsor.urls")), ] diff --git a/requirements.txt b/requirements.txt index e5c04b1..71dd51c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ Markdown==3.4.1 mysql-connector-python==8.0.32 mysqlclient==2.1.1 sqlparse==0.4.3 -tzdata==2022.7 \ No newline at end of file +tzdata==2022.7 +sorl-thumbnail==12.9.0 \ No newline at end of file diff --git a/sponsor/__init__.py b/sponsor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sponsor/admin.py b/sponsor/admin.py new file mode 100644 index 0000000..52de445 --- /dev/null +++ b/sponsor/admin.py @@ -0,0 +1,25 @@ +from django.contrib import admin +from sponsor.models import Sponsor, SponsorLevel + + +class SponsorAdmin(SummernoteModelAdmin): + formfield_overrides = {models.TextField: { + 'widget': SummernoteWidgetWithCustomToolbar}} + autocomplete_fields = ('creator', 'manager_id',) + list_display = ('creator', 'name', 'level', 'manager_name', 'manager_email', 'manager_id', + 'submitted', 'accepted', 'paid_at',) + list_filter = ('accepted',) + ordering = ('-created_at',) + + +admin.site.register(Sponsor, SponsorAdmin) + + +class SponsorLevelAdmin(SummernoteModelAdmin): + list_display = ('id', 'order', 'name', 'slug', 'price', 'limit',) + list_editable = ('order', 'slug',) + ordering = ('order',) + search_fields = ('name',) + + +admin.site.register(SponsorLevel, SponsorLevelAdmin) diff --git a/sponsor/apps.py b/sponsor/apps.py new file mode 100644 index 0000000..674f284 --- /dev/null +++ b/sponsor/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SponsorConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "sponsor" diff --git a/sponsor/models.py b/sponsor/models.py new file mode 100644 index 0000000..65d9ffb --- /dev/null +++ b/sponsor/models.py @@ -0,0 +1,138 @@ +from django.db import models +from django.contrib.auth import get_user_model +from sorl.thumbnail import ImageField as SorlImageField + +User = get_user_model() + + +class SponsorLevelManager(models.Manager): + def get_queryset(self): + return super(SponsorLevelManager, self).get_queryset().all().order_by("order") + + +class SponsorLevel(models.Model): + name = models.CharField(max_length=255, blank=True, default="", help_text="후원 등급명") + desc = models.TextField( + null=True, blank=True, help_text="후원 혜택을 입력하면 될 거 같아요 :) 후원사가 등급을 정할 때 볼 문구입니다." + ) + visible = models.BooleanField(default=True) + price = models.IntegerField(default=0) + 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) + + objects = SponsorLevelManager() + + @property + def current_remaining_number(self): + return ( + 0 + if self.limit - self.accepted_count < 0 + else self.limit - self.accepted_count + ) + + @property + def paid_count(self): + return Sponsor.objects.filter( + level=self, submitted=True, accepted=True, paid_at__isnull=False + ).count() + + @property + def accepted_count(self): + return Sponsor.objects.filter(level=self, submitted=True, accepted=True).count() + + def __str__(self): + return self.name + + +def registration_file_upload_to(instance, filename): + return f"sponsor/business_registration/{instance.id}/{filename}" + + +def logo_image_upload_to(instance, filename): + return f"sponsor/logo/{instance.id}/{filename}" + + +class Sponsor(models.Model): + class Meta: + ordering = ["paid_at", "id"] + + creator = models.ForeignKey( + User, + null=True, # TODO: 추루 로그인 적용 후 입력 + blank=True, # TODO: 추루 로그인 적용 후 입력 + on_delete=models.CASCADE, + help_text="후원사를 등록한 유저", + related_name="sponsor_creator", + ) + name = models.CharField( + max_length=255, help_text="후원사의 이름입니다. 서비스나 회사 이름이 될 수 있습니다." + ) + level = models.ForeignKey( + SponsorLevel, + null=True, + on_delete=models.SET_NULL, + blank=True, + help_text="후원을 원하시는 등급을 선택해주십시오. 모두 판매된 등급은 선택할 수 없습니다.", + ) + desc = models.TextField( + null=True, blank=True, help_text="후원사 설명입니다. 이 설명은 국문 홈페이지에 게시됩니다." + ) + eng_desc = models.TextField( + null=True, blank=True, help_text="후원사 영문 설명입니다. 이 설명은 영문 홈페이지에 게시됩니다." + ) + manager_name = models.CharField( + max_length=100, help_text="준비위원회와 후원과 관련된 논의를 진행할 담당자의 이름을 입력해주십시오." + ) + manager_email = models.CharField( + max_length=100, + help_text="입력하신 메일로 후원과 관련된 안내 메일이나 문의를 보낼 예정입니다. 후원 담당자의 이메일 주소를 입력해주십시오.", + ) + manager_id = models.ForeignKey( + User, + null=True, + blank=True, + on_delete=models.CASCADE, + help_text="후원사를 위한 추가 아이디", + related_name="sponsor_temp_id", + ) + business_registration_number = models.CharField( + max_length=100, + null=True, + blank=True, + help_text="후원사 사업자 등록번호입니다. 세금 계산서 발급에 사용됩니다.", + ) + business_registration_file = models.FileField( + null=True, + blank=True, + upload_to=registration_file_upload_to, + help_text="후원사 사업자 등록증 스캔본입니다. 세금 계산서 발급에 사용됩니다.", + ) + url = models.CharField( + max_length=255, + null=True, + blank=True, + help_text="파이콘 홈페이지에 공개되는 후원사 홈페이지 주소입니다.", + ) + logo_image = SorlImageField( + upload_to=logo_image_upload_to, + null=True, + blank=True, + help_text="홈페이지에 공개되는 후원사 로고 이미지입니다.", + ) + submitted = models.BooleanField( + default=False, + help_text="사용자가 제출했는지 여부를 저장합니다. 요청이 제출되면 준비위원회에서 검토하고 받아들일지를 결정합니다.", + ) + accepted = models.BooleanField( + default=False, help_text="후원사 신청이 접수되었고, 입금 대기 상태인 경우 True로 설정됩니다." + ) + paid_at = models.DateTimeField( + null=True, blank=True, help_text="후원금이 입금된 일시입니다. 아직 입금되지 않았을 경우 None이 들어갑니다." + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str_(self): + return f"{self.name}/{self.level}" diff --git a/sponsor/tests.py b/sponsor/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/sponsor/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/sponsor/urls.py b/sponsor/urls.py new file mode 100644 index 0000000..dcfc3c4 --- /dev/null +++ b/sponsor/urls.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + # path("", ), # TODO +] \ No newline at end of file diff --git a/sponsor/views.py b/sponsor/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/sponsor/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.