diff --git a/v2/src/ctrlfbe/constants.py b/v2/src/ctrlfbe/constants.py index f14fd14..cb17ba8 100644 --- a/v2/src/ctrlfbe/constants.py +++ b/v2/src/ctrlfbe/constants.py @@ -3,6 +3,6 @@ ERR_TOPIC_NOT_FOUND = "토픽을 찾을 수 없습니다." ERR_PAGE_NOT_FOUND = "페이지를 찾을 수 없습니다." ERR_UNEXPECTED = "알 수 없는 에러가 발생 하였습니다." - +ERR_KEY_INPUT_MSG = "을(를) 입력하세요." ERR_NOT_FOUND_MSG_MAP = {"note": ERR_NOTE_NOT_FOUND, "topic": ERR_TOPIC_NOT_FOUND, "page": ERR_PAGE_NOT_FOUND} diff --git a/v2/src/ctrlfbe/page_urls.py b/v2/src/ctrlfbe/page_urls.py index 6d0e3b0..3e9bd3a 100644 --- a/v2/src/ctrlfbe/page_urls.py +++ b/v2/src/ctrlfbe/page_urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import PageDetailUpdateDeleteView +from .views import PageCreateView, PageDetailUpdateDeleteView app_name = "pages" urlpatterns = [ + path("", PageCreateView.as_view(), name="page_create"), path("", PageDetailUpdateDeleteView.as_view(), name="page_detail"), ] diff --git a/v2/src/ctrlfbe/serializers.py b/v2/src/ctrlfbe/serializers.py index c04a13c..e7f498e 100644 --- a/v2/src/ctrlfbe/serializers.py +++ b/v2/src/ctrlfbe/serializers.py @@ -1,9 +1,12 @@ -from rest_framework import serializers +from rest_framework import serializers, status +from rest_framework.exceptions import ValidationError +from .constants import ERR_NOTE_NOT_FOUND, ERR_TOPIC_NOT_FOUND from .models import ( ContentRequest, CtrlfActionType, CtrlfContentType, + CtrlfIssueStatus, Issue, Note, Page, @@ -61,12 +64,146 @@ class NoteListQuerySerializer(serializers.Serializer): class TopicSerializer(serializers.ModelSerializer): class Meta: model = Topic - fields = "__all__" + fields = ["id", "created_at", "updated_at", "title", "note", "is_approved"] read_only_fields = ["id", "created_at"] + def create(self, validated_data): + owner = validated_data.pop("owner") + topic = Topic.objects.create(**validated_data) + topic.owners.add(owner) + topic.save() + return topic + class PageSerializer(serializers.ModelSerializer): class Meta: model = Page - fields = "__all__" + fields = ["id", "created_at", "updated_at", "title", "content", "topic", "is_approved"] read_only_fields = ["id", "created_at"] + + def create(self, validated_data): + owner = validated_data.pop("owner") + page = Page.objects.create(**validated_data) + page.owners.add(owner) + page.save() + return page + + +class ContentRequestSerializer(serializers.ModelSerializer): + class Meta: + model = ContentRequest + fields = "__all__" + + def create(self, validated_data): + content_request = ContentRequest.objects.create(**validated_data) + content_request.save() + return content_request + + +class IssueSerializer(serializers.ModelSerializer): + class Meta: + model = Issue + fields = "__all__" + + def create(self, validated_data): + issue = Issue.objects.create(**validated_data) + issue.save() + return issue + + +class TopicCreateSerializer(serializers.Serializer): + note = serializers.IntegerField() + title = serializers.CharField() + content = serializers.CharField() + + def validate(self, request_data): + try: + Note.objects.get(id=request_data["note"]) + except Note.DoesNotExist: + raise ValidationError(detail=ERR_NOTE_NOT_FOUND, code=status.HTTP_404_NOT_FOUND) + return request_data + + def create(self, validated_data): + topic_data = { + "note": validated_data["note"], + "title": validated_data["title"], + } + topic = TopicSerializer(data=topic_data) + if not topic.is_valid(): + raise ValidationError(detail="topic 생성 실패", code=status.HTTP_400_BAD_REQUEST) + topic = topic.save(owner=validated_data["owner"]) + + content_request_data = { + "type": CtrlfContentType.TOPIC, + "action": CtrlfActionType.CREATE, + "sub_id": topic.id, + "user": validated_data["owner"].id, + } + content_request = ContentRequestSerializer(data=content_request_data) + if not content_request.is_valid(): + raise ValidationError(detail="content_request 생성 실패", code=status.HTTP_400_BAD_REQUEST) + content_request = content_request.save() + + issue_data = { + "owner": validated_data["owner"].id, + "content_request": content_request.id, + "title": validated_data["title"], + "content": validated_data["content"], + "status": CtrlfIssueStatus.REQUESTED, + } + issue = IssueSerializer(data=issue_data) + if not issue.is_valid(): + raise ValidationError(detail="issue 생성 실패", code=status.HTTP_400_BAD_REQUEST) + issue = issue.save() + + return issue + + +class PageCreateSerializer(serializers.Serializer): + topic = serializers.IntegerField() + title = serializers.CharField() + content = serializers.CharField() + issue_content = serializers.CharField() + + def validate(self, request_data): + try: + Topic.objects.get(id=request_data["topic"]) + except Topic.DoesNotExist: + raise ValidationError(detail=ERR_TOPIC_NOT_FOUND, code=status.HTTP_404_NOT_FOUND) + return request_data + + def create(self, validated_data): + page_data = { + "topic": validated_data["topic"], + "title": validated_data["title"], + "content": validated_data["content"], + } + page = PageSerializer(data=page_data) + if not page.is_valid(): + raise ValidationError(detail="page 생성 실패", code=status.HTTP_400_BAD_REQUEST) + page = page.save(owner=validated_data["owner"]) + + content_request_data = { + "type": CtrlfContentType.PAGE, + "action": CtrlfActionType.CREATE, + "sub_id": page.id, + "user": validated_data["owner"].id, + } + content_request = ContentRequestSerializer(data=content_request_data) + if not content_request.is_valid(): + raise ValidationError(detail="content_request 생성 실패", code=status.HTTP_400_BAD_REQUEST) + content_request = content_request.save() + + issue_data = { + "owner": validated_data["owner"].id, + "content_request": content_request.id, + "title": validated_data["title"], + "content": validated_data["issue_content"], + "status": CtrlfIssueStatus.REQUESTED, + } + issue = IssueSerializer(data=issue_data) + if not issue.is_valid(): + raise ValidationError(detail="issue 생성 실패", code=status.HTTP_400_BAD_REQUEST) + issue = issue.save() + + return issue diff --git a/v2/src/ctrlfbe/swagger.py b/v2/src/ctrlfbe/swagger.py index 2c1c223..ee0fe6c 100644 --- a/v2/src/ctrlfbe/swagger.py +++ b/v2/src/ctrlfbe/swagger.py @@ -2,7 +2,9 @@ NoteCreateRequestBodySerializer, NoteListQuerySerializer, NoteSerializer, + PageCreateSerializer, PageSerializer, + TopicCreateSerializer, TopicSerializer, ) @@ -54,3 +56,19 @@ "operation_description": "page_id에 해당하는 Page의 상세 내용을 리턴합니다", "tags": ["디테일 화면"], } + +SWAGGER_TOPIC_CREATE_VIEW = { + "request_body": TopicCreateSerializer, + "responses": {201: ""}, + "operation_summary": "Topic Create API", + "operation_description": "미승인 토픽을 생성하고 토픽 생성 이슈를 등록합니다.", + "tags": ["디테일 화면"], +} + +SWAGGER_PAGE_CREATE_VIEW = { + "request_body": PageCreateSerializer, + "responses": {201: ""}, + "operation_summary": "Page Create API", + "operation_description": "미승인 페이지를 생성하고 페이지 생성 이슈를 등록합니다.", + "tags": ["디테일 화면"], +} diff --git a/v2/src/ctrlfbe/topic_urls.py b/v2/src/ctrlfbe/topic_urls.py index 7ecf2bb..d6cb8da 100644 --- a/v2/src/ctrlfbe/topic_urls.py +++ b/v2/src/ctrlfbe/topic_urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import PageListView, TopicDetailUpdateDeleteView +from .views import PageListView, TopicCreateView, TopicDetailUpdateDeleteView app_name = "topics" urlpatterns = [ + path("", TopicCreateView.as_view(), name="topic_create"), path("/pages", PageListView.as_view(), name="page_list"), path("", TopicDetailUpdateDeleteView.as_view(), name="topic_detail"), ] diff --git a/v2/src/ctrlfbe/views.py b/v2/src/ctrlfbe/views.py index f3069e9..f0a5667 100644 --- a/v2/src/ctrlfbe/views.py +++ b/v2/src/ctrlfbe/views.py @@ -5,8 +5,10 @@ SWAGGER_NOTE_CREATE_VIEW, SWAGGER_NOTE_DETAIL_VIEW, SWAGGER_NOTE_LIST_VIEW, + SWAGGER_PAGE_CREATE_VIEW, SWAGGER_PAGE_DETAIL_VIEW, SWAGGER_PAGE_LIST_VIEW, + SWAGGER_TOPIC_CREATE_VIEW, SWAGGER_TOPIC_DETAIL_VIEW, SWAGGER_TOPIC_LIST_VIEW, ) @@ -16,12 +18,19 @@ from rest_framework.response import Response from rest_framework.views import APIView -from .constants import ERR_NOT_FOUND_MSG_MAP, ERR_UNEXPECTED, MAX_PRINTABLE_NOTE_COUNT +from .constants import ( + ERR_KEY_INPUT_MSG, + ERR_NOT_FOUND_MSG_MAP, + ERR_UNEXPECTED, + MAX_PRINTABLE_NOTE_COUNT, +) from .models import CtrlfIssueStatus, Note, Page, Topic from .serializers import ( IssueCreateSerializer, NoteSerializer, + PageCreateSerializer, PageSerializer, + TopicCreateSerializer, TopicSerializer, ) @@ -108,6 +117,23 @@ def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) +class TopicCreateView(CtrlfAuthenticationMixin, APIView): + @swagger_auto_schema(**SWAGGER_TOPIC_CREATE_VIEW) + def post(self, request, *args, **kwargs): + ctrlf_user = self._ctrlf_authentication(request) + serializer = TopicCreateSerializer(data=request.data) + + if serializer.is_valid(): + serializer.save(owner=ctrlf_user) + return Response(status=status.HTTP_201_CREATED) + else: + for key, message in serializer.errors.items(): + err = message[0] + if err.code == "required": + return Response({"message": key + ERR_KEY_INPUT_MSG}, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": err}, status=err.code) + + class TopicDetailUpdateDeleteView(BaseContentView): parent_model = Topic serializer = TopicSerializer @@ -135,3 +161,20 @@ class PageDetailUpdateDeleteView(BaseContentView): @swagger_auto_schema(**SWAGGER_PAGE_DETAIL_VIEW) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) + + +class PageCreateView(CtrlfAuthenticationMixin, APIView): + @swagger_auto_schema(**SWAGGER_PAGE_CREATE_VIEW) + def post(self, request, *args, **kwargs): + ctrlf_user = self._ctrlf_authentication(request) + serializer = PageCreateSerializer(data=request.data) + + if serializer.is_valid(): + serializer.save(owner=ctrlf_user) + return Response(status=status.HTTP_201_CREATED) + else: + for key, message in serializer.errors.items(): + err = message[0] + if err.code == "required": + return Response({"message": key + ERR_KEY_INPUT_MSG}, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": err}, status=err.code) diff --git a/v2/src/tests/test_content_list_detail.py b/v2/src/tests/test_content_list_detail.py index d1a393c..5859bed 100644 --- a/v2/src/tests/test_content_list_detail.py +++ b/v2/src/tests/test_content_list_detail.py @@ -1,5 +1,6 @@ from ctrlf_auth.models import CtrlfUser -from ctrlfbe.models import Note, Page, Topic +from ctrlf_auth.serializers import LoginSerializer +from ctrlfbe.models import Issue, Note, Page, Topic from django.test import Client, TestCase from django.urls import reverse from rest_framework import status @@ -219,3 +220,271 @@ def test_page_detail_should_return_404_by_invalid_page_id(self): # And : 메세지는 "페이지를 찾을 수 없습니다." 이어야 한다. response = response.data self.assertEqual(response["message"], "페이지를 찾을 수 없습니다.") + + +class TestTopicCreate(TestCase): + def setUp(self) -> None: + self.client = Client() + self.data = { + "email": "test@test.com", + "password": "12345", + } + self.user = CtrlfUser.objects.create_user(**self.data) + + def _login(self): + serializer = LoginSerializer() + return serializer.validate(self.data)["token"] + + def _call_api(self, request_body, token=None): + if token: + header = {"HTTP_AUTHORIZATION": f"Bearer {token}"} + else: + header = {} + return self.client.post(reverse("topics:topic_create"), request_body, **header) + + def make_note(self): + self.note = Note.objects.create(title="test note title") + self.note.owners.add(self.user) + + def test_topic_create_should_return_201(self): + # Given: 미리 생성된 노트, 로그인 하여 얻은 토큰, 유효한 토픽 생성 정보. + self.make_note() + token = self._login() + request_body = {"note": self.note.id, "title": "test title", "content": "test issue content"} + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 201이어야 함. + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # And : 생성된 토픽 정보가 일치해야 한다. + topic = Topic.objects.all()[0] + self.assertEqual(topic.note, self.note) + self.assertEqual(topic.title, "test title") + + # And : 생성된 이슈 정보와 일치해야 한다. + issue = Issue.objects.all()[0] + self.assertEqual(issue.title, "test title") + self.assertEqual(issue.content, "test issue content") + + def test_topic_create_should_return_401_without_login(self): + # Given: 미리 생성된 노트, 유효한 토픽 생성 정보 + self.make_note() + request_body = {"note": self.note.id, "title": "test title", "content": "test issue content"} + + # When : API 실행 + response = self._call_api(request_body) + + # Then : 상태코드 401이어야 함. + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_topic_create_should_return_404_by_invalid_note_id(self): + # Given: 미리 생성된 노트, 로그인 하여 얻은 토큰, 유효하지 않은 노트 ID. + self.make_note() + token = self._login() + invalid_note_id = 1234 + request_body = {"note": invalid_note_id, "title": "test title", "content": "test issue content"} + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 404이어야 함. + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # And : 메세지는 노트를 찾을 수 없습니다. 이어야 함. + response = response.data + self.assertEqual(response["message"], "노트를 찾을 수 없습니다.") + + def test_topic_create_should_return_400_without_title(self): + # Given: 미리 생성된 노트, 로그인 하여 얻은 토큰, title 없음. + self.make_note() + token = self._login() + request_body = {"note": self.note.id, "content": "test issue content"} + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 400이어야 함. + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # And : 메세지는 title을(를) 입력하세요. 이어야 함. + response = response.data + self.assertEqual(response["message"], "title을(를) 입력하세요.") + + def test_topic_create_should_return_400_without_content(self): + # Given: 미리 생성된 노트, 로그인 하여 얻은 토큰, content 없음. + self.make_note() + token = self._login() + request_body = {"note": self.note.id, "title": "test title"} + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 400이어야 함. + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # And : 메세지는 content을(를) 입력하세요. 이어야 함. + response = response.data + self.assertEqual(response["message"], "content을(를) 입력하세요.") + + +class TestPageCreate(TestCase): + def setUp(self) -> None: + self.client = Client() + self.data = { + "email": "test@test.com", + "password": "12345", + } + self.user = CtrlfUser.objects.create_user(**self.data) + + def _login(self): + serializer = LoginSerializer() + return serializer.validate(self.data)["token"] + + def _call_api(self, request_body, token=None): + if token: + header = {"HTTP_AUTHORIZATION": f"Bearer {token}"} + else: + header = {} + return self.client.post(reverse("pages:page_create"), request_body, **header) + + def make_note(self): + self.note = Note.objects.create(title="test note title") + self.note.owners.add(self.user) + + def make_topic(self): + self.topic = Topic.objects.create(note=self.note, title="test topic title") + self.topic.owners.add(self.user) + + def test_page_create_should_return_201(self): + # Given: 미리 생성된 노트, 토픽, 로그인 하여 얻은 토큰, 유효한 페이지 생성 정보. + self.make_note() + self.make_topic() + token = self._login() + request_body = { + "topic": self.topic.id, + "title": "test title", + "content": "test page content", + "issue_content": "test issue content", + } + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 201이어야 함. + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # And : 생성된 페이지 정보가 일치해야 한다. + page = Page.objects.all()[0] + self.assertEqual(page.topic, self.topic) + self.assertEqual(page.title, "test title") + self.assertEqual(page.content, "test page content") + + # And : 생성된 이슈 정보와 일치해야 한다. + issue = Issue.objects.all()[0] + self.assertEqual(issue.title, "test title") + self.assertEqual(issue.content, "test issue content") + + def test_page_create_should_return_401_without_login(self): + # Given: 미리 생성된 노트, 토픽, 유효한 페이지 생성 정보 + self.make_note() + self.make_topic() + request_body = { + "topic": self.topic.id, + "title": "test title", + "content": "test page content", + "issue_content": "test issue content", + } + + # When : API 실행 + response = self._call_api(request_body) + + # Then : 상태코드 401이어야 함. + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_page_create_should_return_404_by_invalid_topic_id(self): + # Given: 미리 생성된 노트, 토픽, 로그인 하여 얻은 토큰, 유효하지 않은 토픽 ID. + self.make_note() + self.make_topic() + token = self._login() + invalid_topic_id = 1234 + request_body = { + "topic": invalid_topic_id, + "title": "test title", + "content": "test page content", + "issue_content": "test issue content", + } + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 404이어야 함. + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # And : 메세지는 토픽을 찾을 수 없습니다. 이어야 함. + response = response.data + self.assertEqual(response["message"], "토픽을 찾을 수 없습니다.") + + def test_page_create_should_return_400_without_title(self): + # Given: 미리 생성된 노트, 토픽, 로그인 하여 얻은 토큰, title 없음. + self.make_note() + self.make_topic() + token = self._login() + request_body = { + "topic": self.topic.id, + "content": "test page content", + "issue_content": "test issue content", + } + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 400이어야 함. + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # And : 메세지는 title을(를) 입력하세요. 이어야 함. + response = response.data + self.assertEqual(response["message"], "title을(를) 입력하세요.") + + def test_page_create_should_return_400_without_content(self): + # Given: 미리 생성된 노트, 토픽, 로그인 하여 얻은 토큰, content 없음. + self.make_note() + self.make_topic() + token = self._login() + request_body = { + "topic": self.topic.id, + "title": "test title", + "issue_content": "test issue content", + } + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 400이어야 함. + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # And : 메세지는 content을(를) 입력하세요. 이어야 함. + response = response.data + self.assertEqual(response["message"], "content을(를) 입력하세요.") + + def test_page_create_should_return_400_without_issue_content(self): + # Given: 미리 생성된 노트, 토픽, 로그인 하여 얻은 토큰, issue_content 없음. + self.make_note() + self.make_topic() + token = self._login() + request_body = { + "topic": self.topic.id, + "title": "test title", + "content": "test content", + } + + # When : API 실행. + response = self._call_api(request_body, token=token) + + # Then : 상태코드 400이어야 함. + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # And : 메세지는 issue_content을(를) 입력하세요. 이어야 함. + response = response.data + self.assertEqual(response["message"], "issue_content을(를) 입력하세요.")