diff --git a/seahub/api2/endpoints/file.py b/seahub/api2/endpoints/file.py index 14a234ac61..0671db97d7 100644 --- a/seahub/api2/endpoints/file.py +++ b/seahub/api2/endpoints/file.py @@ -29,8 +29,8 @@ from seahub.constants import PERMISSION_READ_WRITE from seahub.utils.repo import parse_repo_perm, is_repo_admin, is_repo_owner from seahub.utils.file_types import MARKDOWN, TEXT, SEADOC from seahub.tags.models import FileUUIDMap -from seahub.seadoc.models import SeadocHistoryName, SeadocDraft - +from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocCommentReply +from seahub.base.models import FileComment from seahub.settings import MAX_UPLOAD_FILE_NAME_LEN, OFFICE_TEMPLATE_ROOT from seahub.drafts.models import Draft @@ -720,10 +720,12 @@ class FileView(APIView): if filetype == SEADOC: from seahub.seadoc.utils import get_seadoc_file_uuid file_uuid = get_seadoc_file_uuid(repo, path) + FileComment.objects.filter(uuid=file_uuid).delete() FileUUIDMap.objects.delete_fileuuidmap_by_path( repo_id, parent_dir, file_name, is_dir=False) SeadocHistoryName.objects.filter(doc_uuid=file_uuid).delete() SeadocDraft.objects.filter(doc_uuid=file_uuid).delete() + SeadocCommentReply.objects.filter(doc_uuid=file_uuid).delete() except Exception as e: logger.error(e) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index ec442fbd13..bade85992f 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -74,7 +74,7 @@ from seahub.utils import gen_file_get_url, gen_token, gen_file_upload_url, \ normalize_file_path, get_no_duplicate_obj_name, normalize_dir_path from seahub.tags.models import FileUUIDMap -from seahub.seadoc.models import SeadocHistoryName, SeadocDraft +from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocCommentReply from seahub.utils.file_types import IMAGE, SEADOC from seahub.utils.file_revisions import get_file_revisions_after_renamed from seahub.utils.devices import do_unlink_device @@ -3166,10 +3166,12 @@ class FileView(APIView): if filetype == SEADOC: from seahub.seadoc.utils import get_seadoc_file_uuid file_uuid = get_seadoc_file_uuid(repo, path) + FileComment.objects.filter(uuid=file_uuid).delete() FileUUIDMap.objects.delete_fileuuidmap_by_path( repo_id, parent_dir, file_name, is_dir=False) SeadocHistoryName.objects.filter(doc_uuid=file_uuid).delete() SeadocDraft.objects.filter(doc_uuid=file_uuid).delete() + SeadocCommentReply.objects.filter(doc_uuid=file_uuid).delete() except Exception as e: logger.error(e) diff --git a/seahub/base/models.py b/seahub/base/models.py index 27bf5beeb9..cfb8f74caa 100644 --- a/seahub/base/models.py +++ b/seahub/base/models.py @@ -99,8 +99,16 @@ class FileComment(models.Model): def normalize_path(self, path): return path.rstrip('/') if path != '/' else '/' - def to_dict(self): + def to_dict(self, reply_queryset=None): + from seahub.api2.utils import user_to_dict o = self + replies = [] + if reply_queryset: + r = reply_queryset.filter(comment_id=o.pk) + for reply in r: + data = reply.to_dict() + data.update(user_to_dict(reply.author)) + replies.append(data) return { 'id': o.pk, 'repo_id': o.uuid.repo_id, @@ -111,6 +119,7 @@ class FileComment(models.Model): 'updated_at': datetime_to_isoformat_timestr(o.updated_at), 'resolved': o.resolved, 'detail': o.detail, + 'replies': replies, } diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index 7fd8f89752..f11df13394 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -34,7 +34,7 @@ from seahub.tags.models import FileUUIDMap from seahub.utils.error_msg import file_type_error_msg from seahub.utils.repo import parse_repo_perm from seahub.utils.file_revisions import get_file_revisions_within_limit -from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocRevision +from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocRevision, SeadocCommentReply from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email @@ -744,8 +744,10 @@ class SeadocCommentsView(APIView): comment_resolved = to_python_boolean(resolved) file_comments = FileComment.objects.list_by_file_uuid(file_uuid).filter(resolved=comment_resolved)[start: end] + reply_queryset = SeadocCommentReply.objects.list_by_doc_uuid(file_uuid) + for file_comment in file_comments: - comment = file_comment.to_dict() + comment = file_comment.to_dict(reply_queryset) comment.update(user_to_dict(file_comment.author, request=request, avatar_size=avatar_size)) comments.append(comment) @@ -771,7 +773,7 @@ class SeadocCommentsView(APIView): detail = request.data.get('detail', '') author = request.data.get('author', '') username = payload.get('username', '') or author - if not comment: + if comment is None: return api_error(status.HTTP_400_BAD_REQUEST, 'comment invalid.') if not username: return api_error(status.HTTP_400_BAD_REQUEST, 'author invalid.') @@ -830,6 +832,7 @@ class SeadocCommentView(APIView): return api_error(status.HTTP_404_NOT_FOUND, 'comment not found: %s' % comment_id) file_comment.delete() + SeadocCommentReply.objects.filter(comment_id=comment_id).delete() return Response({'success': True}) def put(self, request, file_uuid, comment_id): @@ -863,22 +866,207 @@ class SeadocCommentView(APIView): return api_error(status.HTTP_404_NOT_FOUND, 'comment not found: %s' % comment_id) if resolved is not None: + # do not refresh updated_at comment_resolved = to_python_boolean(resolved) file_comment.resolved = comment_resolved - if detail is not None: - file_comment.detail = detail - if comment is not None: - file_comment.comment = comment + file_comment.save(update_fields=['resolved']) - # save - file_comment.updated_at = timezone.now() - file_comment.save() + if detail is not None or comment is not None: + if detail is not None: + file_comment.detail = detail + if comment is not None: + file_comment.comment = comment + # save + file_comment.updated_at = timezone.now() + file_comment.save() comment = file_comment.to_dict() comment.update(user_to_dict(file_comment.author, request=request, avatar_size=avatar_size)) return Response(comment) +class SeadocCommentRepliesView(APIView): + authentication_classes = () + throttle_classes = (UserRateThrottle,) + + def get(self, request, file_uuid, comment_id): + """list comment replies of a sdoc + """ + auth = request.headers.get('authorization', '').split() + if not is_valid_seadoc_access_token(auth, file_uuid): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + avatar_size = int(request.GET.get('avatar_size', 32)) + except ValueError: + avatar_size = 32 + start = None + end = None + page = request.GET.get('page', '') + if page: + try: + page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '25')) + except ValueError: + page = 1 + per_page = 25 + start = (page - 1) * per_page + end = page * per_page + + # resource check + file_comment = FileComment.objects.filter( + id=comment_id, uuid=file_uuid).first() + if not file_comment: + return api_error(status.HTTP_404_NOT_FOUND, 'comment not found.') + + total_count = SeadocCommentReply.objects.list_by_comment_id(comment_id).count() + replies = [] + reply_queryset = SeadocCommentReply.objects.list_by_comment_id(comment_id)[start: end] + for reply in reply_queryset: + data = reply.to_dict() + data.update( + user_to_dict(reply.author, request=request, avatar_size=avatar_size)) + replies.append(data) + + result = {'replies': replies, 'total_count': total_count} + return Response(result) + + def post(self, request, file_uuid, comment_id): + """post a comment reply of a sdoc. + """ + # argument check + auth = request.headers.get('authorization', '').split() + is_valid, payload = is_valid_seadoc_access_token(auth, file_uuid, return_payload=True) + if not is_valid: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + avatar_size = int(request.GET.get('avatar_size', 32)) + except ValueError: + avatar_size = 32 + reply_content = request.data.get('reply', '') + type_content = request.data.get('type', 'reply') + author = request.data.get('author', '') + username = payload.get('username', '') or author + if reply_content is None: + return api_error(status.HTTP_400_BAD_REQUEST, 'reply invalid.') + if not username: + return api_error(status.HTTP_400_BAD_REQUEST, 'author invalid.') + + # resource check + file_comment = FileComment.objects.filter( + id=comment_id, uuid=file_uuid).first() + if not file_comment: + return api_error(status.HTTP_404_NOT_FOUND, 'comment not found.') + + reply = SeadocCommentReply.objects.create( + author=username, + reply=str(reply_content), + type=str(type_content), + comment_id=comment_id, + doc_uuid=file_uuid, + ) + data = reply.to_dict() + data.update( + user_to_dict(reply.author, request=request, avatar_size=avatar_size)) + return Response(data) + + +class SeadocCommentReplyView(APIView): + authentication_classes = () + throttle_classes = (UserRateThrottle,) + + def get(self, request, file_uuid, comment_id, reply_id): + """Get a comment reply + """ + auth = request.headers.get('authorization', '').split() + if not is_valid_seadoc_access_token(auth, file_uuid): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + avatar_size = int(request.GET.get('avatar_size', 32)) + except ValueError: + avatar_size = 32 + + # resource check + file_comment = FileComment.objects.filter( + id=comment_id, uuid=file_uuid).first() + if not file_comment: + return api_error(status.HTTP_404_NOT_FOUND, 'comment not found.') + + reply = SeadocCommentReply.objects.filter( + id=reply_id, doc_uuid=file_uuid, comment_id=comment_id).first() + if not reply: + return api_error(status.HTTP_404_NOT_FOUND, 'reply not found.') + + data = reply.to_dict() + data.update( + user_to_dict(reply.author, request=request, avatar_size=avatar_size)) + return Response(data) + + def delete(self, request, file_uuid, comment_id, reply_id): + """Delete a comment reply + """ + auth = request.headers.get('authorization', '').split() + if not is_valid_seadoc_access_token(auth, file_uuid): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # resource check + file_comment = FileComment.objects.filter( + id=comment_id, uuid=file_uuid).first() + if not file_comment: + return api_error(status.HTTP_404_NOT_FOUND, 'comment not found.') + + reply = SeadocCommentReply.objects.filter( + id=reply_id, doc_uuid=file_uuid, comment_id=comment_id).first() + if not reply: + return api_error(status.HTTP_404_NOT_FOUND, 'reply not found.') + reply.delete() + return Response({'success': True}) + + def put(self, request, file_uuid, comment_id, reply_id): + """Update a comment reply + """ + auth = request.headers.get('authorization', '').split() + if not is_valid_seadoc_access_token(auth, file_uuid): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # argument check + reply_content = request.data.get('reply') + if reply_content is None: + return api_error(status.HTTP_400_BAD_REQUEST, 'reply invalid.') + try: + avatar_size = int(request.GET.get('avatar_size', 32)) + except ValueError: + avatar_size = 32 + + # resource check + file_comment = FileComment.objects.filter( + id=comment_id, uuid=file_uuid).first() + if not file_comment: + return api_error(status.HTTP_404_NOT_FOUND, 'comment not found.') + + reply = SeadocCommentReply.objects.filter( + id=reply_id, doc_uuid=file_uuid, comment_id=comment_id).first() + if not reply: + return api_error(status.HTTP_404_NOT_FOUND, 'reply not found.') + + # save + reply.reply = str(reply_content) + reply.updated_at = timezone.now() + reply.save() + + data = reply.to_dict() + data.update( + user_to_dict(reply.author, request=request, avatar_size=avatar_size)) + return Response(data) + + class SeadocStartRevise(APIView): # sdoc editor use jwt token authentication_classes = (SdocJWTTokenAuthentication, TokenAuthentication, SessionAuthentication) diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py index d352521c4d..bc02d464ad 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -199,3 +199,38 @@ class SeadocRevision(models.Model): 'created_at': datetime_to_isoformat_timestr(self.created_at), 'updated_at': datetime_to_isoformat_timestr(self.updated_at), } + + +class SeadocCommentReplyManager(models.Manager): + def list_by_comment_id(self, comment_id): + return self.filter(comment_id=comment_id) + + def list_by_doc_uuid(self, doc_uuid): + return self.filter(doc_uuid=doc_uuid) + + +class SeadocCommentReply(models.Model): + author = models.CharField(max_length=255) + reply = models.TextField() + type = models.CharField(max_length=36) + comment_id = models.IntegerField(db_index=True) + doc_uuid = models.CharField(max_length=36, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + objects = SeadocCommentReplyManager() + + class Meta: + db_table = 'sdoc_comment_reply' + + def to_dict(self): + return { + 'id': self.pk, + 'author': self.author, + 'reply': self.reply, + 'type': self.type, + 'comment_id': self.comment_id, + 'doc_uuid': self.doc_uuid, + 'created_at': datetime_to_isoformat_timestr(self.created_at), + 'updated_at': datetime_to_isoformat_timestr(self.updated_at), + } diff --git a/seahub/seadoc/urls.py b/seahub/seadoc/urls.py index 4a9d59f8bd..5de2fbf245 100644 --- a/seahub/seadoc/urls.py +++ b/seahub/seadoc/urls.py @@ -1,7 +1,8 @@ from django.urls import re_path from .apis import SeadocAccessToken, SeadocUploadLink, SeadocDownloadLink, SeadocRevisionDownloadLinks, SeadocUploadFile, \ SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory, SeadocDrafts, SeadocMaskAsDraft, \ - SeadocCommentsView, SeadocCommentView, SeadocStartRevise, SeadocPublishRevision, SeadocRevisionsCount, SeadocRevisions + SeadocCommentsView, SeadocCommentView, SeadocStartRevise, SeadocPublishRevision, SeadocRevisionsCount, SeadocRevisions, \ + SeadocCommentRepliesView, SeadocCommentReplyView # api/v2.1/seadoc/ @@ -19,6 +20,8 @@ urlpatterns = [ re_path(r'^mark-as-draft/(?P[-0-9a-f]{36})/$', SeadocMaskAsDraft.as_view(), name='seadoc_mark_as_draft'), re_path(r'^comments/(?P[-0-9a-f]{36})/$', SeadocCommentsView.as_view(), name='seadoc_comments'), re_path(r'^comment/(?P[-0-9a-f]{36})/(?P\d+)/$', SeadocCommentView.as_view(), name='seadoc_comment'), + re_path(r'^comment/(?P[-0-9a-f]{36})/(?P\d+)/replies/$', SeadocCommentRepliesView.as_view(), name='seadoc_comment_replies'), + re_path(r'^comment/(?P[-0-9a-f]{36})/(?P\d+)/replies/(?P\d+)/$', SeadocCommentReplyView.as_view(), name='seadoc_comment_reply'), re_path(r'^start-revise/$', SeadocStartRevise.as_view(), name='seadoc_start_revise'), re_path(r'^publish-revision/(?P[-0-9a-f]{36})/$', SeadocPublishRevision.as_view(), name='seadoc_publish_revision'), re_path(r'^revisions-count/(?P[-0-9a-f]{36})/$', SeadocRevisionsCount.as_view(), name='seadoc_revisions_count'), diff --git a/sql/mysql.sql b/sql/mysql.sql index 86ef516cb5..d1f0c3e517 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1420,3 +1420,17 @@ CREATE TABLE `sdoc_revision` ( KEY `sdoc_revision_username` (`username`), KEY `sdoc_revision_is_published` (`is_published`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `sdoc_comment_reply` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `author` varchar(255) NOT NULL, + `reply` longtext NOT NULL, + `type` varchar(36) NOT NULL, + `comment_id` int(11) NOT NULL, + `doc_uuid` varchar(36) NOT NULL, + `created_at` datetime(6) NOT NULL DEFAULT current_timestamp(6), + `updated_at` datetime(6) NOT NULL DEFAULT current_timestamp(6) ON UPDATE current_timestamp(6), + PRIMARY KEY (`id`), + KEY `sdoc_comment_reply_comment_id` (`comment_id`), + KEY `sdoc_comment_reply_doc_uuid` (`doc_uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;