Merge pull request #66 from plers-org/sm/#64

Sm/#64
This commit is contained in:
NKEY
2025-06-05 18:36:51 +09:00
committed by GitHub
15 changed files with 353 additions and 27 deletions

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -167,7 +167,7 @@ TIME_ZONE = 'Asia/Seoul'
USE_I18N = True
USE_TZ = False
USE_TZ = True
MEDIA_URL = '/media/'

View File

@@ -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:

View File

@@ -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()
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)

View File

@@ -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

View File

@@ -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

29
nocodetools/services.py Normal file
View File

@@ -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)

11
nocodetools/urls.py Normal file
View File

@@ -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()),
]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)