diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index 0212a61271..199bc3b1ac 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -264,6 +264,12 @@ class DirentListItem extends React.Component { case 'Lock': this.onLockItem(); break; + case 'Mask as draft': + this.onMaskAsDraft(); + break; + case 'Unmask as draft': + this.onUnmaskAsDraft(); + break; case 'Comment': this.props.onDirentClick(this.props.dirent); this.props.showDirentDetail('comments'); @@ -353,6 +359,28 @@ class DirentListItem extends React.Component { }); } + onMaskAsDraft = () => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(this.props.dirent); + seafileAPI.sdocMaskAsDraft(repoID, filePath).then((res) => { + this.props.updateDirent(this.props.dirent, 'is_sdoc_draft', true); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onUnmaskAsDraft = () => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(this.props.dirent); + seafileAPI.sdocUnmaskAsDraft(repoID, filePath).then((res) => { + this.props.updateDirent(this.props.dirent, 'is_sdoc_draft', false); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + onHistory = () => { let repoID = this.props.repoID; let filePath = this.getDirentPath(this.props.dirent); @@ -697,6 +725,9 @@ class DirentListItem extends React.Component { {dirent.name} : {dirent.name} } + {(Utils.isSdocFile(dirent.name) && dirent.is_sdoc_draft) && + {'(draft)'} + } )} diff --git a/frontend/src/models/dirent.js b/frontend/src/models/dirent.js index afa25203bd..f071dd4933 100644 --- a/frontend/src/models/dirent.js +++ b/frontend/src/models/dirent.js @@ -37,6 +37,9 @@ class Dirent { if (json.encoded_thumbnail_src) { this.encoded_thumbnail_src = json.encoded_thumbnail_src; } + if (Utils.isSdocFile(json.name)) { + this.is_sdoc_draft = json.is_sdoc_draft; + } } } diff --git a/frontend/src/utils/text-translation.js b/frontend/src/utils/text-translation.js index 6b57f3055e..404785c489 100644 --- a/frontend/src/utils/text-translation.js +++ b/frontend/src/utils/text-translation.js @@ -16,6 +16,8 @@ const TextTranslation = { 'OPEN_VIA_CLIENT' : {key : 'Open via Client', value : gettext('Open via Client')}, 'LOCK' : {key : 'Lock', value : gettext('Lock')}, 'UNLOCK' : {key : 'Unlock', value : gettext('Unlock')}, + 'MASK_AS_DRAFT' : {key : 'Mask as draft', value : gettext('Mask as draft')}, + 'UNMASK_AS_DRAFT' : {key : 'Unmask as draft', value : gettext('Unmask as draft')}, 'COMMENT' : {key : 'Comment', value : gettext('Comment')}, 'HISTORY' : {key : 'History', value : gettext('History')}, 'ACCESS_LOG' : {key : 'Access Log', value : gettext('Access Log')}, diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 45b571e117..82b7853b1d 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -526,7 +526,7 @@ export const Utils = { getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { let list = []; - const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK, + const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK, MASK_AS_DRAFT, UNMASK_AS_DRAFT, COMMENT, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT, ONLYOFFICE_CONVERT } = TextTranslation; const permission = dirent.permission; const { isCustomPermission, customPermission } = Utils.getUserPermission(permission); @@ -594,6 +594,13 @@ export const Utils = { } list.push('Divider'); + if (Utils.isSdocFile(dirent.name)) { + if (dirent.is_sdoc_draft) { + list.push(UNMASK_AS_DRAFT); + } else { + list.push(MASK_AS_DRAFT); + } + } if (enableFileComment) { list.push(COMMENT); } @@ -804,6 +811,20 @@ export const Utils = { } }, + isSdocFile: function(filePath) { + let index = filePath.lastIndexOf('.'); + if (index === -1) { + return false; + } else { + let type = filePath.substring(index).toLowerCase(); + if (type === '.sdoc') { + return true; + } else { + return false; + } + } + }, + isInternalFileLink: function(url, repoID) { var re = new RegExp(serviceURL + '/lib/' + repoID + '/file.*'); return re.test(url); diff --git a/seahub/api2/endpoints/dir.py b/seahub/api2/endpoints/dir.py index 31e394239f..7329ad8010 100644 --- a/seahub/api2/endpoints/dir.py +++ b/seahub/api2/endpoints/dir.py @@ -20,10 +20,10 @@ from seahub.api2.views import get_dir_file_recursively from seahub.thumbnail.utils import get_thumbnail_src from seahub.views import check_folder_permission from seahub.utils import check_filename_with_rename, is_valid_dirent_name, \ - normalize_dir_path, is_pro_version, FILEEXT_TYPE_MAP + normalize_dir_path, is_pro_version, FILEEXT_TYPE_MAP, get_file_type_and_ext from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.utils.file_tags import get_files_tags_in_dir -from seahub.utils.file_types import IMAGE, VIDEO, XMIND +from seahub.utils.file_types import IMAGE, VIDEO, XMIND, SEADOC from seahub.base.models import UserStarredFiles from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email @@ -98,6 +98,17 @@ def get_dir_file_info_list(username, request_type, repo_obj, parent_dir, logger.error(e) files_tags_in_dir = {} + try: + from seahub.tags.models import FileUUIDMap + from seahub.seadoc.models import SeadocDraft + file_uuid_queryset = FileUUIDMap.objects.get_fileuuidmaps_by_parent_path( + repo_id, parent_dir) + file_uuid_list = [item.uuid for item in file_uuid_queryset] + seadoc_draft_queryset = SeadocDraft.objects.list_by_doc_uuids( + file_uuid_list) + except Exception as e: + logger.error(e) + for dirent in file_list: file_name = dirent.obj_name @@ -166,6 +177,22 @@ def get_dir_file_info_list(username, request_type, repo_obj, parent_dir, src = get_thumbnail_src(repo_id, thumbnail_size, file_path) file_info['encoded_thumbnail_src'] = quote(src) + # sdoc + filetype, fileext = get_file_type_and_ext(file_info['name']) + if filetype == SEADOC: + try: + file_uuid_map = file_uuid_queryset.filter( + filename=file_name).first() + if file_uuid_map: + sdoc_draft = seadoc_draft_queryset.filter( + doc_uuid=file_uuid_map.uuid).first() + if sdoc_draft: + file_info['is_sdoc_draft'] = True + else: + file_info['is_sdoc_draft'] = False + except Exception as e: + logger.error(e) + file_info_list.append(file_info) dir_info_list.sort(key=lambda x: x['name'].lower()) diff --git a/seahub/api2/endpoints/file.py b/seahub/api2/endpoints/file.py index 03effbfe5b..14a234ac61 100644 --- a/seahub/api2/endpoints/file.py +++ b/seahub/api2/endpoints/file.py @@ -29,6 +29,7 @@ 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.settings import MAX_UPLOAD_FILE_NAME_LEN, OFFICE_TEMPLATE_ROOT @@ -717,8 +718,12 @@ class FileView(APIView): try: # rm sdoc fileuuid filetype, fileext = get_file_type_and_ext(file_name) if filetype == SEADOC: + from seahub.seadoc.utils import get_seadoc_file_uuid + file_uuid = get_seadoc_file_uuid(repo, path) 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() except Exception as e: logger.error(e) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index a5ae2a6149..9b671cc51d 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -73,6 +73,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.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 @@ -3154,8 +3155,12 @@ class FileView(APIView): try: # rm sdoc fileuuid filetype, fileext = get_file_type_and_ext(file_name) if filetype == SEADOC: + from seahub.seadoc.utils import get_seadoc_file_uuid + file_uuid = get_seadoc_file_uuid(repo, path) 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() except Exception as e: logger.error(e) diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index c26a36acac..3e74d34c71 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -31,7 +31,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 +from seahub.seadoc.models import SeadocHistoryName, SeadocDraft from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email @@ -504,3 +504,89 @@ class SeadocHistory(APIView): 'obj_id': obj_id, 'name': new_name, }) + + +class SeadocMaskAsDraft(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle, ) + + def post(self, request, repo_id): + """ Mask as draft + """ + username = request.user.username + # argument check + path = request.data.get('p', None) + if not path: + error_msg = 'p invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + path = normalize_file_path(path) + parent_dir = os.path.dirname(path) + filename = os.path.basename(path) + + filetype, fileext = get_file_type_and_ext(filename) + if filetype != SEADOC: + error_msg = 'seadoc file type %s invalid.' % filetype + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + permission = check_folder_permission(request, repo_id, path) + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # + file_uuid = get_seadoc_file_uuid(repo, path) + exist_draft = SeadocDraft.objects.get_by_doc_uuid(file_uuid) + if exist_draft: + error_msg = '%s is already draft' % filename + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + draft = SeadocDraft.objects.mask_as_draft(file_uuid, username) + + return Response(draft.to_dict()) + + def delete(self, request, repo_id): + """ Unmask as draft + """ + username = request.user.username + # argument check + path = request.data.get('p', None) + if not path: + error_msg = 'p invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + path = normalize_file_path(path) + parent_dir = os.path.dirname(path) + filename = os.path.basename(path) + + filetype, fileext = get_file_type_and_ext(filename) + if filetype != SEADOC: + error_msg = 'seadoc file type %s invalid.' % filetype + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + permission = check_folder_permission(request, repo_id, path) + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # + file_uuid = get_seadoc_file_uuid(repo, path) + SeadocDraft.objects.unmask_as_draft(file_uuid) + + return Response({'success': True}) diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py index f84c2bc6da..47f9ee1a89 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -1,5 +1,7 @@ from django.db import models +from seahub.utils.timeutils import datetime_to_isoformat_timestr + class SeadocHistoryNameManager(models.Manager): def update_name(self, doc_uuid, obj_id, name): @@ -31,3 +33,40 @@ class SeadocHistoryName(models.Model): 'obj_id': self.obj_id, 'name': self.name, } + + +class SeadocDraftManager(models.Manager): + + def get_by_doc_uuid(self, doc_uuid): + return self.filter(doc_uuid=doc_uuid).first() + + def mask_as_draft(self, doc_uuid, username): + return self.create(doc_uuid=doc_uuid, username=username) + + def unmask_as_draft(self, doc_uuid): + return self.filter(doc_uuid=doc_uuid).delete() + + def list_by_doc_uuids(self, doc_uuid_list): + return self.filter(doc_uuid__in=doc_uuid_list) + + def list_by_username(self, username): + return self.filter(username=username) + + +class SeadocDraft(models.Model): + doc_uuid = models.CharField(max_length=36, unique=True) + username = models.CharField(max_length=255, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + + objects = SeadocDraftManager() + + class Meta: + db_table = 'sdoc_draft' + + def to_dict(self): + return { + 'id': self.pk, + 'doc_uuid': self.doc_uuid, + 'username': self.username, + 'created_at': datetime_to_isoformat_timestr(self.created_at), + } diff --git a/seahub/seadoc/urls.py b/seahub/seadoc/urls.py index 956956aee4..0db00e0b17 100644 --- a/seahub/seadoc/urls.py +++ b/seahub/seadoc/urls.py @@ -1,6 +1,6 @@ from django.urls import re_path from .apis import SeadocAccessToken, SeadocUploadLink, SeadocDownloadLink, SeadocUploadFile, \ - SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory + SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory, SeadocMaskAsDraft urlpatterns = [ re_path(r'^access-token/(?P[-0-9a-f]{36})/$', SeadocAccessToken.as_view(), name='seadoc_access_token'), @@ -11,4 +11,5 @@ urlpatterns = [ re_path(r'^download-image/(?P[-0-9a-f]{36})/(?P.*)$', SeadocDownloadImage.as_view(), name='seadoc_download_image'), re_path(r'^copy-history-file/(?P[-0-9a-f]{36})/$', SeadocCopyHistoryFile.as_view(), name='seadoc_copy_history_file'), re_path(r'^history/(?P[-0-9a-f]{36})/$', SeadocHistory.as_view(), name='seadoc_history'), + re_path(r'^mask-as-draft/(?P[-0-9a-f]{36})/$', SeadocMaskAsDraft.as_view(), name='seadoc_mask_as_draft'), ] diff --git a/sql/mysql.sql b/sql/mysql.sql index be7c8e69b4..325b479d51 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1385,3 +1385,13 @@ CREATE TABLE `history_name` ( KEY `history_name_doc_uuid` (`doc_uuid`), UNIQUE KEY `history_name_doc_uuid_obj_id` (`doc_uuid`, `obj_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `sdoc_draft` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `doc_uuid` char(36) NOT NULL, + `username` varchar(255) NOT NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `sdoc_draft_doc_uuid` (`doc_uuid`), + KEY `sdoc_draft_username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;