diff --git a/common/utils/fileManager.py b/common/utils/fileManager.py index 6a19d18..b51d7e6 100644 --- a/common/utils/fileManager.py +++ b/common/utils/fileManager.py @@ -1,18 +1,20 @@ import os +from django.utils.deconstruct import deconstructible -def dynamic_upload_to(prefix, field_name_func): - def wrapper(instance, filename): +@deconstructible +class DynamicUploadTo: + def __init__(self, prefix, field_name): + self.prefix = prefix + self.field_name = field_name + + def __call__(self, instance, filename): ext = filename.split('.')[-1] - field_name = field_name_func(instance) - - if prefix == 'user': - filename = f'{instance.nickname}-{field_name}.{ext}' + fname = self.field_name(instance) if callable(self.field_name) else self.field_name + if self.prefix == "user": + filename = f"{instance.nickname}-{fname}.{ext}" else: - filename = f'{instance.id}-{field_name}.{ext}' - - return os.path.join(prefix, filename) - - return wrapper + filename = f"{instance.id}-{fname}.{ext}" + return os.path.join(self.prefix, filename) def file_delete(obj, field): getattr(obj, field).delete(save=False) diff --git a/common/utils/mongodb.py b/common/utils/mongodb.py index 80dc3cc..bdcb564 100644 --- a/common/utils/mongodb.py +++ b/common/utils/mongodb.py @@ -4,5 +4,6 @@ from django.conf import settings def connect_colio_mongo(): mongoengine.connect( db=settings.MONGODB_NAME, - host=settings.MONGODB_URI + host=settings.MONGODB_URI, + tz_aware=True, ) \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index a407419..2127470 100644 --- a/config/settings.py +++ b/config/settings.py @@ -167,7 +167,7 @@ TIME_ZONE = 'Asia/Seoul' USE_I18N = True -USE_TZ = False +USE_TZ = True MEDIA_URL = '/media/' diff --git a/config/urls.py b/config/urls.py index 0e2116a..2507916 100644 --- a/config/urls.py +++ b/config/urls.py @@ -5,12 +5,13 @@ from django.conf import settings from django.conf.urls.static import static urlpatterns = [ - path('admin/', admin.site.urls), + # path('admin/', admin.site.urls), path('api/user/', include('users.urls')), path('api/code/', include('codes.urls')), path('api/portfolio/', include('portfolios.urls')), path('api/project/', include('projects.urls')), path('api/notification/', include('notifications.urls')), + path('api/nocodetool/', include('nocodetools.urls')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # if settings.DEBUG: diff --git a/nocodetools/models.py b/nocodetools/models.py index 0213803..f4d86bc 100644 --- a/nocodetools/models.py +++ b/nocodetools/models.py @@ -1,14 +1,25 @@ from django.db import models +from django.utils import timezone import mongoengine as me + class Element(me.EmbeddedDocument): + element_id = me.StringField() element_type = me.StringField() content = me.StringField() css = me.DictField() -class Page(me.Document): +class Page(me.EmbeddedDocument): cut = me.IntField() elements = me.ListField(me.EmbeddedDocumentField(Element)) - created_at = me.DateTimeField() - updated_at = me.DateTimeField() \ No newline at end of file + +class Code(me.Document): + pages = me.ListField(me.EmbeddedDocumentField(Page)) + created_at = me.DateTimeField(default=timezone.now) + updated_at = me.DateTimeField(default=timezone.now) + + def save(self, *args, **kwargs): + self.updated_at = timezone.now() + return super().save(*args, **kwargs) + \ No newline at end of file diff --git a/nocodetools/permissions.py b/nocodetools/permissions.py new file mode 100644 index 0000000..30b032f --- /dev/null +++ b/nocodetools/permissions.py @@ -0,0 +1,26 @@ +# permissions.py +from rest_framework.permissions import BasePermission +from projects.models import Project, ProjectTeamList +from portfolios.models import Portfolio + +UNSAFE_REQUEST = ["POST", "PUT", "PATCH", "DELETE"] + +class IsOwnerOrMemberInCreateAndUpdateAndDelete(BasePermission): + def has_permission(self, request, view): + if request.method not in UNSAFE_REQUEST: + return True + + related_type = request.query_params.get("type") + related_id = request.query_params.get("id") + + if not related_type or not related_id: + return False + + user = request.user + + if related_type == "project": + return ProjectTeamList.objects.filter(project=related_id, user=user).exists() + elif related_type == "portfolio": + return Portfolio.objects.filter(id=related_id, owner=user).exists() + else: + return False \ No newline at end of file diff --git a/nocodetools/serializers.py b/nocodetools/serializers.py new file mode 100644 index 0000000..293b271 --- /dev/null +++ b/nocodetools/serializers.py @@ -0,0 +1,67 @@ +from rest_framework import serializers +from .models import Code, Page, Element + +# from datetime import datetime, timezone + +from django.utils import timezone +# from zoneinfo import ZoneInfo + +# KST = ZoneInfo("Asia/Seoul") + +class ElementSerializer(serializers.Serializer): + element_id = serializers.CharField(required=False) + element_type = serializers.CharField() + content = serializers.CharField(allow_blank=True) + css = serializers.DictField() + +class PageSerializer(serializers.Serializer): + cut = serializers.IntegerField() + elements = ElementSerializer(many=True) + +class CodeSerializer(serializers.Serializer): + id = serializers.SerializerMethodField() + pages = PageSerializer(many=True, required=False) + created_at = serializers.SerializerMethodField() + updated_at = serializers.SerializerMethodField() + + def get_created_at(self, obj): + return timezone.localtime(obj.created_at).isoformat(timespec="seconds") + + def get_updated_at(self, obj): + return timezone.localtime(obj.updated_at).isoformat(timespec="seconds") + + def get_id(self, obj): + return str(obj.id) + + def create(self, validated_data): + pages_data = validated_data.pop('pages') + pages = [ + Page( + cut=page['cut'], + elements=[Element(**el) for el in page['elements']] + ) for page in pages_data + ] + code = Code(pages=pages, **validated_data) + code.save() + return code + + def update(self, instance, validated_data): + update_pages_data = validated_data.get('pages', []) + existing = {p.cut: p for p in instance.pages} + + for page in update_pages_data: + cut = page.get('cut') + if cut and cut in existing: + page_obj = existing[cut] + page_obj.elements = [Element(**el) for el in page["elements"]] + + else: + instance.pages.append( + Page( + cut=cut, + elements=[Element(**el) for el in page["elements"]] + ) + ) + + instance.save() + return instance diff --git a/nocodetools/services.py b/nocodetools/services.py new file mode 100644 index 0000000..b19203b --- /dev/null +++ b/nocodetools/services.py @@ -0,0 +1,29 @@ +from .models import * +from .serializers import * + +from projects.models import Project +from portfolios.models import Portfolio + +from projects.serializers import ProjectNocodetoolSerializer +from portfolios.serializers import PortfolioNocodetoolSerializer + +NOCODETOOL_MODEL_MAP = { + 'project': Project, + 'portfolio': Portfolio, + } + +NOCODETOOL_SERIALIZER_MAP = { + 'project': ProjectNocodetoolSerializer, + 'portfolio': PortfolioNocodetoolSerializer, + } + +class NocodetoolObjectMapService: + @staticmethod + def mapping_model_instance(related_type: str, related_id: str): + object_model = NOCODETOOL_MODEL_MAP.get(related_type) + if not object_model: + return None + return object_model.objects.filter(id=related_id).first() + + def mapping_model_serializer(related_type: str): + return NOCODETOOL_SERIALIZER_MAP.get(related_type, None) diff --git a/nocodetools/urls.py b/nocodetools/urls.py new file mode 100644 index 0000000..83fcb97 --- /dev/null +++ b/nocodetools/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from .views import * + + +app_name = 'nocodetools' + +urlpatterns = [ + path('', NoCodeToolAPIView.as_view()), + path('working/', NocodeToolWorkingAPIView.as_view()), +] \ No newline at end of file diff --git a/nocodetools/views.py b/nocodetools/views.py index d43f642..839149a 100644 --- a/nocodetools/views.py +++ b/nocodetools/views.py @@ -1 +1,163 @@ -# views.py +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status, mixins, viewsets +from rest_framework.permissions import AllowAny, IsAuthenticated + +from django.db import transaction + +from .models import Code, Page, Element +from .serializers import CodeSerializer +from .permissions import IsOwnerOrMemberInCreateAndUpdateAndDelete +from .services import NocodetoolObjectMapService + +from users.models import User +from portfolios.models import Portfolio +from projects.models import Project + +from bson import ObjectId + + +class NoCodeToolAPIView(APIView): + permission_classes = [IsAuthenticated, IsOwnerOrMemberInCreateAndUpdateAndDelete] + + def get(self, request): + related_type = request.query_params.get("type") + related_id = request.query_params.get("id") + code_id = None + if obj := NocodetoolObjectMapService.mapping_model_instance(related_type, related_id): + code_id = ObjectId(obj.code_id) + + if not code_id: + return Response({"message": "Not validated type or no object"}, status=status.HTTP_400_BAD_REQUEST) + + obj_serializer = NocodetoolObjectMapService.mapping_model_serializer(related_type) + + code = Code.objects.get(id=code_id) + return Response({ + "obj_info": obj_serializer(obj).data, + "codes": CodeSerializer(code).data + }, status=status.HTTP_200_OK) + + @transaction.atomic + def post(self, request): + related_type = request.query_params.get("type") + related_id = request.query_params.get("id") + obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id) + if not obj: + return Response({"message": "No object"}, status=status.HTTP_400_BAD_REQUEST) + + if not obj.now_worker: + return Response({"message": "start edit first"}, status=status.HTTP_400_BAD_REQUEST) + + if obj.now_worker != request.user.nickname: + return Response({"message": f"{obj.now_worker} is working now"}, status=status.HTTP_400_BAD_REQUEST) + + data = request.data.copy() + thumbnail_file = data.pop("thumbnail", None) + + obj_serializer = NocodetoolObjectMapService.mapping_model_serializer(related_type) + + serializer = CodeSerializer(data=request.data) + if serializer.is_valid(): + code = serializer.save() + obj.code_id = str(code.id) + obj.save() + + if thumbnail_file: + obj.thumbnail = thumbnail_file + obj.save(update_fields=["thumbnail"]) + + return Response({ + "obj_info": obj_serializer(obj).data, + "codes": CodeSerializer(code).data + }, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @transaction.atomic + def patch(self, request): + related_type = request.query_params.get("type") + related_id = request.query_params.get("id") + obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id) + if not obj: + return Response({"message": "No object"}, status=status.HTTP_400_BAD_REQUEST) + + if not obj.now_worker: + return Response({"message": "start edit first"}, status=status.HTTP_400_BAD_REQUEST) + + if obj.now_worker != request.user.nickname: + return Response({"message": f"{obj.now_worker} is working now"}, status=status.HTTP_400_BAD_REQUEST) + + code = Code.objects.filter(id=ObjectId(obj.code_id)).first() + if not code: + return Response({'message': 'No code object'}, status=status.HTTP_400_BAD_REQUEST) + + data = request.data.copy() + thumbnail_file = data.pop("thumbnail", None) + + obj_serializer = NocodetoolObjectMapService.mapping_model_serializer(related_type) + serializer = CodeSerializer(code, data=data, partial=True) + if serializer.is_valid(): + updated_code = serializer.save() + + if thumbnail_file: + obj.thumbnail = thumbnail_file + obj.save(update_fields=["thumbnail"]) + + return Response({ + "obj_info": obj_serializer(obj).data, + "codes": CodeSerializer(updated_code).data + }, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @transaction.atomic + def delete(self, request): + related_type = request.query_params.get("type") + related_id = request.query_params.get("id") + obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id) + if not obj: + return Response({"message": "No object"}, status=status.HTTP_400_BAD_REQUEST) + + if not obj.now_worker: + return Response({"message": "start edit first"}, status=status.HTTP_400_BAD_REQUEST) + + if obj.now_worker != request.user.nickname: + return Response({"message": f"{obj.now_worker} is working now"}, status=status.HTTP_400_BAD_REQUEST) + + code = Code.objects.filter(id=ObjectId(obj.code_id)).first() + if not code: + return Response({'message': 'No code object'}, status=status.HTTP_400_BAD_REQUEST) + code.delete() + obj.code_id = None + obj.thumbnail = None + obj.save() + return Response({"message": "delete code success"}, status=status.HTTP_200_OK) + +class NocodeToolWorkingAPIView(APIView): + permission_classes = [IsAuthenticated, IsOwnerOrMemberInCreateAndUpdateAndDelete] + @transaction.atomic + def patch(self, request): # 수정 시작 or 종료 + related_type = request.query_params.get("type") + related_id = request.query_params.get("id") + action = request.query_params.get("action") + obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id) + if not obj: + return Response({"message": "No object"}, status=status.HTTP_400_BAD_REQUEST) + + if action == "start": + if obj.now_worker: + return Response({"message": f"{obj.now_worker} is working"}, status=status.HTTP_400_BAD_REQUEST) + obj.now_worker = request.user.nickname + elif action == "end": + if obj.now_worker and obj.now_worker == request.user.nickname: + obj.now_worker = None + else: + return Response( + { + "message": f"{obj.now_worker} is working" if obj.now_worker else "nobody working now" + }, status=status.HTTP_400_BAD_REQUEST) + else: + return Response({"message": "Not supported action"}, status=status.HTTP_400_BAD_REQUEST) + + obj.save() + + return Response({"message": f"work {action}"}, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/portfolios/models.py b/portfolios/models.py index f66ec11..d80dc3a 100644 --- a/portfolios/models.py +++ b/portfolios/models.py @@ -2,7 +2,7 @@ from django.db import models from common.models.baseModels import BaseModel -from common.utils.fileManager import dynamic_upload_to +from common.utils.fileManager import DynamicUploadTo from django.contrib.postgres.fields import ArrayField from django.conf import settings @@ -10,6 +10,8 @@ from django.conf import settings from users.models import User +NICKNAME_LEN = 20 + class Portfolio(BaseModel): title = models.CharField(max_length=20) category = ArrayField(models.CharField(max_length=20), default=list) @@ -18,8 +20,9 @@ class Portfolio(BaseModel): like_count = models.IntegerField(default=0) scrap_count = models.IntegerField(default=0) is_represent = models.BooleanField(default=False) - thumbnail = models.ImageField(upload_to=dynamic_upload_to('portfolio', lambda instance: 'thumbnail'), blank=True) - code_id = models.CharField(max_length=26, blank=True) + now_worker = models.CharField(max_length=NICKNAME_LEN, blank=True) + thumbnail = models.ImageField(upload_to=DynamicUploadTo("portfolio", "thumbnail"), null=True, blank=True) + code_id = models.CharField(max_length=26, null=True, blank=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='owned_portfolios', to_field="id") likers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked_portfolios', blank=True) scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_portfolios', blank=True) diff --git a/portfolios/serializers.py b/portfolios/serializers.py index 6d8e8bc..e1f37d2 100644 --- a/portfolios/serializers.py +++ b/portfolios/serializers.py @@ -10,6 +10,11 @@ class PortfolioListViewSerializer(serializers.ModelSerializer): model = Portfolio fields = ['id', 'category', 'thumbnail', 'title', 'nickname', 'profile_image', 'view_count', 'like_count', 'scrap_count'] +class PortfolioNocodetoolSerializer(serializers.ModelSerializer): + class Meta: + model = Portfolio + fields = ['title', 'is_published', 'now_worker'] + class PortfolioCreateSerializer(serializers.ModelSerializer): class Meta: model = Portfolio diff --git a/projects/models.py b/projects/models.py index d03f375..246ece8 100644 --- a/projects/models.py +++ b/projects/models.py @@ -3,7 +3,7 @@ from django.db import models from common.models.baseModels import BaseModel from common.models.choiceModels import InvitationStatus -from common.utils.fileManager import dynamic_upload_to +from common.utils.fileManager import DynamicUploadTo from django.contrib.postgres.fields import ArrayField from django.conf import settings @@ -12,6 +12,8 @@ from notifications.models import Notification from users.models import User +NICKNAME_LEN = 20 + class Project(BaseModel): title = models.CharField(max_length=20) is_team = models.BooleanField(default=False) @@ -22,8 +24,9 @@ class Project(BaseModel): like_count = models.IntegerField(default=0) scrap_count = models.IntegerField(default=0) is_represent = models.BooleanField(default=False) - thumbnail = models.ImageField(upload_to=dynamic_upload_to('project', lambda instance: 'thumbnail'), blank=True) - code_id = models.CharField(max_length=26, blank=True) + now_worker = models.CharField(max_length=NICKNAME_LEN, blank=True) + thumbnail = models.ImageField(upload_to=DynamicUploadTo("project", "thumbnail"), null=True, blank=True) + code_id = models.CharField(max_length=26, null=True, 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) diff --git a/projects/serializers.py b/projects/serializers.py index a5f8902..d4a2fd4 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -13,6 +13,11 @@ class ProjectListViewSerializer(serializers.ModelSerializer): model = Project fields = ['id', 'category', 'thumbnail', 'title', 'view_count', 'like_count', 'scrap_count'] +class ProjectNocodetoolSerializer(serializers.ModelSerializer): + class Meta: + model = Project + fields = ['title', 'is_published', 'now_worker'] + class ProjectCreateSerializer(serializers.ModelSerializer): members = serializers.ListField( child=serializers.CharField(), write_only=True, required=False diff --git a/users/models.py b/users/models.py index 3a24fad..fa9b869 100644 --- a/users/models.py +++ b/users/models.py @@ -2,7 +2,7 @@ from django.db import models from common.models.baseModels import BaseModel from common.models.choiceModels import GenderChoices, CertificateCodeUseType from common.utils.codeManger import set_expire -from common.utils.fileManager import dynamic_upload_to +from common.utils.fileManager import DynamicUploadTo from django.contrib.postgres.fields import ArrayField from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin @@ -52,8 +52,8 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin): skills = ArrayField(models.CharField(max_length=20), default=list, blank=True) external_links = ArrayField(models.TextField(), default=list, blank=True) short_bio = models.CharField(max_length=100, blank=True) - profile_image = models.ImageField(upload_to=dynamic_upload_to('user', lambda instance: 'profile'), blank=True) - banner_image = models.ImageField(upload_to=dynamic_upload_to('user', lambda instance: 'banner'), blank=True) + profile_image = models.ImageField(upload_to=DynamicUploadTo("user", "profile"), blank=True) + banner_image = models.ImageField(upload_to=DynamicUploadTo("user", "banner"), blank=True) is_staff = models.BooleanField(default=False) is_active = models.BooleanField(default=True)