diff --git a/apps/account/v1/apis/mypage_api.py b/apps/account/v1/apis/mypage_api.py index 8cc780a..cfd58f9 100644 --- a/apps/account/v1/apis/mypage_api.py +++ b/apps/account/v1/apis/mypage_api.py @@ -7,11 +7,11 @@ from apps.account.models import CustomUser, UserProfileImage from apps.account.services.user_service import UserService from apps.account.v1.serializers.user_serializer import ( + UserProfileImageSerializer, UserReadSerializer, UserUpdateSerializer, ) from apps.friend.services.friend_selector import FriendSelector -from apps.friend.services.friend_service import FriendService class MyPageViewSet(viewsets.ModelViewSet): @@ -36,7 +36,7 @@ def get_serializer_class(self): if self.action == "list": serializer_class = UserReadSerializer - elif self.action in ["update", "partial_update"]: + elif self.action == "create": serializer_class = UserUpdateSerializer return serializer_class @@ -47,22 +47,22 @@ def list(self, request, *args, **kwargs): friend_requests = FriendSelector.get_recieved_requests(user_id=user.id) youtubes = cache.get_or_set( cache_key.USER_YOUTUBE.format(user_id=user.id), - self.get_queryset().youtube.all(), + self.get_queryset().get().youtube.all(), timeout=86400, ) news = cache.get_or_set( cache_key.USER_NEWS.format(user_id=user.id), - self.get_queryset().news.all(), + self.get_queryset().get().news.all(), timeout=86400, ) books = cache.get_or_set( cache_key.USER_BOOK.format(user_id=user.id), - self.get_queryset().book.all(), + self.get_queryset().get().book.all(), timeout=86400, ) shoppings = cache.get_or_set( cache_key.USER_SHOPPING.format(user_id=user.id), - self.get_queryset().shopping.all(), + self.get_queryset().get().shopping.all(), timeout=86400, ) @@ -79,38 +79,17 @@ def list(self, request, *args, **kwargs): context, status=status.HTTP_200_OK, template_name="account/mypage.html" ) - def update(self, request, *args, **kwargs): + def create(self, request, *args, **kwargs): """회원 정보를 업데이트 합니다.""" user = request.user + serializer = self.get_serializer(instance=user, data=request.data, partial=True) - request_data = { - "nickname": request.data.get("nickname"), - "password": request.data.get("password"), - "bio": request.data.get("bio"), - "profile_image": request.data.get("img"), - } - - serializer = self.get_serializer(data=request_data) - - if serializer.is_valid(raise_exception=True): - validated_data = serializer.validated_data - - user.email = validated_data["email"] - user.nickname = validated_data["nickname"] - user.bio = validated_data["bio"] - - if validated_data.get("profile_image"): - img = validated_data["profile_image"] - UserProfileImage.objects.create(user_id=user.id, img=img) - - if validated_data.get("password"): - password = validated_data["password"] - user.set_password(password) + if serializer.is_valid(): + serializer.save() - user.save() + data = {"msg": "ok", "data": UserReadSerializer(instance=user).data} - data = {"msg": "ok", "user": UserReadSerializer(instance=user).data} return Response(data=data, status=status.HTTP_200_OK) return Response( data=serializer.error_messages, status=status.HTTP_400_BAD_REQUEST diff --git a/apps/account/v1/apis/user_api.py b/apps/account/v1/apis/user_api.py index 4a5b63d..fe68666 100644 --- a/apps/account/v1/apis/user_api.py +++ b/apps/account/v1/apis/user_api.py @@ -39,13 +39,11 @@ def post(self, request, *args, **kwargs): """ " 회원가입을 통해 유저를 생성합니다. """ - request_data = { - "email": request.data.get("email"), - "nickname": request.data.get("nickname"), - "password": request.data.get("password"), - "bio": request.data.get("bio"), - "profile_image": request.data.get("img", None), - } + + request_data = request.data.copy() + + if request.data.get("img"): + request_data.update({"profile_image": request.data["img"]}) is_present = UserSelector.check_email_duplication(email=request_data["email"]) diff --git a/apps/account/v1/serializers/user_serializer.py b/apps/account/v1/serializers/user_serializer.py index c5650c2..e51f283 100644 --- a/apps/account/v1/serializers/user_serializer.py +++ b/apps/account/v1/serializers/user_serializer.py @@ -5,12 +5,16 @@ from apps.account.services.user_selector import UserSelector +class UserProfileImageSerializer(serializers.Serializer): + profile_image = serializers.ImageField(write_only=True) + + class UserCreateSerializer(serializers.ModelSerializer): email = serializers.CharField(required=True) nickname = serializers.CharField(required=True) password = serializers.CharField(write_only=True) - profile_image = serializers.ImageField(required=False) + profile_image = serializers.ImageField(required=False, write_only=True) class Meta: model = CustomUser @@ -21,12 +25,19 @@ class UserUpdateSerializer(serializers.ModelSerializer): nickname = serializers.CharField(required=False) password = serializers.CharField(required=False, write_only=True) - profile_image = serializers.ImageField(required=False) bio = serializers.CharField(required=False) + profile_image = serializers.ImageField(required=False, write_only=True) class Meta: model = CustomUser - fields = ("nickname", "password", "profile_image", "bio") + fields = ("nickname", "password", "bio", "profile_image") + + def update(self, instance, validated_data): + if validated_data.get("profile_image"): + img = validated_data.pop("profile_image") + UserProfileImage.objects.create(user_id=instance.id, img=img) + + return super().update(instance, validated_data) class UserReadSerializer(serializers.ModelSerializer): @@ -54,7 +65,7 @@ def get_profile_image(self, obj): profile_img = UserProfileImage.objects.filter(user_id=obj.id).first() if not profile_img: return DEFAULT_IMG - return profile_img.img + return profile_img.img.url class UserLikeKeywordSerilaizer(serializers.ModelSerializer): diff --git a/apps/account/v1/urls.py b/apps/account/v1/urls.py index 35ed0b8..fd99b9c 100644 --- a/apps/account/v1/urls.py +++ b/apps/account/v1/urls.py @@ -1,10 +1,17 @@ -from django.urls import path +from django.urls import include, path +from rest_framework.routers import DefaultRouter +from rest_framework_simplejwt.views import TokenBlacklistView from apps.account.v1.apis import user_api +from apps.account.v1.apis.mypage_api import MyPageViewSet app_name = "account" +router = DefaultRouter() +router.register("mypage", MyPageViewSet, "mypage") + urlpatterns = [ + path("", include(router.urls)), path("signup", user_api.SignUpView.as_view(), name="signup"), path( "signup/verification//", @@ -17,4 +24,5 @@ user_api.TemporaryPasswordView.as_view(), name="temporary_password", ), + path("logout", TokenBlacklistView.as_view(), name="logout"), ] diff --git a/apps/friend/v1/apis/friend_api.py b/apps/friend/v1/apis/friend_api.py index 33e3594..fdb54f4 100644 --- a/apps/friend/v1/apis/friend_api.py +++ b/apps/friend/v1/apis/friend_api.py @@ -35,7 +35,7 @@ def list(self, request, *args, **kwargs): user = request.user recommend_friend = cache.get_or_set( cache_key.RECOMMEND_FRIEND.format(user_id=user.id), - FriendService.recommend_friend(user_id=user.id), + FriendService.recommend_friend(user=user), timeout=86400, ) context = { diff --git a/static/js/base.js b/static/js/base.js index bd7509d..d78bcfc 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -5,19 +5,19 @@ $(document).ready(function () { // 친구 리스트 이동 let friendListIcon = $(".user_icon"); - friendListIcon.on('click', function(){ - window.location.href = `/accounts/friends/`; + friendListIcon.on('click', function () { + window.location.href = `/friend/v1/friends`; }) // 마이페이지 이동 let myPageIcon = $(".mypage_icon"); - myPageIcon.on('click', function(){ - window.location.href = `/accounts/mypage/`; + myPageIcon.on('click', function () { + window.location.href = `/account/v1/mypage`; }) // 채팅 이동 let ChatIcon = $(".chat_icon"); - ChatIcon.on('click', function(){ + ChatIcon.on('click', function () { window.location.href = `/api/search/`; }) }) @@ -27,19 +27,19 @@ function switch_chat() { $('#left_wrap').empty(); $('#left_wrap').load("/api/search/chat"); $('#right_wrap').load("/api/search/recommend"); - setTimeout(function(){ + setTimeout(function () { reload(); console.log('recommend_toggle!! -> ', document.getElementById('recommend_toggle')) }, 500) console.log('switch_chat()!') } -function toggle_recommend(){ +function toggle_recommend() { let toggle_menu = document.getElementById('toggle_content') - if(toggle_menu.style.display!=='none'){ - toggle_menu.style.display='none' + if (toggle_menu.style.display !== 'none') { + toggle_menu.style.display = 'none' } - else{ - toggle_menu.style.display='block' + else { + toggle_menu.style.display = 'block' } } \ No newline at end of file diff --git a/tests/account/conftest.py b/tests/account/conftest.py index eb00602..535c4dd 100644 --- a/tests/account/conftest.py +++ b/tests/account/conftest.py @@ -2,6 +2,7 @@ import pytest from django.core.files import File +from django.core.files.uploadedfile import SimpleUploadedFile from PIL import Image from apps.account.constants import TEST_IMG_NAME @@ -39,3 +40,13 @@ def get_test_image(): file.seek(0) return File(file=file, name=name) + + +@pytest.fixture() +def simple_upload_image(): + image_data = BytesIO() + image = Image.new("RGB", (100, 100), "white") + image.save(image_data, format="png") + image_data.seek(0) + + return SimpleUploadedFile("test.png", image_data.read(), content_type="image/png") diff --git a/tests/account/services/test_user_service.py b/tests/account/services/test_user_service.py index 6bb36a6..b9ff5bc 100644 --- a/tests/account/services/test_user_service.py +++ b/tests/account/services/test_user_service.py @@ -82,3 +82,26 @@ def test_유저_좋아요_컨텐츠_노출_off_변경시_필드_변경(): user = UserService.like_public_setting(user_id=user.id, value=value) assert user.is_like_public is False + + +def test_유저_탙퇴시_is_deleted_필드값_변경(): + + user = UserFactory.create(is_active=True) + deleted_user = UserService.delete_user_account(user_id=user.id) + + assert deleted_user.is_deleted is True + + +def test_유저_임시비밀번호_발송_비밀번호_체크_확인(mocker): + + user = UserFactory.create(is_active=True) + temp_str = "temporary" + + mocker.patch( + "apps.account.services.user_service.random_string_generator", + return_value=temp_str, + ) + + after_user = UserService.change_temporary_password(user_id=user.id) + + assert after_user.check_password(temp_str) is True diff --git a/tests/account/v1/apis/test_mypage_api.py b/tests/account/v1/apis/test_mypage_api.py new file mode 100644 index 0000000..036c32b --- /dev/null +++ b/tests/account/v1/apis/test_mypage_api.py @@ -0,0 +1,142 @@ +import io + +import pytest +from django.core import mail +from django.core.files.base import ContentFile +from django.test import Client +from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart +from django.urls import reverse +from PIL import Image +from rest_framework import status + +from apps.account.models import UserProfileImage +from tests.account.factories import UserFactory, UserProfileImageFactory +from tests.friend.factories import FriendRequestFactory +from tests.helpers import authorization_header +from tests.search.factories import ( + BookFactory, + NewsFactory, + ShoppingFactory, + YoutubeFactory, +) + +pytestmark = pytest.mark.django_db + + +def test_등록되지_않은_유저_마이페이지_접근시_401에러_발생(client: Client): + + res = client.get(reverse("account:v1:mypage-list")) + + assert res.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_active_인증_받지_않은_유저_접근시_401에러_발생(client: Client): + + user = UserFactory.create(is_active=False) + + res = client.get(reverse("account:v1:mypage-list"), **authorization_header(user)) + + assert res.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_유저_마이페이지_템플릿_렌더링_확인(client: Client): + + user = UserFactory.create(is_active=True) + + res = client.get(reverse("account:v1:mypage-list"), **authorization_header(user)) + + assert res.status_code == status.HTTP_200_OK + assert "account/mypage.html" in [template.name for template in res.templates] + + +def test_유저_마이페이지_유저_정보_및_받은_친구_요청_반환(client: Client): + + user = UserFactory.create(is_active=True) + friends_requests = FriendRequestFactory.create_batch(size=10, target_user=user) + + res = client.get(reverse("account:v1:mypage-list"), **authorization_header(user)) + + assert res.status_code == status.HTTP_200_OK + assert res.data["user"].email == user.email + assert len(res.data["friend_requests"]) == len(friends_requests) + + +def test_유저_마이페이지_유튜브_뉴스_책_쇼핑_데이터_반환(client: Client): + + user = UserFactory.create(is_active=True) + size = 5 + + youtubes = YoutubeFactory.create_batch(size=size, user=user) + news = NewsFactory.create_batch(size=size, user=user) + books = BookFactory.create_batch(size=size, user=user) + shoppings = ShoppingFactory.create_batch(size=size, user=user) + + res = client.get(reverse("account:v1:mypage-list"), **authorization_header(user)) + + assert res.status_code == status.HTTP_200_OK + assert res.data["user"].email == user.email + assert len(res.data["youtubes"]) == len(youtubes) + assert len(res.data["news"]) == len(news) + assert len(res.data["books"]) == len(books) + assert len(res.data["shoppings"]) == len(shoppings) + assert sorted([obj.id for obj in res.data["youtubes"]]) == sorted( + [obj.id for obj in youtubes] + ) + assert sorted([obj.id for obj in res.data["news"]]) == sorted( + [obj.id for obj in news] + ) + assert sorted([obj.id for obj in res.data["books"]]) == sorted( + [obj.id for obj in books] + ) + assert sorted([obj.id for obj in res.data["shoppings"]]) == sorted( + [obj.id for obj in shoppings] + ) + + +# def test_유저_회원정보_이미지_제외한_정보_업데이트(client: Client): + +# user = UserFactory.create(is_active=True) + +# nickname = "updated" + +# data = { +# "nickname": nickname +# } + +# res = client.patch(f"/account/v1/mypage/{user.id}/", +# **authorization_header(user), +# data=data, +# content_type="application/json") + + +# res_data = res.data['data'] + +# assert res.status_code == status.HTTP_200_OK +# assert res.data['msg'] == 'ok' +# assert res_data['nickname'] == nickname +# assert res_data['email'] == user.email +# assert res_data['bio'] == user.bio + + +def test_유저_회원정보_프로필_이미지_업데이트_확인(client: Client, get_test_image): + + user = UserFactory.create(is_active=True) + user_img = UserProfileImageFactory.create() + + data = {"profile_image": user_img.img, "bio": "test"} + + res = client.post( + reverse("account:v1:mypage-list"), + **authorization_header(user), + data=data, + media_type="multipart/form-data" + ) + + res_data = res.data["data"] + + assert res.status_code == status.HTTP_200_OK + assert res.data["msg"] == "ok" + assert UserProfileImage.objects.filter(user=user).exists() is True + assert res_data["nickname"] == user.nickname + assert res_data["email"] == user.email + assert res_data["bio"] == "test" diff --git a/tests/account/v1/apis/test_user_api.py b/tests/account/v1/apis/test_user_api.py index 97c15d0..2d4bedb 100644 --- a/tests/account/v1/apis/test_user_api.py +++ b/tests/account/v1/apis/test_user_api.py @@ -10,9 +10,10 @@ from alaltalk.settings.base import CELERY_RESULT_BACKEND from apps.account.constants import DEFAULT_IMG, EMAIL_VERIFY_TITLE +from apps.account.models import UserProfileImage from apps.account.tasks import send_email_verification from apps.account.utils import accounts_verify_token -from tests.account.factories import UserFactory +from tests.account.factories import UserFactory, UserProfileImageFactory pytestmark = pytest.mark.django_db @@ -26,7 +27,7 @@ def test_유저_생성페이지_렌더링(client: Client): @pytest.mark.django_db(transaction=True) -def test_유저_생성( +def test_유저_생성시_transaction_callback_및_메일전송_확인( client: Client, create_user_data: Callable, django_capture_on_commit_callbacks ): @@ -36,7 +37,7 @@ def test_유저_생성( with django_capture_on_commit_callbacks(execute=True): res = client.post( - reverse("account:v1:signup"), data=data, content_type="application/json" + reverse("account:v1:signup"), data=data, media_type="multipart/form-data" ) assert res.status_code == status.HTTP_201_CREATED @@ -47,6 +48,35 @@ def test_유저_생성( assert mail.outbox[0].subject == EMAIL_VERIFY_TITLE +def test_유저_생성시_프로필_이미지_업로드_레코드_생성_및_시리얼라이저_데이터_반환(client: Client): + + user = UserFactory.create(email="test_email234@alaltalk.com") + + user_image = UserProfileImageFactory.create() + + data = { + "email": user.email, + "password": user.password, + "nickname": user.nickname, + "bio": user.bio, + } + + data.update({"img": user_image.img}) + + res = client.post( + reverse("account:v1:signup"), data=data, media_type="multipart/form-data" + ) + + assert res.data is None + assert res.status_code == status.HTTP_201_CREATED + assert res.data["user"]["email"] == user.email + assert UserProfileImage.objects.filter(user=user).exists() is True + assert ( + res.data["user"]["profile_image"] + == UserProfileImage.objects.get(user_id=user.id).img + ) + + def test_유저_인증_성공시_status_user_반환(client: Client): user = UserFactory.create() diff --git a/tests/account/v1/serializers/test_user_serializer.py b/tests/account/v1/serializers/test_user_serializer.py index 97e3c60..c24e2dc 100644 --- a/tests/account/v1/serializers/test_user_serializer.py +++ b/tests/account/v1/serializers/test_user_serializer.py @@ -7,6 +7,7 @@ from apps.account.v1.serializers.user_serializer import ( UserCreateSerializer, UserReadSerializer, + UserUpdateSerializer, ) from tests.account.factories import UserFactory, UserProfileImageFactory @@ -69,4 +70,18 @@ def test_유저_조회_시리얼라이저_이미지_있을시_이미지_반환() serializer = UserReadSerializer(instance=user) data = serializer.data - assert data["profile_image"] == profile_image.img + assert data["profile_image"] == profile_image.img.url + + +def test_유저_업데이트_시리얼라이저_프로필_이미지_업로드_validated_data_반환(): + + user = UserFactory.create(is_active=True) + profile_image = UserProfileImageFactory.create() + data = {"profile_image": profile_image.img} + + serializer = UserUpdateSerializer(instance=user, data=data, partial=True) + + assert serializer.is_valid() is True + + validated_data = serializer.validated_data + assert validated_data["profile_image"].url == profile_image.img.url