@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -167,7 +167,7 @@ TIME_ZONE = 'Asia/Seoul'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = False
|
||||
USE_TZ = True
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
26
nocodetools/permissions.py
Normal file
26
nocodetools/permissions.py
Normal 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
|
||||
67
nocodetools/serializers.py
Normal file
67
nocodetools/serializers.py
Normal 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
29
nocodetools/services.py
Normal 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
11
nocodetools/urls.py
Normal 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()),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user