From f7298053c52625b2a754838620082c2533c20510 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Thu, 5 Sep 2024 17:29:18 +0900 Subject: [PATCH 01/28] =?UTF-8?q?signals.py=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/signals.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 customk/questions/signals.py diff --git a/customk/questions/signals.py b/customk/questions/signals.py new file mode 100644 index 0000000..d91a571 --- /dev/null +++ b/customk/questions/signals.py @@ -0,0 +1,9 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.core.cache import cache +from .models import Question + +@receiver(post_save, sender=Question) +def question_post_save(sender, instance, created, **kwargs): + if created: + cache.set('new_question_created', True, 300) # 5분 동안 캐시에 저장 \ No newline at end of file From 6e3f2a66bf8a13fc010445a1b5f7a4f1e65d1652 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Thu, 5 Sep 2024 17:55:30 +0900 Subject: [PATCH 02/28] =?UTF-8?q?TODO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/signals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/customk/questions/signals.py b/customk/questions/signals.py index d91a571..331e059 100644 --- a/customk/questions/signals.py +++ b/customk/questions/signals.py @@ -4,6 +4,7 @@ from .models import Question @receiver(post_save, sender=Question) +# TODO 관리자 페이지 알림 def question_post_save(sender, instance, created, **kwargs): if created: cache.set('new_question_created', True, 300) # 5분 동안 캐시에 저장 \ No newline at end of file From 747835dabef4c5ec028161ce8d650eafe3ac6328 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Thu, 5 Sep 2024 18:33:21 +0900 Subject: [PATCH 03/28] =?UTF-8?q?=EC=9D=98=EB=AF=B8=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/signals.py | 6 ++++-- customk/reviews/views.py | 4 ---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/customk/questions/signals.py b/customk/questions/signals.py index 331e059..30e51ae 100644 --- a/customk/questions/signals.py +++ b/customk/questions/signals.py @@ -1,10 +1,12 @@ +from django.core.cache import cache from django.db.models.signals import post_save from django.dispatch import receiver -from django.core.cache import cache + from .models import Question + @receiver(post_save, sender=Question) # TODO 관리자 페이지 알림 def question_post_save(sender, instance, created, **kwargs): if created: - cache.set('new_question_created', True, 300) # 5분 동안 캐시에 저장 \ No newline at end of file + cache.set("new_question_created", True, 300) # 5분 동안 캐시에 저장 diff --git a/customk/reviews/views.py b/customk/reviews/views.py index 8ac8de4..b1017a9 100644 --- a/customk/reviews/views.py +++ b/customk/reviews/views.py @@ -24,10 +24,6 @@ class AllReviewsListView(APIView): - def get_permissions(self): - if self.request.method == "GET": - return [AllowAny()] - @extend_schema( methods=["GET"], summary="전체 리뷰 목록 조회", From d85664cce02acf4b1595da9aec7f1cfee5b0eb68 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 00:33:44 +0900 Subject: [PATCH 04/28] =?UTF-8?q?=EC=9D=98=EB=AF=B8=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/signals.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 customk/questions/signals.py diff --git a/customk/questions/signals.py b/customk/questions/signals.py deleted file mode 100644 index 30e51ae..0000000 --- a/customk/questions/signals.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.core.cache import cache -from django.db.models.signals import post_save -from django.dispatch import receiver - -from .models import Question - - -@receiver(post_save, sender=Question) -# TODO 관리자 페이지 알림 -def question_post_save(sender, instance, created, **kwargs): - if created: - cache.set("new_question_created", True, 300) # 5분 동안 캐시에 저장 From e0a0c919653da38e538949a8850e1003de96adc5 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 16:51:36 +0900 Subject: [PATCH 05/28] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=95=8C=EB=A6=BC=20=EC=B4=88=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/config/settings.py | 6 +++- customk/notifications/__init__.py | 0 customk/notifications/admin.py | 15 +++++++++ customk/notifications/apps.py | 6 ++++ .../notifications/migrations/0001_initial.py | 31 +++++++++++++++++++ .../migrations/0002_auto_20240906_1518.py | 13 ++++++++ .../migrations/0003_auto_20240906_1550.py | 13 ++++++++ customk/notifications/migrations/__init__.py | 0 customk/notifications/models.py | 11 +++++++ customk/notifications/tests.py | 3 ++ customk/notifications/urls.py | 6 ++++ customk/notifications/views.py | 6 ++++ customk/questions/admin.py | 3 ++ customk/questions/apps.py | 3 ++ customk/questions/signals.py | 12 +++++++ .../questions/static/question/css/styles.css | 27 ++++++++++++++++ .../questions/static/question/js/scripts.js | 15 +++++++++ .../questions/templates/admin/base_site.html | 25 +++++++++++++++ customk/questions/urls.py | 4 +++ 19 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 customk/notifications/__init__.py create mode 100644 customk/notifications/admin.py create mode 100644 customk/notifications/apps.py create mode 100644 customk/notifications/migrations/0001_initial.py create mode 100644 customk/notifications/migrations/0002_auto_20240906_1518.py create mode 100644 customk/notifications/migrations/0003_auto_20240906_1550.py create mode 100644 customk/notifications/migrations/__init__.py create mode 100644 customk/notifications/models.py create mode 100644 customk/notifications/tests.py create mode 100644 customk/notifications/urls.py create mode 100644 customk/notifications/views.py create mode 100644 customk/questions/signals.py create mode 100644 customk/questions/static/question/css/styles.css create mode 100644 customk/questions/static/question/js/scripts.js create mode 100644 customk/questions/templates/admin/base_site.html diff --git a/customk/config/settings.py b/customk/config/settings.py index c67b686..a606a99 100644 --- a/customk/config/settings.py +++ b/customk/config/settings.py @@ -34,6 +34,7 @@ "reactions", "corsheaders", "favorites", + "notifications", ] @@ -78,7 +79,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [BASE_DIR / "questions/templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -190,6 +191,9 @@ STATIC_URL = "/static/" STATIC_ROOT = "/vol/web/static" +STATICFILES_DIRS = [ + BASE_DIR / "questions/static", +] DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/customk/notifications/__init__.py b/customk/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customk/notifications/admin.py b/customk/notifications/admin.py new file mode 100644 index 0000000..28261b8 --- /dev/null +++ b/customk/notifications/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from .models import Notification + +@admin.register(Notification) +class NotificationAdmin(admin.ModelAdmin): + list_display = ('message', 'class_field', 'created_at', 'is_read') + list_filter = ('is_read', 'question__class_id') + search_fields = ('message', 'question__class_id__title') + + + def class_field(self, obj): + return obj.question.class_id.title # Access class_id title through the question + + class_field.admin_order_field = 'question__class_id__title' + class_field.short_description = 'Class Title' \ No newline at end of file diff --git a/customk/notifications/apps.py b/customk/notifications/apps.py new file mode 100644 index 0000000..001b4f9 --- /dev/null +++ b/customk/notifications/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'notifications' diff --git a/customk/notifications/migrations/0001_initial.py b/customk/notifications/migrations/0001_initial.py new file mode 100644 index 0000000..69a0579 --- /dev/null +++ b/customk/notifications/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1 on 2024-09-06 05:59 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('questions', '0006_remove_question_answer_user_id_alter_question_answer'), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('message', models.CharField(max_length=255)), + ('is_read', models.BooleanField(default=False)), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.question')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/customk/notifications/migrations/0002_auto_20240906_1518.py b/customk/notifications/migrations/0002_auto_20240906_1518.py new file mode 100644 index 0000000..31845b0 --- /dev/null +++ b/customk/notifications/migrations/0002_auto_20240906_1518.py @@ -0,0 +1,13 @@ +# Generated by Django 5.1 on 2024-09-06 06:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0001_initial'), + ] + + operations = [ + ] diff --git a/customk/notifications/migrations/0003_auto_20240906_1550.py b/customk/notifications/migrations/0003_auto_20240906_1550.py new file mode 100644 index 0000000..12b37e7 --- /dev/null +++ b/customk/notifications/migrations/0003_auto_20240906_1550.py @@ -0,0 +1,13 @@ +# Generated by Django 5.1 on 2024-09-06 06:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0002_auto_20240906_1518'), + ] + + operations = [ + ] diff --git a/customk/notifications/migrations/__init__.py b/customk/notifications/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customk/notifications/models.py b/customk/notifications/models.py new file mode 100644 index 0000000..3cbf4f9 --- /dev/null +++ b/customk/notifications/models.py @@ -0,0 +1,11 @@ +from django.db import models +from common.models import CommonModel +from questions.models import Question + +class Notification(CommonModel): + message = models.CharField(max_length=255) + question = models.ForeignKey(Question, on_delete=models.CASCADE) + is_read = models.BooleanField(default=False) + + def __str__(self): + return self.message \ No newline at end of file diff --git a/customk/notifications/tests.py b/customk/notifications/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/customk/notifications/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/customk/notifications/urls.py b/customk/notifications/urls.py new file mode 100644 index 0000000..4dbc168 --- /dev/null +++ b/customk/notifications/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import unread_notifications_count + +urlpatterns = [ + path('admin/api/unread_notifications_count/', unread_notifications_count, name='unread-notifications-count'), +] \ No newline at end of file diff --git a/customk/notifications/views.py b/customk/notifications/views.py new file mode 100644 index 0000000..233b00e --- /dev/null +++ b/customk/notifications/views.py @@ -0,0 +1,6 @@ +from django.http import JsonResponse +from .models import Notification + +def unread_notifications_count(request): + count = Notification.objects.filter(is_read=False).count() + return JsonResponse({'count': count}) \ No newline at end of file diff --git a/customk/questions/admin.py b/customk/questions/admin.py index 204059f..a2bafd6 100644 --- a/customk/questions/admin.py +++ b/customk/questions/admin.py @@ -43,6 +43,9 @@ def changelist_view( ) -> HttpResponse: unanswered_questions_count = Question.objects.filter(answer="").count() + extra_context = extra_context or {} + extra_context["new_question_count"] = unanswered_questions_count + if unanswered_questions_count > 0: messages.warning( request, diff --git a/customk/questions/apps.py b/customk/questions/apps.py index 363a7c8..99ac5bb 100644 --- a/customk/questions/apps.py +++ b/customk/questions/apps.py @@ -4,3 +4,6 @@ class QuestionsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "questions" + + def ready(self): + import questions.signals diff --git a/customk/questions/signals.py b/customk/questions/signals.py new file mode 100644 index 0000000..0ab2cfc --- /dev/null +++ b/customk/questions/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Question +from notifications.models import Notification + +@receiver(post_save, sender=Question) +def create_notification(sender, instance, created, **kwargs): + if created: + Notification.objects.create( + message=f"새로운 질문이 생성되었습니다: {instance.question_title}", + question=instance + ) \ No newline at end of file diff --git a/customk/questions/static/question/css/styles.css b/customk/questions/static/question/css/styles.css new file mode 100644 index 0000000..1a649e5 --- /dev/null +++ b/customk/questions/static/question/css/styles.css @@ -0,0 +1,27 @@ +#notification-bell { + position: relative; + display: inline-block; + margin-left: 10px; +} + +.bell-icon { + font-size: 24px; + color: gray; + cursor: pointer; +} + +.bell-icon:hover { + color: blue; +} + +.badge { + position: absolute; + top: -10px; + right: -10px; + background-color: red; + color: white; + border-radius: 50%; + padding: 2px 8px; + font-size: 12px; + vertical-align: top; +} diff --git a/customk/questions/static/question/js/scripts.js b/customk/questions/static/question/js/scripts.js new file mode 100644 index 0000000..1611223 --- /dev/null +++ b/customk/questions/static/question/js/scripts.js @@ -0,0 +1,15 @@ +document.addEventListener('DOMContentLoaded', function() { + const notificationIcon = document.getElementById('notification-icon'); + const notificationBadge = document.getElementById('notification-badge'); + + fetch('/v1/notifications/unread_count/') + .then(response => response.json()) + .then(data => { + if (data.count > 0) { + notificationBadge.textContent = data.count; + notificationBadge.style.display = 'inline'; + notificationIcon.style.color = 'blue'; + } + }) + .catch(error => console.error('Error fetching notification count:', error)); +}); diff --git a/customk/questions/templates/admin/base_site.html b/customk/questions/templates/admin/base_site.html new file mode 100644 index 0000000..7fe127a --- /dev/null +++ b/customk/questions/templates/admin/base_site.html @@ -0,0 +1,25 @@ +{% extends "admin/base_site.html" %} + +{% load static %} + +{% block extrahead %} +{{ block.super }} + + +{% endblock %} + +{% block branding %} + +{% endblock %} diff --git a/customk/questions/urls.py b/customk/questions/urls.py index dc9d52d..6dc2c1f 100644 --- a/customk/questions/urls.py +++ b/customk/questions/urls.py @@ -2,9 +2,13 @@ from questions.views import AllQuestionsListView, QuestionListView +from django.urls import path +from .views import unanswered_questions_count + urlpatterns = [ re_path(r"^$", AllQuestionsListView.as_view(), name="all-questions"), re_path( r"^(?P\d+)/?$", QuestionListView.as_view(), name="class-question-list" ), + path('admin/api/unanswered_questions_count/', unanswered_questions_count, name='unanswered_questions_count'), ] From e4329d1c8d731bb7904489d5ded5711ce1edd38d Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 17:07:07 +0900 Subject: [PATCH 06/28] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/notifications/urls.py | 2 +- .../questions/static/question/css/styles.css | 28 +++++++++---------- .../questions/static/question/js/scripts.js | 7 +++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/customk/notifications/urls.py b/customk/notifications/urls.py index 4dbc168..588fd50 100644 --- a/customk/notifications/urls.py +++ b/customk/notifications/urls.py @@ -2,5 +2,5 @@ from .views import unread_notifications_count urlpatterns = [ - path('admin/api/unread_notifications_count/', unread_notifications_count, name='unread-notifications-count'), + path('unread_count/', unread_notifications_count, name='unread-notifications-count'), ] \ No newline at end of file diff --git a/customk/questions/static/question/css/styles.css b/customk/questions/static/question/css/styles.css index 1a649e5..cc43a4c 100644 --- a/customk/questions/static/question/css/styles.css +++ b/customk/questions/static/question/css/styles.css @@ -1,27 +1,25 @@ #notification-bell { position: relative; display: inline-block; - margin-left: 10px; } .bell-icon { font-size: 24px; - color: gray; - cursor: pointer; -} - -.bell-icon:hover { - color: blue; + color: #000; } .badge { position: absolute; - top: -10px; - right: -10px; - background-color: red; + top: -5px; /* 조정 필요시 변경 */ + right: -5px; /* 조정 필요시 변경 */ + background: red; /* 빨간 배경색 */ color: white; - border-radius: 50%; - padding: 2px 8px; - font-size: 12px; - vertical-align: top; -} + border-radius: 50%; /* 완전히 둥글게 */ + width: 18px; /* 배지의 너비 */ + height: 18px; /* 배지의 높이 */ + display: flex; + align-items: center; /* 텍스트 수직 중앙 정렬 */ + justify-content: center; /* 텍스트 수평 중앙 정렬 */ + font-size: 10px; /* 글자 크기 조절 */ + font-weight: bold; +} \ No newline at end of file diff --git a/customk/questions/static/question/js/scripts.js b/customk/questions/static/question/js/scripts.js index 1611223..1384e56 100644 --- a/customk/questions/static/question/js/scripts.js +++ b/customk/questions/static/question/js/scripts.js @@ -6,10 +6,13 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { if (data.count > 0) { - notificationBadge.textContent = data.count; + notificationBadge.textContent = ''; notificationBadge.style.display = 'inline'; notificationIcon.style.color = 'blue'; + } else { + notificationBadge.style.display = 'none'; + notificationIcon.style.color = '#000'; } }) .catch(error => console.error('Error fetching notification count:', error)); -}); +});ㅊ From 2be1d183a6344ca9949318f3b73aaa2ae5966017 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 17:08:54 +0900 Subject: [PATCH 07/28] ruff --- customk/notifications/admin.py | 13 ++++---- customk/notifications/apps.py | 4 +-- .../notifications/migrations/0001_initial.py | 33 +++++++++++++------ .../migrations/0002_auto_20240906_1518.py | 6 ++-- .../migrations/0003_auto_20240906_1550.py | 6 ++-- customk/notifications/models.py | 4 ++- customk/notifications/urls.py | 7 ++-- customk/notifications/views.py | 4 ++- customk/questions/signals.py | 9 +++-- customk/questions/urls.py | 6 +--- 10 files changed, 54 insertions(+), 38 deletions(-) diff --git a/customk/notifications/admin.py b/customk/notifications/admin.py index 28261b8..3b01bc9 100644 --- a/customk/notifications/admin.py +++ b/customk/notifications/admin.py @@ -1,15 +1,16 @@ from django.contrib import admin + from .models import Notification + @admin.register(Notification) class NotificationAdmin(admin.ModelAdmin): - list_display = ('message', 'class_field', 'created_at', 'is_read') - list_filter = ('is_read', 'question__class_id') - search_fields = ('message', 'question__class_id__title') - + list_display = ("message", "class_field", "created_at", "is_read") + list_filter = ("is_read", "question__class_id") + search_fields = ("message", "question__class_id__title") def class_field(self, obj): return obj.question.class_id.title # Access class_id title through the question - class_field.admin_order_field = 'question__class_id__title' - class_field.short_description = 'Class Title' \ No newline at end of file + class_field.admin_order_field = "question__class_id__title" # type: ignore + class_field.short_description = "Class Title" # type: ignore diff --git a/customk/notifications/apps.py b/customk/notifications/apps.py index 001b4f9..3a08476 100644 --- a/customk/notifications/apps.py +++ b/customk/notifications/apps.py @@ -2,5 +2,5 @@ class NotificationsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'notifications' + default_auto_field = "django.db.models.BigAutoField" + name = "notifications" diff --git a/customk/notifications/migrations/0001_initial.py b/customk/notifications/migrations/0001_initial.py index 69a0579..b99b104 100644 --- a/customk/notifications/migrations/0001_initial.py +++ b/customk/notifications/migrations/0001_initial.py @@ -6,26 +6,39 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('questions', '0006_remove_question_answer_user_id_alter_question_answer'), + ("questions", "0006_remove_question_answer_user_id_alter_question_answer"), ] operations = [ migrations.CreateModel( - name='Notification', + name="Notification", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(default=django.utils.timezone.now)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('message', models.CharField(max_length=255)), - ('is_read', models.BooleanField(default=False)), - ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.question')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(default=django.utils.timezone.now)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("message", models.CharField(max_length=255)), + ("is_read", models.BooleanField(default=False)), + ( + "question", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="questions.question", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/customk/notifications/migrations/0002_auto_20240906_1518.py b/customk/notifications/migrations/0002_auto_20240906_1518.py index 31845b0..4f875fd 100644 --- a/customk/notifications/migrations/0002_auto_20240906_1518.py +++ b/customk/notifications/migrations/0002_auto_20240906_1518.py @@ -4,10 +4,8 @@ class Migration(migrations.Migration): - dependencies = [ - ('notifications', '0001_initial'), + ("notifications", "0001_initial"), ] - operations = [ - ] + operations = [] diff --git a/customk/notifications/migrations/0003_auto_20240906_1550.py b/customk/notifications/migrations/0003_auto_20240906_1550.py index 12b37e7..10b3eb2 100644 --- a/customk/notifications/migrations/0003_auto_20240906_1550.py +++ b/customk/notifications/migrations/0003_auto_20240906_1550.py @@ -4,10 +4,8 @@ class Migration(migrations.Migration): - dependencies = [ - ('notifications', '0002_auto_20240906_1518'), + ("notifications", "0002_auto_20240906_1518"), ] - operations = [ - ] + operations = [] diff --git a/customk/notifications/models.py b/customk/notifications/models.py index 3cbf4f9..67735fd 100644 --- a/customk/notifications/models.py +++ b/customk/notifications/models.py @@ -1,11 +1,13 @@ from django.db import models + from common.models import CommonModel from questions.models import Question + class Notification(CommonModel): message = models.CharField(max_length=255) question = models.ForeignKey(Question, on_delete=models.CASCADE) is_read = models.BooleanField(default=False) def __str__(self): - return self.message \ No newline at end of file + return self.message diff --git a/customk/notifications/urls.py b/customk/notifications/urls.py index 588fd50..06ffaa4 100644 --- a/customk/notifications/urls.py +++ b/customk/notifications/urls.py @@ -1,6 +1,9 @@ from django.urls import path + from .views import unread_notifications_count urlpatterns = [ - path('unread_count/', unread_notifications_count, name='unread-notifications-count'), -] \ No newline at end of file + path( + "unread_count/", unread_notifications_count, name="unread-notifications-count" + ), +] diff --git a/customk/notifications/views.py b/customk/notifications/views.py index 233b00e..f2d4e5a 100644 --- a/customk/notifications/views.py +++ b/customk/notifications/views.py @@ -1,6 +1,8 @@ from django.http import JsonResponse + from .models import Notification + def unread_notifications_count(request): count = Notification.objects.filter(is_read=False).count() - return JsonResponse({'count': count}) \ No newline at end of file + return JsonResponse({"count": count}) diff --git a/customk/questions/signals.py b/customk/questions/signals.py index 0ab2cfc..5d52bb3 100644 --- a/customk/questions/signals.py +++ b/customk/questions/signals.py @@ -1,12 +1,15 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from .models import Question + from notifications.models import Notification +from .models import Question + + @receiver(post_save, sender=Question) def create_notification(sender, instance, created, **kwargs): if created: Notification.objects.create( message=f"새로운 질문이 생성되었습니다: {instance.question_title}", - question=instance - ) \ No newline at end of file + question=instance, + ) diff --git a/customk/questions/urls.py b/customk/questions/urls.py index 6dc2c1f..d26c354 100644 --- a/customk/questions/urls.py +++ b/customk/questions/urls.py @@ -1,14 +1,10 @@ -from django.urls import re_path +from django.urls import path, re_path from questions.views import AllQuestionsListView, QuestionListView -from django.urls import path -from .views import unanswered_questions_count - urlpatterns = [ re_path(r"^$", AllQuestionsListView.as_view(), name="all-questions"), re_path( r"^(?P\d+)/?$", QuestionListView.as_view(), name="class-question-list" ), - path('admin/api/unanswered_questions_count/', unanswered_questions_count, name='unanswered_questions_count'), ] From f0e0b4b5bbe6d4498a74763dcb1de0e054f5f88b Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 17:13:42 +0900 Subject: [PATCH 08/28] ruff --- customk/questions/static/question/js/scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customk/questions/static/question/js/scripts.js b/customk/questions/static/question/js/scripts.js index 1384e56..e6815b9 100644 --- a/customk/questions/static/question/js/scripts.js +++ b/customk/questions/static/question/js/scripts.js @@ -15,4 +15,4 @@ document.addEventListener('DOMContentLoaded', function() { } }) .catch(error => console.error('Error fetching notification count:', error)); -});ㅊ +}); From de5ecee4fc8a5b1a8c5f7d83dbd290c647bad9ec Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 17:17:03 +0900 Subject: [PATCH 09/28] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20url=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/config/urls.py | 1 + customk/notifications/admin.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/customk/config/urls.py b/customk/config/urls.py index 5a61b7e..1828be0 100644 --- a/customk/config/urls.py +++ b/customk/config/urls.py @@ -29,4 +29,5 @@ re_path(r"^v1/reviews/?", include("reviews.urls")), re_path(r"^v1/favorites/?", include("favorites.urls")), re_path(r"^v1/reactions/?", include("reactions.urls")), + re_path(r"^v1/notifications/?", include("notifications.urls")), ] diff --git a/customk/notifications/admin.py b/customk/notifications/admin.py index 3b01bc9..8df58f9 100644 --- a/customk/notifications/admin.py +++ b/customk/notifications/admin.py @@ -12,5 +12,5 @@ class NotificationAdmin(admin.ModelAdmin): def class_field(self, obj): return obj.question.class_id.title # Access class_id title through the question - class_field.admin_order_field = "question__class_id__title" # type: ignore - class_field.short_description = "Class Title" # type: ignore + class_field.admin_order_field = "question__class_id__title" # type: ignore + class_field.short_description = "Class Title" # type: ignore From b26c04122f08f92813ca9ebd1d720e2cd103ba48 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 19:24:08 +0900 Subject: [PATCH 10/28] =?UTF-8?q?=EC=8B=9C=EB=A6=AC=EC=96=BC=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=A0=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/static/question/css/styles.css | 6 +++--- customk/reviews/serializers.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/customk/questions/static/question/css/styles.css b/customk/questions/static/question/css/styles.css index cc43a4c..964cc70 100644 --- a/customk/questions/static/question/css/styles.css +++ b/customk/questions/static/question/css/styles.css @@ -15,11 +15,11 @@ background: red; /* 빨간 배경색 */ color: white; border-radius: 50%; /* 완전히 둥글게 */ - width: 18px; /* 배지의 너비 */ - height: 18px; /* 배지의 높이 */ + width: 12px; !important;/* 배지의 너비 */ + height: 12px; !important;/* 배지의 높이 */ display: flex; align-items: center; /* 텍스트 수직 중앙 정렬 */ justify-content: center; /* 텍스트 수평 중앙 정렬 */ - font-size: 10px; /* 글자 크기 조절 */ + font-size: 8px; !important;/* 글자 크기 조절 */ font-weight: bold; } \ No newline at end of file diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index fa6829f..8de4fc6 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -68,7 +68,8 @@ class ReviewSerializer(serializers.ModelSerializer): class Meta: model = Review - fields = "__all__" + fields = ["id", "review", "rating", "user", "images"] + def create(self, validated_data): images_data64 = validated_data.pop("images", []) From 22decb1ef3e573c38f24f4f3887ca47337076ce8 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Fri, 6 Sep 2024 20:03:33 +0900 Subject: [PATCH 11/28] ruff --- customk/reviews/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index a24766c..d8c9eac 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -72,7 +72,6 @@ class Meta: model = Review fields = ["id", "review", "rating", "user", "images"] - def create(self, validated_data): images_data64 = validated_data.pop("images", []) class_id = validated_data.pop("class_id") From aab20f1cbecc50123f9925a6bd611c3ceddb6884 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Sun, 8 Sep 2024 22:15:04 +0900 Subject: [PATCH 12/28] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=EC=95=8C=EB=A6=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/notifications/admin.py | 21 +++++-- ...tification_paymentnotification_and_more.py | 62 +++++++++++++++++++ .../0005_alter_paymentnotification_payment.py | 23 +++++++ customk/notifications/models.py | 16 ++++- customk/notifications/urls.py | 11 +++- customk/notifications/views.py | 11 +++- customk/payments/apps.py | 3 + customk/payments/signals.py | 15 +++++ customk/questions/signals.py | 6 +- .../questions/static/question/css/styles.css | 35 +++++++---- .../questions/static/question/js/scripts.js | 26 ++++++-- .../questions/templates/admin/base_site.html | 11 +++- customk/reviews/serializers.py | 2 +- 13 files changed, 212 insertions(+), 30 deletions(-) create mode 100644 customk/notifications/migrations/0004_rename_notification_paymentnotification_and_more.py create mode 100644 customk/notifications/migrations/0005_alter_paymentnotification_payment.py create mode 100644 customk/payments/signals.py diff --git a/customk/notifications/admin.py b/customk/notifications/admin.py index 8df58f9..419e7db 100644 --- a/customk/notifications/admin.py +++ b/customk/notifications/admin.py @@ -1,16 +1,29 @@ from django.contrib import admin -from .models import Notification +from .models import PaymentNotification, QuestionNotification -@admin.register(Notification) -class NotificationAdmin(admin.ModelAdmin): +@admin.register(QuestionNotification) +class QuestionNotificationAdmin(admin.ModelAdmin): list_display = ("message", "class_field", "created_at", "is_read") list_filter = ("is_read", "question__class_id") search_fields = ("message", "question__class_id__title") def class_field(self, obj): - return obj.question.class_id.title # Access class_id title through the question + return obj.question.class_id.title class_field.admin_order_field = "question__class_id__title" # type: ignore class_field.short_description = "Class Title" # type: ignore + + +@admin.register(PaymentNotification) +class PaymentNotificationAdmin(admin.ModelAdmin): + list_display = ("message", "payment_method", "created_at", "is_read") + list_filter = ("is_read", "payment__payment_method") + search_fields = ("message", "payment__order_id") + + def payment_method(self, obj): + return obj.payment.payment_method + + payment_method.admin_order_field = "payment__payment_method" # type: ignore + payment_method.short_description = "Payment Method" # type: ignore diff --git a/customk/notifications/migrations/0004_rename_notification_paymentnotification_and_more.py b/customk/notifications/migrations/0004_rename_notification_paymentnotification_and_more.py new file mode 100644 index 0000000..85f8f87 --- /dev/null +++ b/customk/notifications/migrations/0004_rename_notification_paymentnotification_and_more.py @@ -0,0 +1,62 @@ +# Generated by Django 5.1 on 2024-09-08 12:36 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("notifications", "0003_auto_20240906_1550"), + ("payments", "0002_payment_refunded_amount"), + ("questions", "0006_remove_question_answer_user_id_alter_question_answer"), + ] + + operations = [ + migrations.RenameModel( + old_name="Notification", + new_name="PaymentNotification", + ), + migrations.RemoveField( + model_name="paymentnotification", + name="question", + ), + migrations.AddField( + model_name="paymentnotification", + name="payment", + field=models.ForeignKey( + default="", + on_delete=django.db.models.deletion.CASCADE, + to="payments.payment", + ), + preserve_default=False, + ), + migrations.CreateModel( + name="QuestionNotification", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(default=django.utils.timezone.now)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("message", models.CharField(max_length=255)), + ("is_read", models.BooleanField(default=False)), + ( + "question", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="questions.question", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/customk/notifications/migrations/0005_alter_paymentnotification_payment.py b/customk/notifications/migrations/0005_alter_paymentnotification_payment.py new file mode 100644 index 0000000..bc4eaf4 --- /dev/null +++ b/customk/notifications/migrations/0005_alter_paymentnotification_payment.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1 on 2024-09-08 12:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("notifications", "0004_rename_notification_paymentnotification_and_more"), + ("payments", "0002_payment_refunded_amount"), + ] + + operations = [ + migrations.AlterField( + model_name="paymentnotification", + name="payment", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="payments.payment", + ), + ), + ] diff --git a/customk/notifications/models.py b/customk/notifications/models.py index 67735fd..cfc1383 100644 --- a/customk/notifications/models.py +++ b/customk/notifications/models.py @@ -1,13 +1,27 @@ from django.db import models from common.models import CommonModel +from payments.models import Payment from questions.models import Question -class Notification(CommonModel): +class QuestionNotification(CommonModel): message = models.CharField(max_length=255) question = models.ForeignKey(Question, on_delete=models.CASCADE) is_read = models.BooleanField(default=False) def __str__(self): return self.message + + +class PaymentNotification(CommonModel): + message = models.CharField(max_length=255) + payment = models.ForeignKey( + Payment, + on_delete=models.CASCADE, + null=True, + ) + is_read = models.BooleanField(default=False) + + def __str__(self): + return self.message diff --git a/customk/notifications/urls.py b/customk/notifications/urls.py index 06ffaa4..449c1d5 100644 --- a/customk/notifications/urls.py +++ b/customk/notifications/urls.py @@ -1,9 +1,16 @@ from django.urls import path -from .views import unread_notifications_count +from . import views urlpatterns = [ path( - "unread_count/", unread_notifications_count, name="unread-notifications-count" + "unread_question_notifications_count/", + views.unread_question_notifications_count, + name="unread_question_notifications_count", + ), + path( + "unread_payment_notifications_count/", + views.unread_payment_notifications_count, + name="unread_payment_notifications_count", ), ] diff --git a/customk/notifications/views.py b/customk/notifications/views.py index f2d4e5a..af8662f 100644 --- a/customk/notifications/views.py +++ b/customk/notifications/views.py @@ -1,8 +1,13 @@ from django.http import JsonResponse -from .models import Notification +from .models import PaymentNotification, QuestionNotification -def unread_notifications_count(request): - count = Notification.objects.filter(is_read=False).count() +def unread_question_notifications_count(request): + count = QuestionNotification.objects.filter(is_read=False).count() + return JsonResponse({"count": count}) + + +def unread_payment_notifications_count(request): + count = PaymentNotification.objects.filter(is_read=False).count() return JsonResponse({"count": count}) diff --git a/customk/payments/apps.py b/customk/payments/apps.py index 61898af..2c965eb 100644 --- a/customk/payments/apps.py +++ b/customk/payments/apps.py @@ -4,3 +4,6 @@ class PaymentsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "payments" + + def ready(self): + import payments.signals diff --git a/customk/payments/signals.py b/customk/payments/signals.py new file mode 100644 index 0000000..7a0ed11 --- /dev/null +++ b/customk/payments/signals.py @@ -0,0 +1,15 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from notifications.models import PaymentNotification + +from .models import Payment + + +@receiver(post_save, sender=Payment) +def create_payment_notification(sender, instance, created, **kwargs): + if created: + PaymentNotification.objects.create( + message=f"새로운 결제가 생성되었습니다: 주문 ID {instance.order_id}, 금액 {instance.amount} {instance.currency}", + payment=instance, + ) diff --git a/customk/questions/signals.py b/customk/questions/signals.py index 5d52bb3..a8e4c96 100644 --- a/customk/questions/signals.py +++ b/customk/questions/signals.py @@ -1,15 +1,15 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from notifications.models import Notification +from notifications.models import QuestionNotification from .models import Question @receiver(post_save, sender=Question) -def create_notification(sender, instance, created, **kwargs): +def create_question_notification(sender, instance, created, **kwargs): if created: - Notification.objects.create( + QuestionNotification.objects.create( message=f"새로운 질문이 생성되었습니다: {instance.question_title}", question=instance, ) diff --git a/customk/questions/static/question/css/styles.css b/customk/questions/static/question/css/styles.css index 964cc70..9bf0f92 100644 --- a/customk/questions/static/question/css/styles.css +++ b/customk/questions/static/question/css/styles.css @@ -1,25 +1,38 @@ -#notification-bell { +.icon-container { position: relative; display: inline-block; } -.bell-icon { - font-size: 24px; +.icon { + font-size: 24px; /* 아이콘의 크기 */ color: #000; } .badge { position: absolute; - top: -5px; /* 조정 필요시 변경 */ - right: -5px; /* 조정 필요시 변경 */ - background: red; /* 빨간 배경색 */ - color: white; - border-radius: 50%; /* 완전히 둥글게 */ - width: 12px; !important;/* 배지의 너비 */ - height: 12px; !important;/* 배지의 높이 */ + top: -5px; /* 배지 위치 조정 */ + right: -5px; /* 배지 위치 조정 */ + background: red; /* 배경색 */ + color: transparent; + border-radius: 50%; /* 둥글게 */ + width: 12px; /* 배지 너비 */ + height: 12px; /* 배지 높이 */ display: flex; align-items: center; /* 텍스트 수직 중앙 정렬 */ justify-content: center; /* 텍스트 수평 중앙 정렬 */ - font-size: 8px; !important;/* 글자 크기 조절 */ + font-size: 8px; /* 글자 크기 */ font-weight: bold; +} + +/* 아이콘 컨테이너 */ +#notification-bell, +#payment-notification { + position: relative; + display: inline-block; + margin-right: 20px; +} + +.bell-icon, +.money-icon { + font-size: 24px; } \ No newline at end of file diff --git a/customk/questions/static/question/js/scripts.js b/customk/questions/static/question/js/scripts.js index e6815b9..2f405e9 100644 --- a/customk/questions/static/question/js/scripts.js +++ b/customk/questions/static/question/js/scripts.js @@ -1,12 +1,16 @@ document.addEventListener('DOMContentLoaded', function() { + // 질문 알림 관련 요소 const notificationIcon = document.getElementById('notification-icon'); const notificationBadge = document.getElementById('notification-badge'); - fetch('/v1/notifications/unread_count/') + const paymentIcon = document.getElementById('payment-icon'); + const paymentBadge = document.getElementById('payment-badge'); + + fetch('/v1/notifications/unread_question_notifications_count/') .then(response => response.json()) .then(data => { if (data.count > 0) { - notificationBadge.textContent = ''; + notificationBadge.textContent = data.count; notificationBadge.style.display = 'inline'; notificationIcon.style.color = 'blue'; } else { @@ -14,5 +18,19 @@ document.addEventListener('DOMContentLoaded', function() { notificationIcon.style.color = '#000'; } }) - .catch(error => console.error('Error fetching notification count:', error)); -}); + .catch(error => console.error('Error fetching question notification count:', error)); + + fetch('/v1/notifications/unread_payment_notifications_count/') + .then(response => response.json()) + .then(data => { + if (data.count > 0) { + paymentBadge.textContent = data.count; + paymentBadge.style.display = 'inline'; + paymentIcon.style.color = 'green'; + } else { + paymentBadge.style.display = 'none'; + paymentIcon.style.color = '#000'; + } + }) + .catch(error => console.error('Error fetching payment notification count:', error)); +}); \ No newline at end of file diff --git a/customk/questions/templates/admin/base_site.html b/customk/questions/templates/admin/base_site.html index 7fe127a..e0fb8a3 100644 --- a/customk/questions/templates/admin/base_site.html +++ b/customk/questions/templates/admin/base_site.html @@ -14,12 +14,21 @@

CustomK Admin

+ + {% endblock %} diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index d8c9eac..f7ee062 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -70,7 +70,7 @@ class ReviewSerializer(serializers.ModelSerializer): class Meta: model = Review - fields = ["id", "review", "rating", "user", "images"] + fields = "__all__" def create(self, validated_data): images_data64 = validated_data.pop("images", []) From eec68a2a1cf313c76dabfa993f349c2d7c4fb74f Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 01:02:08 +0900 Subject: [PATCH 13/28] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/reviews/tests/test_review.py | 0 customk/reviews/tests/test_review_api.py | 80 ++++++++++++++++++++++ customk/reviews/tests/test_review_model.py | 40 +++++++++++ customk/reviews/urls.py | 2 +- 4 files changed, 121 insertions(+), 1 deletion(-) delete mode 100644 customk/reviews/tests/test_review.py create mode 100644 customk/reviews/tests/test_review_api.py create mode 100644 customk/reviews/tests/test_review_model.py diff --git a/customk/reviews/tests/test_review.py b/customk/reviews/tests/test_review.py deleted file mode 100644 index e69de29..0000000 diff --git a/customk/reviews/tests/test_review_api.py b/customk/reviews/tests/test_review_api.py new file mode 100644 index 0000000..5ab2bd7 --- /dev/null +++ b/customk/reviews/tests/test_review_api.py @@ -0,0 +1,80 @@ +import pytest +from django.urls import reverse +from rest_framework import status + +from .test_review_model import api_client, user, class_instance, review, review_image + + +@pytest.mark.django_db +def test_all_reviews_list(api_client, review, user): + api_client.force_authenticate(user=user) + + url = reverse("all-reviews") + response = api_client.get(url, {"page": 1, "size": 10}) + + assert response.status_code == status.HTTP_200_OK + assert "total_count" in response.data + assert "reviews" in response.data + + +@pytest.mark.django_db +def test_review_list(api_client, class_instance, review): + url = reverse("review-list", kwargs={"class_id": class_instance.id}) + response = api_client.get(url, {"page": 1, "size": 10}) + + assert response.status_code == status.HTTP_200_OK + assert "total_count" in response.data + assert "reviews" in response.data + + +@pytest.mark.django_db +def test_review_update(api_client, class_instance, review, user): + api_client.force_authenticate(user=user) + + url = reverse( + "review-update-delete", + kwargs={"class_id": class_instance.id, "review_id": review.id}, + ) + data = {"review": "Updated review content", "rating": "5.0"} + response = api_client.patch(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + assert response.data["review"]["review"] == "Updated review content" + assert response.data["review"]["rating"] == "5.0" + + +@pytest.mark.django_db +def test_review_delete(api_client, class_instance, review, user): + api_client.force_authenticate(user=user) + + url = reverse( + "review-update-delete", + kwargs={"class_id": class_instance.id, "review_id": review.id}, + ) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + +@pytest.mark.django_db +def test_review_image_list(api_client, class_instance, review, review_image, user): + url = reverse( + "review-image-list", + kwargs={"class_id": class_instance.id, "review_id": review.id}, + ) + response = api_client.get(url, {"page": 1, "size": 10}) + + assert response.status_code == status.HTTP_200_OK + assert "total_count" in response.data + assert "total_pages" in response.data + assert "current_page" in response.data + assert "images" in response.data + + +@pytest.mark.django_db +def test_photo_review_list(api_client, class_instance, review_image): + url = reverse("photo-review-list", kwargs={"class_id": class_instance.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert "images" in response.data diff --git a/customk/reviews/tests/test_review_model.py b/customk/reviews/tests/test_review_model.py new file mode 100644 index 0000000..58215f6 --- /dev/null +++ b/customk/reviews/tests/test_review_model.py @@ -0,0 +1,40 @@ +import pytest +from users.models import User +from classes.models import Class +from reviews.models import Review, ReviewImage + + +@pytest.fixture +def api_client(): + from rest_framework.test import APIClient + + return APIClient() + + +@pytest.fixture +def user(api_client): + return User.objects.create_user( + email="testuser@example.com", password="testpassword" + ) + + +@pytest.fixture +def class_instance(): + return Class.objects.create(title="Test Class") + + +@pytest.fixture +def review(class_instance, user): + return Review.objects.create( + user=user, + class_id=class_instance, + review="Test review content", + rating="4.5", + ) + + +@pytest.fixture +def review_image(review): + return ReviewImage.objects.create( + review=review, image_url="http://example.com/image.jpg" + ) diff --git a/customk/reviews/urls.py b/customk/reviews/urls.py index 90aa4a1..c45f663 100644 --- a/customk/reviews/urls.py +++ b/customk/reviews/urls.py @@ -14,7 +14,7 @@ re_path( r"^(?P\d+)/update/(?P\d+)/?$", ReviewUpdateView.as_view(), - name="review-update", + name="review-update-delete", ), re_path( r"^(?P\d+)/images/(?P\d+)/list/?$", From e9bb9cf66a7306f0b61cc8133d80b8139a306354 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 01:11:43 +0900 Subject: [PATCH 14/28] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/reviews/tests/conftest.py | 30 +++++++++++++--- customk/reviews/tests/test_review_api.py | 30 ++++++++-------- customk/reviews/tests/test_review_model.py | 40 ---------------------- 3 files changed, 40 insertions(+), 60 deletions(-) delete mode 100644 customk/reviews/tests/test_review_model.py diff --git a/customk/reviews/tests/conftest.py b/customk/reviews/tests/conftest.py index 5c5470a..d16c861 100644 --- a/customk/reviews/tests/conftest.py +++ b/customk/reviews/tests/conftest.py @@ -1,6 +1,7 @@ import pytest +from users.models import User +from reviews.models import Review, ReviewImage from rest_framework.test import APIClient - from classes.models import Class @@ -8,17 +9,36 @@ def api_client(): return APIClient() +@pytest.fixture +def sample_user(api_client): + return User.objects.create_user( + email="testuser@example.com", password="testpassword" + ) + @pytest.fixture def sample_class(): - """ - Class 생성 샘플 코드입니다. - """ return Class.objects.create( title="Sample Class", description="This is a sample class", max_person=10, require_person=5, price=50000, - address={"state": "Seoul", "city": "Gangnam", "street": "Teheran-ro"}, + address="서울시 강남구 테헤란로", + ) + +@pytest.fixture +def review(sample_class, sample_user): + return Review.objects.create( + user=sample_user, + class_id=sample_class, + review="Test review content", + rating="4.5", ) + + +@pytest.fixture +def review_image(review): + return ReviewImage.objects.create( + review=review, image_url="http://example.com/image.jpg" + ) \ No newline at end of file diff --git a/customk/reviews/tests/test_review_api.py b/customk/reviews/tests/test_review_api.py index 5ab2bd7..e96bd61 100644 --- a/customk/reviews/tests/test_review_api.py +++ b/customk/reviews/tests/test_review_api.py @@ -2,12 +2,12 @@ from django.urls import reverse from rest_framework import status -from .test_review_model import api_client, user, class_instance, review, review_image +from .conftest import api_client, sample_class, review_image, sample_user @pytest.mark.django_db -def test_all_reviews_list(api_client, review, user): - api_client.force_authenticate(user=user) +def test_all_reviews_list(api_client, review, sample_user): + api_client.force_authenticate(user=sample_user) url = reverse("all-reviews") response = api_client.get(url, {"page": 1, "size": 10}) @@ -18,8 +18,8 @@ def test_all_reviews_list(api_client, review, user): @pytest.mark.django_db -def test_review_list(api_client, class_instance, review): - url = reverse("review-list", kwargs={"class_id": class_instance.id}) +def test_review_list(api_client, sample_class, review): + url = reverse("review-list", kwargs={"class_id": sample_class.id}) response = api_client.get(url, {"page": 1, "size": 10}) assert response.status_code == status.HTTP_200_OK @@ -28,12 +28,12 @@ def test_review_list(api_client, class_instance, review): @pytest.mark.django_db -def test_review_update(api_client, class_instance, review, user): - api_client.force_authenticate(user=user) +def test_review_update(api_client, sample_class, review, sample_user): + api_client.force_authenticate(user=sample_user) url = reverse( "review-update-delete", - kwargs={"class_id": class_instance.id, "review_id": review.id}, + kwargs={"class_id": sample_class.id, "review_id": review.id}, ) data = {"review": "Updated review content", "rating": "5.0"} response = api_client.patch(url, data, format="json") @@ -44,12 +44,12 @@ def test_review_update(api_client, class_instance, review, user): @pytest.mark.django_db -def test_review_delete(api_client, class_instance, review, user): - api_client.force_authenticate(user=user) +def test_review_delete(api_client, sample_class, review, sample_user): + api_client.force_authenticate(user=sample_user) url = reverse( "review-update-delete", - kwargs={"class_id": class_instance.id, "review_id": review.id}, + kwargs={"class_id": sample_class.id, "review_id": review.id}, ) response = api_client.delete(url) @@ -57,10 +57,10 @@ def test_review_delete(api_client, class_instance, review, user): @pytest.mark.django_db -def test_review_image_list(api_client, class_instance, review, review_image, user): +def test_review_image_list(api_client, sample_class, review, review_image): url = reverse( "review-image-list", - kwargs={"class_id": class_instance.id, "review_id": review.id}, + kwargs={"class_id": sample_class.id, "review_id": review.id}, ) response = api_client.get(url, {"page": 1, "size": 10}) @@ -72,8 +72,8 @@ def test_review_image_list(api_client, class_instance, review, review_image, use @pytest.mark.django_db -def test_photo_review_list(api_client, class_instance, review_image): - url = reverse("photo-review-list", kwargs={"class_id": class_instance.id}) +def test_photo_review_list(api_client, sample_class, review_image): + url = reverse("photo-review-list", kwargs={"class_id": sample_class.id}) response = api_client.get(url) assert response.status_code == status.HTTP_200_OK diff --git a/customk/reviews/tests/test_review_model.py b/customk/reviews/tests/test_review_model.py deleted file mode 100644 index 58215f6..0000000 --- a/customk/reviews/tests/test_review_model.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest -from users.models import User -from classes.models import Class -from reviews.models import Review, ReviewImage - - -@pytest.fixture -def api_client(): - from rest_framework.test import APIClient - - return APIClient() - - -@pytest.fixture -def user(api_client): - return User.objects.create_user( - email="testuser@example.com", password="testpassword" - ) - - -@pytest.fixture -def class_instance(): - return Class.objects.create(title="Test Class") - - -@pytest.fixture -def review(class_instance, user): - return Review.objects.create( - user=user, - class_id=class_instance, - review="Test review content", - rating="4.5", - ) - - -@pytest.fixture -def review_image(review): - return ReviewImage.objects.create( - review=review, image_url="http://example.com/image.jpg" - ) From d37e7219eb5b5783cb35d5a7e2a32dde89d8ad37 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 01:55:56 +0900 Subject: [PATCH 15/28] =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/serializers.py | 9 +++---- customk/questions/tests/conftest.py | 38 +++++++++++++++++++++++++++++ customk/questions/views.py | 2 +- 3 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 customk/questions/tests/conftest.py diff --git a/customk/questions/serializers.py b/customk/questions/serializers.py index 7ae4008..d7d5782 100644 --- a/customk/questions/serializers.py +++ b/customk/questions/serializers.py @@ -1,14 +1,11 @@ from rest_framework import serializers - -from users.models import User - from .models import Question class QuestionSerializer(serializers.ModelSerializer): - created_at = serializers.DateTimeField(read_only=True) # created_at 필드 read_only - class_id = serializers.IntegerField(read_only=True) # class_id 필드 read_only - user_id = serializers.IntegerField(read_only=True) + created_at = serializers.DateTimeField(read_only=True) + class_id = serializers.PrimaryKeyRelatedField(read_only=True) + user_id = serializers.PrimaryKeyRelatedField(read_only=True) class Meta: model = Question diff --git a/customk/questions/tests/conftest.py b/customk/questions/tests/conftest.py new file mode 100644 index 0000000..fbce74f --- /dev/null +++ b/customk/questions/tests/conftest.py @@ -0,0 +1,38 @@ +import pytest +from users.models import User +from reviews.models import Review +from rest_framework.test import APIClient +from classes.models import Class +from questions.models import Question + + +@pytest.fixture +def api_client(): + return APIClient() + +@pytest.fixture +def sample_user(): + return User.objects.create_user( + email="testuser@example.com", password="testpassword" + ) + + +@pytest.fixture +def sample_class(): + return Class.objects.create( + title="Sample Class", + description="This is a sample class", + max_person=10, + require_person=5, + price=50000, + address="서울시 강남구 테헤란로", + ) + +@pytest.fixture +def question(sample_user, sample_class): + return Question.objects.create( + user_id=sample_user, + class_id=sample_class, + question="Test question content", + question_title="Test question title" + ) \ No newline at end of file diff --git a/customk/questions/views.py b/customk/questions/views.py index cd29b3b..9ee8164 100644 --- a/customk/questions/views.py +++ b/customk/questions/views.py @@ -214,7 +214,7 @@ def post( serializer = QuestionSerializer(data=data) if serializer.is_valid(): - serializer.save() + serializer.save(user_id=request.user, class_id=class_instance) response_data = { "status": "success", "message": "Question or Answer submitted successfully", From 27ba8323fca9147bd7b3e5f3a64a45fa30f47bdd Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 01:56:17 +0900 Subject: [PATCH 16/28] =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 customk/questions/tests/__init__.py diff --git a/customk/questions/tests/__init__.py b/customk/questions/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 1b21371e8e0ef085585eff04663223cf367ad944 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 02:05:58 +0900 Subject: [PATCH 17/28] =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/tests/conftest.py | 10 +-- customk/questions/tests/test_question_api.py | 69 ++++++++++++++++++++ customk/reviews/tests/conftest.py | 9 ++- customk/reviews/tests/test_review_api.py | 4 +- 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 customk/questions/tests/test_question_api.py diff --git a/customk/questions/tests/conftest.py b/customk/questions/tests/conftest.py index fbce74f..4f05015 100644 --- a/customk/questions/tests/conftest.py +++ b/customk/questions/tests/conftest.py @@ -1,15 +1,16 @@ import pytest -from users.models import User -from reviews.models import Review from rest_framework.test import APIClient + from classes.models import Class from questions.models import Question +from users.models import User @pytest.fixture def api_client(): return APIClient() + @pytest.fixture def sample_user(): return User.objects.create_user( @@ -28,11 +29,12 @@ def sample_class(): address="서울시 강남구 테헤란로", ) + @pytest.fixture def question(sample_user, sample_class): return Question.objects.create( user_id=sample_user, class_id=sample_class, question="Test question content", - question_title="Test question title" - ) \ No newline at end of file + question_title="Test question title", + ) diff --git a/customk/questions/tests/test_question_api.py b/customk/questions/tests/test_question_api.py new file mode 100644 index 0000000..dce5324 --- /dev/null +++ b/customk/questions/tests/test_question_api.py @@ -0,0 +1,69 @@ +import pytest +from django.urls import reverse +from rest_framework import status + + +@pytest.mark.django_db +def test_all_questions_list(api_client, sample_user, question): + api_client.force_authenticate(user=sample_user) + + url = reverse("all-questions") + response = api_client.get(url, {"page": 1, "size": 10}) + + assert response.status_code == status.HTTP_200_OK + assert "total_count" in response.data + assert "questions" in response.data + + +@pytest.mark.django_db +def test_question_list(api_client, sample_user, sample_class, question): + api_client.force_authenticate(user=sample_user) + + url = reverse("class-question-list", kwargs={"class_id": sample_class.id}) + response = api_client.get(url, {"page": 1, "size": 10}) + + assert response.status_code == status.HTTP_200_OK + assert "total_count" in response.data + assert "questions" in response.data + + +@pytest.mark.django_db +def test_question_create(api_client, sample_user, sample_class): + api_client.force_authenticate(user=sample_user) + + url = reverse("class-question-list", kwargs={"class_id": sample_class.id}) + data = {"question": "New test question", "question_title": "New question title"} + response = api_client.post(url, data, format="json") + + assert response.status_code == status.HTTP_201_CREATED + assert response.data["status"] == "success" + assert response.data["message"] == "Question or Answer submitted successfully" + + +@pytest.mark.django_db +def test_question_update(api_client, sample_user, sample_class, question): + api_client.force_authenticate(user=sample_user) + + url = reverse("class-question-list", kwargs={"class_id": sample_class.id}) + data = { + "question": "Updated test question", + "question_title": "Updated question title", + } + response = api_client.patch(f"{url}?question_id={question.id}", data, format="json") + + assert response.status_code == status.HTTP_200_OK + assert response.data["status"] == "success" + assert response.data["message"] == "Question or Answer updated successfully" + assert response.data["data"]["question"] == "Updated test question" + + +@pytest.mark.django_db +def test_question_delete(api_client, sample_user, sample_class, question): + api_client.force_authenticate(user=sample_user) + + url = reverse("class-question-list", kwargs={"class_id": sample_class.id}) + response = api_client.delete(f"{url}?question_id={question.id}") + + assert response.status_code == status.HTTP_200_OK + assert response.data["status"] == "success" + assert response.data["message"] == "Question or Answer deleted successfully" diff --git a/customk/reviews/tests/conftest.py b/customk/reviews/tests/conftest.py index d16c861..a1fdac6 100644 --- a/customk/reviews/tests/conftest.py +++ b/customk/reviews/tests/conftest.py @@ -1,14 +1,16 @@ import pytest -from users.models import User -from reviews.models import Review, ReviewImage from rest_framework.test import APIClient + from classes.models import Class +from reviews.models import Review, ReviewImage +from users.models import User @pytest.fixture def api_client(): return APIClient() + @pytest.fixture def sample_user(api_client): return User.objects.create_user( @@ -27,6 +29,7 @@ def sample_class(): address="서울시 강남구 테헤란로", ) + @pytest.fixture def review(sample_class, sample_user): return Review.objects.create( @@ -41,4 +44,4 @@ def review(sample_class, sample_user): def review_image(review): return ReviewImage.objects.create( review=review, image_url="http://example.com/image.jpg" - ) \ No newline at end of file + ) diff --git a/customk/reviews/tests/test_review_api.py b/customk/reviews/tests/test_review_api.py index e96bd61..ca5b398 100644 --- a/customk/reviews/tests/test_review_api.py +++ b/customk/reviews/tests/test_review_api.py @@ -1,8 +1,10 @@ +# ruff: noqa: F811 + import pytest from django.urls import reverse from rest_framework import status -from .conftest import api_client, sample_class, review_image, sample_user +from .conftest import api_client, review_image, sample_class, sample_user @pytest.mark.django_db From 6ecbb046674ca72da9f6f38a11e0cb455e0a1f4f Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 02:06:10 +0900 Subject: [PATCH 18/28] =?UTF-8?q?=EC=9D=98=EB=AF=B8=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/questions/tests.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 customk/questions/tests.py diff --git a/customk/questions/tests.py b/customk/questions/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/customk/questions/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From f8c82cd77374f1eed6d26249a6aec59d66807d33 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 15:57:08 +0900 Subject: [PATCH 19/28] =?UTF-8?q?popular=20=ED=83=9C=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/classes/serializers.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/customk/classes/serializers.py b/customk/classes/serializers.py index da68c17..91074e7 100644 --- a/customk/classes/serializers.py +++ b/customk/classes/serializers.py @@ -4,12 +4,15 @@ from django.db.models import Avg from django.utils import timezone +from django.db.models import Count +from django.core.cache import cache from rest_framework import serializers from common.services.ncp_api_conf import ObjectStorage from config.logger import logger -from .models import Category, Class, ClassDate, ClassImages, Genre +from .models import Category, Class, ClassDate, ClassImages +from payments.models import Payment def upload_image_to_object_storage(base64_image: str) -> str: @@ -80,6 +83,7 @@ class ClassSerializer(serializers.ModelSerializer): ) average_rating = serializers.FloatField(read_only=True) created_at = serializers.DateTimeField(read_only=True) + is_popular = serializers.SerializerMethodField() class Meta: model = Class @@ -114,6 +118,30 @@ def get_is_best(self, obj): return average_rating >= 3.5 + def get_is_popular(self, obj): + popular_classes = cache.get('popular_classes', None) + + if popular_classes is None: + one_month_ago = timezone.now() - timedelta(days=30) + + classes_with_payments = ( + Payment.objects.filter(created_at__gte=one_month_ago) + .values('class_id') + .annotate(payment_count=Count('id')) + .order_by('-payment_count') + ) + + total_classes = len(classes_with_payments) + top_20_percent_count = max(1, int(total_classes * 0.2)) + + popular_class_ids = [entry['class_id'] for entry in classes_with_payments[:top_20_percent_count]] + + cache.set('popular_classes', popular_class_ids, timeout=60 * 60 * 24 * 14) + + is_popular = obj.id in popular_classes + print(f"Class ID: {obj.id}, Is Popular: {is_popular}") + return is_popular + def create(self, validated_data): dates_data = validated_data.pop("dates", []) images_data64 = validated_data.pop("images", []) From 3684e77f6a4146299a80c343884ca57a18b3a147 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 16:06:38 +0900 Subject: [PATCH 20/28] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/reviews/serializers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index f7ee062..c68f5e8 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -88,3 +88,17 @@ def create(self, validated_data): ReviewImage.objects.create(review=review, image_url=image_url) return review + + def update(self, instance, validated_data): + images_data64 = validated_data.pop("images", []) + + instance.review = validated_data.get('review', instance.review) + instance.rating = validated_data.get('rating', instance.rating) + instance.save() + + instance.images.all().delete() + for image_data64 in images_data64: + image_url = upload_image_to_object_storage(image_data64["image_url"]) + ReviewImage.objects.create(review=instance, image_url=image_url) + + return instance From 69e306a1449b4f343aeba85fb4ae8cf7b5d09e99 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 16:10:01 +0900 Subject: [PATCH 21/28] =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/payments/admin.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/customk/payments/admin.py b/customk/payments/admin.py index c6aef01..08d1423 100644 --- a/customk/payments/admin.py +++ b/customk/payments/admin.py @@ -1,8 +1,30 @@ from django.contrib import admin -from payments.models import ReferralCode +from payments.models import Payment, ReferralCode @admin.register(ReferralCode) class ReferralCodeAdmin(admin.ModelAdmin): # type: ignore pass + + +@admin.register(Payment) +class PaymentAdmin(admin.ModelAdmin): + list_display = ( + "order_id", + "status", + "amount", + "refunded_amount", + "currency", + "payment_method", + "payer_email", + "user_id", + "class_id", + "class_date_id", + "quantity", + "referral_code", + "transaction_id", + ) + search_fields = ("order_id", "payment_method", "payer_email", "transaction_id") + list_filter = ("status", "currency", "payment_method") + readonly_fields = [field.name for field in Payment._meta.fields] From 988eab2a9fccad2be330dd35f4e7a4eab3f4f07a Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 16:10:59 +0900 Subject: [PATCH 22/28] ruff --- customk/classes/serializers.py | 22 ++++++++++++---------- customk/questions/serializers.py | 1 + customk/reviews/serializers.py | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/customk/classes/serializers.py b/customk/classes/serializers.py index 91074e7..61e44e4 100644 --- a/customk/classes/serializers.py +++ b/customk/classes/serializers.py @@ -2,17 +2,16 @@ import uuid from datetime import timedelta -from django.db.models import Avg -from django.utils import timezone -from django.db.models import Count from django.core.cache import cache +from django.db.models import Avg, Count +from django.utils import timezone from rest_framework import serializers from common.services.ncp_api_conf import ObjectStorage from config.logger import logger +from payments.models import Payment from .models import Category, Class, ClassDate, ClassImages -from payments.models import Payment def upload_image_to_object_storage(base64_image: str) -> str: @@ -119,24 +118,27 @@ def get_is_best(self, obj): return average_rating >= 3.5 def get_is_popular(self, obj): - popular_classes = cache.get('popular_classes', None) + popular_classes = cache.get("popular_classes", None) if popular_classes is None: one_month_ago = timezone.now() - timedelta(days=30) classes_with_payments = ( Payment.objects.filter(created_at__gte=one_month_ago) - .values('class_id') - .annotate(payment_count=Count('id')) - .order_by('-payment_count') + .values("class_id") + .annotate(payment_count=Count("id")) + .order_by("-payment_count") ) total_classes = len(classes_with_payments) top_20_percent_count = max(1, int(total_classes * 0.2)) - popular_class_ids = [entry['class_id'] for entry in classes_with_payments[:top_20_percent_count]] + popular_class_ids = [ + entry["class_id"] + for entry in classes_with_payments[:top_20_percent_count] + ] - cache.set('popular_classes', popular_class_ids, timeout=60 * 60 * 24 * 14) + cache.set("popular_classes", popular_class_ids, timeout=60 * 60 * 24 * 14) is_popular = obj.id in popular_classes print(f"Class ID: {obj.id}, Is Popular: {is_popular}") diff --git a/customk/questions/serializers.py b/customk/questions/serializers.py index d7d5782..537152e 100644 --- a/customk/questions/serializers.py +++ b/customk/questions/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from .models import Question diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index c68f5e8..ca1cc81 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -92,8 +92,8 @@ def create(self, validated_data): def update(self, instance, validated_data): images_data64 = validated_data.pop("images", []) - instance.review = validated_data.get('review', instance.review) - instance.rating = validated_data.get('rating', instance.rating) + instance.review = validated_data.get("review", instance.review) + instance.rating = validated_data.get("rating", instance.rating) instance.save() instance.images.all().delete() From e3d20abf0fea8fd442de413d5d35ff51d80b626b Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 16:14:48 +0900 Subject: [PATCH 23/28] =?UTF-8?q?popular=20=ED=83=9C=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/classes/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/customk/classes/serializers.py b/customk/classes/serializers.py index 61e44e4..409998d 100644 --- a/customk/classes/serializers.py +++ b/customk/classes/serializers.py @@ -139,6 +139,7 @@ def get_is_popular(self, obj): ] cache.set("popular_classes", popular_class_ids, timeout=60 * 60 * 24 * 14) + popular_classes = popular_class_ids is_popular = obj.id in popular_classes print(f"Class ID: {obj.id}, Is Popular: {is_popular}") From 2846b914f8321114db98ea9119cec5c717eb13f6 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 16:55:26 +0900 Subject: [PATCH 24/28] =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/users/serializers/user_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customk/users/serializers/user_serializer.py b/customk/users/serializers/user_serializer.py index 9d33305..40eca14 100644 --- a/customk/users/serializers/user_serializer.py +++ b/customk/users/serializers/user_serializer.py @@ -67,7 +67,7 @@ class Meta: def get_profile_image_url(self, obj) -> str: return obj.profile_image - def velidate(self, data): + def validate(self, data): user = User(**data) errors = dict() From b03859a008ede9d3e1e31ad3e194547771a69b6e Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 17:00:44 +0900 Subject: [PATCH 25/28] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20delete=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/reviews/serializers.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index ca1cc81..4964a1a 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -1,7 +1,7 @@ import base64 import uuid -from datetime import timedelta +from django.core.exceptions import ValidationError from rest_framework import serializers from classes.models import Class @@ -96,7 +96,18 @@ def update(self, instance, validated_data): instance.rating = validated_data.get("rating", instance.rating) instance.save() - instance.images.all().delete() + for image_instance in instance.images.all(): + if image_instance.image_url: + obj = ObjectStorage() + obj_status_code = obj.delete_object(image_instance.image_url) + + if obj_status_code != 204: + raise ValidationError( + { + "review_image": "Failed to delete existing image. Status code: {obj_status_code}" + } + ) + for image_data64 in images_data64: image_url = upload_image_to_object_storage(image_data64["image_url"]) ReviewImage.objects.create(review=instance, image_url=image_url) From d9013d0097fd28ffa6262f186e49f95861fdc02f Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 17:27:06 +0900 Subject: [PATCH 26/28] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20delete=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/reviews/serializers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index 4964a1a..7e021c0 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -96,17 +96,18 @@ def update(self, instance, validated_data): instance.rating = validated_data.get("rating", instance.rating) instance.save() + for image_instance in instance.images.all(): - if image_instance.image_url: obj = ObjectStorage() obj_status_code = obj.delete_object(image_instance.image_url) if obj_status_code != 204: raise ValidationError( { - "review_image": "Failed to delete existing image. Status code: {obj_status_code}" + "review_image": f"Failed to delete existing image. Status code: {obj_status_code}" } ) + image_instance.delete() for image_data64 in images_data64: image_url = upload_image_to_object_storage(image_data64["image_url"]) From 926645cc830b7b8072601161feacee778fc8516a Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 17:28:20 +0900 Subject: [PATCH 27/28] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20delete=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/reviews/serializers.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/customk/reviews/serializers.py b/customk/reviews/serializers.py index 7e021c0..49ba2d7 100644 --- a/customk/reviews/serializers.py +++ b/customk/reviews/serializers.py @@ -96,18 +96,17 @@ def update(self, instance, validated_data): instance.rating = validated_data.get("rating", instance.rating) instance.save() - for image_instance in instance.images.all(): - obj = ObjectStorage() - obj_status_code = obj.delete_object(image_instance.image_url) - - if obj_status_code != 204: - raise ValidationError( - { - "review_image": f"Failed to delete existing image. Status code: {obj_status_code}" - } - ) - image_instance.delete() + obj = ObjectStorage() + obj_status_code = obj.delete_object(image_instance.image_url) + + if obj_status_code != 204: + raise ValidationError( + { + "review_image": f"Failed to delete existing image. Status code: {obj_status_code}" + } + ) + image_instance.delete() for image_data64 in images_data64: image_url = upload_image_to_object_storage(image_data64["image_url"]) From 750697ef6902443255b25e50934c6e7ee3890f44 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 9 Sep 2024 17:42:13 +0900 Subject: [PATCH 28/28] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EB=A6=AC=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../questions/static/question/css/styles.css | 27 +++++++------------ .../questions/static/question/js/scripts.js | 4 +-- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/customk/questions/static/question/css/styles.css b/customk/questions/static/question/css/styles.css index 9bf0f92..e946f3c 100644 --- a/customk/questions/static/question/css/styles.css +++ b/customk/questions/static/question/css/styles.css @@ -1,11 +1,7 @@ -.icon-container { - position: relative; - display: inline-block; -} - -.icon { - font-size: 24px; /* 아이콘의 크기 */ - color: #000; +/* 아이콘 크기 조정 */ +.bell-icon, +.money-icon { + font-size: 24px; /* 아이콘 크기 증가 */ } .badge { @@ -13,14 +9,14 @@ top: -5px; /* 배지 위치 조정 */ right: -5px; /* 배지 위치 조정 */ background: red; /* 배경색 */ - color: transparent; + color: white; /* 글자 색상 변경 */ border-radius: 50%; /* 둥글게 */ - width: 12px; /* 배지 너비 */ - height: 12px; /* 배지 높이 */ + width: 16px; /* 배지 너비 증가 */ + height: 16px; /* 배지 높이 증가 */ display: flex; align-items: center; /* 텍스트 수직 중앙 정렬 */ justify-content: center; /* 텍스트 수평 중앙 정렬 */ - font-size: 8px; /* 글자 크기 */ + font-size: 10px; /* 글자 크기 증가 */ font-weight: bold; } @@ -29,10 +25,5 @@ #payment-notification { position: relative; display: inline-block; - margin-right: 20px; -} - -.bell-icon, -.money-icon { - font-size: 24px; + margin-right: 30px; /* 아이콘 사이 간격 조정 */ } \ No newline at end of file diff --git a/customk/questions/static/question/js/scripts.js b/customk/questions/static/question/js/scripts.js index 2f405e9..b420d37 100644 --- a/customk/questions/static/question/js/scripts.js +++ b/customk/questions/static/question/js/scripts.js @@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { if (data.count > 0) { - notificationBadge.textContent = data.count; + notificationBadge.textContent = ""; notificationBadge.style.display = 'inline'; notificationIcon.style.color = 'blue'; } else { @@ -24,7 +24,7 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { if (data.count > 0) { - paymentBadge.textContent = data.count; + paymentBadge.textContent = ""; paymentBadge.style.display = 'inline'; paymentIcon.style.color = 'green'; } else {