From 4b109c5e315db4f5a1b6f57708b86d97d9b02c08 Mon Sep 17 00:00:00 2001 From: ALbertIM0427 Date: Thu, 10 Oct 2024 19:39:47 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20curriculum=20=EC=97=90=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- courses/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/courses/views.py b/courses/views.py index dfc25e0..3ce0a3f 100644 --- a/courses/views.py +++ b/courses/views.py @@ -170,6 +170,14 @@ class CurriculumListCreateView(generics.ListCreateAPIView): queryset = Curriculum.objects.all() serializer_class = CurriculumSummarySerializer permission_classes = [IsStaffOrReadOnly] + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + search_fields = ["title", "description"] + filterset_fields = ["category", "skill_level"] + ordering_fields = ["created_at", "price"] def get_serializer_class(self): """ From ab829bca66c5bbd60af28ee258937486a8759d96 Mon Sep 17 00:00:00 2001 From: ALbertIM0427 Date: Thu, 10 Oct 2024 16:04:25 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20course=20=EB=B0=8F=20curriculum?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- courses/admin.py | 2 + ...ourse_level_course_skill_level_and_more.py | 28 +++++++ .../0009_course_author_curriculum_author.py | 28 +++++++ courses/mixins.py | 11 ++- courses/models.py | 52 ++++++++++-- courses/serializers.py | 47 ++++++++++- courses/test/conftest.py | 7 +- courses/test/test_mixins.py | 65 ++++++++------- courses/test/test_models.py | 50 ++++++++---- courses/test/test_serializers.py | 22 +++-- courses/test/test_views.py | 80 +++++++++++++------ courses/views.py | 7 +- 12 files changed, 303 insertions(+), 96 deletions(-) create mode 100644 courses/migrations/0008_rename_course_level_course_skill_level_and_more.py create mode 100644 courses/migrations/0009_course_author_curriculum_author.py diff --git a/courses/admin.py b/courses/admin.py index b6574db..452f0cf 100644 --- a/courses/admin.py +++ b/courses/admin.py @@ -4,6 +4,7 @@ from .models import ( Assignment, Course, + Curriculum, Lecture, MultipleChoiceQuestion, MultipleChoiceQuestionChoice, @@ -17,3 +18,4 @@ admin.site.register(Assignment) admin.site.register(MultipleChoiceQuestion) admin.site.register(MultipleChoiceQuestionChoice) + admin.site.register(Curriculum) diff --git a/courses/migrations/0008_rename_course_level_course_skill_level_and_more.py b/courses/migrations/0008_rename_course_level_course_skill_level_and_more.py new file mode 100644 index 0000000..166153d --- /dev/null +++ b/courses/migrations/0008_rename_course_level_course_skill_level_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.1 on 2024-10-10 02:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0007_alter_course_category'), + ] + + operations = [ + migrations.RenameField( + model_name='course', + old_name='course_level', + new_name='skill_level', + ), + migrations.AddField( + model_name='curriculum', + name='category', + field=models.CharField(choices=[('JavaScript', 'JavaScript'), ('Python', 'Python'), ('Django', 'Django'), ('React', 'React'), ('Vue', 'Vue'), ('Node', 'Node'), ('AWS', 'AWS'), ('Docker', 'Docker'), ('DB', 'DB')], default='JavaScript', max_length=255, verbose_name='카테고리'), + ), + migrations.AddField( + model_name='curriculum', + name='skill_level', + field=models.CharField(choices=[('beginner', '초급'), ('intermediate', '중급'), ('advanced', '고급')], default='beginner', max_length=255, verbose_name='난이도'), + ), + ] diff --git a/courses/migrations/0009_course_author_curriculum_author.py b/courses/migrations/0009_course_author_curriculum_author.py new file mode 100644 index 0000000..ba274bb --- /dev/null +++ b/courses/migrations/0009_course_author_curriculum_author.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.1 on 2024-10-10 02:31 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0008_rename_course_level_course_skill_level_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='author', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='courses', to=settings.AUTH_USER_MODEL, verbose_name='작성자'), + preserve_default=False, + ), + migrations.AddField( + model_name='curriculum', + name='author', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='curriculums', to=settings.AUTH_USER_MODEL, verbose_name='작성자'), + preserve_default=False, + ), + ] diff --git a/courses/mixins.py b/courses/mixins.py index 0995e34..c4f2000 100644 --- a/courses/mixins.py +++ b/courses/mixins.py @@ -16,12 +16,14 @@ class CourseMixin: """ @transaction.atomic - def create_course_with_lectures_and_topics(self, course_data, lectures_data): + def create_course_with_lectures_and_topics( + self, course_data, lectures_data, author + ): """ course 및 하위 모델 lecture, topic, assignment, quiz 등을 함께 생성합니다. """ - course = self._create_course(course_data) + course = self._create_course(course_data, author) for lecture_data in lectures_data: lecture = self._create_lecture(lecture_data, course) for topic_data in lecture_data.get("topics", []): @@ -46,7 +48,7 @@ def update_course_with_lectures_and_topics( topic = self._create_topic(topic_data, lecture) self._handle_topic_type(topic, topic_data) - def _create_course(self, course_data): + def _create_course(self, course_data, author): """ course 인스턴스를 생성합니다. """ @@ -56,8 +58,9 @@ def _create_course(self, course_data): short_description=course_data.get("short_description"), description=course_data.get("description"), category=course_data.get("category"), - course_level=course_data.get("course_level"), + skill_level=course_data.get("skill_level"), price=course_data.get("price"), + author=author, ) def _create_lecture(self, lecture_data, course): diff --git a/courses/models.py b/courses/models.py index f9bfc75..f51e067 100644 --- a/courses/models.py +++ b/courses/models.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.db import models @@ -6,9 +7,44 @@ class Curriculum(models.Model): 커리큘럼 모델입니다. """ + category_choices = [ + ("JavaScript", "JavaScript"), + ("Python", "Python"), + ("Django", "Django"), + ("React", "React"), + ("Vue", "Vue"), + ("Node", "Node"), + ("AWS", "AWS"), + ("Docker", "Docker"), + ("DB", "DB"), + ] + skill_level_choices = [ + ("beginner", "초급"), + ("intermediate", "중급"), + ("advanced", "고급"), + ] + + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="curriculums", + verbose_name="작성자", + ) name = models.CharField(max_length=255, verbose_name="커리큘럼 이름") description = models.TextField(verbose_name="설명") price = models.PositiveIntegerField(verbose_name="가격") + category = models.CharField( + max_length=255, + verbose_name="카테고리", + choices=category_choices, + default="JavaScript", + ) + skill_level = models.CharField( + max_length=255, + verbose_name="난이도", + choices=skill_level_choices, + default="beginner", + ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="생성일") updated_at = models.DateTimeField(auto_now=True, verbose_name="수정일") @@ -37,7 +73,7 @@ class Course(models.Model): ("Docker", "Docker"), ("DB", "DB"), ] - course_level_choices = [ + skill_level_choices = [ ("beginner", "초급"), ("intermediate", "중급"), ("advanced", "고급"), @@ -51,6 +87,12 @@ class Course(models.Model): related_name="courses", verbose_name="커리큘럼", ) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="courses", + verbose_name="작성자", + ) title = models.CharField(max_length=255, verbose_name="코스 제목") short_description = models.TextField(verbose_name="간단한 설명") description = models.JSONField(verbose_name="설명") @@ -60,10 +102,10 @@ class Course(models.Model): choices=category_choices, default="JavaScript", ) - course_level = models.CharField( + skill_level = models.CharField( max_length=255, verbose_name="난이도", - choices=course_level_choices, + choices=skill_level_choices, default="beginner", ) price = models.PositiveIntegerField(verbose_name="가격") @@ -83,7 +125,7 @@ def update(self, **kwargs): - short_description: 간단한 설명 - description: 설명 - category: 카테고리 - - course_level: 난이도 + - skill_level: 난이도 - price: 가격 """ for key, value in kwargs.items(): @@ -92,7 +134,7 @@ def update(self, **kwargs): "short_description", "description", "category", - "course_level", + "skill_level", "price", ]: continue diff --git a/courses/serializers.py b/courses/serializers.py index 277d951..9b9e86e 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -111,7 +111,7 @@ class Meta: "created_at", "updated_at", "lectures", - "course_level", + "skill_level", "price", ] read_only_fields = ["created_at", "updated_at", "id"] @@ -124,6 +124,8 @@ class CourseSummarySerializer(serializers.ModelSerializer): lectures_count = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField() + author_image = serializers.SerializerMethodField() + author_name = serializers.SerializerMethodField() class Meta: model = Course @@ -134,9 +136,11 @@ class Meta: "category", "created_at", "updated_at", - "course_level", + "skill_level", "lectures_count", "thumbnail", + "author_image", + "author_name", ] read_only_fields = [ "created_at", @@ -144,6 +148,8 @@ class Meta: "id", "lectures_count", "thumbnail", + "author_image", + "author_name", ] def get_lectures_count(self, obj): @@ -152,6 +158,12 @@ def get_lectures_count(self, obj): def get_thumbnail(self, obj): return obj.get_thumbnail() + def get_author_image(self, obj): + return "https://paullab.co.kr/images/weniv-licat.png" + + def get_author_name(self, obj): + return obj.author.nickname + class CurriculumReadSerializer(serializers.ModelSerializer): """ @@ -196,6 +208,10 @@ class CurriculumSummarySerializer(serializers.ModelSerializer): Curriculum 모델을 위한 Serializer입니다. 직렬화 할 때만 사용합니다. """ + author_image = serializers.SerializerMethodField() + author_name = serializers.SerializerMethodField() + courses_count = serializers.SerializerMethodField() + class Meta: model = Curriculum fields = [ @@ -204,5 +220,30 @@ class Meta: "price", "created_at", "updated_at", + "author_image", + "author_name", + "category", + "skill_level", + "description", + "courses_count", ] - read_only_fields = ["created_at", "updated_at", "id"] + read_only_fields = [ + "created_at", + "updated_at", + "id", + "author_image", + "author_name", + "category", + "skill_level", + "description", + "courses_count", + ] + + def get_author_image(self, obj): + return "https://paullab.co.kr/images/weniv-licat.png" + + def get_author_name(self, obj): + return obj.author.nickname + + def get_courses_count(self, obj): + return obj.courses.count() diff --git a/courses/test/conftest.py b/courses/test/conftest.py index 29e2588..70e5c09 100644 --- a/courses/test/conftest.py +++ b/courses/test/conftest.py @@ -17,7 +17,7 @@ COURSE_SHORT_DESCRIPTION = "Test Course" COURSE_DESCRIPTION = {} COURSE_CATEGORY = "JavaScript" -COURSE_COURSE_LEVEL = "beginner" +COURSE_SKILL_LEVEL = "beginner" COURSE_PRICE = 10000 LECTURE1_TITLE = "Test Lecture 1" LECTURE1_ORDER = 1 @@ -46,17 +46,18 @@ @pytest.fixture -def setup_course_data(): +def setup_course_data(create_staff_user): """ 테스트에서 사용할 Course, Lecture, Topic, Assignment, MultipleChoiceQuestion, MultipleChoiceQuestionChoice 인스턴스를 생성합니다. """ course = Course.objects.create( title=COURSE_TITLE, + author=create_staff_user, short_description=COURSE_SHORT_DESCRIPTION, description=COURSE_DESCRIPTION, category=COURSE_CATEGORY, - course_level=COURSE_COURSE_LEVEL, + skill_level=COURSE_SKILL_LEVEL, price=COURSE_PRICE, ) lecture1 = Lecture.objects.create( diff --git a/courses/test/test_mixins.py b/courses/test/test_mixins.py index 0d06488..5eb717a 100644 --- a/courses/test/test_mixins.py +++ b/courses/test/test_mixins.py @@ -7,7 +7,7 @@ @pytest.mark.django_db class TestCourseMixin: - def test_create_course_with_lectures_and_topics(self): + def test_create_course_with_lectures_and_topics(self, create_staff_user): # Given course_mixin = CourseMixin() course_data = { @@ -15,7 +15,7 @@ def test_create_course_with_lectures_and_topics(self): "short_description": "course_short_description", "description": "course_description", "category": "JavaScript", - "course_level": "beginner", + "skill_level": "beginner", "price": 10000, } lectures_data = [ @@ -87,7 +87,7 @@ def test_create_course_with_lectures_and_topics(self): # When course = course_mixin.create_course_with_lectures_and_topics( - course_data, lectures_data + course_data, lectures_data, create_staff_user ) # Then @@ -104,7 +104,7 @@ def test_create_course_with_lectures_and_topics(self): assert course.short_description == course_data["short_description"] assert course.description == course_data["description"] assert course.category == course_data["category"] - assert course.course_level == course_data["course_level"] + assert course.skill_level == course_data["skill_level"] assert course.price == course_data["price"] lectures = course.lectures.all() @@ -128,7 +128,7 @@ def test_create_course_with_lectures_and_topics(self): == 4 ) - def test_create_course(self): + def test_create_course(self, create_staff_user): # Given course_mixin = CourseMixin() course_data = { @@ -136,12 +136,12 @@ def test_create_course(self): "short_description": "course_short_description", "description": "course_description", "category": "JavaScript", - "course_level": "beginner", + "skill_level": "beginner", "price": 10000, } # When - course = course_mixin._create_course(course_data) + course = course_mixin._create_course(course_data, create_staff_user) # Then assert course is not None @@ -149,9 +149,9 @@ def test_create_course(self): assert course.short_description == course_data["short_description"] assert course.description == course_data["description"] assert course.category == course_data["category"] - assert course.course_level == course_data["course_level"] + assert course.skill_level == course_data["skill_level"] - def test_create_lecture(self): + def test_create_lecture(self, create_staff_user): # Given course_mixin = CourseMixin() lecture_data = { @@ -161,10 +161,11 @@ def test_create_lecture(self): course = Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ) # When @@ -176,7 +177,7 @@ def test_create_lecture(self): assert lecture.order == lecture_data["order"] assert lecture.course == course - def test_create_topic(self): + def test_create_topic(self, create_staff_user): # Given course_mixin = CourseMixin() topic_data = { @@ -189,10 +190,11 @@ def test_create_topic(self): lecture = Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ).lectures.create(title="lecture_title", order=1) # When @@ -207,7 +209,7 @@ def test_create_topic(self): assert topic.is_premium == topic_data["is_premium"] assert topic.lecture == lecture - def test_handle_topic_type_assignment(self): + def test_handle_topic_type_assignment(self, create_staff_user): # Given course_mixin = CourseMixin() topic_data = { @@ -223,10 +225,11 @@ def test_handle_topic_type_assignment(self): lecture = Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ).lectures.create(title="lecture_title", order=1) # When @@ -237,7 +240,7 @@ def test_handle_topic_type_assignment(self): assert topic.assignment is not None assert topic.assignment.question == topic_data["assignment"]["question"] - def test_handle_topic_type_quiz(self): + def test_handle_topic_type_quiz(self, create_staff_user): # Given course_mixin = CourseMixin() topic_data = { @@ -259,10 +262,11 @@ def test_handle_topic_type_quiz(self): lecture = Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ).lectures.create(title="lecture_title", order=1) # When @@ -279,7 +283,7 @@ def test_handle_topic_type_quiz(self): topic.multiple_choice_question.multiple_choice_question_choices.count() == 4 ) - def test_create_assignment(self): + def test_create_assignment(self, create_staff_user): # Given course_mixin = CourseMixin() assignment_data = { @@ -289,10 +293,11 @@ def test_create_assignment(self): Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ) .lectures.create(title="lecture_title", order=1) .topics.create( @@ -311,7 +316,7 @@ def test_create_assignment(self): assert topic.assignment is not None assert topic.assignment.question == assignment_data["question"] - def test_create_quiz(self): + def test_create_quiz(self, create_staff_user): # Given course_mixin = CourseMixin() multiple_choice_question_data = { @@ -327,10 +332,11 @@ def test_create_quiz(self): Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ) .lectures.create(title="lecture_title", order=1) .topics.create( @@ -361,16 +367,17 @@ def test_create_quiz(self): == 1 ) - def test_create_multiple_choice_question_choice(self): + def test_create_multiple_choice_question_choice(self, create_staff_user): # Given course_mixin = CourseMixin() course = Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ) lecture = Lecture.objects.create(title="lecture_title", course=course, order=1) topic = Topic.objects.create( @@ -403,16 +410,17 @@ def test_create_multiple_choice_question_choice(self): == 1 ) - def test_create_multiple_choice_question(self): + def test_create_multiple_choice_question(self, create_staff_user): # Given course_mixin = CourseMixin() course = Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ) lecture = Lecture.objects.create(title="lecture_title", course=course, order=1) topic = Topic.objects.create( @@ -452,23 +460,24 @@ def test_create_multiple_choice_question(self): == 1 ) - def test_update_course(self): + def test_update_course(self, create_staff_user): # Given course_mixin = CourseMixin() course = Course.objects.create( title="course_title", category="JavaScript", - course_level="beginner", + skill_level="beginner", short_description="course_short_description", description="course_description", price=10000, + author=create_staff_user, ) course_data = { "title": "updated_course_title", "short_description": "updated_course_short_description", "description": "updated_course_description", "category": "Python", - "course_level": "intermediate", + "skill_level": "intermediate", "price": 20000, } lectures_data = [ @@ -517,7 +526,7 @@ def test_update_course(self): assert course.short_description == course_data["short_description"] assert course.description == course_data["description"] assert course.category == course_data["category"] - assert course.course_level == course_data["course_level"] + assert course.skill_level == course_data["skill_level"] assert course.price == course_data["price"] assert course.lectures.count() == 1 assert course.lectures.first().topics.count() == 2 diff --git a/courses/test/test_models.py b/courses/test/test_models.py index 60e2268..1a833da 100644 --- a/courses/test/test_models.py +++ b/courses/test/test_models.py @@ -5,10 +5,13 @@ @pytest.mark.django_db -def test_course_생성(): +def test_course_생성(create_staff_user): # Given curriculum = Curriculum.objects.create( - name="Test Curriculum", description="Test Description", price=1000 + name="Test Curriculum", + description="Test Description", + price=1000, + author=create_staff_user, ) # When @@ -18,8 +21,9 @@ def test_course_생성(): short_description="Short Description", description={"content": "Detailed Description"}, category="Python", - course_level="beginner", + skill_level="beginner", price=500, + author=create_staff_user, ) # Then @@ -27,7 +31,7 @@ def test_course_생성(): assert course.short_description == "Short Description" assert course.description == {"content": "Detailed Description"} assert course.category == "Python" - assert course.course_level == "beginner" + assert course.skill_level == "beginner" assert course.price == 500 assert course.curriculum == curriculum assert course.created_at <= timezone.now() @@ -35,10 +39,13 @@ def test_course_생성(): @pytest.mark.django_db -def test_course_업데이트(): +def test_course_업데이트(create_staff_user): # Given curriculum = Curriculum.objects.create( - name="Test Curriculum", description="Test Description", price=1000 + name="Test Curriculum", + description="Test Description", + price=1000, + author=create_staff_user, ) course = Course.objects.create( curriculum=curriculum, @@ -46,8 +53,9 @@ def test_course_업데이트(): short_description="Short Description", description={"content": "Detailed Description"}, category="Python", - course_level="beginner", + skill_level="beginner", price=500, + author=create_staff_user, ) # When @@ -56,7 +64,7 @@ def test_course_업데이트(): short_description="Updated Short Description", description={"content": "Updated Detailed Description"}, category="Django", - course_level="intermediate", + skill_level="intermediate", price=700, ) @@ -66,15 +74,18 @@ def test_course_업데이트(): assert updated_course.short_description == "Updated Short Description" assert updated_course.description == {"content": "Updated Detailed Description"} assert updated_course.category == "Django" - assert updated_course.course_level == "intermediate" + assert updated_course.skill_level == "intermediate" assert updated_course.price == 700 @pytest.mark.django_db -def test_course_업데이트_course의_없는_필드는_무시된다(): +def test_course_업데이트_course의_없는_필드는_무시된다(create_staff_user): # Given curriculum = Curriculum.objects.create( - name="Test Curriculum", description="Test Description", price=1000 + name="Test Curriculum", + description="Test Description", + price=1000, + author=create_staff_user, ) course = Course.objects.create( curriculum=curriculum, @@ -82,8 +93,9 @@ def test_course_업데이트_course의_없는_필드는_무시된다(): short_description="Short Description", description={"content": "Detailed Description"}, category="Python", - course_level="beginner", + skill_level="beginner", price=500, + author=create_staff_user, ) # When @@ -95,15 +107,18 @@ def test_course_업데이트_course의_없는_필드는_무시된다(): assert updated_course.short_description == "Short Description" assert updated_course.description == {"content": "Detailed Description"} assert updated_course.category == "Python" - assert updated_course.course_level == "beginner" + assert updated_course.skill_level == "beginner" assert updated_course.price == 500 @pytest.mark.django_db -def test_course_업데이트_특정_필드만_업데이트(): +def test_course_업데이트_특정_필드만_업데이트(create_staff_user): # Given curriculum = Curriculum.objects.create( - name="Test Curriculum", description="Test Description", price=1000 + name="Test Curriculum", + description="Test Description", + price=1000, + author=create_staff_user, ) course = Course.objects.create( curriculum=curriculum, @@ -111,8 +126,9 @@ def test_course_업데이트_특정_필드만_업데이트(): short_description="Short Description", description={"content": "Detailed Description"}, category="Python", - course_level="beginner", + skill_level="beginner", price=500, + author=create_staff_user, ) # When @@ -124,6 +140,6 @@ def test_course_업데이트_특정_필드만_업데이트(): assert updated_course.short_description == "Short Description" assert updated_course.description == {"content": "Detailed Description"} assert updated_course.category == "Python" - assert updated_course.course_level == "beginner" + assert updated_course.skill_level == "beginner" assert updated_course.price == 500 assert updated_course.lectures.count() == 0 diff --git a/courses/test/test_serializers.py b/courses/test/test_serializers.py index 585c442..30ef826 100644 --- a/courses/test/test_serializers.py +++ b/courses/test/test_serializers.py @@ -36,7 +36,7 @@ def test_course_직렬화(self, setup_course_data): assert data["short_description"] == conftest.COURSE_SHORT_DESCRIPTION assert data["description"] == conftest.COURSE_DESCRIPTION assert data["category"] == conftest.COURSE_CATEGORY - assert data["course_level"] == conftest.COURSE_COURSE_LEVEL + assert data["skill_level"] == conftest.COURSE_SKILL_LEVEL assert data["price"] == conftest.COURSE_PRICE assert len(data["lectures"]) == 2 assert data["lectures"][0]["title"] == conftest.LECTURE1_TITLE @@ -132,7 +132,7 @@ def test_course_역직렬화(self): "short_description": "Test Course", "description": {}, "category": "JavaScript", - "course_level": "beginner", + "skill_level": "beginner", "price": 10000, "lectures": [ { @@ -197,7 +197,7 @@ def test_course_summary_직렬화(self, setup_course_data): assert data["title"] == conftest.COURSE_TITLE assert data["short_description"] == conftest.COURSE_SHORT_DESCRIPTION assert data["category"] == conftest.COURSE_CATEGORY - assert data["course_level"] == conftest.COURSE_COURSE_LEVEL + assert data["skill_level"] == conftest.COURSE_SKILL_LEVEL assert data["lectures_count"] == 2 def test_course_summary_역직렬화(self): @@ -206,7 +206,7 @@ def test_course_summary_역직렬화(self): "title": "Test Course", "short_description": "Test Course", "category": "JavaScript", - "course_level": "beginner", + "skill_level": "beginner", } # When @@ -601,10 +601,13 @@ def test_curriculum_create_and_update_역직렬화(self): @pytest.mark.django_db class TestCurriculumReadSerializer: - def test_curriculum_직렬화(self, setup_course_data): + def test_curriculum_직렬화(self, setup_course_data, create_staff_user): # Given curriculum = Curriculum.objects.create( - name="Test Curriculum", description="Test Description", price=1000 + name="Test Curriculum", + description="Test Description", + price=1000, + author=create_staff_user, ) course = setup_course_data["course"] curriculum.courses.add(course) @@ -623,10 +626,13 @@ def test_curriculum_직렬화(self, setup_course_data): @pytest.mark.django_db class TestCurriculumSummarySerializer: - def test_curriculum_직렬화(self): + def test_curriculum_summary_직렬화(self, create_staff_user): # Given curriculum = Curriculum.objects.create( - name="Test Curriculum", description="Test Description", price=1000 + name="Test Curriculum", + description="Test Description", + price=1000, + author=create_staff_user, ) # When diff --git a/courses/test/test_views.py b/courses/test/test_views.py index 38e2cdc..544e7ce 100644 --- a/courses/test/test_views.py +++ b/courses/test/test_views.py @@ -21,7 +21,7 @@ def test_course_조회(self, api_client, setup_course_data): assert response.data["title"] == conftest.COURSE_TITLE assert response.data["short_description"] == conftest.COURSE_SHORT_DESCRIPTION assert response.data["category"] == conftest.COURSE_CATEGORY - assert response.data["course_level"] == conftest.COURSE_COURSE_LEVEL + assert response.data["skill_level"] == conftest.COURSE_SKILL_LEVEL assert response.data["created_at"] is not None assert response.data["updated_at"] is not None assert len(response.data["lectures"]) == 2 @@ -54,7 +54,7 @@ def test_course_수정(self, api_client, setup_course_data, staff_user_token): "short_description": "Updated Test Course", "description": {}, "category": "Python", - "course_level": "intermediate", + "skill_level": "intermediate", "price": 20000, "lectures": [ { @@ -104,7 +104,7 @@ def test_course_수정(self, api_client, setup_course_data, staff_user_token): assert response.data["title"] == "Updated Test Course" assert response.data["short_description"] == "Updated Test Course" assert response.data["category"] == "Python" - assert response.data["course_level"] == "intermediate" + assert response.data["skill_level"] == "intermediate" assert response.data["created_at"] is not None assert response.data["updated_at"] is not None assert len(response.data["lectures"]) == 2 @@ -129,15 +129,16 @@ def test_course_수정(self, api_client, setup_course_data, staff_user_token): == "Updated Choice 1" ) - def test_course_수정_실패_로그인하지않은경우(self, api_client): + def test_course_수정_실패_로그인하지않은경우(self, api_client, create_staff_user): # Given course = Course.objects.create( title="Test Course", short_description="Test Course", description={}, category="JavaScript", - course_level="beginner", + skill_level="beginner", price=10000, + author=create_staff_user, ) url = reverse("courses:course-detail", args=[course.id]) data = { @@ -145,7 +146,7 @@ def test_course_수정_실패_로그인하지않은경우(self, api_client): "short_description": "Updated Test Course", "description": {}, "category": "Python", - "course_level": "intermediate", + "skill_level": "intermediate", "price": 20000, } @@ -158,15 +159,18 @@ def test_course_수정_실패_로그인하지않은경우(self, api_client): "detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다." } - def test_course_수정_실패_일반유저인_경우(self, api_client, user_token): + def test_course_수정_실패_일반유저인_경우( + self, api_client, user_token, create_staff_user + ): # Given course = Course.objects.create( title="Test Course", short_description="Test Course", description={}, category="JavaScript", - course_level="beginner", + skill_level="beginner", price=10000, + author=create_staff_user, ) url = reverse("courses:course-detail", args=[course.id]) api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {user_token}") @@ -175,7 +179,7 @@ def test_course_수정_실패_일반유저인_경우(self, api_client, user_toke "short_description": "Updated Test Course", "description": {}, "category": "Python", - "course_level": "intermediate", + "skill_level": "intermediate", "price": 20000, } @@ -188,15 +192,16 @@ def test_course_수정_실패_일반유저인_경우(self, api_client, user_toke "detail": "이 작업을 수행할 권한(permission)이 없습니다." } - def test_course_삭제(self, api_client, staff_user_token): + def test_course_삭제(self, api_client, staff_user_token, create_staff_user): # Given course = Course.objects.create( title="Test Course", short_description="Test Course", description={}, category="JavaScript", - course_level="beginner", + skill_level="beginner", price=10000, + author=create_staff_user, ) url = reverse("courses:course-detail", args=[course.id]) api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {staff_user_token}") @@ -208,15 +213,16 @@ def test_course_삭제(self, api_client, staff_user_token): assert response.status_code == status.HTTP_204_NO_CONTENT assert Course.objects.count() == 0 - def test_course_삭제_실패_로그인하지않은경우(self, api_client): + def test_course_삭제_실패_로그인하지않은경우(self, api_client, create_staff_user): # Given course = Course.objects.create( title="Test Course", short_description="Test Course", description={}, category="JavaScript", - course_level="beginner", + skill_level="beginner", price=10000, + author=create_staff_user, ) url = reverse("courses:course-detail", args=[course.id]) @@ -229,15 +235,18 @@ def test_course_삭제_실패_로그인하지않은경우(self, api_client): "detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다." } - def test_course_삭제_실패_일반유저인_경우(self, api_client, user_token): + def test_course_삭제_실패_일반유저인_경우( + self, api_client, user_token, create_staff_user + ): # Given course = Course.objects.create( title="Test Course", short_description="Test Course", description={}, category="JavaScript", - course_level="beginner", + skill_level="beginner", price=10000, + author=create_staff_user, ) url = reverse("courses:course-detail", args=[course.id]) api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {user_token}") @@ -258,7 +267,7 @@ def get_course_data(): "short_description": "Test Course", "description": {}, "category": "JavaScript", - "course_level": "beginner", + "skill_level": "beginner", "price": 10000, "lectures": [ { @@ -353,8 +362,9 @@ def test_course_목록_조회(self, api_client, create_user): short_description="Test Course", description={}, category="JavaScript", - course_level="beginner", + skill_level="beginner", price=10000, + author=create_user, ) url = reverse("courses:course-list") api_client.login( @@ -434,13 +444,16 @@ def test_curriculum_생성_요청_실패_로그인하지않은경우( "detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다." } - def test_curriculum_목록_조회(self, api_client, setup_course_data): + def test_curriculum_목록_조회( + self, api_client, setup_course_data, create_staff_user + ): # Given for i in range(5): Curriculum.objects.create( name=f"Test Curriculum {i}", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-list") api_client.login( @@ -458,12 +471,13 @@ def test_curriculum_목록_조회(self, api_client, setup_course_data): @pytest.mark.django_db class TestCurriculumDetail: - def test_curriculum_조회(self, api_client, setup_course_data): + def test_curriculum_조회(self, api_client, setup_course_data, create_staff_user): # Given curriculum = Curriculum.objects.create( name="Test Curriculum", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-detail", args=[curriculum.id]) course = setup_course_data["course"] @@ -485,12 +499,15 @@ def test_curriculum_조회(self, api_client, setup_course_data): assert response.data["updated_at"] is not None assert len(response.data["courses"]) == 1 - def test_curriculum_수정(self, api_client, setup_course_data, staff_user_token): + def test_curriculum_수정( + self, api_client, setup_course_data, staff_user_token, create_staff_user + ): # Given curriculum = Curriculum.objects.create( name="Test Curriculum", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-detail", args=[curriculum.id]) api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {staff_user_token}") @@ -511,13 +528,14 @@ def test_curriculum_수정(self, api_client, setup_course_data, staff_user_token assert response.data["price"] == 2000 def test_curriculum_수정_실패_일반유저인_경우( - self, api_client, setup_course_data, user_token + self, api_client, setup_course_data, user_token, create_staff_user ): # Given curriculum = Curriculum.objects.create( name="Test Curriculum", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-detail", args=[curriculum.id]) api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {user_token}") @@ -538,13 +556,14 @@ def test_curriculum_수정_실패_일반유저인_경우( } def test_curriculum_수정_실패_로그인하지않은경우( - self, api_client, setup_course_data + self, api_client, setup_course_data, create_staff_user ): # Given curriculum = Curriculum.objects.create( name="Test Curriculum", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-detail", args=[curriculum.id]) data = { @@ -563,12 +582,13 @@ def test_curriculum_수정_실패_로그인하지않은경우( "detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다." } - def test_curriculum_삭제(self, api_client, staff_user_token): + def test_curriculum_삭제(self, api_client, staff_user_token, create_staff_user): # Given curriculum = Curriculum.objects.create( name="Test Curriculum", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-detail", args=[curriculum.id]) api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {staff_user_token}") @@ -580,12 +600,15 @@ def test_curriculum_삭제(self, api_client, staff_user_token): assert response.status_code == status.HTTP_204_NO_CONTENT assert Curriculum.objects.count() == 0 - def test_curriculum_삭제_실패_일반유저인_경우(self, api_client, user_token): + def test_curriculum_삭제_실패_일반유저인_경우( + self, api_client, user_token, create_staff_user + ): # Given curriculum = Curriculum.objects.create( name="Test Curriculum", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-detail", args=[curriculum.id]) api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {user_token}") @@ -599,12 +622,15 @@ def test_curriculum_삭제_실패_일반유저인_경우(self, api_client, user_ "detail": "이 작업을 수행할 권한(permission)이 없습니다." } - def test_curriculum_삭제_실패_로그인하지않은경우(self, api_client): + def test_curriculum_삭제_실패_로그인하지않은경우( + self, api_client, create_staff_user + ): # Given curriculum = Curriculum.objects.create( name="Test Curriculum", description="Test Description", price=1000, + author=create_staff_user, ) url = reverse("courses:curriculum-detail", args=[curriculum.id]) @@ -618,7 +644,9 @@ def test_curriculum_삭제_실패_로그인하지않은경우(self, api_client): } def test_curriculum_수정_실패_존재하지않는_curriculum인_경우( - self, api_client, staff_user_token + self, + api_client, + staff_user_token, ): # Given url = reverse("courses:curriculum-detail", args=[1]) diff --git a/courses/views.py b/courses/views.py index 3ce0a3f..e5be7ac 100644 --- a/courses/views.py +++ b/courses/views.py @@ -123,7 +123,7 @@ class CourseListCreateView(CourseMixin, generics.ListCreateAPIView): filters.OrderingFilter, ] search_fields = ["title", "short_description", "description"] - filterset_fields = ["category", "course_level"] + filterset_fields = ["category", "skill_level"] ordering_fields = ["created_at", "price"] def get_serializer_class(self): @@ -143,9 +143,10 @@ def perform_create(self, serializer): """ course 및 하위 모델 lecture, topic, assignment, quiz 등을 함께 생성합니다. """ + author = self.request.user if self.request.user.is_staff else None self.create_course_with_lectures_and_topics( - serializer.data, serializer.data.get("lectures", []) + serializer.data, serializer.data.get("lectures", []), author ) @@ -196,10 +197,12 @@ def perform_create(self, serializer): """ curriculum을 생성합니다. """ + author = self.request.user if self.request.user.is_staff else None curriculum = Curriculum.objects.create( name=serializer.data.get("name"), description=serializer.data.get("description"), price=serializer.data.get("price"), + author=author, ) courses_ids = serializer.data.get("courses_ids", []) Course.objects.filter(id__in=courses_ids).update(curriculum=curriculum) From d4f4745556090d291a350fe579811b1d38a1d665 Mon Sep 17 00:00:00 2001 From: ALbertIM0427 Date: Sun, 13 Oct 2024 20:58:23 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 5 ++ .../0010_remove_topic_description.py | 17 ++++ courses/mixins.py | 13 ++- courses/models.py | 5 +- courses/serializers.py | 80 ++++++++++++++++- courses/test/conftest.py | 18 +++- courses/test/test_mixins.py | 6 -- courses/test/test_serializers.py | 88 ++++--------------- courses/test/test_views.py | 17 ++-- courses/views.py | 34 +++---- jwtauth/utils/token_generator.py | 6 +- materials/admin.py | 5 +- ..._image_file_remove_image_image_and_more.py | 65 ++++++++++++++ materials/migrations/0005_image_author.py | 22 +++++ .../migrations/0006_alter_video_topic.py | 20 +++++ materials/models.py | 23 +++-- materials/serializers.py | 77 ++++++++-------- materials/views.py | 40 +++++---- payments/mixins.py | 10 ++- payments/tests/conftest.py | 22 +++-- payments/views.py | 27 +++--- weaverse/settings.py | 2 +- 22 files changed, 386 insertions(+), 216 deletions(-) create mode 100644 courses/migrations/0010_remove_topic_description.py create mode 100644 materials/migrations/0004_remove_image_file_remove_image_image_and_more.py create mode 100644 materials/migrations/0005_image_author.py create mode 100644 materials/migrations/0006_alter_video_topic.py diff --git a/accounts/models.py b/accounts/models.py index b5252dc..c308052 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -97,3 +97,8 @@ class CustomUser(AbstractUser): def __str__(self): return self.email + + def get_image_url(self): + if getattr(self, "image", None): + return self.image.image_url + return "https://paullab.co.kr/images/weniv-licat.png" diff --git a/courses/migrations/0010_remove_topic_description.py b/courses/migrations/0010_remove_topic_description.py new file mode 100644 index 0000000..4fbcdcf --- /dev/null +++ b/courses/migrations/0010_remove_topic_description.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.1 on 2024-10-13 08:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0009_course_author_curriculum_author'), + ] + + operations = [ + migrations.RemoveField( + model_name='topic', + name='description', + ), + ] diff --git a/courses/mixins.py b/courses/mixins.py index c4f2000..f813cdb 100644 --- a/courses/mixins.py +++ b/courses/mixins.py @@ -1,5 +1,7 @@ from django.db import transaction +from materials.models import Image, Video + from .models import ( Assignment, Course, @@ -53,7 +55,7 @@ def _create_course(self, course_data, author): course 인스턴스를 생성합니다. """ - return Course.objects.create( + course = Course.objects.create( title=course_data.get("title"), short_description=course_data.get("short_description"), description=course_data.get("description"), @@ -62,6 +64,10 @@ def _create_course(self, course_data, author): price=course_data.get("price"), author=author, ) + Image.objects.filter(id=course_data.get("thumbnail_id")).update(course=course) + Video.objects.filter(id=course_data.get("video_id")).update(course=course) + + return course def _create_lecture(self, lecture_data, course): """ @@ -79,14 +85,15 @@ def _create_topic(self, topic_data, lecture): topic 인스턴스를 생성합니다. """ - return Topic.objects.create( + topic = Topic.objects.create( lecture=lecture, title=topic_data.get("title"), type=topic_data.get("type"), - description=topic_data.get("description"), order=topic_data.get("order"), is_premium=topic_data.get("is_premium"), ) + Video.objects.filter(id=topic_data.get("video_id")).update(topic=topic) + return topic def _handle_topic_type(self, topic, topic_data): """ diff --git a/courses/models.py b/courses/models.py index f51e067..e373f7c 100644 --- a/courses/models.py +++ b/courses/models.py @@ -164,7 +164,7 @@ class Lecture(models.Model): updated_at = models.DateTimeField(auto_now=True, verbose_name="수정일") def __str__(self): - return f"{self.course.title} - {self.title}" + return f"{self.id} - {self.title}" class Meta: ordering = ["order"] @@ -194,14 +194,13 @@ class Topic(models.Model): choices=topic_type_choices, default="video", ) - description = models.TextField(verbose_name="설명") order = models.PositiveIntegerField(verbose_name="순서") is_premium = models.BooleanField(verbose_name="프리미엄 여부", default=False) created_at = models.DateTimeField(auto_now_add=True, verbose_name="생성일") updated_at = models.DateTimeField(auto_now=True, verbose_name="수정일") def __str__(self): - return f"{self.lecture.title} - {self.title}" + return f"{self.id} - {self.title}" class Meta: ordering = ["order"] diff --git a/courses/serializers.py b/courses/serializers.py index 9b9e86e..1102366 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -59,6 +59,9 @@ class TopicSerializer(serializers.ModelSerializer): multiple_choice_question = MultipleChoiceQuestionSerializer(required=False) assignment = AssignmentSerializer(required=False) + video_url = serializers.SerializerMethodField() + video_id = serializers.IntegerField(write_only=True, required=False) + video_duration = serializers.SerializerMethodField() class Meta: model = Topic @@ -66,15 +69,33 @@ class Meta: "id", "title", "type", - "description", "order", "is_premium", "created_at", "updated_at", "multiple_choice_question", "assignment", + "video_url", + "video_id", + "video_duration", ] - read_only_fields = ["created_at", "updated_at", "id"] + read_only_fields = [ + "created_at", + "updated_at", + "id", + "video_url", + "video_duration", + ] + + def get_video_url(self, obj): + if getattr(obj, "video", None): + return obj.video.video_url + return None + + def get_video_duration(self, obj): + if getattr(obj, "video", None): + return 0 + return None class LectureSerializer(serializers.ModelSerializer): @@ -95,10 +116,18 @@ class CourseDetailSerializer(serializers.ModelSerializer): Course 모델을 위한 Serializer입니다 """ + video_id = serializers.IntegerField(write_only=True) + thumbnail_id = serializers.IntegerField(write_only=True) lectures = LectureSerializer( many=True, required=False, ) + video_url = serializers.SerializerMethodField() + thumbnail_url = serializers.SerializerMethodField() + author_image = serializers.SerializerMethodField() + author_name = serializers.SerializerMethodField() + author_id = serializers.SerializerMethodField() + author_introduction = serializers.SerializerMethodField() class Meta: model = Course @@ -113,8 +142,53 @@ class Meta: "lectures", "skill_level", "price", + "thumbnail_id", + "video_id", + "video_url", + "thumbnail_url", + "author_image", + "author_name", + "author_id", + "author_introduction", ] - read_only_fields = ["created_at", "updated_at", "id"] + read_only_fields = [ + "created_at", + "updated_at", + "id", + "video_url", + "thumbnail_url", + "author_image", + "author_name", + "author_id", + "author_introduction", + ] + + def get_author_image(self, obj): + print(obj.author.image.image_url) + if getattr(obj.author, "image", None): + return obj.author.image.image_url + return None + + def get_author_name(self, obj): + return obj.author.nickname + + def get_video_url(self, obj): + if getattr(obj, "video", None): + return obj.video.video_url + return None + + def get_thumbnail_url(self, obj): + if getattr(obj, "thumbnail", None): + return obj.thumbnail.url + return None + + def get_author_id(self, obj): + return obj.author.id + + def get_author_introduction(self, obj): + return ( + obj.author.introduction if obj.author.introduction else "소개가 없습니다." + ) class CourseSummarySerializer(serializers.ModelSerializer): diff --git a/courses/test/conftest.py b/courses/test/conftest.py index 70e5c09..728d30a 100644 --- a/courses/test/conftest.py +++ b/courses/test/conftest.py @@ -11,6 +11,7 @@ Topic, ) from jwtauth.utils.token_generator import generate_access_token +from materials.models import Image, Video # 테스트에서 사용할 상수를 정의합니다. COURSE_TITLE = "Test Course" @@ -72,7 +73,6 @@ def setup_course_data(create_staff_user): title=TOPIC1_TITLE, lecture=lecture1, type=TOPIC1_TYPE, - description=TOPIC1_DESCRIPTION, order=1, is_premium=True, ) @@ -80,7 +80,6 @@ def setup_course_data(create_staff_user): title=TOPIC2_TITLE, lecture=lecture2, type=TOPIC2_TYPE, - description=TOPIC2_DESCRIPTION, order=TOPIC2_ORDER, is_premium=True, ) @@ -134,19 +133,23 @@ def api_client(): @pytest.fixture(autouse=True) def create_user(): - return User.objects.create_user( + user = User.objects.create_user( email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD, nickname="testuser" ) + Image.objects.create(user=user, image_url="test.jpg") + return user @pytest.fixture(autouse=True) def create_staff_user(): - return User.objects.create_user( + user = User.objects.create_user( email=TEST_STAFF_USER_EMAIL, password=TEST_STAFF_USER_PASSWORD, is_staff=True, nickname="staffuser", ) + Image.objects.create(user=user, image_url="test.jpg") + return user @pytest.fixture() @@ -157,3 +160,10 @@ def user_token(create_user): @pytest.fixture() def staff_user_token(create_staff_user): return generate_access_token(create_staff_user) + + +@pytest.fixture +def create_video(): + return Video.objects.create( + video_url="https://www.youtube.com/watch?v=123456", + ) diff --git a/courses/test/test_mixins.py b/courses/test/test_mixins.py index 5eb717a..37d736b 100644 --- a/courses/test/test_mixins.py +++ b/courses/test/test_mixins.py @@ -183,7 +183,6 @@ def test_create_topic(self, create_staff_user): topic_data = { "title": "topic_title", "type": "assignment", - "description": "topic_description", "order": 1, "is_premium": True, } @@ -204,7 +203,6 @@ def test_create_topic(self, create_staff_user): assert topic is not None assert topic.title == topic_data["title"] assert topic.type == topic_data["type"] - assert topic.description == topic_data["description"] assert topic.order == topic_data["order"] assert topic.is_premium == topic_data["is_premium"] assert topic.lecture == lecture @@ -303,7 +301,6 @@ def test_create_assignment(self, create_staff_user): .topics.create( title="topic_title", type="assignment", - description="topic_description", order=1, is_premium=True, ) @@ -342,7 +339,6 @@ def test_create_quiz(self, create_staff_user): .topics.create( title="topic_title", type="quiz", - description="topic_description", order=1, is_premium=True, ) @@ -384,7 +380,6 @@ def test_create_multiple_choice_question_choice(self, create_staff_user): title="topic_title", lecture=lecture, type="quiz", - description="topic_description", order=1, is_premium=True, ) @@ -427,7 +422,6 @@ def test_create_multiple_choice_question(self, create_staff_user): title="topic_title", lecture=lecture, type="quiz", - description="topic_description", order=1, is_premium=True, ) diff --git a/courses/test/test_serializers.py b/courses/test/test_serializers.py index 30ef826..c723ee4 100644 --- a/courses/test/test_serializers.py +++ b/courses/test/test_serializers.py @@ -34,7 +34,6 @@ def test_course_직렬화(self, setup_course_data): assert data["id"] == self.course.id assert data["title"] == conftest.COURSE_TITLE assert data["short_description"] == conftest.COURSE_SHORT_DESCRIPTION - assert data["description"] == conftest.COURSE_DESCRIPTION assert data["category"] == conftest.COURSE_CATEGORY assert data["skill_level"] == conftest.COURSE_SKILL_LEVEL assert data["price"] == conftest.COURSE_PRICE @@ -46,10 +45,6 @@ def test_course_직렬화(self, setup_course_data): assert len(data["lectures"][0]["topics"]) == 1 assert data["lectures"][0]["topics"][0]["title"] == conftest.TOPIC1_TITLE assert data["lectures"][0]["topics"][0]["type"] == conftest.TOPIC1_TYPE - assert ( - data["lectures"][0]["topics"][0]["description"] - == conftest.TOPIC1_DESCRIPTION - ) assert data["lectures"][0]["topics"][0]["order"] == conftest.TOPIC1_ORDER assert data["lectures"][0]["topics"][0]["is_premium"] is True assert ( @@ -58,10 +53,6 @@ def test_course_직렬화(self, setup_course_data): ) assert data["lectures"][1]["topics"][0]["title"] == conftest.TOPIC2_TITLE assert data["lectures"][1]["topics"][0]["type"] == conftest.TOPIC2_TYPE - assert ( - data["lectures"][1]["topics"][0]["description"] - == conftest.TOPIC2_DESCRIPTION - ) assert data["lectures"][1]["topics"][0]["order"] == conftest.TOPIC2_ORDER assert data["lectures"][1]["topics"][0]["is_premium"] is True assert ( @@ -134,6 +125,8 @@ def test_course_역직렬화(self): "category": "JavaScript", "skill_level": "beginner", "price": 10000, + "thumbnail_id": 1, + "video_id": 3, "lectures": [ { "title": "Test Lecture", @@ -146,6 +139,7 @@ def test_course_역직렬화(self): "order": 1, "is_premium": True, "assignment": {"question": "Test Assignment"}, + "video_id": 1, } ], }, @@ -156,7 +150,6 @@ def test_course_역직렬화(self): { "title": "Test Topic 2", "type": "assignment", - "description": "Test Description", "order": 1, "is_premium": True, "multiple_choice_question": { @@ -168,6 +161,7 @@ def test_course_역직렬화(self): {"choice": "Choice 4", "is_correct": False}, ], }, + "video_id": 2, } ], }, @@ -179,7 +173,17 @@ def test_course_역직렬화(self): serializer.is_valid(raise_exception=True) # Then - assert serializer.validated_data == data + assert serializer.validated_data["title"] == "Test Course" + assert serializer.validated_data["short_description"] == "Test Course" + assert serializer.validated_data["category"] == "JavaScript" + assert serializer.validated_data["skill_level"] == "beginner" + assert serializer.validated_data["price"] == 10000 + assert serializer.validated_data["lectures"][0]["title"] == "Test Lecture" + assert serializer.validated_data["lectures"][0]["order"] == 1 + assert ( + serializer.validated_data["lectures"][0]["topics"][0]["title"] + == "Test Topic" + ) @pytest.mark.django_db @@ -235,7 +239,6 @@ def test_lecture_직렬화(self, setup_course_data): assert len(data["topics"]) == 1 assert data["topics"][0]["title"] == conftest.TOPIC2_TITLE assert data["topics"][0]["type"] == conftest.TOPIC2_TYPE - assert data["topics"][0]["description"] == conftest.TOPIC2_DESCRIPTION assert data["topics"][0]["order"] == conftest.TOPIC2_ORDER assert data["topics"][0]["is_premium"] is True assert ( @@ -299,39 +302,6 @@ def test_lecture_직렬화(self, setup_course_data): is False ) - def test_lecture_역직렬화(self): - # Given - data = { - "title": "Test Lecture", - "order": 1, - "topics": [ - { - "title": "Test Topic", - "type": "quiz", - "description": "Test Description", - "order": 1, - "is_premium": True, - "assignment": {"question": "Test Assignment"}, - "multiple_choice_question": { - "question": "Test Multiple Choice Question", - "multiple_choice_question_choices": [ - {"choice": "Choice 1", "is_correct": True}, - {"choice": "Choice 2", "is_correct": False}, - {"choice": "Choice 3", "is_correct": False}, - {"choice": "Choice 4", "is_correct": False}, - ], - }, - } - ], - } - - # When - serializer = LectureSerializer(data=data) - serializer.is_valid(raise_exception=True) - - # Then - assert serializer.validated_data == data - @pytest.mark.django_db class TestTopicSerializer: @@ -348,7 +318,6 @@ def test_topic_직렬화(self, setup_course_data): assert data["id"] == self.topic.id assert data["title"] == conftest.TOPIC2_TITLE assert data["type"] == conftest.TOPIC2_TYPE - assert data["description"] == conftest.TOPIC2_DESCRIPTION assert data["order"] == conftest.TOPIC2_ORDER assert data["is_premium"] is True assert data["multiple_choice_question"]["question"] == conftest.MCQ_QUESTION @@ -405,33 +374,6 @@ def test_topic_직렬화(self, setup_course_data): is False ) - def test_topic_역직렬화(self): - # Given - data = { - "title": "Test Topic", - "type": "quiz", - "description": "Test Description", - "order": 1, - "is_premium": True, - "assignment": {"question": "Test Assignment"}, - "multiple_choice_question": { - "question": "Test Multiple Choice Question", - "multiple_choice_question_choices": [ - {"choice": "Choice 1", "is_correct": True}, - {"choice": "Choice 2", "is_correct": False}, - {"choice": "Choice 3", "is_correct": False}, - {"choice": "Choice 4", "is_correct": False}, - ], - }, - } - - # When - serializer = TopicSerializer(data=data) - serializer.is_valid(raise_exception=True) - - # Then - assert serializer.validated_data == data - @pytest.mark.django_db class TestAssignmentSerializer: diff --git a/courses/test/test_views.py b/courses/test/test_views.py index 544e7ce..5c2681d 100644 --- a/courses/test/test_views.py +++ b/courses/test/test_views.py @@ -56,6 +56,8 @@ def test_course_수정(self, api_client, setup_course_data, staff_user_token): "category": "Python", "skill_level": "intermediate", "price": 20000, + "video_id": 1, + "thumbnail_id": 1, "lectures": [ { "title": "Updated Test Lecture", @@ -63,11 +65,11 @@ def test_course_수정(self, api_client, setup_course_data, staff_user_token): "topics": [ { "title": "Updated Test Topic", - "type": "assignment", - "description": "Updated Test Description", + "type": "video", "order": 1, "is_premium": True, "assignment": {"question": "Updated Test Assignment"}, + "video_id": 1, } ], }, @@ -90,6 +92,7 @@ def test_course_수정(self, api_client, setup_course_data, staff_user_token): {"choice": "Updated Choice 4", "is_correct": False}, ], }, + "video_id": 2, } ], }, @@ -112,10 +115,6 @@ def test_course_수정(self, api_client, setup_course_data, staff_user_token): assert response.data["lectures"][1]["title"] == "Updated Test Lecture 2" assert len(response.data["lectures"][0]["topics"]) == 1 assert len(response.data["lectures"][1]["topics"]) == 1 - assert ( - response.data["lectures"][0]["topics"][0]["assignment"]["question"] - == "Updated Test Assignment" - ) assert ( response.data["lectures"][1]["topics"][0]["multiple_choice_question"][ "question" @@ -269,6 +268,8 @@ def get_course_data(): "category": "JavaScript", "skill_level": "beginner", "price": 10000, + "video_id": 1, + "thumbnail_id": 1, "lectures": [ { "title": "Test Lecture", @@ -276,11 +277,11 @@ def get_course_data(): "topics": [ { "title": "Test Topic", - "type": "assignment", + "type": "video", "description": "Test Description", "order": 1, "is_premium": True, - "assignment": {"question": "Test Assignment"}, + "video_id": 1, } ], }, diff --git a/courses/views.py b/courses/views.py index e5be7ac..cf95ae1 100644 --- a/courses/views.py +++ b/courses/views.py @@ -57,6 +57,7 @@ class CourseDetailRetrieveUpdateDestroyView( queryset = Course.objects.prefetch_related( "lectures__topics__multiple_choice_question__multiple_choice_question_choices", "lectures__topics__assignment", + "author", ) serializer_class = CourseDetailSerializer permission_classes = [IsStaffOrReadOnly] @@ -71,6 +72,7 @@ def get_permissions(self): return [] return super().get_permissions() + @transaction.atomic def update(self, request, *args, **kwargs): """ course를 수정합니다. @@ -79,22 +81,16 @@ def update(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) course = self.get_object() - self.perform_update(serializer, course) + self.update_course_with_lectures_and_topics( + course, + serializer.validated_data, + serializer.validated_data.get("lectures", []), + ) if getattr(course, "_prefetched_objects_cache", None): course._prefetched_objects_cache = {} serializer = self.get_serializer(course) return Response(serializer.data) - @transaction.atomic - def perform_update(self, serializer, course): - """ - course 및 하위 모델 lecture, topic, assignment, quiz 등을 함께 수정합니다. - """ - - self.update_course_with_lectures_and_topics( - course, serializer.data, serializer.data.get("lectures", []) - ) - @extend_schema_view( get=extend_schema( @@ -139,15 +135,23 @@ def get_serializer_class(self): return CourseSummarySerializer @transaction.atomic - def perform_create(self, serializer): + def create(self, request, *args, **kwargs): """ - course 및 하위 모델 lecture, topic, assignment, quiz 등을 함께 생성합니다. + course를 생성합니다. """ + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) author = self.request.user if self.request.user.is_staff else None - self.create_course_with_lectures_and_topics( - serializer.data, serializer.data.get("lectures", []), author + course = self.create_course_with_lectures_and_topics( + serializer.validated_data, + serializer.validated_data.get("lectures", []), + author, ) + serializer = self.get_serializer(course) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=201, headers=headers) @extend_schema_view( diff --git a/jwtauth/utils/token_generator.py b/jwtauth/utils/token_generator.py index 2aae7ea..5d22f5b 100644 --- a/jwtauth/utils/token_generator.py +++ b/jwtauth/utils/token_generator.py @@ -1,5 +1,6 @@ -import jwt from datetime import timedelta + +import jwt from django.conf import settings from django.utils import timezone @@ -8,6 +9,7 @@ def generate_access_token(user): """ 사용자 정보를 받아서 access token을 생성합니다. """ + payload = { "user_id": user.id, "is_staff": user.is_staff, @@ -15,7 +17,7 @@ def generate_access_token(user): "iat": timezone.now(), "nickname": user.nickname, "email": user.email, - "image": user.image, + "image": user.get_image_url(), "exp": timezone.now() + timedelta(minutes=30), } return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") diff --git a/materials/admin.py b/materials/admin.py index 8c38f3f..f2b6b26 100644 --- a/materials/admin.py +++ b/materials/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin -# Register your models here. +from .models import Image, Video + +admin.site.register(Image) +admin.site.register(Video) diff --git a/materials/migrations/0004_remove_image_file_remove_image_image_and_more.py b/materials/migrations/0004_remove_image_file_remove_image_image_and_more.py new file mode 100644 index 0000000..269a662 --- /dev/null +++ b/materials/migrations/0004_remove_image_file_remove_image_image_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 5.1.1 on 2024-10-13 09:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0010_remove_topic_description'), + ('materials', '0003_image_image_image_user_alter_image_course_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='image', + name='file', + ), + migrations.RemoveField( + model_name='image', + name='image', + ), + migrations.RemoveField( + model_name='image', + name='title', + ), + migrations.RemoveField( + model_name='video', + name='file', + ), + migrations.RemoveField( + model_name='video', + name='title', + ), + migrations.AddField( + model_name='image', + name='image_url', + field=models.URLField(default='', verbose_name='이미지 파일'), + preserve_default=False, + ), + migrations.AddField( + model_name='video', + name='course', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='video', to='courses.course'), + ), + migrations.AddField( + model_name='video', + name='video_url', + field=models.URLField(default='', verbose_name='비디오 파일'), + preserve_default=False, + ), + migrations.AddField( + model_name='videoeventdata', + name='user', + field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='video_event_datas', to=settings.AUTH_USER_MODEL, verbose_name='시청 기록의 해당 사용자'), + preserve_default=False, + ), + migrations.AlterField( + model_name='videoeventdata', + name='video', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='video_event_datas', to='materials.video', verbose_name='시청 기록의 해당 비디오'), + ), + ] diff --git a/materials/migrations/0005_image_author.py b/materials/migrations/0005_image_author.py new file mode 100644 index 0000000..20b01d1 --- /dev/null +++ b/materials/migrations/0005_image_author.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.1 on 2024-10-13 11:34 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materials', '0004_remove_image_file_remove_image_image_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='image', + name='author', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='images', to=settings.AUTH_USER_MODEL, verbose_name='이미지를 등록한 사용자'), + preserve_default=False, + ), + ] diff --git a/materials/migrations/0006_alter_video_topic.py b/materials/migrations/0006_alter_video_topic.py new file mode 100644 index 0000000..1730b34 --- /dev/null +++ b/materials/migrations/0006_alter_video_topic.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.1 on 2024-10-13 11:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0010_remove_topic_description'), + ('materials', '0005_image_author'), + ] + + operations = [ + migrations.AlterField( + model_name='video', + name='topic', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='video', to='courses.topic'), + ), + ] diff --git a/materials/models.py b/materials/models.py index 01888fe..1f35e51 100644 --- a/materials/models.py +++ b/materials/models.py @@ -1,8 +1,9 @@ import uuid +from django.db import models + from accounts.models import CustomUser from courses.models import Course, Topic -from django.db import models def upload_to(instance, filename): @@ -11,7 +12,7 @@ def upload_to(instance, filename): - 모델 인스턴스가 save() 호출될 때, 파일이 저장되기 전 upload_to에 정의된 경로를 생성하기 위해 호출됩니다. - ImageField의 upload_to 인자로 전달됩니다. - 생성된 경로를 반환하며, 이 경로는 Django가 해당 파일을 저장할 때 사용됩니다. - - (장점) 사용자 접근성을 높이면서 중복 파일 이름 문제를 해결합니다. + - (장점) 사용자 접근성을 높이면서 중복 파일 이름 문제를 해결합니다. """ ext = filename.split(".")[-1] return f"images/{uuid.uuid4()}.{ext}" @@ -36,9 +37,13 @@ class Image(models.Model): null=True, blank=True, ) - image_url = models.URLField( - upload_to="images/", blank=True, null=True, verbose_name="이미지 파일" + author = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + related_name="images", + verbose_name="이미지를 등록한 사용자", ) + image_url = models.URLField(verbose_name="이미지 파일") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -59,16 +64,18 @@ def save(self, *args, **kwargs): class Video(models.Model): - topic = models.OneToOneField(Topic, on_delete=models.CASCADE, related_name="video") + topic = models.OneToOneField( + Topic, on_delete=models.CASCADE, related_name="video", null=True, blank=True + ) course = models.OneToOneField( - Course, on_delete=models.CASCADE, related_name="video" + Course, on_delete=models.CASCADE, related_name="video", null=True, blank=True ) - video_url = models.FileField(upload_to="videos/", verbose_name="비디오 파일") + video_url = models.URLField(verbose_name="비디오 파일") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return f"{self.topic.title} - {self.title}" + return f"{self.id}" class VideoEventData(models.Model): diff --git a/materials/serializers.py b/materials/serializers.py index 23325e6..bf67290 100644 --- a/materials/serializers.py +++ b/materials/serializers.py @@ -8,9 +8,11 @@ class ImageSerializer(serializers.ModelSerializer): """ 이미지 생성(업로드)을 위한 시리얼라이저입니다. - - 형식, 손상 여부에 대해 유효성 검사를 합니다. + - 형식, 손상 여부에 대해 유효성 검사를 합니다. """ + file = serializers.ImageField(write_only=True) + class Meta: model = Image fields = [ @@ -18,53 +20,46 @@ class Meta: "image_url", "created_at", "updated_at", + "file", + ] + read_only_fields = [ + "id", + "created_at", + "updated_at", + "image_url", ] - read_only_fields = ["id", "created_at", "updated_at"] - def validate_image_url(self, value): + def validate_file(self, value): allowed_image_extensions = (".png", ".jpg", ".jpeg") if not value.name.endswith(allowed_image_extensions): raise serializers.ValidationError( "지원하지 않는 파일 형식입니다. PNG, JPG, JPEG만 가능합니다." ) - try: img = PILImage.open(value) img.verify() except Exception: raise serializers.ValidationError("유효한 이미지 파일이 아닙니다.") - return value class VideoSerializer(serializers.ModelSerializer): - topic_title = serializers.CharField(source="topic.title", read_only=True) - course_title = serializers.CharField(source="topic.course.title", read_only=True) + + file = serializers.FileField(write_only=True) class Meta: model = Video fields = [ "id", - "topic", - "topic_title", - "course_title", "video_url", "created_at", "updated_at", + "file", ] - read_only_fields = ["id", "created_at", "updated_at"] - - def validate_topic(self, value): - request = self.context.get("request") - if request and request.user.is_authenticated: - if not request.user.is_staff and value.course.tutor != request.user: - raise serializers.ValidationError( - "이 topic에 영상을 넣을 권한이 없습니다." - ) - return value + read_only_fields = ["id", "created_at", "updated_at", "video_url"] - def validate_image_url(self, value): + def validate_file(self, value): # 영상 형식과 크기 유효성 검사 allowed_extensions = ["mp4", "avi", "mov", "wmv"] max_size = 100 * 1024 * 1024 # 100MB @@ -73,32 +68,30 @@ def validate_image_url(self, value): raise serializers.ValidationError( f"허용되지 않는 파일 형식입니다. 다음 형식만 가능합니다: {', '.join(allowed_extensions)}." ) - if value.size > max_size: raise serializers.ValidationError( "파일 크기가 너무 큽니다. 최대 크기는 100MB입니다." ) - # 영상 손상 여부 검사 - try: - cap = cv2.VideoCapture(value) - if not cap.isOpened(): - raise serializers.ValidationError( - "비디오 파일을 열 수 없습니다. 파일이 손상되었을 수 있습니다." - ) - - ret, frame = cap.read() - if not ret: - raise serializers.ValidationError( - "비디오 파일을 읽을 수 없습니다. 파일이 손상되었을 수 있습니다." - ) - - except Exception as e: - raise serializers.ValidationError( - f"비디오 파일 검사 중 오류가 발생했습니다: {str(e)}" - ) - finally: - cap.release() + # try: + # cap = cv2.VideoCapture(value) + # if not cap.isOpened(): + # raise serializers.ValidationError( + # "비디오 파일을 열 수 없습니다. 파일이 손상되었을 수 있습니다." + # ) + + # ret, frame = cap.read() + # if not ret: + # raise serializers.ValidationError( + # "비디오 파일을 읽을 수 없습니다. 파일이 손상되었을 수 있습니다." + # ) + + # except Exception as e: + # raise serializers.ValidationError( + # f"비디오 파일 검사 중 오류가 발생했습니다: {str(e)}" + # ) + # finally: + # cap.release() return value diff --git a/materials/views.py b/materials/views.py index 9d95627..bae2c30 100644 --- a/materials/views.py +++ b/materials/views.py @@ -1,25 +1,32 @@ import io import boto3 -import ffmpeg from botocore.exceptions import ClientError from django.conf import settings +from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 from PIL import Image as PILImage from PIL import ImageFilter -from rest_framework import generics, permissions, status +from rest_framework import generics, status from rest_framework.exceptions import PermissionDenied from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response -from rest_framework.views import APIView + +from courses.models import Course from .models import Image, Video, VideoEventData -from .serializers import (ImageSerializer, UserViewEventListSerializer, - VideoEventSerializer, VideoSerializer) +from .serializers import ( + ImageSerializer, + UserViewEventListSerializer, + VideoEventSerializer, + VideoSerializer, +) + +User = get_user_model() # 리팩토링할 때 중복 함수 이곳에 작성 -def optimize_image(self, image_file): +def optimize_image(image_file): """ 이미지를 최적화하는 메서드입니다. - 포맷 변환 @@ -53,7 +60,7 @@ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - image_file = request.FILES.get("image_url") + image_file = request.FILES.get("file") if not image_file: return Response( @@ -61,7 +68,7 @@ def create(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST, ) - optimized_image = self.optimize_image(image_file) + optimized_image = optimize_image(image_file) try: # 최적화된 이미지를 임시로 메모리에 저장 @@ -77,14 +84,10 @@ def create(self, request, *args, **kwargs): region_name=settings.AWS_S3_REGION_NAME, ) - user = get_object_or_404(CustomUser, id=request.data.get("user_id")) - course = get_object_or_404(Course, id=request.data.get("course_id")) - # 파일 이름 생성: 사용자 ID와 코스 ID를 포함 - if user: + user = request.user + if request.user: file_name = f"images/user_{user.id}/{image_file.name}" - elif course: - file_name = f"images/course_{course.id}/{image_file.name}" else: return Response( {"error": "유효한 사용자 또는 코스가 필요합니다."}, @@ -102,8 +105,7 @@ def create(self, request, *args, **kwargs): file_url = f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{file_name}" # 시리얼라이저에 전달 후 저장 - serializer.validated_data["image_url"] = file_url - image = serializer.save(file=file_url) + image = Image.objects.create(image_url=file_url, author=user) return Response( self.get_serializer(image).data, status=status.HTTP_201_CREATED @@ -159,8 +161,8 @@ def create(self, request, *args, **kwargs): max_image_size = 5 * 1024 * 1024 # 5MB - if value.size > max_image_size: - raise serializers.ValidationError( + if file.size > max_image_size: + raise serializer.ValidationError( "파일 크기는 5MB를 초과할 수 없습니다." ) @@ -186,7 +188,7 @@ def create(self, request, *args, **kwargs): file_url = f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{file_name}" # 비디오 객체 생성 및 저장 - video = serializer.save(file=file_url) + video = Video.objects.create(video_url=file_url) return Response( self.get_serializer(video).data, status=status.HTTP_201_CREATED diff --git a/payments/mixins.py b/payments/mixins.py index 6378e8d..63553f5 100644 --- a/payments/mixins.py +++ b/payments/mixins.py @@ -1,9 +1,10 @@ -from rest_framework.exceptions import NotFound, ValidationError, PermissionDenied -from rest_framework.response import Response -from rest_framework import status +from django.db import transaction from django.utils import timezone +from rest_framework import status +from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError +from rest_framework.response import Response -from .models import Cart, CartItem, Order, UserBillingAddress, Payment +from .models import Cart, CartItem, Order, Payment, UserBillingAddress from .services import KakaoPayService @@ -29,6 +30,7 @@ def get_cart(self, user): def get_cart_item(self, cart, **kwargs): return self.get_object_or_404(CartItem.objects.filter(cart=cart), **kwargs) + @transaction.atomic def add_to_cart(self, cart, serializer): existing_item = CartItem.objects.filter( cart=cart, diff --git a/payments/tests/conftest.py b/payments/tests/conftest.py index c87dda0..a3a950f 100644 --- a/payments/tests/conftest.py +++ b/payments/tests/conftest.py @@ -1,19 +1,20 @@ +from unittest.mock import MagicMock + import pytest -from django.contrib.auth import get_user_model -from rest_framework.test import APIClient from django.conf import settings -from unittest.mock import MagicMock +from django.contrib.auth import get_user_model from django.utils import timezone +from rest_framework.test import APIClient +from courses.models import Course, Curriculum from payments.models import ( Cart, + CartItem, Order, + OrderItem, Payment, UserBillingAddress, - CartItem, - OrderItem, ) -from courses.models import Course, Curriculum from payments.services import KakaoPayService @@ -47,17 +48,20 @@ def cart(user): @pytest.fixture -def course(): +def course(staff_user): return Course.objects.create( title="Test Course", + author=staff_user, price=10000, description="This is a test course description", ) @pytest.fixture -def curriculum(): - return Curriculum.objects.create(name="Test Curriculum", price=20000) +def curriculum(staff_user): + return Curriculum.objects.create( + name="Test Curriculum", price=20000, author=staff_user + ) @pytest.fixture diff --git a/payments/views.py b/payments/views.py index b37f78e..2884ed5 100644 --- a/payments/views.py +++ b/payments/views.py @@ -1,29 +1,27 @@ from django.core.exceptions import ValidationError from django.db import transaction - +from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import generics, status -from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework.response import Response -from .models import CartItem, Order, UserBillingAddress, Payment -from .serializers import ( - CartItemSerializer, - CartSerializer, - OrderItemSerializer, - OrderSerializer, - UserBillingAddressSerializer, - PaymentSerializer, -) from .mixins import ( CartMixin, OrderMixin, - UserBillingAddressMixin, PaymentMixin, ReceiptMixin, - OrderMixin, + UserBillingAddressMixin, ) +from .models import CartItem, Order, Payment, UserBillingAddress from .permissions import IsOwnerPermission +from .serializers import ( + CartItemSerializer, + CartSerializer, + OrderItemSerializer, + OrderSerializer, + PaymentSerializer, + UserBillingAddressSerializer, +) @extend_schema_view( @@ -75,7 +73,6 @@ def post(self, request): cart = self.get_cart(request.user) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.save(cart=cart) return self.add_to_cart(cart, serializer) def delete(self, request, pk): diff --git a/weaverse/settings.py b/weaverse/settings.py index 0ff6f93..6b2c4b6 100644 --- a/weaverse/settings.py +++ b/weaverse/settings.py @@ -151,7 +151,7 @@ # CORS 설정 if DEBUG: CORS_ALLOWED_ORIGINS = [ - "https://www.weaverse.site/", # 프로덕션 환경 + "https://www.weaverse.site", # 프로덕션 환경 "http://localhost:3000", # 개발 환경 프론트엔드 ] else: From 28416ae9016938d6d769918983e82c54071c10e0 Mon Sep 17 00:00:00 2001 From: ALbertIM0427 Date: Sun, 13 Oct 2024 21:06:55 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0007_alter_image_author.py | 21 +++++++++++++++++++ materials/models.py | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 materials/migrations/0007_alter_image_author.py diff --git a/materials/migrations/0007_alter_image_author.py b/materials/migrations/0007_alter_image_author.py new file mode 100644 index 0000000..69afed1 --- /dev/null +++ b/materials/migrations/0007_alter_image_author.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.1 on 2024-10-13 12:05 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materials', '0006_alter_video_topic'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to=settings.AUTH_USER_MODEL, verbose_name='이미지를 등록한 사용자'), + ), + ] diff --git a/materials/models.py b/materials/models.py index 1f35e51..9ebc840 100644 --- a/materials/models.py +++ b/materials/models.py @@ -42,6 +42,8 @@ class Image(models.Model): on_delete=models.CASCADE, related_name="images", verbose_name="이미지를 등록한 사용자", + null=True, + blank=True, ) image_url = models.URLField(verbose_name="이미지 파일") created_at = models.DateTimeField(auto_now_add=True)