@@ -56,6 +56,7 @@ INSTALLED_APPS = [
|
|||||||
'portfolios',
|
'portfolios',
|
||||||
'projects',
|
'projects',
|
||||||
'codes',
|
'codes',
|
||||||
|
'notifications',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
# path('api/code/', include('codes.urls')),
|
# path('api/code/', include('codes.urls')),
|
||||||
path('api/portfolio/', include('portfolios.urls')),
|
path('api/portfolio/', include('portfolios.urls')),
|
||||||
path('api/project/', include('projects.urls')),
|
path('api/project/', include('projects.urls')),
|
||||||
|
path('api/notification/', include('notifications.urls')),
|
||||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
0
notifications/__init__.py
Normal file
0
notifications/__init__.py
Normal file
3
notifications/admin.py
Normal file
3
notifications/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
notifications/apps.py
Normal file
6
notifications/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'notifications'
|
||||||
11
notifications/models.py
Normal file
11
notifications/models.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from users.models import *
|
||||||
|
|
||||||
|
from common.models.baseModels import *
|
||||||
|
|
||||||
|
class Notification(BaseModel):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
|
||||||
|
content = models.TextField(blank=True)
|
||||||
|
is_read = models.BooleanField(default=False)
|
||||||
|
note_type = models.CharField(max_length=10, choices=NotificationType.choices)
|
||||||
32
notifications/serializers.py
Normal file
32
notifications/serializers.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from .models import *
|
||||||
|
from projects.models import *
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class NotificationSerializer(serializers.ModelSerializer):
|
||||||
|
meta = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Notification
|
||||||
|
fields = ['id', 'content', 'note_type', 'is_read', 'meta']
|
||||||
|
|
||||||
|
def get_meta(self, obj):
|
||||||
|
|
||||||
|
REL_SERIALIZER_MAP = {
|
||||||
|
'project_invitation' : ProjectInvitationMetaSerializer,
|
||||||
|
}
|
||||||
|
|
||||||
|
for rel_name, serializer_cls in REL_SERIALIZER_MAP.items():
|
||||||
|
rel_obj = getattr(obj, rel_name, None)
|
||||||
|
if rel_obj is not None:
|
||||||
|
return serializer_cls(rel_obj).data
|
||||||
|
return None
|
||||||
|
|
||||||
|
class ProjectInvitationMetaSerializer(serializers.ModelSerializer):
|
||||||
|
project_invitation_id = serializers.CharField(source='id')
|
||||||
|
project_title = serializers.CharField(source='project.title')
|
||||||
|
from_user_nickname = serializers.CharField(source='from_user.nickname')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ProjectInvitation
|
||||||
|
fields = ['project_invitation_id', 'project_title', 'from_user_nickname', 'status']
|
||||||
|
|
||||||
19
notifications/services.py
Normal file
19
notifications/services.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from users.models import *
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
from common.models.choiceModels import *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 알림 관련 서비스 로직
|
||||||
|
class NotifiationService:
|
||||||
|
@staticmethod
|
||||||
|
def set_content():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_notification(user: User, note_type: NotificationType):
|
||||||
|
return Notification.objects.create(
|
||||||
|
user = user,
|
||||||
|
note_type=note_type
|
||||||
|
)
|
||||||
3
notifications/tests.py
Normal file
3
notifications/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
14
notifications/urls.py
Normal file
14
notifications/urls.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
app_name = 'notifications'
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'', NotificationReadViewSet, basename='notification')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(router.urls)),
|
||||||
|
]
|
||||||
34
notifications/views.py
Normal file
34
notifications/views.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from .serializers import *
|
||||||
|
from .models import *
|
||||||
|
from .services import *
|
||||||
|
|
||||||
|
from users.services import *
|
||||||
|
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
class NotificationReadViewSet(ReadOnlyModelViewSet):
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
|
||||||
|
# 30일 이전 알림만 가져옴
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = UserToNotificationService.get_all_notification(self.request.user)
|
||||||
|
print(qs)
|
||||||
|
return qs.select_related(
|
||||||
|
'project_invitation__project',
|
||||||
|
'project_invitation__from_user',
|
||||||
|
)
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
instance.is_read = True
|
||||||
|
instance.save()
|
||||||
|
serializer = self.get_serializer(instance)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@@ -8,7 +8,7 @@ class PortfolioListViewSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Portfolio
|
model = Portfolio
|
||||||
fields = ['id', 'category', 'thumbnail', 'nickname', 'profile_image', 'view_count', 'like_count', 'scrap_count']
|
fields = ['id', 'category', 'thumbnail', 'title', 'nickname', 'profile_image', 'view_count', 'like_count', 'scrap_count']
|
||||||
|
|
||||||
class PortfolioCreateSerializer(serializers.ModelSerializer):
|
class PortfolioCreateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from common.models.choiceModels import InvitationStatus
|
|||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from notifications.models import Notification
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
|
|
||||||
@@ -35,3 +36,4 @@ class ProjectInvitation(BaseModel):
|
|||||||
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_invitations')
|
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_invitations')
|
||||||
project = models.ForeignKey(Project, on_delete=models.CASCADE)
|
project = models.ForeignKey(Project, on_delete=models.CASCADE)
|
||||||
status = models.CharField(max_length=10, choices=InvitationStatus.choices)
|
status = models.CharField(max_length=10, choices=InvitationStatus.choices)
|
||||||
|
notification = models.OneToOneField(Notification, on_delete=models.CASCADE, related_name='project_invitation', null=True, blank=True)
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
from .models import *
|
from .models import *
|
||||||
|
from .services import *
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from notifications.services import *
|
||||||
|
|
||||||
|
from common.models.choiceModels import *
|
||||||
class ProjectListViewSerializer(serializers.ModelSerializer):
|
class ProjectListViewSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -32,8 +36,13 @@ class ProjectCreateSerializer(serializers.ModelSerializer):
|
|||||||
users = list(users) + [validated_data["owner"]]
|
users = list(users) + [validated_data["owner"]]
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
ProjectTeamList.objects.create(user=user, project=project)
|
new_notification = NotifiationService.create_notification(user=user, note_type=NotificationType.INVITE)
|
||||||
|
ProjectInvitationService.create_project_invitation(
|
||||||
|
project=project,
|
||||||
|
from_user=validated_data['owner'],
|
||||||
|
to_user=user,
|
||||||
|
notification=new_notification
|
||||||
|
)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
class ProjectTeamSerializer(serializers.ModelSerializer):
|
class ProjectTeamSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from .models import Project
|
from .models import *
|
||||||
|
from .serializers import *
|
||||||
|
|
||||||
|
from users.models import *
|
||||||
|
from common.models.choiceModels import InvitationStatus
|
||||||
|
|
||||||
|
|
||||||
ACTION_FIELD_MAP = {
|
ACTION_FIELD_MAP = {
|
||||||
@@ -38,3 +42,16 @@ class ProjectStateChangeService:
|
|||||||
now_count = current_count+1 if add else max(current_count-1, 0)
|
now_count = current_count+1 if add else max(current_count-1, 0)
|
||||||
setattr(project, field_name, now_count)
|
setattr(project, field_name, now_count)
|
||||||
project.save(update_fields=[field_name])
|
project.save(update_fields=[field_name])
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectInvitationService:
|
||||||
|
@staticmethod
|
||||||
|
def create_project_invitation(project: Project, from_user: User, to_user: User, notification: Notification):
|
||||||
|
return ProjectInvitation.objects.create(
|
||||||
|
project=project,
|
||||||
|
from_user=from_user,
|
||||||
|
to_user=to_user,
|
||||||
|
status= InvitationStatus.PENDING,
|
||||||
|
notification=notification
|
||||||
|
)
|
||||||
|
|
||||||
@@ -12,5 +12,5 @@ urlpatterns = [
|
|||||||
path('<str:pk>/set-represent/', ProjectSetRepresentAPIView.as_view()),
|
path('<str:pk>/set-represent/', ProjectSetRepresentAPIView.as_view()),
|
||||||
path('<str:pk>/set-publish/', ProjectSetPublishAPIView.as_view()),
|
path('<str:pk>/set-publish/', ProjectSetPublishAPIView.as_view()),
|
||||||
path('<str:pk>/change-state/', ProjectChangeState.as_view()),
|
path('<str:pk>/change-state/', ProjectChangeState.as_view()),
|
||||||
|
path('invite/action/', ProjectInvitationAPIView.as_view()),
|
||||||
]
|
]
|
||||||
@@ -12,10 +12,12 @@ from rest_framework.generics import ListAPIView
|
|||||||
from .serializers import ProjectListViewSerializer, ProjectCreateSerializer, ProjectTeamSerializer
|
from .serializers import ProjectListViewSerializer, ProjectCreateSerializer, ProjectTeamSerializer
|
||||||
from .paginations import ProjectPagination
|
from .paginations import ProjectPagination
|
||||||
from .filters import ProjectFilter
|
from .filters import ProjectFilter
|
||||||
from .services import ProjectStateChangeService, ProjectBeforeRelCheckService
|
from .services import *
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
|
from notifications.services import *
|
||||||
|
|
||||||
|
|
||||||
class ProjectListView(ListAPIView):
|
class ProjectListView(ListAPIView):
|
||||||
queryset = Project.objects.filter(is_published=True).order_by('-created_at')
|
queryset = Project.objects.filter(is_published=True).order_by('-created_at')
|
||||||
@@ -29,6 +31,7 @@ class ProjectListView(ListAPIView):
|
|||||||
class ProjectCreateAPIView(APIView):
|
class ProjectCreateAPIView(APIView):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
user = request.user
|
||||||
serializer = ProjectCreateSerializer(data=request.data, context={"request": request})
|
serializer = ProjectCreateSerializer(data=request.data, context={"request": request})
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
@@ -36,7 +39,7 @@ class ProjectCreateAPIView(APIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
class ProjectTeamManageAPIView(APIView):
|
class ProjectTeamManageAPIView(APIView):
|
||||||
# 팀원 초대
|
# 팀원 초대 및 알림
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
user = request.user
|
user = request.user
|
||||||
@@ -47,11 +50,14 @@ class ProjectTeamManageAPIView(APIView):
|
|||||||
new_member = get_object_or_404(User, nickname=nickname)
|
new_member = get_object_or_404(User, nickname=nickname)
|
||||||
if ProjectTeamList.objects.filter(project=project, user=new_member).exists():
|
if ProjectTeamList.objects.filter(project=project, user=new_member).exists():
|
||||||
return Response({"message": "already team member"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"message": "already team member"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
serializer = ProjectTeamSerializer(data={'user':new_member.id, 'project':project.id})
|
new_notification = NotifiationService.create_notification(user=new_member, note_type=NotificationType.INVITE)
|
||||||
if serializer.is_valid():
|
ProjectInvitationService.create_project_invitation(
|
||||||
serializer.save()
|
project=project,
|
||||||
return Response({"message": "invite success"}, status=status.HTTP_200_OK)
|
from_user=user,
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
to_user=new_member,
|
||||||
|
notification=new_notification
|
||||||
|
)
|
||||||
|
return Response({"message": "invite success"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# 팀원 추방
|
# 팀원 추방
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@@ -140,4 +146,27 @@ class ProjectChangeState(APIView):
|
|||||||
return Response({
|
return Response({
|
||||||
'message': f'{action_type} {"added" if add else "removed"}'
|
'message': f'{action_type} {"added" if add else "removed"}'
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class ProjectInvitationAPIView(APIView):
|
||||||
|
# 팀원 초대 수락/거절
|
||||||
|
@transaction.atomic
|
||||||
|
def post(self, request):
|
||||||
|
accept = request.data['accept']
|
||||||
|
project_invitation = get_object_or_404(ProjectInvitation, id=request.data['project_invitation_id'])
|
||||||
|
new_member = project_invitation.to_user
|
||||||
|
project = project_invitation.project
|
||||||
|
if request.user != new_member:
|
||||||
|
return Response({"message": "Not account owner"}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
if project_invitation.status != InvitationStatus.PENDING:
|
||||||
|
return Response({"message": "already handled invitation"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if accept:
|
||||||
|
project_invitation.status = InvitationStatus.ACCEPTED
|
||||||
|
project_invitation.save()
|
||||||
|
serializer = ProjectTeamSerializer(data={'user':new_member.id, 'project':project.id})
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({"message": "invitation accepted"}, status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
project_invitation.status = InvitationStatus.REJECTED
|
||||||
|
project_invitation.save()
|
||||||
|
return Response({"message": "invitation rejected"}, status=status.HTTP_200_OK)
|
||||||
@@ -63,8 +63,3 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin):
|
|||||||
return self.nickname
|
return self.nickname
|
||||||
|
|
||||||
|
|
||||||
class Notification(BaseModel):
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
|
|
||||||
content = models.TextField()
|
|
||||||
is_read = models.BooleanField(default=False)
|
|
||||||
note_type = models.CharField(max_length=10, choices=NotificationType.choices)
|
|
||||||
|
|||||||
@@ -62,8 +62,3 @@ class UserMemberInfoSerializer(serializers.ModelSerializer):
|
|||||||
'custom_url',
|
'custom_url',
|
||||||
'job_and_interests'
|
'job_and_interests'
|
||||||
]
|
]
|
||||||
|
|
||||||
class NotificationSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Notification
|
|
||||||
fields = ['id', 'content', 'note_type', 'is_read']
|
|
||||||
@@ -16,22 +16,17 @@ class CheckUserFieldDuplicateService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def check_custom_url_duplicate():
|
def check_custom_url_duplicate():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 알림 관련 서비스 로직
|
|
||||||
class NotifiationService:
|
|
||||||
@staticmethod
|
|
||||||
def set_content_by_type():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserToNotificationService:
|
class UserToNotificationService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_new_notification_count(user: User):
|
def get_new_notification_count(user: User):
|
||||||
return user.notifications.filter(created_at__lt=thirty_days_ago, is_read=False).count()
|
return user.notifications.filter(created_at__gt=thirty_days_ago, is_read=False).count()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_notification(user: User):
|
def get_all_notification(user: User):
|
||||||
return user.notifications.filter(created_at__lt=thirty_days_ago)
|
return user.notifications.filter(created_at__gt=thirty_days_ago)
|
||||||
|
|
||||||
|
|
||||||
# 유저 -> 포트폴리오 관련 서비스 로직
|
# 유저 -> 포트폴리오 관련 서비스 로직
|
||||||
@@ -82,4 +77,3 @@ class UserToProjectService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_scrap_project(user: User):
|
def get_scrap_project(user: User):
|
||||||
return user.scrapped_projects.filter(is_published=True)
|
return user.scrapped_projects.filter(is_published=True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path
|
||||||
|
|
||||||
from .views import *
|
from .views import *
|
||||||
|
|
||||||
from rest_framework.routers import DefaultRouter
|
|
||||||
|
|
||||||
app_name = 'users'
|
app_name = 'users'
|
||||||
|
|
||||||
router = DefaultRouter()
|
|
||||||
router.register(r'notifications', NotificationReadViewSet, basename='notification')
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('refresh-token/', RefreshAPIView.as_view()),
|
path('refresh-token/', RefreshAPIView.as_view()),
|
||||||
path('join/', JoinAPIView.as_view()),
|
path('join/', JoinAPIView.as_view()),
|
||||||
@@ -20,5 +16,4 @@ urlpatterns = [
|
|||||||
path('mypage/profile/<str:nickname>/', MyPageProfileAPIView.as_view()),
|
path('mypage/profile/<str:nickname>/', MyPageProfileAPIView.as_view()),
|
||||||
path('mypage/works/<str:nickname>/', MyPageWorkListAPIView.as_view()),
|
path('mypage/works/<str:nickname>/', MyPageWorkListAPIView.as_view()),
|
||||||
path('mypage/my-info/', MyPageMemberInfoAPIView.as_view()),
|
path('mypage/my-info/', MyPageMemberInfoAPIView.as_view()),
|
||||||
path('', include(router.urls)),
|
|
||||||
]
|
]
|
||||||
@@ -249,21 +249,3 @@ class MyPageMemberInfoAPIView(APIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
class NotificationReadViewSet(ReadOnlyModelViewSet):
|
|
||||||
serializer_class = NotificationSerializer
|
|
||||||
|
|
||||||
# 30일 이전 알림만 가져옴
|
|
||||||
def get_queryset(self):
|
|
||||||
return UserToNotificationService.get_all_notification(self.request.user)
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
instance.is_read = True
|
|
||||||
instance.save()
|
|
||||||
serializer = self.get_serializer(instance)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
Reference in New Issue
Block a user