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 (
+
+
+
+
+
+ {(!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 (
+
+
+
+ {gettext('User')} |
+ {gettext('File_name')} |
+ {gettext('Ctime')} |
+
+
+
+ {validRevisions.map(revision => {
+ return (
+
+ {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;