1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-25 18:20:48 +00:00

Merge pull request #5527 from haiwen/SeadocRevisions

Seadoc revisions
This commit is contained in:
杨国璇 2023-07-08 14:19:14 +08:00 committed by GitHub
commit 7b7d4ca53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1258 additions and 48 deletions

View File

@ -9,6 +9,8 @@ const entryFiles = {
fileHistory: "/file-history.js", fileHistory: "/file-history.js",
fileHistoryOld: "/file-history-old.js", fileHistoryOld: "/file-history-old.js",
sdocFileHistory: "/pages/sdoc-file-history/index.js", sdocFileHistory: "/pages/sdoc-file-history/index.js",
sdocRevision: "/pages/sdoc-revision/index.js",
sdocRevisions: "/pages/sdoc-revisions/index.js",
app: "/app.js", app: "/app.js",
draft: "/draft.js", draft: "/draft.js",
sharedDirView: "/shared-dir-view.js", sharedDirView: "/shared-dir-view.js",

View File

@ -11,7 +11,7 @@
"@gatsbyjs/reach-router": "1.3.9", "@gatsbyjs/reach-router": "1.3.9",
"@seafile/react-image-lightbox": "2.0.2", "@seafile/react-image-lightbox": "2.0.2",
"@seafile/resumablejs": "1.1.16", "@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-calendar": "0.0.12",
"@seafile/seafile-editor": "0.3.132", "@seafile/seafile-editor": "0.3.132",
"@seafile/slate-react": "0.54.13", "@seafile/slate-react": "0.54.13",
@ -43,7 +43,7 @@
"react-select": "5.7.0", "react-select": "5.7.0",
"react-transition-group": "4.4.5", "react-transition-group": "4.4.5",
"reactstrap": "8.9.0", "reactstrap": "8.9.0",
"seafile-js": "0.2.202", "seafile-js": "0.2.204",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"svg-sprite-loader": "^6.0.11", "svg-sprite-loader": "^6.0.11",
"svgo-loader": "^3.0.1", "svgo-loader": "^3.0.1",
@ -5198,9 +5198,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@seafile/sdoc-editor": { "node_modules/@seafile/sdoc-editor": {
"version": "0.1.68", "version": "0.1.69",
"resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.68.tgz", "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.69.tgz",
"integrity": "sha512-HlZHBImvff5EDO243NLHY22h4DnSJ3X8G34f78nPVo0VrNRz2q2odOiVDDOT3eKggVFoVlrutKp4/NOjRTNs1A==", "integrity": "sha512-HcUWdXSmN5jChHl75zew/Uh/8usavDiHS278IpHDjRsWjldVcMuB89/hZKzGOKiMO/QNJ/XZBmAASkUlXjXW1w==",
"dependencies": { "dependencies": {
"@seafile/react-image-lightbox": "2.0.2", "@seafile/react-image-lightbox": "2.0.2",
"@seafile/slate": "0.91.8", "@seafile/slate": "0.91.8",
@ -24900,9 +24900,9 @@
} }
}, },
"node_modules/seafile-js": { "node_modules/seafile-js": {
"version": "0.2.202", "version": "0.2.204",
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.202.tgz", "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.204.tgz",
"integrity": "sha512-OE85UMZxgaM4hq7+ey7xn/O8JmGyaYV5J4nCG7s0a6/JXgzJXNENdFT9Jid40Hlf4Ks7aHtuMM7qx8SbmROlOw==", "integrity": "sha512-t+tDh2TQiT0g9DB2Lzx9TzVb/Pc6lKUScobklnP6WhHEgK8YVhFIWi+JcJR0KDmTbClKV2DSFlCWPG9Vr79KGQ==",
"dependencies": { "dependencies": {
"@babel/polyfill": "7.12.1", "@babel/polyfill": "7.12.1",
"axios": "1.2.1", "axios": "1.2.1",
@ -33359,9 +33359,9 @@
"version": "1.1.16" "version": "1.1.16"
}, },
"@seafile/sdoc-editor": { "@seafile/sdoc-editor": {
"version": "0.1.68", "version": "0.1.69",
"resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.68.tgz", "resolved": "https://registry.npmjs.org/@seafile/sdoc-editor/-/sdoc-editor-0.1.69.tgz",
"integrity": "sha512-HlZHBImvff5EDO243NLHY22h4DnSJ3X8G34f78nPVo0VrNRz2q2odOiVDDOT3eKggVFoVlrutKp4/NOjRTNs1A==", "integrity": "sha512-HcUWdXSmN5jChHl75zew/Uh/8usavDiHS278IpHDjRsWjldVcMuB89/hZKzGOKiMO/QNJ/XZBmAASkUlXjXW1w==",
"requires": { "requires": {
"@seafile/react-image-lightbox": "2.0.2", "@seafile/react-image-lightbox": "2.0.2",
"@seafile/slate": "0.91.8", "@seafile/slate": "0.91.8",
@ -47245,9 +47245,9 @@
} }
}, },
"seafile-js": { "seafile-js": {
"version": "0.2.202", "version": "0.2.204",
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.202.tgz", "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.204.tgz",
"integrity": "sha512-OE85UMZxgaM4hq7+ey7xn/O8JmGyaYV5J4nCG7s0a6/JXgzJXNENdFT9Jid40Hlf4Ks7aHtuMM7qx8SbmROlOw==", "integrity": "sha512-t+tDh2TQiT0g9DB2Lzx9TzVb/Pc6lKUScobklnP6WhHEgK8YVhFIWi+JcJR0KDmTbClKV2DSFlCWPG9Vr79KGQ==",
"requires": { "requires": {
"@babel/polyfill": "7.12.1", "@babel/polyfill": "7.12.1",
"axios": "1.2.1", "axios": "1.2.1",

View File

@ -6,7 +6,7 @@
"@gatsbyjs/reach-router": "1.3.9", "@gatsbyjs/reach-router": "1.3.9",
"@seafile/react-image-lightbox": "2.0.2", "@seafile/react-image-lightbox": "2.0.2",
"@seafile/resumablejs": "1.1.16", "@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-calendar": "0.0.12",
"@seafile/seafile-editor": "0.3.132", "@seafile/seafile-editor": "0.3.132",
"@seafile/slate-react": "0.54.13", "@seafile/slate-react": "0.54.13",
@ -38,7 +38,7 @@
"react-select": "5.7.0", "react-select": "5.7.0",
"react-transition-group": "4.4.5", "react-transition-group": "4.4.5",
"reactstrap": "8.9.0", "reactstrap": "8.9.0",
"seafile-js": "0.2.202", "seafile-js": "0.2.204",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"svg-sprite-loader": "^6.0.11", "svg-sprite-loader": "^6.0.11",
"svgo-loader": "^3.0.1", "svgo-loader": "^3.0.1",

View File

@ -150,6 +150,18 @@ class DirentGridView extends React.Component{
case 'Lock': case 'Lock':
this.onLockItem(currentObject); this.onLockItem(currentObject);
break; 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': case 'Comment':
this.onCommentItem(); this.onCommentItem();
break; 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 = () => { onCommentItem = () => {
this.props.showDirentDetail('comments'); this.props.showDirentDetail('comments');
} }

View File

@ -270,6 +270,12 @@ class DirentListItem extends React.Component {
case 'Unmask as draft': case 'Unmask as draft':
this.onUnmaskAsDraft(); this.onUnmaskAsDraft();
break; break;
case 'Start revise':
this.onStartRevise();
break;
case 'List revisions':
this.openRevisionsPage();
break;
case 'Comment': case 'Comment':
this.props.onDirentClick(this.props.dirent); this.props.onDirentClick(this.props.dirent);
this.props.showDirentDetail('comments'); 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 = () => { onHistory = () => {
let repoID = this.props.repoID; let repoID = this.props.repoID;
let filePath = this.getDirentPath(this.props.dirent); let filePath = this.getDirentPath(this.props.dirent);

View File

@ -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 = () => { onCommentItem = () => {
this.props.showDirentDetail('comments'); this.props.showDirentDetail('comments');
} }
@ -171,6 +205,15 @@ class MultipleDirOperationToolbar extends React.Component {
case 'Unlock': case 'Unlock':
this.unlockFile(dirent); this.unlockFile(dirent);
break; 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': case 'Comment':
this.onCommentItem(); this.onCommentItem();
break; break;

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
.sdoc-revisions .sdoc-revision:hover {
background-color: #f8f8f8;
}

View File

@ -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 (
<div className="sdoc-revision-viewer h-100 d-flex align-items-center justify-content-center">
<Loading />
</div>
);
}
if (errorMessage) {
return (
<div className="sdoc-revision-viewer h-100 d-flex align-items-center justify-content-center">
{gettext(errorMessage)}
</div>
);
}
return (
<DiffViewer
currentContent={revisionContent}
lastContent={originContent}
/>
);
}
render() {
const { isLoading, errorMessage } = this.state;
return (
<div className="sdoc-revision d-flex h-100 w-100 o-hidden">
<div className="sdoc-revision-container d-flex flex-column">
<div className="sdoc-revision-header pl-4 pr-4 d-flex justify-content-between w-100 o-hidden">
<div className={classnames('sdoc-revision-header-left h-100 d-flex align-items-center o-hidden')}>
<GoBack />
<div className="file-name text-truncate">{fileName}</div>
</div>
<div className="sdoc-revision-header-right h-100 d-flex align-items-center">
{(!isLoading && !errorMessage) && (
<>
<Button color="success" className="mr-4" onClick={this.edit}>{gettext('Edit')}</Button>
<Button color="success" onClick={this.publishRevision}>{gettext('Publish')}</Button>
</>
)}
</div>
</div>
<div className="sdoc-revision-content f-flex">
{this.renderContent()}
</div>
</div>
</div>
);
}
}
ReactDom.render(<SdocRevision />, document.getElementById('wrapper'));

View File

@ -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 (<span className={classnames('', className)}>{perPageCount}</span>);
}
return (
<a href={`?per_page=${perPageCount}${extraHref}`} className={classnames('per-page', className)}>{perPageCount}</a>
);
}
renderRevisions = () => {
if (!Array.isArray(validRevisions) || validRevisions.length === 0) {
return (
<div className="empty-tips">
<h2 className="alc">{gettext('This file has not revisions yet')}</h2>
</div>
);
}
return (
<table className="file-audit-list">
<thead>
<tr>
<th width="25%" className="user">{gettext('User')}</th>
<th width="50%">{gettext('File_name')}</th>
<th width="25%">{gettext('Ctime')}</th>
</tr>
</thead>
<tbody>
{validRevisions.map(revision => {
return (
<tr key={revision.doc_uuid} className="sdoc-revision">
<td width="25%" className="user">{revision.nickname}</td>
<td width="50%">
<a href={`${siteRoot}lib/${repo['id']}/file${revision.file_path}`}>
{revision.filename}
</a>
</td>
<td width="25%">{moment(revision.created_at).format('YYYY-MM-DD HH:MM')}</td>
</tr>
);
})}
</tbody>
</table>
);
}
renderFooter = () => {
return (
<div id="paginator">
{currentPage !== 1 && (
<a href={`?page=${prevPage}&per_page=${perPage}${extraHref}`} className="mr-1">{gettext('Previous')}</a>
)}
{pageNext && (
<a href={`?page=${nextPage}&per_page=${perPage}${extraHref}`} className="mr-1">{gettext('Next')}</a>
)}
{(currentPage !== 1 || pageNext) && (<span className="mr-1">{'|'}</span>)}
<span className="mr-1">{gettext('Per page: ')}</span>
{this.renderPerPage(25, 'mr-1')}
{this.renderPerPage(50, 'mr-1')}
{this.renderPerPage(100)}
</div>
);
}
render() {
return (
<>
<div id="header" className="d-flex">
<a href={siteRoot} id="logo">
<img src={`${mediaUrl}${logoPath}`} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />
</a>
</div>
<div id="main" className="container-fluid w100 flex-auto ov-auto sdoc-revisions">
<div id="wide-panel-noframe" className="row">
<div className="col-md-10 col-md-offset-1">
<h2 className="file-access-hd">
<span className="op-target mr-1">{filename}</span>
{gettext('Revisions')}
</h2>
<div className="file-audit-list-topbar">
<p className="path">
<span className="mr-1">{gettext('Current Path:')}</span>
{validZipped.map((item, index) => {
if (forloopLast) {
return (<a key={index} href={`${viewLibFile.slice(0, -1)}${item[1]}`} target="_blank" rel="noreferrer">{item[0]}</a>);
}
return (
<Fragment key={index}>
<a href={`${viewLibFile.slice(0, -1)}${item[1]}`} target="_blank" rel="noreferrer">{item[0]}</a>
{index !== validZipped.length - 1 && (
<span className="mr-1 ml-1">{'/'}</span>
)}
</Fragment>
);
})}
</p>
</div>
{this.renderRevisions()}
{this.renderFooter()}
</div>
</div>
</div>
</>
);
}
}
ReactDom.render(<SdocRevisions />, document.getElementById('wrapper'));

View File

@ -18,6 +18,8 @@ const TextTranslation = {
'UNLOCK' : {key : 'Unlock', value : gettext('Unlock')}, 'UNLOCK' : {key : 'Unlock', value : gettext('Unlock')},
'MASK_AS_DRAFT' : {key : 'Mask as draft', value : gettext('Mark as draft')}, 'MASK_AS_DRAFT' : {key : 'Mask as draft', value : gettext('Mark as draft')},
'UNMASK_AS_DRAFT' : {key : 'Unmask as draft', value : gettext('Unmark 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')}, 'COMMENT' : {key : 'Comment', value : gettext('Comment')},
'HISTORY' : {key : 'History', value : gettext('History')}, 'HISTORY' : {key : 'History', value : gettext('History')},
'ACCESS_LOG' : {key : 'Access Log', value : gettext('Access Log')}, 'ACCESS_LOG' : {key : 'Access Log', value : gettext('Access Log')},

View File

@ -527,7 +527,7 @@ export const Utils = {
getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) {
let list = []; let list = [];
const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK, MASK_AS_DRAFT, UNMASK_AS_DRAFT, 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 permission = dirent.permission;
const { isCustomPermission, customPermission } = Utils.getUserPermission(permission); const { isCustomPermission, customPermission } = Utils.getUserPermission(permission);
@ -600,6 +600,8 @@ export const Utils = {
} else { } else {
list.push(MASK_AS_DRAFT); list.push(MASK_AS_DRAFT);
} }
list.push(START_REVISE);
list.push(LIST_REVISIONS);
} }
if (enableFileComment) { if (enableFileComment) {
list.push(COMMENT); list.push(COMMENT);
@ -1542,6 +1544,17 @@ export const Utils = {
generateHistoryURL: function(siteRoot, repoID, path) { generateHistoryURL: function(siteRoot, repoID, path) {
if (!siteRoot || !repoID || !path) return ''; if (!siteRoot || !repoID || !path) return '';
return siteRoot + 'repo/file_revisions/' + repoID + '/?p=' + this.encodePath(path); 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);
} }
}; };

View File

@ -10,7 +10,8 @@ const { serviceURL, avatarURL, siteRoot } = window.app.config;
const { username, name } = window.app.userInfo; const { username, name } = window.app.userInfo;
const { const {
repoID, repoName, parentDir, filePerm, repoID, repoName, parentDir, filePerm,
docPath, docName, docUuid, seadocAccessToken, seadocServerUrl, assetsUrl docPath, docName, docUuid, seadocAccessToken, seadocServerUrl, assetsUrl,
isSdocRevision, isPublished, originFilename, revisionCreatedAt,
} = window.app.pageOptions; } = window.app.pageOptions;
window.seafile = { window.seafile = {
@ -31,7 +32,12 @@ window.seafile = {
parentFolderURL: `${siteRoot}library/${repoID}/${Utils.encodePath(repoName + parentDir)}`, parentFolderURL: `${siteRoot}library/${repoID}/${Utils.encodePath(repoName + parentDir)}`,
assetsUrl, assetsUrl,
isShowInternalLink: true, isShowInternalLink: true,
isStarIconShown: true // for star/unstar isStarIconShown: true, // for star/unstar
isSdocRevision,
isPublished,
revisionURL: Utils.generateRevisionURL(siteRoot, repoID, docPath),
originFilename,
revisionCreatedAt,
}; };
ReactDom.render( ReactDom.render(

View File

@ -185,3 +185,28 @@ class RepoAPITokenAuthentication(BaseAuthentication):
request.repo_api_token_obj = rat request.repo_api_token_obj = rat
return AnonymousUser(), auth[1] 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]

View File

@ -1,5 +1,6 @@
import os import os
import json import json
import uuid
import logging import logging
import requests import requests
import posixpath import posixpath
@ -18,7 +19,7 @@ from django.utils import timezone
from seaserv import seafile_api, check_quota from seaserv import seafile_api, check_quota
from seahub.views import check_folder_permission 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.utils import api_error, user_to_dict, to_python_boolean
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.seadoc.utils import is_valid_seadoc_access_token, get_seadoc_upload_link, \ 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.error_msg import file_type_error_msg
from seahub.utils.repo import parse_repo_perm from seahub.utils.repo import parse_repo_perm
from seahub.utils.file_revisions import get_file_revisions_within_limit 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.avatar.templatetags.avatar_tags import api_avatar_url
from seahub.base.templatetags.seahub_tags import email2nickname, \ from seahub.base.templatetags.seahub_tags import email2nickname, \
email2contact_email email2contact_email
from seahub.utils.timeutils import utc_datetime_to_isoformat_timestr, timestamp_to_isoformat_timestr from seahub.utils.timeutils import utc_datetime_to_isoformat_timestr, timestamp_to_isoformat_timestr
from seahub.base.models import FileComment 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__) logger = logging.getLogger(__name__)
@ -514,36 +517,44 @@ class SeadocDrafts(APIView):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle, ) throttle_classes = (UserRateThrottle, )
def get(self, request, repo_id): def get(self, request):
"""list drafts """list drafts
""" """
username = request.user.username username = request.user.username
# argument check # 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 if repo_id:
repo = seafile_api.get_repo(repo_id) # resource check
if not repo: repo = seafile_api.get_repo(repo_id)
error_msg = 'Library %s not found.' % repo_id if not repo:
return api_error(status.HTTP_404_NOT_FOUND, error_msg) error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check # permission check
permission = check_folder_permission(request, repo_id, '/') permission = check_folder_permission(request, repo_id, '/')
if not permission: if not permission:
error_msg = 'Permission denied.' error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# draft_queryset = SeadocDraft.objects.list_by_repo_id(repo_id, start, limit)
if owned: count = SeadocDraft.objects.filter(repo_id=repo_id).count()
draft_queryset = SeadocDraft.objects.filter(
repo_id=repo_id, username=username)
# all
else: 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] drafts = [draft.to_dict() for draft in draft_queryset]
return Response({'drafts': drafts}) return Response({'drafts': drafts, 'count': count})
class SeadocMaskAsDraft(APIView): class SeadocMaskAsDraft(APIView):
@ -810,3 +821,271 @@ class SeadocCommentView(APIView):
comment = file_comment.to_dict() comment = file_comment.to_dict()
comment.update(user_to_dict(file_comment.author, request=request, avatar_size=avatar_size)) comment.update(user_to_dict(file_comment.author, request=request, avatar_size=avatar_size))
return Response(comment) 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())

View File

@ -1,6 +1,10 @@
import os
import posixpath
from django.db import models from django.db import models
from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.base.templatetags.seahub_tags import email2nickname
class SeadocHistoryNameManager(models.Manager): class SeadocHistoryNameManager(models.Manager):
@ -28,7 +32,6 @@ class SeadocHistoryName(models.Model):
def to_dict(self): def to_dict(self):
return { return {
'id': self.pk,
'doc_uuid': self.doc_uuid, 'doc_uuid': self.doc_uuid,
'obj_id': self.obj_id, 'obj_id': self.obj_id,
'name': self.name, 'name': self.name,
@ -50,11 +53,11 @@ class SeadocDraftManager(models.Manager):
def list_by_doc_uuids(self, doc_uuid_list): def list_by_doc_uuids(self, doc_uuid_list):
return self.filter(doc_uuid__in=doc_uuid_list) return self.filter(doc_uuid__in=doc_uuid_list)
def list_by_username(self, username): def list_by_username(self, username, start, limit):
return self.filter(username=username) return self.filter(username=username).order_by('-id')[start:limit]
def list_by_repo_id(self, repo_id): def list_by_repo_id(self, repo_id, start, limit):
return self.filter(repo_id=repo_id) return self.filter(repo_id=repo_id).order_by('-id')[start:limit]
class SeadocDraft(models.Model): class SeadocDraft(models.Model):
@ -75,3 +78,98 @@ class SeadocDraft(models.Model):
'username': self.username, 'username': self.username,
'created_at': datetime_to_isoformat_timestr(self.created_at), '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),
}

View File

@ -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)

View File

@ -1,7 +1,8 @@
from django.urls import re_path from django.urls import re_path
from .apis import SeadocAccessToken, SeadocUploadLink, SeadocDownloadLink, SeadocUploadFile, \ from .apis import SeadocAccessToken, SeadocUploadLink, SeadocDownloadLink, SeadocUploadFile, \
SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory, SeadocDrafts, SeadocMaskAsDraft, \ SeadocUploadImage, SeadocDownloadImage, SeadocCopyHistoryFile, SeadocHistory, SeadocDrafts, SeadocMaskAsDraft, \
SeadocCommentsView, SeadocCommentView SeadocCommentsView, SeadocCommentView, SeadocRevisions, SeadocPublishRevision
urlpatterns = [ urlpatterns = [
re_path(r'^access-token/(?P<repo_id>[-0-9a-f]{36})/$', SeadocAccessToken.as_view(), name='seadoc_access_token'), re_path(r'^access-token/(?P<repo_id>[-0-9a-f]{36})/$', SeadocAccessToken.as_view(), name='seadoc_access_token'),
@ -12,8 +13,10 @@ urlpatterns = [
re_path(r'^download-image/(?P<file_uuid>[-0-9a-f]{36})/(?P<filename>.*)$', SeadocDownloadImage.as_view(), name='seadoc_download_image'), re_path(r'^download-image/(?P<file_uuid>[-0-9a-f]{36})/(?P<filename>.*)$', SeadocDownloadImage.as_view(), name='seadoc_download_image'),
re_path(r'^copy-history-file/(?P<repo_id>[-0-9a-f]{36})/$', SeadocCopyHistoryFile.as_view(), name='seadoc_copy_history_file'), re_path(r'^copy-history-file/(?P<repo_id>[-0-9a-f]{36})/$', SeadocCopyHistoryFile.as_view(), name='seadoc_copy_history_file'),
re_path(r'^history/(?P<file_uuid>[-0-9a-f]{36})/$', SeadocHistory.as_view(), name='seadoc_history'), re_path(r'^history/(?P<file_uuid>[-0-9a-f]{36})/$', SeadocHistory.as_view(), name='seadoc_history'),
re_path(r'^drafts/(?P<repo_id>[-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<repo_id>[-0-9a-f]{36})/$', SeadocMaskAsDraft.as_view(), name='seadoc_mask_as_draft'), re_path(r'^mask-as-draft/(?P<repo_id>[-0-9a-f]{36})/$', SeadocMaskAsDraft.as_view(), name='seadoc_mask_as_draft'),
re_path(r'^comments/(?P<file_uuid>[-0-9a-f]{36})/$', SeadocCommentsView.as_view(), name='seadoc_comments'), re_path(r'^comments/(?P<file_uuid>[-0-9a-f]{36})/$', SeadocCommentsView.as_view(), name='seadoc_comments'),
re_path(r'^comment/(?P<file_uuid>[-0-9a-f]{36})/(?P<comment_id>\d+)/$', SeadocCommentView.as_view(), name='seadoc_comment'), re_path(r'^comment/(?P<file_uuid>[-0-9a-f]{36})/(?P<comment_id>\d+)/$', SeadocCommentView.as_view(), name='seadoc_comment'),
re_path(r'^revisions/$', SeadocRevisions.as_view(), name='seadoc_revisions'),
re_path(r'^publish-revision/(?P<file_uuid>[-0-9a-f]{36})/$', SeadocPublishRevision.as_view(), name='seadoc_publish_revision'),
] ]

View File

@ -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.views import check_folder_permission
from seahub.base.templatetags.seahub_tags import email2nickname from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.avatar.templatetags.avatar_tags import api_avatar_url
from seahub.seadoc.models import SeadocRevision
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -172,3 +173,50 @@ def can_access_seadoc_asset(request, repo_id, path, file_uuid):
return True return True
return False 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

139
seahub/seadoc/views.py Normal file
View File

@ -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,
})

View File

@ -14,6 +14,18 @@ docUuid: '{{ file_uuid }}',
assetsUrl: '{{ assets_url }}', assetsUrl: '{{ assets_url }}',
seadocAccessToken: '{{ seadoc_access_token }}', seadocAccessToken: '{{ seadoc_access_token }}',
seadocServerUrl: '{{ seadoc_server_url }}', 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 %} {% endblock %}
{% block render_bundle %} {% block render_bundle %}

View File

@ -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 %}
<script type="text/javascript">
window.app.config.lang = '{{LANGUAGE_CODE}}';
window.sdocRevision = {
repoID: '{{ repo.id }}',
repoName: '{{ repo.name }}',
filePath: '{{ path|escapejs }}',
fileName: '{{ u_filename|escapejs }}',
docUuid: '{{ file_uuid }}',
domain: '{{ domain }}',
protocol: '{{ protocol }}',
assetsUrl: '{{ assets_url }}',
fileDownloadLink: '{{ file_download_link }}',
isSdocRevision: {% if is_sdoc_revision %}true{% else %}false{% endif %},
originDocUuid: '{{ origin_doc_uuid }}',
originFileDownloadLink: '{{ origin_file_download_link }}',
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 }}',
}
</script>
{% render_bundle 'sdocRevision' 'js'%}
{% endblock %}

View File

@ -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 %}
<link rel="stylesheet" type="text/css" href="{% static "css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/seahub.css?t=1398068110" />
{% render_bundle 'sdocRevisions' 'css'%}
{% endblock %}
{% block extra_script %}
<script type="text/javascript">
window.app.config.lang = '{{LANGUAGE_CODE}}';
console.log('{{page_next}}')
window.sdocRevisions = {
repo: { "id": "{{ repo.id }}", "name": "{{ repo.name }}" },
revisions: '{{ revisions|escapejs }}',
count: '{{ count }}',
filePath: '{{ path|escapejs }}',
docUuid: '{{ docUuid }}',
perPage: '{{ per_page }}',
filename: '{{ filename }}',
zipped: '{{ zipped|escapejs }}',
currentPage: Number('{{ current_page }}'),
prevPage: Number('{{ prev_page }}'),
nextPage: Number('{{ next_page }}'),
perPage: Number('{{ per_page }}'),
pageNext: '{{ page_next }}' === 'False' ? false : true,
extraHref: '{{ extra_href|escapejs }}',
}
{% if not forloop.last %}
window.sdocRevisions['forloopLast'] = false;
window.sdocRevisions['viewLibFile'] = '{% url 'lib_view' repo.id repo.name '' %}';
{% else %}
window.sdocRevisions['forloopLast'] = true;
window.sdocRevisions['viewLibFile'] = '{% url 'view_lib_file' repo.id '' %}';
{% endif %}
</script>
{% render_bundle 'sdocRevisions' 'js'%}
{% endblock %}

View File

@ -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.file_participants import FileParticipantsView, FileParticipantView
from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView
from seahub.api2.endpoints.repo_auto_delete import RepoAutoDeleteView 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 from seahub.ocm.settings import OCM_ENDPOINT
@ -220,6 +221,8 @@ urlpatterns = [
path('repo/upload_check/', validate_filename), path('repo/upload_check/', validate_filename),
re_path(r'^repo/download_dir/(?P<repo_id>[-0-9a-f]{36})/$', repo_download_dir, name='repo_download_dir'), re_path(r'^repo/download_dir/(?P<repo_id>[-0-9a-f]{36})/$', repo_download_dir, name='repo_download_dir'),
re_path(r'^repo/file_revisions/(?P<repo_id>[-0-9a-f]{36})/$', file_revisions, name='file_revisions'), re_path(r'^repo/file_revisions/(?P<repo_id>[-0-9a-f]{36})/$', file_revisions, name='file_revisions'),
re_path(r'^repo/sdoc_revision/(?P<repo_id>[-0-9a-f]{36})/$', sdoc_revision, name='sdoc_revision'),
re_path(r'^repo/sdoc_revisions/(?P<repo_id>[-0-9a-f]{36})/$', sdoc_revisions, name='sdoc_revisions'),
re_path(r'^repo/file-access/(?P<repo_id>[-0-9a-f]{36})/$', file_access, name='file_access'), re_path(r'^repo/file-access/(?P<repo_id>[-0-9a-f]{36})/$', file_access, name='file_access'),
re_path(r'^repo/text_diff/(?P<repo_id>[-0-9a-f]{36})/$', text_diff, name='text_diff'), re_path(r'^repo/text_diff/(?P<repo_id>[-0-9a-f]{36})/$', text_diff, name='text_diff'),
re_path(r'^repo/history/(?P<repo_id>[-0-9a-f]{36})/$', repo_history, name='repo_history'), re_path(r'^repo/history/(?P<repo_id>[-0-9a-f]{36})/$', repo_history, name='repo_history'),

View File

@ -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 XMIND_IMAGE_SIZE, get_share_link_thumbnail_src, get_thumbnail_image_path
from seahub.drafts.utils import get_file_draft, \ from seahub.drafts.utils import get_file_draft, \
is_draft_file, has_draft_file 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: if HAS_OFFICE_CONVERTER:
from seahub.utils import ( 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['can_edit_file'] = can_edit_file
return_dict['seadoc_access_token'] = gen_seadoc_access_token(file_uuid, filename, username, permission=seadoc_perm) 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') send_file_access_msg(request, repo, path, 'web')
return render(request, template, return_dict) return render(request, template, return_dict)

View File

@ -1397,3 +1397,24 @@ CREATE TABLE `sdoc_draft` (
KEY `sdoc_draft_repo_id` (`repo_id`), KEY `sdoc_draft_repo_id` (`repo_id`),
KEY `sdoc_draft_username` (`username`) KEY `sdoc_draft_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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;