diff --git a/frontend/config/webpack.entry.js b/frontend/config/webpack.entry.js index 575b9281da..ce7ab8628c 100644 --- a/frontend/config/webpack.entry.js +++ b/frontend/config/webpack.entry.js @@ -9,6 +9,8 @@ const entryFiles = { fileHistory: "/file-history.js", fileHistoryOld: "/file-history-old.js", sdocFileHistory: "/pages/sdoc-file-history/index.js", + sdocRevision: "/pages/sdoc-revision/index.js", + sdocRevisions: "/pages/sdoc-revisions/index.js", app: "/app.js", draft: "/draft.js", sharedDirView: "/shared-dir-view.js", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b478368808..95370a0db3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "@gatsbyjs/reach-router": "1.3.9", "@seafile/react-image-lightbox": "2.0.2", "@seafile/resumablejs": "1.1.16", - "@seafile/sdoc-editor": "0.1.68", + "@seafile/sdoc-editor": "0.1.69", "@seafile/seafile-calendar": "0.0.12", "@seafile/seafile-editor": "0.3.132", "@seafile/slate-react": "0.54.13", @@ -43,7 +43,7 @@ "react-select": "5.7.0", "react-transition-group": "4.4.5", "reactstrap": "8.9.0", - "seafile-js": "0.2.202", + "seafile-js": "0.2.204", "socket.io-client": "^2.2.0", "svg-sprite-loader": "^6.0.11", "svgo-loader": "^3.0.1", @@ -5198,9 +5198,9 @@ "license": "MIT" }, "node_modules/@seafile/sdoc-editor": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.68.tgz", - "integrity": "sha512-HlZHBImvff5EDO243NLHY22h4DnSJ3X8G34f78nPVo0VrNRz2q2odOiVDDOT3eKggVFoVlrutKp4/NOjRTNs1A==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.69.tgz", + "integrity": "sha512-HcUWdXSmN5jChHl75zew/Uh/8usavDiHS278IpHDjRsWjldVcMuB89/hZKzGOKiMO/QNJ/XZBmAASkUlXjXW1w==", "dependencies": { "@seafile/react-image-lightbox": "2.0.2", "@seafile/slate": "0.91.8", @@ -24900,9 +24900,9 @@ } }, "node_modules/seafile-js": { - "version": "0.2.202", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.202.tgz", - "integrity": "sha512-OE85UMZxgaM4hq7+ey7xn/O8JmGyaYV5J4nCG7s0a6/JXgzJXNENdFT9Jid40Hlf4Ks7aHtuMM7qx8SbmROlOw==", + "version": "0.2.204", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.204.tgz", + "integrity": "sha512-t+tDh2TQiT0g9DB2Lzx9TzVb/Pc6lKUScobklnP6WhHEgK8YVhFIWi+JcJR0KDmTbClKV2DSFlCWPG9Vr79KGQ==", "dependencies": { "@babel/polyfill": "7.12.1", "axios": "1.2.1", @@ -33359,9 +33359,9 @@ "version": "1.1.16" }, "@seafile/sdoc-editor": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.68.tgz", - "integrity": "sha512-HlZHBImvff5EDO243NLHY22h4DnSJ3X8G34f78nPVo0VrNRz2q2odOiVDDOT3eKggVFoVlrutKp4/NOjRTNs1A==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.69.tgz", + "integrity": "sha512-HcUWdXSmN5jChHl75zew/Uh/8usavDiHS278IpHDjRsWjldVcMuB89/hZKzGOKiMO/QNJ/XZBmAASkUlXjXW1w==", "requires": { "@seafile/react-image-lightbox": "2.0.2", "@seafile/slate": "0.91.8", @@ -47245,9 +47245,9 @@ } }, "seafile-js": { - "version": "0.2.202", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.202.tgz", - "integrity": "sha512-OE85UMZxgaM4hq7+ey7xn/O8JmGyaYV5J4nCG7s0a6/JXgzJXNENdFT9Jid40Hlf4Ks7aHtuMM7qx8SbmROlOw==", + "version": "0.2.204", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.204.tgz", + "integrity": "sha512-t+tDh2TQiT0g9DB2Lzx9TzVb/Pc6lKUScobklnP6WhHEgK8YVhFIWi+JcJR0KDmTbClKV2DSFlCWPG9Vr79KGQ==", "requires": { "@babel/polyfill": "7.12.1", "axios": "1.2.1", diff --git a/frontend/package.json b/frontend/package.json index 7a241a626b..a5e10c33af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "@gatsbyjs/reach-router": "1.3.9", "@seafile/react-image-lightbox": "2.0.2", "@seafile/resumablejs": "1.1.16", - "@seafile/sdoc-editor": "0.1.68", + "@seafile/sdoc-editor": "0.1.69", "@seafile/seafile-calendar": "0.0.12", "@seafile/seafile-editor": "0.3.132", "@seafile/slate-react": "0.54.13", @@ -38,7 +38,7 @@ "react-select": "5.7.0", "react-transition-group": "4.4.5", "reactstrap": "8.9.0", - "seafile-js": "0.2.202", + "seafile-js": "0.2.204", "socket.io-client": "^2.2.0", "svg-sprite-loader": "^6.0.11", "svgo-loader": "^3.0.1", diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js index 34167a6a6d..38686c2ab8 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js @@ -150,6 +150,18 @@ class DirentGridView extends React.Component{ case 'Lock': this.onLockItem(currentObject); break; + case 'Mask as draft': + this.onMaskAsDraft(currentObject); + break; + case 'Unmask as draft': + this.onUnmaskAsDraft(currentObject); + break; + case 'Start revise': + this.onStartRevise(currentObject); + break; + case 'List revisions': + this.openRevisionsPage(currentObject); + break; case 'Comment': this.onCommentItem(); break; @@ -262,6 +274,48 @@ class DirentGridView extends React.Component{ }); } + onMaskAsDraft = (currentObject) => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(currentObject); + seafileAPI.sdocMaskAsDraft(repoID, filePath).then((res) => { + this.props.updateDirent(currentObject, 'is_sdoc_draft', true); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onUnmaskAsDraft = (currentObject) => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(currentObject); + seafileAPI.sdocUnmaskAsDraft(repoID, filePath).then((res) => { + this.props.updateDirent(currentObject, 'is_sdoc_draft', false); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onStartRevise = (currentObject) => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(currentObject); + seafileAPI.sdocStartRevise(repoID, filePath).then((res) => { + const url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(res.data.file_path); + window.open(url); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + openRevisionsPage = (currentObject) => { + const repoID = this.props.repoID; + const filePath = this.getDirentPath(currentObject); + const url = Utils.generateRevisionsURL(siteRoot, repoID, filePath); + if (!url) return; + window.open(url); + } + onCommentItem = () => { this.props.showDirentDetail('comments'); } 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 199bc3b1ac..87f72f41fe 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -270,6 +270,12 @@ class DirentListItem extends React.Component { case 'Unmask as draft': this.onUnmaskAsDraft(); break; + case 'Start revise': + this.onStartRevise(); + break; + case 'List revisions': + this.openRevisionsPage(); + break; case 'Comment': this.props.onDirentClick(this.props.dirent); this.props.showDirentDetail('comments'); @@ -381,6 +387,26 @@ class DirentListItem extends React.Component { }); } + onStartRevise = () => { + const repoID = this.props.repoID; + const filePath = this.getDirentPath(this.props.dirent); + seafileAPI.sdocStartRevise(repoID, filePath).then((res) => { + const url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(res.data.file_path); + window.open(url); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + openRevisionsPage = () => { + const repoID = this.props.repoID; + const filePath = this.getDirentPath(this.props.dirent); + const url = Utils.generateRevisionsURL(siteRoot, repoID, filePath); + if (!url) return; + window.open(url); + } + onHistory = () => { let repoID = this.props.repoID; let filePath = this.getDirentPath(this.props.dirent); diff --git a/frontend/src/components/toolbar/multiple-dir-operation-toolbar.js b/frontend/src/components/toolbar/multiple-dir-operation-toolbar.js index 7387f9d4a7..0c45006bc9 100644 --- a/frontend/src/components/toolbar/multiple-dir-operation-toolbar.js +++ b/frontend/src/components/toolbar/multiple-dir-operation-toolbar.js @@ -121,6 +121,40 @@ class MultipleDirOperationToolbar extends React.Component { }); } + onMaskAsDraft = (dirent) => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(dirent); + seafileAPI.sdocMaskAsDraft(repoID, filePath).then((res) => { + this.props.updateDirent(dirent, 'is_sdoc_draft', true); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onUnmaskAsDraft = (dirent) => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(dirent); + seafileAPI.sdocUnmaskAsDraft(repoID, filePath).then((res) => { + this.props.updateDirent(dirent, 'is_sdoc_draft', false); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onStartRevise = (dirent) => { + let repoID = this.props.repoID; + let filePath = this.getDirentPath(dirent); + seafileAPI.sdocStartRevise(repoID, filePath).then((res) => { + let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(res.data.file_path); + window.open(url); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + onCommentItem = () => { this.props.showDirentDetail('comments'); } @@ -171,6 +205,15 @@ class MultipleDirOperationToolbar extends React.Component { case 'Unlock': this.unlockFile(dirent); break; + case 'Mask as draft': + this.onMaskAsDraft(dirent); + break; + case 'Unmask as draft': + this.onUnmaskAsDraft(dirent); + break; + case 'Start revise': + this.onStartRevise(dirent); + break; case 'Comment': this.onCommentItem(); break; diff --git a/frontend/src/css/sdoc-revision.css b/frontend/src/css/sdoc-revision.css new file mode 100644 index 0000000000..53f1972780 --- /dev/null +++ b/frontend/src/css/sdoc-revision.css @@ -0,0 +1,51 @@ +.sdoc-revision .sdoc-revision-container { + flex: 1; + overflow-x: hidden; +} + +.sdoc-revision .sdoc-revision-header { + height: 50px; + border-bottom: 1px solid #e5e5e5; + background-color: #fff; +} + +.sdoc-revision .sdoc-revision-header .sdoc-revision-header-left { + font-size: 1.25rem; + flex: 1; +} + +.sdoc-revision .sdoc-revision-header .file-name { + flex: 1; +} + +.sdoc-revision .sdoc-revision-header .sdoc-revision-header-right { + height: 100%; + min-width: fit-content; +} + +.sdoc-revision .sdoc-revision-content { + flex: 1; + min-height: 0; + padding: 20px 40px; + background-color: #F5F5F5; + overflow-y: scroll; +} + +.sdoc-revision .sdoc-revision-content .sdoc-revision-viewer { + width: 100%; + min-height: 120px; + flex: 1; + background-color: #fff; + word-break: break-word; + border: 1px solid #e6e6dd; +} + +.sdoc-revision .sdoc-revision-content .sdoc-editor-content { + background-color: #fff; +} + +.sdoc-revision .sdoc-revision-content .article { + width: 100%; + margin: 0; +} + diff --git a/frontend/src/css/sdoc-revisions.css b/frontend/src/css/sdoc-revisions.css new file mode 100644 index 0000000000..3a4813923c --- /dev/null +++ b/frontend/src/css/sdoc-revisions.css @@ -0,0 +1,3 @@ +.sdoc-revisions .sdoc-revision:hover { + background-color: #f8f8f8; +} diff --git a/frontend/src/pages/sdoc-revision/index.js b/frontend/src/pages/sdoc-revision/index.js new file mode 100644 index 0000000000..d1faeee084 --- /dev/null +++ b/frontend/src/pages/sdoc-revision/index.js @@ -0,0 +1,137 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import classnames from 'classnames'; +import { Button } from 'reactstrap'; +import { DiffViewer } from '@seafile/sdoc-editor'; +import { gettext } from '../../utils/constants'; +import Loading from '../../components/loading'; +import GoBack from '../../components/common/go-back'; +import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../utils/seafile-api'; + +import '../../css/layout.css'; +import '../../css/sdoc-revision.css'; +import toaster from '../../components/toast'; + +const { serviceURL, avatarURL, siteRoot } = window.app.config; +const { username, name } = window.app.pageOptions; +const { repoID, fileName, filePath, docUuid, assetsUrl, fileDownloadLink, originFileDownloadLink } = window.sdocRevision; + +window.seafile = { + repoID, + docPath: filePath, + docName: fileName, + docUuid, + isOpenSocket: false, + serviceUrl: serviceURL, + name, + username, + avatarURL, + siteRoot, + assetsUrl, +}; + +class SdocRevision extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, + errorMessage: '', + revisionContent: '', + originContent: '', + }; + } + + componentDidMount() { + fetch(fileDownloadLink).then(res => { + return res.json(); + }).then(revisionContent => { + fetch(originFileDownloadLink).then(res => { + return res.json(); + }).then(originContent => { + this.setState({ revisionContent, originContent, isLoading: false, errorMessage: '' }); + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error, true); + this.setState({ isLoading: false, errorMessage }); + }); + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error, true); + this.setState({ isLoading: false, errorMessage }); + }); + } + + edit = (event) => { + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + window.location.href = `${siteRoot}lib/${repoID}/file${filePath}`; + } + + publishRevision = (event) => { + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + seafileAPI.sdocPublishRevision(docUuid).then(res => { + const { origin_file_path } = res.data; + window.location.href = `${siteRoot}lib/${repoID}/file${origin_file_path}`; + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error, false); + toaster.danger(gettext(errorMessage)); + }); + } + + renderContent = () => { + const { isLoading, errorMessage, revisionContent, originContent } = this.state; + if (isLoading) { + return ( +
+ +
+ ); + } + + if (errorMessage) { + return ( +
+ {gettext(errorMessage)} +
+ ); + } + + return ( + + ); + } + + render() { + const { isLoading, errorMessage } = this.state; + + return ( +
+
+
+
+ +
{fileName}
+
+
+ {(!isLoading && !errorMessage) && ( + <> + + + + )} +
+
+
+ {this.renderContent()} +
+
+
+ ); + } +} + +ReactDom.render(, document.getElementById('wrapper')); diff --git a/frontend/src/pages/sdoc-revisions/index.js b/frontend/src/pages/sdoc-revisions/index.js new file mode 100644 index 0000000000..4f8caeade1 --- /dev/null +++ b/frontend/src/pages/sdoc-revisions/index.js @@ -0,0 +1,122 @@ +import React, { Component, Fragment } from 'react'; +import ReactDom from 'react-dom'; +import moment from 'moment'; +import classnames from 'classnames'; +import { siteRoot, mediaUrl, logoPath, siteTitle, logoHeight, logoWidth, gettext } from '../../utils/constants'; +import '../../css/sdoc-revisions.css'; + +moment.locale(window.app.config.lang); +const { filename, zipped, forloopLast, repo, viewLibFile, revisions, currentPage, prevPage, + nextPage, perPage, pageNext, extraHref } = window.sdocRevisions; +const validZipped = JSON.parse(zipped); +const validRevisions = JSON.parse(revisions); + +class SdocRevisions extends Component { + + renderPerPage = (perPageCount, className) => { + if (perPage === perPageCount) { + return ({perPageCount}); + } + return ( + {perPageCount} + ); + } + + renderRevisions = () => { + if (!Array.isArray(validRevisions) || validRevisions.length === 0) { + return ( +
+

{gettext('This file has not revisions yet')}

+
+ ); + } + return ( + + + + + + + + + + {validRevisions.map(revision => { + return ( + + + + + + ); + })} + +
{gettext('User')}{gettext('File_name')}{gettext('Ctime')}
{revision.nickname} + + {revision.filename} + + {moment(revision.created_at).format('YYYY-MM-DD HH:MM')}
+ ); + } + + renderFooter = () => { + return ( +
+ {currentPage !== 1 && ( + {gettext('Previous')} + )} + {pageNext && ( + {gettext('Next')} + )} + {(currentPage !== 1 || pageNext) && ({'|'})} + {gettext('Per page: ')} + {this.renderPerPage(25, 'mr-1')} + {this.renderPerPage(50, 'mr-1')} + {this.renderPerPage(100)} +
+ ); + } + + render() { + return ( + <> + +
+
+
+

+ {filename} + {gettext('Revisions')} +

+
+

+ {gettext('Current Path:')} + {validZipped.map((item, index) => { + if (forloopLast) { + return ({item[0]}); + } + return ( + + {item[0]} + {index !== validZipped.length - 1 && ( + {'/'} + )} + + ); + })} +

+
+ {this.renderRevisions()} + {this.renderFooter()} +
+
+
+ + ); + } +} + +ReactDom.render(, document.getElementById('wrapper')); diff --git a/frontend/src/utils/text-translation.js b/frontend/src/utils/text-translation.js index b9525a938f..2afa6fdfb1 100644 --- a/frontend/src/utils/text-translation.js +++ b/frontend/src/utils/text-translation.js @@ -18,6 +18,8 @@ const TextTranslation = { 'UNLOCK' : {key : 'Unlock', value : gettext('Unlock')}, 'MASK_AS_DRAFT' : {key : 'Mask as draft', value : gettext('Mark as draft')}, 'UNMASK_AS_DRAFT' : {key : 'Unmask as draft', value : gettext('Unmark as draft')}, + 'START_REVISE' : {key : 'Start revise', value : gettext('Start revise')}, + 'LIST_REVISIONS': { key: 'List revisions', value: gettext('List revisions') }, '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 82b7853b1d..8dced1abe5 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -527,7 +527,7 @@ export const Utils = { getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { let list = []; 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; + START_REVISE, LIST_REVISIONS, COMMENT, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT, ONLYOFFICE_CONVERT } = TextTranslation; const permission = dirent.permission; const { isCustomPermission, customPermission } = Utils.getUserPermission(permission); @@ -600,6 +600,8 @@ export const Utils = { } else { list.push(MASK_AS_DRAFT); } + list.push(START_REVISE); + list.push(LIST_REVISIONS); } if (enableFileComment) { list.push(COMMENT); @@ -1542,6 +1544,17 @@ export const Utils = { generateHistoryURL: function(siteRoot, repoID, path) { if (!siteRoot || !repoID || !path) return ''; return siteRoot + 'repo/file_revisions/' + repoID + '/?p=' + this.encodePath(path); + }, + + generateRevisionURL: function(siteRoot, repoID, path) { + if (!siteRoot || !repoID || !path) return ''; + return siteRoot + 'repo/sdoc_revision/' + repoID + '/?p=' + this.encodePath(path); + }, + + generateRevisionsURL: function(siteRoot, repoID, path) { + if (!siteRoot || !repoID || !path) return ''; + console.log(siteRoot + 'repo/sdoc_revisions/' + repoID + '/?p=' + this.encodePath(path)) + return siteRoot + 'repo/sdoc_revisions/' + repoID + '/?p=' + this.encodePath(path); } }; diff --git a/frontend/src/view-file-sdoc.js b/frontend/src/view-file-sdoc.js index 48bf8bd91a..0735e04b13 100644 --- a/frontend/src/view-file-sdoc.js +++ b/frontend/src/view-file-sdoc.js @@ -10,7 +10,8 @@ const { serviceURL, avatarURL, siteRoot } = window.app.config; const { username, name } = window.app.userInfo; const { repoID, repoName, parentDir, filePerm, - docPath, docName, docUuid, seadocAccessToken, seadocServerUrl, assetsUrl + docPath, docName, docUuid, seadocAccessToken, seadocServerUrl, assetsUrl, + isSdocRevision, isPublished, originFilename, revisionCreatedAt, } = window.app.pageOptions; window.seafile = { @@ -31,7 +32,12 @@ window.seafile = { parentFolderURL: `${siteRoot}library/${repoID}/${Utils.encodePath(repoName + parentDir)}`, assetsUrl, isShowInternalLink: true, - isStarIconShown: true // for star/unstar + isStarIconShown: true, // for star/unstar + isSdocRevision, + isPublished, + revisionURL: Utils.generateRevisionURL(siteRoot, repoID, docPath), + originFilename, + revisionCreatedAt, }; ReactDom.render( diff --git a/seahub/api2/authentication.py b/seahub/api2/authentication.py index 7f07d23ea3..a23da6f3ae 100644 --- a/seahub/api2/authentication.py +++ b/seahub/api2/authentication.py @@ -185,3 +185,28 @@ class RepoAPITokenAuthentication(BaseAuthentication): request.repo_api_token_obj = rat return AnonymousUser(), auth[1] + + +class SdocJWTTokenAuthentication(BaseAuthentication): + + def authenticate(self, request): + """ sdoc jwt token + """ + from seahub.seadoc.utils import is_valid_seadoc_access_token + file_uuid = request.parser_context['kwargs'].get('file_uuid') + auth = request.headers.get('authorization', '').split() + is_valid, payload = is_valid_seadoc_access_token(auth, file_uuid, return_payload=True) + if not is_valid: + return None + + username = payload.get('username') + if not username: + return None + try: + user = User.objects.get(email=username) + except User.DoesNotExist: + user = None + if not user or not user.is_active: + return None + + return user, auth[1] diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index 18730806df..5ee6fb1adc 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -1,5 +1,6 @@ import os import json +import uuid import logging import requests import posixpath @@ -18,7 +19,7 @@ from django.utils import timezone from seaserv import seafile_api, check_quota from seahub.views import check_folder_permission -from seahub.api2.authentication import TokenAuthentication +from seahub.api2.authentication import TokenAuthentication, SdocJWTTokenAuthentication from seahub.api2.utils import api_error, user_to_dict, to_python_boolean from seahub.api2.throttling import UserRateThrottle from seahub.seadoc.utils import is_valid_seadoc_access_token, get_seadoc_upload_link, \ @@ -32,12 +33,14 @@ 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 +from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocRevision from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email from seahub.utils.timeutils import utc_datetime_to_isoformat_timestr, timestamp_to_isoformat_timestr from seahub.base.models import FileComment +from seahub.constants import PERMISSION_READ_WRITE +from seahub.seadoc.sdoc_server_api import SdocServerAPI logger = logging.getLogger(__name__) @@ -514,36 +517,44 @@ class SeadocDrafts(APIView): permission_classes = (IsAuthenticated,) throttle_classes = (UserRateThrottle, ) - def get(self, request, repo_id): + def get(self, request): """list drafts """ username = request.user.username # argument check - owned = request.GET.get('owned') + repo_id = request.GET.get('repo_id') + 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 + limit = per_page + 1 - # 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) + if repo_id: + # 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, '/') - if not permission: - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) + # permission check + permission = check_folder_permission(request, repo_id, '/') + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) - # - if owned: - draft_queryset = SeadocDraft.objects.filter( - repo_id=repo_id, username=username) - # all + draft_queryset = SeadocDraft.objects.list_by_repo_id(repo_id, start, limit) + count = SeadocDraft.objects.filter(repo_id=repo_id).count() else: - draft_queryset = SeadocDraft.objects.list_by_repo_id(repo_id) + # owned + draft_queryset = SeadocDraft.objects.list_by_username(username, start, limit) + count = SeadocDraft.objects.filter(username=username).count() drafts = [draft.to_dict() for draft in draft_queryset] - return Response({'drafts': drafts}) + return Response({'drafts': drafts, 'count': count}) class SeadocMaskAsDraft(APIView): @@ -810,3 +821,271 @@ class SeadocCommentView(APIView): comment = file_comment.to_dict() comment.update(user_to_dict(file_comment.author, request=request, avatar_size=avatar_size)) return Response(comment) + + +class SeadocRevisions(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle, ) + + def get(self, request): + """list + """ + username = request.user.username + # argument check + origin_file_uuid = request.GET.get('doc_uuid') + repo_id = request.GET.get('repo_id') + 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 + limit = per_page + 1 + + if origin_file_uuid: + origin_uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(origin_file_uuid) + if not origin_uuid_map: + error_msg = 'seadoc uuid %s not found.' % origin_file_uuid + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + repo_id = origin_uuid_map.repo_id + username = request.user.username + path = posixpath.join(origin_uuid_map.parent_path, origin_uuid_map.filename) + # permission check + if not check_folder_permission(request, repo_id, path): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + revision_queryset = SeadocRevision.objects.list_by_origin_doc_uuid(origin_uuid_map.uuid, start, limit) + count = SeadocRevision.objects.filter(origin_doc_uuid=origin_uuid_map.uuid).count() + elif repo_id: + # 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, '/') + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + revision_queryset = SeadocRevision.objects.list_by_repo_id(repo_id, start, limit) + count = SeadocRevision.objects.filter(repo_id=repo_id).count() + else: + # owned + revision_queryset = SeadocRevision.objects.list_by_username(username, start, limit) + count = SeadocRevision.objects.filter(username=username).count() + + uuid_set = set() + for item in revision_queryset: + uuid_set.add(item.doc_uuid) + uuid_set.add(item.origin_doc_uuid) + + fileuuidmap_queryset = FileUUIDMap.objects.filter(uuid__in=list(uuid_set)) + revisions = [revision.to_dict(fileuuidmap_queryset) for revision in revision_queryset] + + return Response({'revisions': revisions, 'count': count}) + + def post(self, request): + """create + """ + 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) + repo_id = request.data.get('repo_id') + if not repo_id: + error_msg = 'repo_id 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, '/') + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # create revision dir if does not exist + revision_dir_id = seafile_api.get_dir_id_by_path(repo_id, '/Revisions') + if revision_dir_id is None: + seafile_api.post_dir(repo_id, '/', 'Revisions', username) + + # + origin_file_uuid = get_seadoc_file_uuid(repo, path) + if SeadocRevision.objects.get_by_doc_uuid(origin_file_uuid): + error_msg = 'seadoc %s is already a revision.' % filename + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + origin_file_id = seafile_api.get_file_id_by_path(repo_id, path) + revision_file_uuid = str(uuid.uuid4()) + revision_filename = revision_file_uuid + '.sdoc' + + # copy file to revision dir + seafile_api.copy_file( + repo_id, parent_dir, + json.dumps([filename]), + repo_id, '/Revisions', + json.dumps([revision_filename]), + username=username, need_progress=0, synchronous=1, + ) + + revision_uuid_map = FileUUIDMap( + uuid=revision_file_uuid, + repo_id=repo_id, + parent_path='/Revisions', + filename=revision_filename, + is_dir=False, + ) + revision_uuid_map.save() + + revision = SeadocRevision.objects.create( + doc_uuid=revision_file_uuid, + origin_doc_uuid=origin_file_uuid, + repo_id=repo_id, + origin_doc_path=path, + username=username, + origin_file_version=origin_file_id, + ) + + # copy image files + origin_image_parent_path = '/images/sdoc/' + origin_file_uuid + '/' + dir_id = seafile_api.get_dir_id_by_path(repo_id, origin_image_parent_path) + if dir_id: + revision_image_parent_path = gen_seadoc_image_parent_path( + revision_file_uuid, repo_id, username) + dirents = seafile_api.list_dir_by_path(repo_id, origin_image_parent_path) + for e in dirents: + obj_name = e.obj_name + seafile_api.copy_file( + repo_id, origin_image_parent_path, + json.dumps([obj_name]), + repo_id, revision_image_parent_path, + json.dumps([obj_name]), + username=username, need_progress=0, synchronous=1 + ) + return Response(revision.to_dict()) + + +class SeadocPublishRevision(APIView): + authentication_classes = (SdocJWTTokenAuthentication, TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def post(self, request, file_uuid): + """publish + """ + force = request.data.get('force') # used when origin file deleted + + # resource check + revision = SeadocRevision.objects.get_by_doc_uuid(file_uuid) + if not revision: + error_msg = 'Revision %s not found.' % file_uuid + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + if revision.is_published: + error_msg = 'Revision %s is already published.' % file_uuid + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + repo_id = revision.repo_id + 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, '/') + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + if permission != PERMISSION_READ_WRITE: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # get origin file info + origin_file_uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid( + revision.origin_doc_uuid) + + if not origin_file_uuid and not force: + error_msg = 'origin sdoc %s not found.' % file_uuid + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if origin_file_uuid: + origin_file_parent_path = origin_file_uuid.parent_path + origin_file_filename = origin_file_uuid.filename + else: + origin_file_parent_path = os.path.dirname(revision.origin_doc_path) + origin_file_filename = os.path.basename(revision.origin_doc_path) + origin_file_path = posixpath.join(origin_file_parent_path, origin_file_filename) + + # check if origin file's parent folder exists + if not seafile_api.get_dir_id_by_path(repo_id, origin_file_parent_path): + dst_parent_path = '/' + else: + dst_parent_path = origin_file_parent_path + + # get revision file info + revision_file_uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid) + revision_parent_path = revision_file_uuid.parent_path + revision_filename = revision_file_uuid.filename + + # move revision file + username = request.user.username + seafile_api.move_file( + repo_id, revision_parent_path, + json.dumps([revision_filename]), + repo_id, dst_parent_path, + json.dumps([origin_file_filename]), + replace=1, username=username, + need_progress=0, synchronous=1, + ) + + dst_file_id = seafile_api.get_file_id_by_path(repo_id, origin_file_path) + SeadocRevision.objects.publish(file_uuid, username, dst_file_id) + + # refresh docs + doc_uuids = [revision.origin_doc_uuid, revision.doc_uuid] + sdoc_server_api = SdocServerAPI( + revision.origin_doc_uuid, origin_file_filename, username) + sdoc_server_api.internal_refresh_docs(doc_uuids) + + # move image files + revision_image_parent_path = '/images/sdoc/' + str(revision_file_uuid.uuid) + '/' + dir_id = seafile_api.get_dir_id_by_path(repo_id, revision_image_parent_path) + if dir_id: + origin_image_parent_path = gen_seadoc_image_parent_path( + str(origin_file_uuid.uuid), repo_id, username) + dirents = seafile_api.list_dir_by_path(repo_id, revision_image_parent_path) + for e in dirents: + obj_name = e.obj_name + seafile_api.move_file( + repo_id, revision_image_parent_path, + json.dumps([obj_name]), + repo_id, origin_image_parent_path, + json.dumps([obj_name]), + replace=1, username=username, + need_progress=0, synchronous=1, + ) + seafile_api.del_file( + repo_id, '/images/sdoc/', json.dumps([str(revision_file_uuid.uuid)]), username) + + revision = SeadocRevision.objects.get_by_doc_uuid(file_uuid) + return Response(revision.to_dict()) diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py index 7db77c44f6..4006f8f82c 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -1,6 +1,10 @@ +import os +import posixpath + from django.db import models from seahub.utils.timeutils import datetime_to_isoformat_timestr +from seahub.base.templatetags.seahub_tags import email2nickname class SeadocHistoryNameManager(models.Manager): @@ -28,7 +32,6 @@ class SeadocHistoryName(models.Model): def to_dict(self): return { - 'id': self.pk, 'doc_uuid': self.doc_uuid, 'obj_id': self.obj_id, 'name': self.name, @@ -50,11 +53,11 @@ class SeadocDraftManager(models.Manager): 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) + def list_by_username(self, username, start, limit): + return self.filter(username=username).order_by('-id')[start:limit] - def list_by_repo_id(self, repo_id): - return self.filter(repo_id=repo_id) + def list_by_repo_id(self, repo_id, start, limit): + return self.filter(repo_id=repo_id).order_by('-id')[start:limit] class SeadocDraft(models.Model): @@ -75,3 +78,98 @@ class SeadocDraft(models.Model): 'username': self.username, 'created_at': datetime_to_isoformat_timestr(self.created_at), } + +class SeadocRevisionManager(models.Manager): + + def get_by_doc_uuid(self, doc_uuid): + return self.filter(doc_uuid=doc_uuid).first() + + def list_by_doc_uuids(self, doc_uuid_list): + return self.filter(doc_uuid__in=doc_uuid_list) + + def list_by_origin_doc_uuid(self, origin_doc_uuid, start, limit): + return self.filter( + origin_doc_uuid=origin_doc_uuid, is_published=False).order_by('-id')[start:limit] + + def list_by_username(self, username, start, limit): + return self.filter( + username=username, is_published=False).order_by('-id')[start:limit] + + def list_by_repo_id(self, repo_id, start, limit): + return self.filter( + repo_id=repo_id, is_published=False).order_by('-id')[start:limit] + + def publish(self, doc_uuid, publisher, publish_file_version): + return self.filter(doc_uuid=doc_uuid).update( + publisher=publisher, + publish_file_version=publish_file_version, + is_published=True, + ) + + +class SeadocRevision(models.Model): + """ + """ + doc_uuid = models.CharField(max_length=36, unique=True) + origin_doc_uuid = models.CharField(max_length=36, db_index=True) + repo_id = models.CharField(max_length=36, db_index=True) + origin_doc_path = models.TextField() # used when origin file deleted + username = models.CharField(max_length=255, db_index=True) + origin_file_version = models.CharField(max_length=100) + publish_file_version = models.CharField(max_length=100, null=True) + publisher = models.CharField(max_length=255, null=True) + is_published = models.BooleanField(default=False, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + objects = SeadocRevisionManager() + + class Meta: + db_table = 'sdoc_revision' + + def to_dict(self, fileuuidmap_queryset=None): + from seahub.tags.models import FileUUIDMap + if fileuuidmap_queryset: + origin_doc_uuid = fileuuidmap_queryset.filter(uuid=self.origin_doc_uuid).first() + else: + origin_doc_uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(self.origin_doc_uuid) + if origin_doc_uuid: + origin_parent_path = origin_doc_uuid.parent_path + origin_filename = origin_doc_uuid.filename + else: + origin_parent_path = os.path.dirname(self.origin_doc_path) + origin_filename = os.path.basename(self.origin_doc_path) + origin_file_path = posixpath.join(origin_parent_path, origin_filename) + + if fileuuidmap_queryset: + doc_uuid = fileuuidmap_queryset.filter(uuid=self.doc_uuid).first() + else: + doc_uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(self.doc_uuid) + if doc_uuid: + parent_path = doc_uuid.parent_path + filename = doc_uuid.filename + else: + parent_path = '/Revisions' + filename = self.doc_uuid + '.sdoc' + file_path = posixpath.join(parent_path, filename) + + return { + 'username': self.username, + 'nickname': email2nickname(self.username), + 'repo_id': self.repo_id, + 'doc_uuid': self.doc_uuid, + 'parent_path': parent_path, + 'filename': filename, + 'file_path': file_path, + 'origin_doc_uuid': self.origin_doc_uuid, + 'origin_parent_path': origin_parent_path, + 'origin_filename': origin_filename, + 'origin_file_path': origin_file_path, + 'origin_file_version': self.origin_file_version, + 'publish_file_version': self.publish_file_version, + 'publisher': self.publisher, + 'publisher_nickname': email2nickname(self.publisher), + 'is_published': self.is_published, + 'created_at': datetime_to_isoformat_timestr(self.created_at), + 'updated_at': datetime_to_isoformat_timestr(self.updated_at), + } diff --git a/seahub/seadoc/sdoc_server_api.py b/seahub/seadoc/sdoc_server_api.py new file mode 100644 index 0000000000..199d34be71 --- /dev/null +++ b/seahub/seadoc/sdoc_server_api.py @@ -0,0 +1,39 @@ +import json +import requests + +from seahub.settings import SEADOC_SERVER_URL +from seahub.seadoc.utils import gen_seadoc_access_token + + +def parse_response(response): + if response.status_code >= 400: + raise ConnectionError(response.status_code, response.text) + else: + try: + data = json.loads(response.text) + return data + except: + pass + + +class SdocServerAPI(object): + + def __init__(self, doc_uuid, filename, username): + self.doc_uuid = doc_uuid + self.filename = filename + self.username = username + self.headers = None + self.sdoc_server_url = SEADOC_SERVER_URL.rstrip('/') + self.timeout = 30 + self._init() + + def _init(self): + sdoc_server_access_token = gen_seadoc_access_token( + self.doc_uuid, self.filename, self.username) + self.headers = {'Authorization': 'Token ' + sdoc_server_access_token} + + def internal_refresh_docs(self, doc_uuids): + url = self.sdoc_server_url + '/api/v1/docs/' + self.doc_uuid + '/internal-refresh-docs/?from=seahub' + data = {"doc_uuids" : doc_uuids} + response = requests.post(url, json=data, headers=self.headers) + return parse_response(response) diff --git a/seahub/seadoc/urls.py b/seahub/seadoc/urls.py index dfbf56be43..ebabbdcf81 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, SeadocUploadFile, \ SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory, SeadocDrafts, SeadocMaskAsDraft, \ - SeadocCommentsView, SeadocCommentView + SeadocCommentsView, SeadocCommentView, SeadocRevisions, SeadocPublishRevision + urlpatterns = [ re_path(r'^access-token/(?P[-0-9a-f]{36})/$', SeadocAccessToken.as_view(), name='seadoc_access_token'), @@ -12,8 +13,10 @@ 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'^drafts/(?P[-0-9a-f]{36})/$', SeadocDrafts.as_view(), name='seadoc_drafts'), + re_path(r'^drafts/$', SeadocDrafts.as_view(), name='seadoc_drafts'), re_path(r'^mask-as-draft/(?P[-0-9a-f]{36})/$', SeadocMaskAsDraft.as_view(), name='seadoc_mask_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'^revisions/$', SeadocRevisions.as_view(), name='seadoc_revisions'), + re_path(r'^publish-revision/(?P[-0-9a-f]{36})/$', SeadocPublishRevision.as_view(), name='seadoc_publish_revision'), ] diff --git a/seahub/seadoc/utils.py b/seahub/seadoc/utils.py index a03f28016b..1d067d8463 100644 --- a/seahub/seadoc/utils.py +++ b/seahub/seadoc/utils.py @@ -15,6 +15,7 @@ from seahub.utils import normalize_file_path, gen_inner_file_get_url, gen_inner_ from seahub.views import check_folder_permission from seahub.base.templatetags.seahub_tags import email2nickname from seahub.avatar.templatetags.avatar_tags import api_avatar_url +from seahub.seadoc.models import SeadocRevision logger = logging.getLogger(__name__) @@ -172,3 +173,50 @@ def can_access_seadoc_asset(request, repo_id, path, file_uuid): return True return False + +def is_seadoc_revision(doc_uuid): + info = {} + revision = SeadocRevision.objects.get_by_doc_uuid(doc_uuid) + if revision: + info = {'is_sdoc_revision': True} + revision_info = revision.to_dict() + info['origin_doc_uuid'] = revision_info['origin_doc_uuid'] + info['origin_parent_path'] = revision_info['origin_parent_path'] + info['origin_filename'] = revision_info['origin_filename'] + info['origin_file_path'] = revision_info['origin_file_path'] + info['origin_file_version'] = revision_info['origin_file_version'] + info['publish_file_version'] = revision_info['publish_file_version'] + info['publisher'] = revision_info['publisher'] + info['publisher_nickname'] = revision_info['publisher_nickname'] + info['is_published'] = revision_info['is_published'] + info['revision_created_at'] = revision_info['created_at'] + info['revision_updated_at'] = revision_info['updated_at'] + else: + info = {'is_sdoc_revision': False} + return info + +def gen_path_link(path, repo_name): + """ + Generate navigate paths and links in repo page. + + """ + if path and path[-1] != '/': + path += '/' + + paths = [] + links = [] + if path and path != '/': + paths = path[1:-1].split('/') + i = 1 + for name in paths: + link = '/' + '/'.join(paths[:i]) + i = i + 1 + links.append(link) + if repo_name: + paths.insert(0, repo_name) + links.insert(0, '/') + + zipped = list(zip(paths, links)) + + return zipped + diff --git a/seahub/seadoc/views.py b/seahub/seadoc/views.py new file mode 100644 index 0000000000..4b949d76c2 --- /dev/null +++ b/seahub/seadoc/views.py @@ -0,0 +1,139 @@ +import os +from django.shortcuts import render +from django.utils.translation import gettext as _ +from seaserv import get_repo +from urllib.parse import quote +import json + +from seahub.auth.decorators import login_required +from seahub.utils import render_error +from seahub.views import check_folder_permission, validate_owner, get_seadoc_file_uuid +from seahub.tags.models import FileUUIDMap +from seahub.seadoc.models import SeadocRevision + +from .utils import is_seadoc_revision, get_seadoc_download_link, gen_path_link + + +@login_required +def sdoc_revision(request, repo_id): + """List file revisions in file version history page. + """ + repo = get_repo(repo_id) + if not repo: + error_msg = _("Library does not exist") + return render_error(request, error_msg) + + # perm check + if not check_folder_permission(request, repo_id, '/'): + error_msg = _("Permission denied.") + return render_error(request, error_msg) + + path = request.GET.get('p', '/') + if not path: + return render_error(request) + + if path[-1] == '/': + path = path[:-1] + + u_filename = os.path.basename(path) + + # Check whether user is repo owner + if validate_owner(request, repo_id): + is_owner = True + else: + is_owner = False + + file_uuid = get_seadoc_file_uuid(repo, path) + uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid) + return_dict = { + 'repo': repo, + 'path': path, + 'u_filename': u_filename, + 'file_uuid': file_uuid, + 'is_owner': is_owner, + 'can_compare': True, + 'assets_url': '/api/v2.1/seadoc/download-image/' + file_uuid, + 'file_download_link': get_seadoc_download_link(uuid_map) + } + + revision_info = is_seadoc_revision(file_uuid) + return_dict.update(revision_info) + + origin_doc_uuid = return_dict.get('origin_doc_uuid', '') + is_published = return_dict.get('is_published', False) + if (origin_doc_uuid and not is_published): + uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(origin_doc_uuid) + return_dict['origin_file_download_link'] = get_seadoc_download_link(uuid_map) + + return render(request, 'sdoc_revision.html', return_dict) + + +@login_required +def sdoc_revisions(request, repo_id): + """List file revisions in file version history page. + """ + repo = get_repo(repo_id) + if not repo: + error_msg = _("Library does not exist") + return render_error(request, error_msg) + + # perm check + if not check_folder_permission(request, repo_id, '/'): + error_msg = _("Permission denied.") + return render_error(request, error_msg) + + path = request.GET.get('p', '/') + if not path: + return render_error(request) + + if path[-1] == '/': + path = path[:-1] + filename = os.path.basename(path) + + # Make sure page request is an int. If not, deliver first page. + try: + current_page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '100')) + except ValueError: + current_page = 1 + per_page = 100 + + start = per_page * (current_page - 1) + limit = per_page + 1 + + file_uuid = get_seadoc_file_uuid(repo, path) + origin_uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid) + + revision_queryset = SeadocRevision.objects.list_by_origin_doc_uuid(origin_uuid_map.uuid, start, limit) + count = SeadocRevision.objects.filter(origin_doc_uuid=origin_uuid_map.uuid).count() + zipped = gen_path_link(path, repo.name) + extra_href = "&p=%s" % quote(path) + + uuid_set = set() + for item in revision_queryset: + uuid_set.add(item.doc_uuid) + uuid_set.add(item.origin_doc_uuid) + + fileuuidmap_queryset = FileUUIDMap.objects.filter(uuid__in=list(uuid_set)) + revisions = [revision.to_dict(fileuuidmap_queryset) for revision in revision_queryset] + + if len(revisions) == per_page + 1: + page_next = True + else: + page_next = False + + return render(request, 'sdoc_revisions.html', { + 'repo': repo, + 'revisions': json.dumps(revisions), + 'count': count, + 'docUuid': file_uuid, + 'path': path, + 'filename': filename, + 'zipped': json.dumps(zipped), + 'extra_href': extra_href, + 'current_page': current_page, + 'prev_page': current_page - 1, + 'next_page': current_page + 1, + 'per_page': per_page, + 'page_next': page_next, + }) diff --git a/seahub/templates/sdoc_file_view_react.html b/seahub/templates/sdoc_file_view_react.html index 4eb0ac9871..44106672a1 100644 --- a/seahub/templates/sdoc_file_view_react.html +++ b/seahub/templates/sdoc_file_view_react.html @@ -14,6 +14,18 @@ docUuid: '{{ file_uuid }}', assetsUrl: '{{ assets_url }}', seadocAccessToken: '{{ seadoc_access_token }}', seadocServerUrl: '{{ seadoc_server_url }}', +isSdocRevision: {% if is_sdoc_revision %}true{% else %}false{% endif %}, +originDocUuid: '{{ origin_doc_uuid }}', +originParentPath: '{{ origin_parent_path }}', +originFilename: '{{ origin_filename }}', +originFilePath: '{{ origin_file_path }}', +originFileVersion: '{{ origin_file_version }}', +publishFileVersion: '{{ publish_file_version }}', +publisher: '{{ publisher }}', +publisherNickname: '{{ publisher_nickname }}', +isPublished: {% if is_published %}true{% else %}false{% endif %}, +revisionCreatedAt: '{{ revision_created_at }}', +revisionUpdatedAt: '{{ revision_updated_at }}', {% endblock %} {% block render_bundle %} diff --git a/seahub/templates/sdoc_revision.html b/seahub/templates/sdoc_revision.html new file mode 100644 index 0000000000..f11c8d6fc1 --- /dev/null +++ b/seahub/templates/sdoc_revision.html @@ -0,0 +1,37 @@ +{% extends "base_for_react.html" %} +{% load render_bundle from webpack_loader %} + +{% block extra_style %} +{% render_bundle 'sdocRevision' 'css'%} +{% endblock %} + +{% block extra_script %} + + {% render_bundle 'sdocRevision' 'js'%} +{% endblock %} diff --git a/seahub/templates/sdoc_revisions.html b/seahub/templates/sdoc_revisions.html new file mode 100644 index 0000000000..9fd6211597 --- /dev/null +++ b/seahub/templates/sdoc_revisions.html @@ -0,0 +1,43 @@ +{% extends "base_for_react.html" %} +{% load render_bundle from webpack_loader %} +{% load seahub_tags avatar_tags i18n static %} + +{% block extra_style %} + + +{% render_bundle 'sdocRevisions' 'css'%} +{% endblock %} + +{% block extra_script %} + + {% render_bundle 'sdocRevisions' 'js'%} +{% endblock %} diff --git a/seahub/urls.py b/seahub/urls.py index 61b148e473..f3b8310641 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -194,6 +194,7 @@ from seahub.api2.endpoints.admin.virus_scan_records import AdminVirusFilesView, from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView from seahub.api2.endpoints.repo_auto_delete import RepoAutoDeleteView +from seahub.seadoc.views import sdoc_revision, sdoc_revisions from seahub.ocm.settings import OCM_ENDPOINT @@ -220,6 +221,8 @@ urlpatterns = [ path('repo/upload_check/', validate_filename), re_path(r'^repo/download_dir/(?P[-0-9a-f]{36})/$', repo_download_dir, name='repo_download_dir'), re_path(r'^repo/file_revisions/(?P[-0-9a-f]{36})/$', file_revisions, name='file_revisions'), + re_path(r'^repo/sdoc_revision/(?P[-0-9a-f]{36})/$', sdoc_revision, name='sdoc_revision'), + re_path(r'^repo/sdoc_revisions/(?P[-0-9a-f]{36})/$', sdoc_revisions, name='sdoc_revisions'), re_path(r'^repo/file-access/(?P[-0-9a-f]{36})/$', file_access, name='file_access'), re_path(r'^repo/text_diff/(?P[-0-9a-f]{36})/$', text_diff, name='text_diff'), re_path(r'^repo/history/(?P[-0-9a-f]{36})/$', repo_history, name='repo_history'), diff --git a/seahub/views/file.py b/seahub/views/file.py index 8f05137c97..7199f82526 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -75,7 +75,7 @@ from seahub.thumbnail.utils import extract_xmind_image, get_thumbnail_src, \ XMIND_IMAGE_SIZE, get_share_link_thumbnail_src, get_thumbnail_image_path from seahub.drafts.utils import get_file_draft, \ is_draft_file, has_draft_file -from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_access_token +from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_access_token, is_seadoc_revision if HAS_OFFICE_CONVERTER: from seahub.utils import ( @@ -666,6 +666,10 @@ def view_lib_file(request, repo_id, path): return_dict['can_edit_file'] = can_edit_file return_dict['seadoc_access_token'] = gen_seadoc_access_token(file_uuid, filename, username, permission=seadoc_perm) + # revision + revision_info = is_seadoc_revision(file_uuid) + return_dict.update(revision_info) + send_file_access_msg(request, repo, path, 'web') return render(request, template, return_dict) diff --git a/sql/mysql.sql b/sql/mysql.sql index ce0d955442..574f41b8fc 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1397,3 +1397,24 @@ CREATE TABLE `sdoc_draft` ( KEY `sdoc_draft_repo_id` (`repo_id`), KEY `sdoc_draft_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `sdoc_revision` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `repo_id` varchar(36) NOT NULL, + `doc_uuid` varchar(36) NOT NULL, + `origin_doc_uuid` varchar(36) NOT NULL, + `origin_doc_path` longtext NOT NULL, + `origin_file_version` varchar(100) NOT NULL, + `publish_file_version` varchar(100) DEFAULT NULL, + `username` varchar(255) NOT NULL, + `publisher` varchar(255) DEFAULT NULL, + `is_published` tinyint(1) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `sdoc_revise_doc_uuid` (`doc_uuid`), + KEY `sdoc_revision_repo_id` (`repo_id`), + KEY `sdoc_revision_origin_doc_uuid` (`origin_doc_uuid`), + KEY `sdoc_revision_username` (`username`), + KEY `sdoc_revision_is_published` (`is_published`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;