diff --git a/config/urls.py b/config/urls.py index 0e9fdf3..af7d012 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ path('api/user/', include('users.urls')), # path('api/code/', include('codes.urls')), path('api/portfolio/', include('portfolios.urls')), + path('api/project/', include('projects.urls')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) if settings.DEBUG: diff --git a/projects/filters.py b/projects/filters.py new file mode 100644 index 0000000..1a8e099 --- /dev/null +++ b/projects/filters.py @@ -0,0 +1,17 @@ +from django_filters import rest_framework as filters +from django.db.models import Q +from .models import Project + +class ProjectFilter(filters.FilterSet): + category = filters.CharFilter(method='filter_category') + + def filter_category(self, queryset, name, value): + categories = value.split(',') + q = Q() + for c in categories: + q |= Q(category__contains=[c]) + return queryset.filter(q) + + class Meta: + model = Project + fields = ['category'] diff --git a/projects/models.py b/projects/models.py index 39cd280..8d40c5b 100644 --- a/projects/models.py +++ b/projects/models.py @@ -8,16 +8,20 @@ from django.conf import settings from users.models import User class Project(BaseModel): - name = models.CharField(max_length=20) + title = models.CharField(max_length=20) is_team = models.BooleanField(default=False) + team_name = models.CharField(max_length=20, blank=True) category = ArrayField(models.CharField(max_length=20), default=list) is_published = models.BooleanField(default=False) view_count = models.IntegerField(default=0) like_count = models.IntegerField(default=0) - scrab_count = models.IntegerField(default=0) + scrap_count = models.IntegerField(default=0) is_represent = models.BooleanField(default=False) thumbnail = models.ImageField(upload_to='', blank=True) code_id = models.CharField(max_length=26, blank=True) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='owned_projects', to_field="id") + likers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked_projects', blank=True) + scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_projects', blank=True) class ProjectTeamList(BaseModel): project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='project_team_list', to_field='id') diff --git a/projects/paginations.py b/projects/paginations.py new file mode 100644 index 0000000..0ed067c --- /dev/null +++ b/projects/paginations.py @@ -0,0 +1,5 @@ +from rest_framework.pagination import PageNumberPagination + +class ProjectPagination(PageNumberPagination): + page_size = 12 + page_query_param = 'page' diff --git a/projects/serializers.py b/projects/serializers.py new file mode 100644 index 0000000..a8d3af0 --- /dev/null +++ b/projects/serializers.py @@ -0,0 +1,42 @@ +from .models import * +from users.models import User + +from rest_framework import serializers + +class ProjectListViewSerializer(serializers.ModelSerializer): + + class Meta: + model = Project + fields = ['id', 'category', 'thumbnail', 'title', 'view_count', 'like_count', 'scrap_count'] + +class ProjectCreateSerializer(serializers.ModelSerializer): + members = serializers.ListField( + child=serializers.CharField(), write_only=True, required=False + ) + team_name = serializers.CharField(required=False) + + class Meta: + model = Project + fields = ['is_team', 'team_name', 'title', 'category', 'members'] + read_only_fields = ['owner'] + + def create(self, validated_data): + nicknames = validated_data.pop('members', []) + validated_data['owner'] = self.context['request'].user + project = Project.objects.create(**validated_data) + + if not validated_data['is_team']: + return project + + users = User.objects.filter(nickname__in=nicknames) + users = list(users) + [validated_data["owner"]] + + for user in users: + ProjectTeamList.objects.create(user=user, project=project) + + return project + +class ProjectTeamSerializer(serializers.ModelSerializer): + class Meta: + model = ProjectTeamList + fields = '__all__' \ No newline at end of file diff --git a/projects/services.py b/projects/services.py new file mode 100644 index 0000000..50b9a64 --- /dev/null +++ b/projects/services.py @@ -0,0 +1,40 @@ +from .models import Project + + +ACTION_FIELD_MAP = { + 'like': 'likers', + 'scrap': 'scrappers', + } + +class ProjectBeforeRelCheckService: + @staticmethod + def check_user_project_rel(action_type: str, project: Project, user): + action = ACTION_FIELD_MAP.get(action_type) + if not action: + raise ValueError(f"'{action_type}' is unsupported action type") + + if getattr(project, action).filter(id=user.id).exists(): + return True + else: + return False + +class ProjectStateChangeService: + @staticmethod + def change_user_project_rel(action_type: str, project: Project, user, add=True): + action = ACTION_FIELD_MAP.get(action_type) + if not action: + raise ValueError(f"'{action_type}' is unsupported action type") + + user_method = getattr(getattr(project, action), 'add' if add else 'remove') + user_method(user) + + @staticmethod + def change_count_project_state(action_type: str, project: Project, add=True): + action = ACTION_FIELD_MAP.get(action_type) + if not action: + raise ValueError(f"'{action_type}' is unsupported action type") + field_name = action_type + '_count' + current_count = getattr(project, field_name) + now_count = current_count+1 if add else max(current_count-1, 0) + setattr(project, field_name, now_count) + project.save(update_fields=[field_name]) diff --git a/projects/urls.py b/projects/urls.py new file mode 100644 index 0000000..47b7f66 --- /dev/null +++ b/projects/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from .views import * + +app_name = 'projects' + +urlpatterns = [ + path('list/', ProjectListView.as_view()), + path('create/', ProjectCreateAPIView.as_view()), + path('/manage-team/', ProjectTeamManageAPIView.as_view()), + path('/', ProjectDeleteAPIView.as_view()), + path('/set-represent/', ProjectSetRepresentAPIView.as_view()), + path('/set-publish/', ProjectSetPublishAPIView.as_view()), + path('/change-state/', ProjectChangeState.as_view()), + +] \ No newline at end of file diff --git a/projects/views.py b/projects/views.py index 91ea44a..56e466b 100644 --- a/projects/views.py +++ b/projects/views.py @@ -1,3 +1,141 @@ -from django.shortcuts import render +from django.shortcuts import get_object_or_404 -# Create your views here. +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status + +from .models import * + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import OrderingFilter +from rest_framework.generics import ListAPIView +from .serializers import ProjectListViewSerializer, ProjectCreateSerializer, ProjectTeamSerializer +from .paginations import ProjectPagination +from .filters import ProjectFilter +from .services import ProjectStateChangeService, ProjectBeforeRelCheckService + +from django.db import transaction + + +class ProjectListView(ListAPIView): + queryset = Project.objects.filter(is_published=True).order_by('-created_at') + serializer_class = ProjectListViewSerializer + pagination_class = ProjectPagination + filterset_class = ProjectFilter + filter_backends = [DjangoFilterBackend, OrderingFilter] + ordering_fields = ['view_count', 'like_count', 'scrap_count', 'created_at'] # 허용할 필드 추천순 제외 + ordering = ['-created_at'] # 기본 정렬은 최신순 + +class ProjectCreateAPIView(APIView): + @transaction.atomic + def post(self, request): + serializer = ProjectCreateSerializer(data=request.data, context={"request": request}) + if serializer.is_valid(): + serializer.save() + return Response({"message": "create project success"}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class ProjectTeamManageAPIView(APIView): + # 팀원 초대 + @transaction.atomic + def post(self, request, pk): + user = request.user + project = get_object_or_404(Project, id=pk) + if user != project.owner: + return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN) + nickname = request.data.get('nickname') + new_member = get_object_or_404(User, nickname=nickname) + if ProjectTeamList.objects.filter(project=project, user=new_member).exists(): + return Response({"message": "already team member"}, status=status.HTTP_400_BAD_REQUEST) + serializer = ProjectTeamSerializer(data={'user':new_member.id, 'project':project.id}) + if serializer.is_valid(): + serializer.save() + return Response({"message": "invite success"}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # 팀원 추방 + @transaction.atomic + def delete(self, request, pk): + user = request.user + project = get_object_or_404(Project, id=pk) + if user != project.owner: + return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN) + nickname = request.data.get('nickname') + member = get_object_or_404(User, nickname=nickname) + user_in_project = get_object_or_404(ProjectTeamList, project=project, user=member) + user_in_project.delete() + return Response({"message": "delete user in project success"}) + +class ProjectDeleteAPIView(APIView): + @transaction.atomic + def delete(self, request, pk): + user = request.user + project = get_object_or_404(Project, id=pk) + if user != project.owner: + return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN) + project.delete() + return Response({"message": "delete success"}, status=status.HTTP_200_OK) + +class ProjectSetRepresentAPIView(APIView): + @transaction.atomic + def patch(self, request, pk): + user = request.user + project = get_object_or_404(Project, id=pk) + if user != project.owner: + return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN) + if before_represent := Project.objects.filter(owner=user, is_represent=True).first(): + before_represent.is_represent = False + project.is_represent = True + return Response({"message": "change represent success"}, status=status.HTTP_200_OK) + + +class ProjectSetPublishAPIView(APIView): + @transaction.atomic + def patch(self, request, pk): + user = request.user + project = get_object_or_404(Project, id=pk) + if user != project.owner: + return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN) + project.is_published = True + project.save() + return Response({"message": "publish success"}, status=status.HTTP_200_OK) + +class ProjectChangeState(APIView): + @transaction.atomic + def patch(self, request, pk): + project = get_object_or_404(Project, pk=pk) + user = request.user + action_type = request.query_params.get('type') + try: + if ProjectBeforeRelCheckService.check_user_project_rel(action_type, project, user): + return Response({"message": "already done"}, status=status.HTTP_400_BAD_REQUEST) + except ValueError as e: + return Response({'message': str(e)}, status=status.HTTP_400_BAD_REQUEST) + return self._handle_action(action_type, project, user, add=True) + + @transaction.atomic + def delete(self, request, pk): + project = get_object_or_404(Project, pk=pk) + user = request.user + action_type = request.query_params.get('type') + try: + if not ProjectBeforeRelCheckService.check_user_project_rel(action_type, project, user): + return Response({"message": "never done before"}, status=status.HTTP_400_BAD_REQUEST) + except ValueError as e: + return Response({'message': str(e)}, status=status.HTTP_400_BAD_REQUEST) + return self._handle_action(action_type, project, user, add=False) + + def _handle_action(self, action_type, project, user, add=True): + if not action_type: + return Response({'message': 'Missing action type'}, status=status.HTTP_400_BAD_REQUEST) + + try: + ProjectStateChangeService.change_user_project_rel(action_type, project, user, add) + ProjectStateChangeService.change_count_project_state(action_type, project, add) + except ValueError as e: + return Response({'message': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + return Response({ + 'message': f'{action_type} {"added" if add else "removed"}' + }, status=status.HTTP_200_OK) + diff --git a/users/serializers.py b/users/serializers.py index ac6d8c5..6fe0281 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -25,4 +25,9 @@ class JoinSerializer(serializers.ModelSerializer): class SetPortofolioRequiredInfoSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['custom_url', 'job_and_interests'] \ No newline at end of file + fields = ['custom_url', 'job_and_interests'] + +class TagUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['nickname', 'profile_image'] \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index 9179f1b..5cb23f5 100644 --- a/users/urls.py +++ b/users/urls.py @@ -10,5 +10,6 @@ urlpatterns = [ path('login/', LoginAPIView.as_view()), path('check/', NicknameAPIView.as_view()), path('portfolio-info/', SetPortofolioRequiredInfoAPIView.as_view()), - path('portfolio-info/check/', SetPortofolioRequiredInfoAPIView.as_view()) + path('portfolio-info/check/', SetPortofolioRequiredInfoAPIView.as_view()), + path('teamtag/', TagUserAPIView.as_view()), ] \ No newline at end of file diff --git a/users/views.py b/users/views.py index 6c88955..c382a2e 100644 --- a/users/views.py +++ b/users/views.py @@ -100,7 +100,7 @@ class NicknameAPIView(APIView): class TagUserAPIView(APIView): def get(self, request): - nickname = request.query_params.get(nickname) + nickname = request.query_params.get('nickname') users = User.objects.filter(nickname__icontains=nickname).annotate( priority=Case( When(nickname__iexact=nickname, then=Value(0)),