mirror of
https://github.com/haiwen/seahub.git
synced 2025-04-28 03:10:45 +00:00
Merge branch '7.0'
This commit is contained in:
commit
26f70c61d9
@ -204,6 +204,11 @@ module.exports = {
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
paths.appSrc + "/repo-history.js",
|
||||
],
|
||||
repoSnapshot: [
|
||||
require.resolve('./polyfills'),
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
paths.appSrc + "/repo-snapshot.js",
|
||||
],
|
||||
repoFolderTrash: [
|
||||
require.resolve('./polyfills'),
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
|
@ -89,6 +89,7 @@ module.exports = {
|
||||
viewFileUnknown: [require.resolve('./polyfills'), paths.appSrc + "/view-file-unknown.js"],
|
||||
settings: [require.resolve('./polyfills'), paths.appSrc + "/settings.js"],
|
||||
repoHistory: [require.resolve('./polyfills'), paths.appSrc + "/repo-history.js"],
|
||||
repoSnapshot: [require.resolve('./polyfills'), paths.appSrc + "/repo-snapshot.js"],
|
||||
repoFolderTrash: [require.resolve('./polyfills'), paths.appSrc + "/repo-folder-trash.js"],
|
||||
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
|
||||
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
|
||||
|
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@ -15921,9 +15921,9 @@
|
||||
}
|
||||
},
|
||||
"seafile-js": {
|
||||
"version": "0.2.91",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.91.tgz",
|
||||
"integrity": "sha512-QNm629wX+NmCUzKiqOh1h/LWRwnRvwKwRXU6qOdOHg2CyubgGXmzk6U9NPMW1oK3p0takfz/1xypPQxNIAsXSw==",
|
||||
"version": "0.2.92",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.92.tgz",
|
||||
"integrity": "sha512-GySMa7cr8LXWj+uyIOG9JAIzUlzuGHZ1dLbx0tKrUcJ9vY10xVnDPCmJz3jEnG59wXNSlqkwcEaQxvaANfdgeg==",
|
||||
"requires": {
|
||||
"axios": "^0.18.0",
|
||||
"form-data": "^2.3.2",
|
||||
|
@ -37,7 +37,7 @@
|
||||
"react-responsive": "^6.1.1",
|
||||
"react-select": "^2.4.1",
|
||||
"reactstrap": "^6.4.0",
|
||||
"seafile-js": "^0.2.91",
|
||||
"seafile-js": "^0.2.92",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"sw-precache-webpack-plugin": "0.11.4",
|
||||
"unified": "^7.0.0",
|
||||
|
46
frontend/src/components/dialog/confirm-restore-repo.js
Normal file
46
frontend/src/components/dialog/confirm-restore-repo.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
restoreRepo: PropTypes.func.isRequired,
|
||||
toggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class ConfirmRestoreRepo extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
btnDisabled: false
|
||||
};
|
||||
}
|
||||
|
||||
action = () => {
|
||||
this.setState({
|
||||
btnDisabled: true
|
||||
});
|
||||
this.props.restoreRepo();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formActionURL, csrfToken, toggle} = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Restore Library')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to restore this library?')}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.action} disabled={this.state.btnDisabled}>{gettext('Restore')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmRestoreRepo.propTypes = propTypes;
|
||||
|
||||
export default ConfirmRestoreRepo;
|
@ -33,6 +33,7 @@ const propTypes = {
|
||||
onItemMove: PropTypes.func.isRequired,
|
||||
onItemCopy: PropTypes.func.isRequired,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
onItemsMove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class DirColumnNav extends React.Component {
|
||||
@ -276,6 +277,7 @@ class DirColumnNav extends React.Component {
|
||||
onItemMove={this.props.onItemMove}
|
||||
currentRepoInfo={this.props.currentRepoInfo}
|
||||
selectedDirentList={this.props.selectedDirentList}
|
||||
onItemsMove={this.props.onItemsMove}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
|
@ -171,6 +171,7 @@ class DirColumnView extends React.Component {
|
||||
onItemMove={this.props.onItemMove}
|
||||
onItemCopy={this.props.onItemCopy}
|
||||
selectedDirentList={this.props.selectedDirentList}
|
||||
onItemsMove={this.props.onItemsMove}
|
||||
/>
|
||||
<div className="dir-content-resize" onMouseDown={this.onResizeMouseDown}></div>
|
||||
<div className="dir-content-main" style={{userSelect: select, flex: mainFlex}}>
|
||||
|
@ -189,7 +189,7 @@ class DirentDetail extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let { dirent } = this.props;
|
||||
let { dirent, repoID, path } = this.props;
|
||||
let { folderDirent } = this.state;
|
||||
if (!dirent && !folderDirent) {
|
||||
return '';
|
||||
@ -198,7 +198,7 @@ class DirentDetail extends React.Component {
|
||||
let bigIconUrl = dirent ? Utils.getDirentIcon(dirent, true) : Utils.getDirentIcon(folderDirent, true);
|
||||
const isImg = dirent ? Utils.imageCheck(dirent.name) : Utils.imageCheck(folderDirent.name);
|
||||
if (isImg) {
|
||||
bigIconUrl = siteRoot + 'thumbnail/' + this.props.repoID + '/1024/' + dirent.name;
|
||||
bigIconUrl = `${siteRoot}thumbnail/${repoID}/1024` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`);
|
||||
}
|
||||
let direntName = dirent ? dirent.name : folderDirent.name;
|
||||
|
||||
|
@ -49,6 +49,7 @@ const propTypes = {
|
||||
onFileTagChanged: PropTypes.func,
|
||||
enableDirPrivateShare: PropTypes.bool.isRequired,
|
||||
showDirentDetail: PropTypes.func.isRequired,
|
||||
onItemsMove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class DirentListItem extends React.Component {
|
||||
@ -349,15 +350,28 @@ class DirentListItem extends React.Component {
|
||||
if (Utils.isIEBrower()) {
|
||||
return false;
|
||||
}
|
||||
let nodeRootPath = '';
|
||||
nodeRootPath = this.props.path === '/' ? `${this.props.path}${this.props.dirent.name}` : `${this.props.path}/${this.props.dirent.name}`;
|
||||
let dragStartItemData = {nodeDirent: this.props.dirent, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath};
|
||||
dragStartItemData = JSON.stringify(dragStartItemData);
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
if (e.dataTransfer && e.dataTransfer.setDragImage) {
|
||||
e.dataTransfer.setDragImage(this.refs.drag_icon, 15, 15);
|
||||
}
|
||||
|
||||
let { selectedDirentList } = this.props;
|
||||
if (selectedDirentList.length > 0 && selectedDirentList.includes(this.props.dirent)) { // drag items and selectedDirentList include item
|
||||
let selectedList = selectedDirentList.map(item => {
|
||||
let nodeRootPath = this.getDirentPath(item);
|
||||
let dragStartItemData = {nodeDirent: item, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath};
|
||||
return dragStartItemData;
|
||||
});
|
||||
selectedList = JSON.stringify(selectedList);
|
||||
e.dataTransfer.setData('applicaiton/drag-item-info', selectedList);
|
||||
return ;
|
||||
}
|
||||
|
||||
let nodeRootPath = this.getDirentPath(this.props.dirent);
|
||||
let dragStartItemData = {nodeDirent: this.props.dirent, nodeParentPath: this.props.path, nodeRootPath: nodeRootPath};
|
||||
dragStartItemData = JSON.stringify(dragStartItemData);
|
||||
|
||||
e.dataTransfer.setData('applicaiton/drag-item-info', dragStartItemData);
|
||||
}
|
||||
|
||||
@ -395,7 +409,22 @@ class DirentListItem extends React.Component {
|
||||
}
|
||||
let dragStartItemData = e.dataTransfer.getData('applicaiton/drag-item-info');
|
||||
dragStartItemData = JSON.parse(dragStartItemData);
|
||||
let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartItemData;
|
||||
if (Array.isArray(dragStartItemData)) { //move items
|
||||
let direntPaths = dragStartItemData.map(draggedItem => {
|
||||
return draggedItem.nodeRootPath
|
||||
});
|
||||
|
||||
let selectedPath = Utils.joinPath(this.props.path, this.props.dirent.name);
|
||||
|
||||
if (direntPaths.some(direntPath => { return direntPath === selectedPath;})) { //eg; A/B, A/C --> A/B
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onItemsMove(this.props.currentRepoInfo, selectedPath);
|
||||
return ;
|
||||
}
|
||||
|
||||
let { nodeDirent, nodeParentPath, nodeRootPath } = dragStartItemData;
|
||||
let dropItemData = this.props.dirent;
|
||||
|
||||
if (nodeDirent.name === dropItemData.name) {
|
||||
|
@ -577,6 +577,11 @@ class DirentListView extends React.Component {
|
||||
let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartItemData;
|
||||
|
||||
if (e.target.className === 'table-container table-drop-active') {
|
||||
|
||||
if (Array.isArray(dragStartItemData)) { //selected items
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeRootPath === this.props.path || nodeParentPath === this.props.path) {
|
||||
return;
|
||||
}
|
||||
@ -664,6 +669,7 @@ class DirentListView extends React.Component {
|
||||
onFileTagChanged={this.props.onFileTagChanged}
|
||||
getDirentItemMenuList={this.getDirentItemMenuList}
|
||||
showDirentDetail={this.props.showDirentDetail}
|
||||
onItemsMove={this.props.onItemsMove}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -417,8 +417,13 @@ class FileUploader extends React.Component {
|
||||
let repoID = this.props.repoID;
|
||||
seafileAPI.getUploadLink(repoID, this.props.path).then(res => {
|
||||
this.resumable.opts.target = res.data;
|
||||
this.uploadInput.current.click();
|
||||
if (Utils.isIEBrower()) {
|
||||
this.uploadInput.current.click();
|
||||
}
|
||||
});
|
||||
if (!Utils.isIEBrower()) {
|
||||
this.uploadInput.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
onFolderUpload = () => {
|
||||
@ -426,8 +431,13 @@ class FileUploader extends React.Component {
|
||||
let repoID = this.props.repoID;
|
||||
seafileAPI.getUploadLink(repoID, this.props.path).then(res => {
|
||||
this.resumable.opts.target = res.data;
|
||||
this.uploadInput.current.click();
|
||||
if (Utils.isIEBrower()) {
|
||||
this.uploadInput.current.click();
|
||||
}
|
||||
});
|
||||
if (!Utils.isIEBrower()) {
|
||||
this.uploadInput.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart = () => {
|
||||
|
@ -18,6 +18,7 @@ const propTypes = {
|
||||
onItemMove: PropTypes.func,
|
||||
currentRepoInfo: PropTypes.object,
|
||||
selectedDirentList: PropTypes.array,
|
||||
onItemsMove: PropTypes.func,
|
||||
};
|
||||
|
||||
const PADDING_LEFT = 20;
|
||||
@ -91,6 +92,20 @@ class TreeView extends React.Component {
|
||||
let {nodeDirent, nodeParentPath, nodeRootPath} = dragStartNodeData;
|
||||
let dropNodeData = node;
|
||||
|
||||
if (Array.isArray(dragStartNodeData)) { //move items
|
||||
if (!dropNodeData) { //move items to root
|
||||
if (dragStartNodeData[0].nodeParentPath === '/') {
|
||||
this.setState({isTreeViewDropTipShow: false});
|
||||
return;
|
||||
}
|
||||
this.props.onItemsMove(this.props.currentRepoInfo, '/');
|
||||
this.setState({isTreeViewDropTipShow: false});
|
||||
return;
|
||||
}
|
||||
this.onMoveItems(dragStartNodeData, dropNodeData, this.props.currentRepoInfo, dropNodeData.path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dropNodeData) {
|
||||
if (nodeParentPath === '/') {
|
||||
this.setState({isTreeViewDropTipShow: false});
|
||||
@ -119,7 +134,8 @@ class TreeView extends React.Component {
|
||||
// copy the dirent to it's child. eg: A/B -> A/B/C
|
||||
if (dropNodeData.object.type === 'dir' && nodeDirent.type === 'dir') {
|
||||
if (dropNodeData.parentNode.path !== nodeParentPath) {
|
||||
if (dropNodeData.path.indexOf(nodeRootPath) !== -1) {
|
||||
let paths = Utils.getPaths(dropNodeData.path);
|
||||
if (paths.includes(nodeRootPath)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -128,6 +144,39 @@ class TreeView extends React.Component {
|
||||
this.onItemMove(this.props.currentRepoInfo, nodeDirent, dropNodeData.path, nodeParentPath);
|
||||
}
|
||||
|
||||
onMoveItems = (dragStartNodeData, dropNodeData, destRepo, destDirentPath) => {
|
||||
let direntPaths = [];
|
||||
let paths = Utils.getPaths(destDirentPath);
|
||||
dragStartNodeData.forEach(dirent => {
|
||||
let path = dirent.nodeRootPath;
|
||||
direntPaths.push(path);
|
||||
});
|
||||
|
||||
if (dropNodeData.object.type !== 'dir') {
|
||||
return;
|
||||
}
|
||||
|
||||
// move dirents to one of them. eg: A/B, A/C -> A/B
|
||||
if (direntPaths.some(direntPath => { return direntPath === destDirentPath;})) {
|
||||
return;
|
||||
}
|
||||
|
||||
// move dirents to current path
|
||||
if (dragStartNodeData[0].nodeParentPath && dragStartNodeData[0].nodeParentPath === dropNodeData.path ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// move dirents to one of their child. eg: A/B, A/D -> A/B/C
|
||||
let isChildPath = direntPaths.some(direntPath => {
|
||||
return paths.includes(direntPath);
|
||||
});
|
||||
if (isChildPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onItemsMove(destRepo, destDirentPath);
|
||||
}
|
||||
|
||||
freezeItem = () => {
|
||||
this.setState({isItemFreezed: true});
|
||||
}
|
||||
|
37
frontend/src/css/repo-snapshot.css
Normal file
37
frontend/src/css/repo-snapshot.css
Normal file
@ -0,0 +1,37 @@
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
.top-header {
|
||||
background: #f4f4f7;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: .5rem 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.go-back {
|
||||
color: #c0c0c0;
|
||||
font-size: 1.75rem;
|
||||
position: absolute;
|
||||
left: -40px;
|
||||
top: -5px;
|
||||
}
|
||||
.op-bar {
|
||||
padding: 9px 10px;
|
||||
background: #f2f2f2;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.op-bar-btn {
|
||||
border-color: #ccc;
|
||||
border-radius: 2px;
|
||||
height: 30px;
|
||||
line-height: 28px;
|
||||
font-weight: normal;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 55px;
|
||||
}
|
||||
.heading-commit-time {
|
||||
font-weight: normal;
|
||||
font-size: 60%;
|
||||
}
|
@ -294,7 +294,7 @@ class Item extends React.Component {
|
||||
{userPerm == 'rw' && (
|
||||
item.isFirstCommit ?
|
||||
<span className={isIconShown ? '': 'invisible'}>{gettext('Current Version')}</span> :
|
||||
<a href={`${siteRoot}repo/history/view/${repoID}/?commit_id=${item.commit_id}`} className={isIconShown ? '': 'invisible'}>{gettext('View Snapshot')}</a>
|
||||
<a href={`${siteRoot}repo/${repoID}/snapshot/?commit_id=${item.commit_id}`} className={isIconShown ? '': 'invisible'}>{gettext('View Snapshot')}</a>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
337
frontend/src/repo-snapshot.js
Normal file
337
frontend/src/repo-snapshot.js
Normal file
@ -0,0 +1,337 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { navigate } from '@reach/router';
|
||||
import moment from 'moment';
|
||||
import { Utils } from './utils/utils';
|
||||
import { gettext, loginUrl, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
|
||||
import { seafileAPI } from './utils/seafile-api';
|
||||
import Loading from './components/loading';
|
||||
import ModalPortal from './components/modal-portal';
|
||||
import toaster from './components/toast';
|
||||
import CommonToolbar from './components/toolbar/common-toolbar';
|
||||
import ConfirmRestoreRepo from './components/dialog/confirm-restore-repo';
|
||||
|
||||
import './css/toolbar.css';
|
||||
import './css/search.css';
|
||||
|
||||
import './css/repo-snapshot.css';
|
||||
|
||||
const {
|
||||
repoID, repoName, isRepoOwner,
|
||||
commitID, commitTime, commitDesc, commitRelativeTime,
|
||||
showAuthor, authorAvatarURL, authorName, authorNickName
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class RepoSnapshot extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
errorMsg: '',
|
||||
folderPath: '/',
|
||||
folderItems: [],
|
||||
isConfirmDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.renderFolder(this.state.folderPath);
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({
|
||||
isConfirmDialogOpen: !this.state.isConfirmDialogOpen
|
||||
});
|
||||
}
|
||||
|
||||
onSearchedClick = (selectedItem) => {
|
||||
if (selectedItem.is_dir === true) {
|
||||
let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
|
||||
navigate(url, {repalce: true});
|
||||
} else {
|
||||
let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
|
||||
let newWindow = window.open('about:blank');
|
||||
newWindow.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
goBack = (e) => {
|
||||
e.preventDefault();
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
renderFolder = (folderPath) => {
|
||||
this.setState({
|
||||
folderPath: folderPath,
|
||||
folderItems: [],
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
seafileAPI.listCommitDir(repoID, commitID, folderPath).then((res) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
folderItems: res.data.dirent_list
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (error.response) {
|
||||
if (error.response.status == 403) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: gettext('Permission denied')
|
||||
});
|
||||
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
|
||||
} else {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: gettext('Error')
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
errorMsg: gettext('Please check the network.')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clickFolderPath = (folderPath, e) => {
|
||||
e.preventDefault();
|
||||
this.renderFolder(folderPath);
|
||||
}
|
||||
|
||||
renderPath = () => {
|
||||
const path = this.state.folderPath;
|
||||
const pathList = path.split('/');
|
||||
|
||||
if (path == '/') {
|
||||
return repoName;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<a href="#" onClick={this.clickFolderPath.bind(this, '/')}>{repoName}</a>
|
||||
<span> / </span>
|
||||
{pathList.map((item, index) => {
|
||||
if (index > 0 && index != pathList.length - 1) {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<a href="#" onClick={this.clickFolderPath.bind(this, pathList.slice(0, index+1).join('/'))}>{pathList[index]}</a>
|
||||
<span> / </span>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
{pathList[pathList.length - 1]}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
restoreRepo = () => {
|
||||
seafileAPI.revertRepo(repoID, commitID).then((res) => {
|
||||
this.toggleDialog();
|
||||
toaster.success(gettext('Successfully restored the library.'));
|
||||
}).catch((error) => {
|
||||
let errorMsg = '';
|
||||
if (error.response) {
|
||||
if (error.response.data && error.response.data['error_msg']) {
|
||||
errorMsg = error.response.data['error_msg'];
|
||||
} else {
|
||||
errorMsg = gettext('Error');
|
||||
}
|
||||
} else {
|
||||
errorMsg = gettext('Please check the network.');
|
||||
}
|
||||
this.toggleDialog();
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isConfirmDialogOpen, folderPath } = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="h-100 d-flex flex-column">
|
||||
<div className="top-header d-flex justify-content-between">
|
||||
<a href={siteRoot}>
|
||||
<img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
|
||||
</a>
|
||||
<CommonToolbar onSearchedClick={this.onSearchedClick} />
|
||||
</div>
|
||||
<div className="flex-auto container-fluid pt-4 pb-6 o-auto">
|
||||
<div className="row">
|
||||
<div className="col-md-10 offset-md-1">
|
||||
<h2 dangerouslySetInnerHTML={{__html: Utils.generateDialogTitle(gettext('{placeholder} Snapshot'), repoName) + ` <span class="heading-commit-time">(${commitTime})</span>`}}></h2>
|
||||
<a href="#" className="go-back" title={gettext('Back')} onClick={this.goBack}>
|
||||
<span className="fas fa-chevron-left"></span>
|
||||
</a>
|
||||
{folderPath == '/' && (
|
||||
<div className="d-flex mb-2 align-items-center">
|
||||
<p className="m-0">{commitDesc}</p>
|
||||
<div className="ml-4 border-left pl-4 d-flex align-items-center">
|
||||
{showAuthor ? (
|
||||
<React.Fragment>
|
||||
<img src={authorAvatarURL} width="20" height="20" alt="" className="rounded mr-1" />
|
||||
<a href={`${siteRoot}profile/${encodeURIComponent(authorName)}/`}>{authorNickName}</a>
|
||||
</React.Fragment>
|
||||
) : <span>{gettext('Unknown')}</span>}
|
||||
<p className="m-0 ml-2" dangerouslySetInnerHTML={{__html: commitRelativeTime}}></p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex justify-content-between align-items-center op-bar">
|
||||
<p className="m-0">{gettext('Current path: ')}{this.renderPath()}</p>
|
||||
{(folderPath == '/' && isRepoOwner) &&
|
||||
<button className="btn btn-secondary op-bar-btn" onClick={this.toggleDialog}>{gettext('Restore')}</button>
|
||||
}
|
||||
</div>
|
||||
<Content
|
||||
data={this.state}
|
||||
renderFolder={this.renderFolder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isConfirmDialogOpen &&
|
||||
<ModalPortal>
|
||||
<ConfirmRestoreRepo
|
||||
restoreRepo={this.restoreRepo}
|
||||
toggle={this.toggleDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Content extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.theadData = [
|
||||
{width: '5%', text: ''},
|
||||
{width: '55%', text: gettext('Name')},
|
||||
{width: '20%', text: gettext('Size')},
|
||||
{width: '20%', text: ''}
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, errorMsg, folderPath, folderItems } = this.props.data;
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
return <p className="error mt-6 text-center">{errorMsg}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
{this.theadData.map((item, index) => {
|
||||
return <th key={index} width={item.width}>{item.text}</th>;
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{folderItems.map((item, index) => {
|
||||
return <FolderItem
|
||||
key={index}
|
||||
item={item}
|
||||
folderPath={folderPath}
|
||||
renderFolder={this.props.renderFolder}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FolderItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isIconShown: false
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseOver = () => {
|
||||
this.setState({isIconShown: true});
|
||||
}
|
||||
|
||||
handleMouseOut = () => {
|
||||
this.setState({isIconShown: false});
|
||||
}
|
||||
|
||||
restoreItem = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const item = this.props.item;
|
||||
const path = Utils.joinPath(this.props.folderPath, item.name);
|
||||
const request = item.type == 'dir' ?
|
||||
seafileAPI.revertFolder(repoID, path, commitID):
|
||||
seafileAPI.revertFile(repoID, path, commitID);
|
||||
request.then((res) => {
|
||||
toaster.success(gettext('Successfully restored 1 item.'));
|
||||
}).catch((error) => {
|
||||
let errorMsg = '';
|
||||
if (error.response) {
|
||||
errorMsg = error.response.data.error_msg || gettext('Error');
|
||||
} else {
|
||||
errorMsg = gettext('Please check the network.');
|
||||
}
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}
|
||||
|
||||
renderFolder = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const item = this.props.item;
|
||||
const { folderPath } = this.props;
|
||||
this.props.renderFolder(Utils.joinPath(folderPath, item.name));
|
||||
}
|
||||
|
||||
render() {
|
||||
const item = this.props.item;
|
||||
const { isIconShown } = this.state;
|
||||
const { folderPath } = this.props;
|
||||
|
||||
return item.type == 'dir' ? (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<td className="text-center"><img src={Utils.getFolderIconUrl()} alt={gettext('Directory')} width="24" /></td>
|
||||
<td><a href="#" onClick={this.renderFolder}>{item.name}</a></td>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="#" className={`action-icon sf2-icon-reply ${isIconShown ? '': 'invisible'}`} onClick={this.restoreItem} title={gettext('Restore')}></a>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<td className="text-center"><img src={Utils.getFileIconUrl(item.name)} alt={gettext('File')} width="24" /></td>
|
||||
<td><a href={`${siteRoot}repo/${repoID}/snapshot/files/?obj_id=${item.obj_id}&commit_id=${commitID}&p=${encodeURIComponent(Utils.joinPath(folderPath, item.name))}`} target="_blank">{item.name}</a></td>
|
||||
<td>{Utils.bytesToSize(item.size)}</td>
|
||||
<td>
|
||||
<a href="#" className={`action-icon sf2-icon-reply ${isIconShown ? '': 'invisible'}`} onClick={this.restoreItem} title={gettext('Restore')}></a>
|
||||
<a href={`${siteRoot}repo/${repoID}/${item.obj_id}/download/?file_name=${encodeURIComponent(item.name)}&p=${encodeURIComponent(Utils.joinPath(folderPath, item.name))}`} className={`action-icon sf2-icon-download ${isIconShown ? '': 'invisible'}`} title={gettext('Download')}></a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<RepoSnapshot />,
|
||||
document.getElementById('wrapper')
|
||||
);
|
@ -116,7 +116,7 @@ class Settings extends React.Component {
|
||||
</a>
|
||||
<CommonToolbar onSearchedClick={this.onSearchedClick} />
|
||||
</div>
|
||||
<div className="flex-auto d-flex">
|
||||
<div className="flex-auto d-flex o-hidden">
|
||||
<div className="side-panel o-auto">
|
||||
<SideNav data={this.sideNavItems} curItemID={this.state.curItemID} />
|
||||
</div>
|
||||
|
@ -6,8 +6,6 @@ from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from seaserv import seafile_api
|
||||
|
||||
from pysearpc import SearpcError
|
||||
|
||||
from seahub.signals import repo_restored
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
@ -71,7 +69,7 @@ class DeletedRepos(APIView):
|
||||
try:
|
||||
seafile_api.restore_repo_from_trash(repo_id)
|
||||
repo_restored.send(sender=None, repo_id=repo_id, operator=username)
|
||||
except SearpcError as e:
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = "Internal Server Error"
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
@ -61,7 +61,7 @@ class RepoCommitDirView(APIView):
|
||||
|
||||
commit = seafile_api.get_commit(repo.id, repo.version, commit_id)
|
||||
if not commit:
|
||||
error_msg = 'Commit %s not found.' % commit
|
||||
error_msg = 'Commit %s not found.' % commit_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
dir_id = seafile_api.get_dir_id_by_commit_and_path(repo_id, commit_id, path)
|
||||
|
70
seahub/api2/endpoints/repo_commit_revert.py
Normal file
70
seahub/api2/endpoints/repo_commit_revert.py
Normal file
@ -0,0 +1,70 @@
|
||||
# Copyright (c) 2012-2019 Seafile Ltd.
|
||||
# encoding: utf-8
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.utils import api_error
|
||||
from seahub.views import check_folder_permission
|
||||
from seaserv import seafile_api
|
||||
from seahub.utils.repo import is_repo_owner
|
||||
from seahub.constants import PERMISSION_READ_WRITE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RepoCommitRevertView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def post(self, request, repo_id, commit_id, format=None):
|
||||
""" revert commit in repo history
|
||||
|
||||
Permission checking:
|
||||
1. only repo owner can perform this action.
|
||||
"""
|
||||
username = request.user.username
|
||||
|
||||
# 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)
|
||||
|
||||
commit = seafile_api.get_commit(repo.id, repo.version, commit_id)
|
||||
if not commit:
|
||||
error_msg = 'Commit %s not found.' % commit_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
# permission check
|
||||
if not is_repo_owner(request, repo_id, username) or \
|
||||
check_folder_permission(request, repo_id, '/') != PERMISSION_READ_WRITE:
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
# main
|
||||
if repo.encrypted:
|
||||
ret = seafile_api.is_password_set(repo_id, username)
|
||||
is_decrypted = False if ret == 0 else True
|
||||
|
||||
if not is_decrypted:
|
||||
error_msg = _('This library has not been decrypted.')
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
try:
|
||||
seafile_api.revert_repo(repo_id, commit_id, username)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'success': True})
|
@ -29,7 +29,7 @@ from seahub.utils import is_org_context, send_perm_audit_msg, \
|
||||
normalize_dir_path, get_folder_permission_recursively, \
|
||||
normalize_file_path, check_filename_with_rename
|
||||
from seahub.utils.repo import get_repo_owner, get_available_repo_perms, \
|
||||
parse_repo_perm
|
||||
parse_repo_perm, get_locked_files_by_dir
|
||||
|
||||
from seahub.views import check_folder_permission
|
||||
from seahub.settings import MAX_PATH
|
||||
@ -1244,9 +1244,18 @@ class ReposAsyncBatchMoveItemView(APIView):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
result = {}
|
||||
# check locked files
|
||||
username = request.user.username
|
||||
locked_files = get_locked_files_by_dir(request, src_repo_id, src_parent_dir)
|
||||
for dirent in src_dirents:
|
||||
# file is locked and lock owner is not current user
|
||||
if dirent in locked_files.keys() and \
|
||||
locked_files[dirent] != username:
|
||||
error_msg = _(u'File %s is locked.') % dirent
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
# move file
|
||||
result = {}
|
||||
formated_src_dirents = [dirent.strip('/') for dirent in src_dirents]
|
||||
src_multi = "\t".join(formated_src_dirents)
|
||||
dst_multi = "\t".join(formated_src_dirents)
|
||||
@ -1445,9 +1454,18 @@ class ReposSyncBatchMoveItemView(APIView):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
result = {}
|
||||
# check locked files
|
||||
username = request.user.username
|
||||
locked_files = get_locked_files_by_dir(request, src_repo_id, src_parent_dir)
|
||||
for dirent in src_dirents:
|
||||
# file is locked and lock owner is not current user
|
||||
if dirent in locked_files.keys() and \
|
||||
locked_files[dirent] != username:
|
||||
error_msg = _(u'File %s is locked.') % dirent
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
# move file
|
||||
result = {}
|
||||
formated_src_dirents = [dirent.strip('/') for dirent in src_dirents]
|
||||
src_multi = "\t".join(formated_src_dirents)
|
||||
dst_multi = "\t".join(formated_src_dirents)
|
||||
|
@ -17,16 +17,17 @@ try:
|
||||
repo_id = kwargs['repo_id']
|
||||
repo_name = kwargs['repo_name']
|
||||
|
||||
|
||||
# Move here to avoid model import during Django setup.
|
||||
# TODO: Don't register signal/hanlders during Seahub start.
|
||||
# TODO: Don't register signal/handlers during Seahub start.
|
||||
|
||||
if org_id > 0:
|
||||
related_users = seafile_api.org_get_shared_users_by_repo(org_id, repo_id)
|
||||
else:
|
||||
related_users = seafile_api.get_shared_users_by_repo(repo_id)
|
||||
org_id = -1
|
||||
|
||||
related_users.append(creator)
|
||||
if creator not in related_users:
|
||||
related_users.append(creator)
|
||||
|
||||
record = {
|
||||
'op_type':'create',
|
||||
@ -78,7 +79,8 @@ try:
|
||||
related_users = seafile_api.get_shared_users_by_repo(repo_id)
|
||||
org_id = -1
|
||||
|
||||
related_users.append(repo_owner)
|
||||
if repo_owner not in related_users:
|
||||
related_users.append(repo_owner)
|
||||
|
||||
record = {
|
||||
'op_type':'delete',
|
||||
@ -113,7 +115,9 @@ try:
|
||||
related_users = seafile_api.get_shared_users_by_repo(repo_id)
|
||||
org_id = -1
|
||||
|
||||
related_users.append(repo_owner)
|
||||
if repo_owner not in related_users:
|
||||
related_users.append(repo_owner)
|
||||
|
||||
record = {
|
||||
'op_type':'clean-up-trash',
|
||||
'obj_type':'repo',
|
||||
@ -144,7 +148,8 @@ try:
|
||||
related_users = seafile_api.get_shared_users_by_repo(repo_id)
|
||||
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||
|
||||
related_users.append(repo_owner)
|
||||
if repo_owner not in related_users:
|
||||
related_users.append(repo_owner)
|
||||
|
||||
record = {
|
||||
'op_type':'recover',
|
||||
@ -154,7 +159,7 @@ try:
|
||||
'repo_name': repo.repo_name,
|
||||
'path': '/',
|
||||
'op_user': operator,
|
||||
'related_users': [related_users],
|
||||
'related_users': related_users,
|
||||
'org_id': org_id,
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,183 @@
|
||||
# Copyright (c) 2012-2019 Seafile Ltd.
|
||||
# encoding: utf-8
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ungettext
|
||||
|
||||
from seahub.base.models import CommandsLastCheck
|
||||
from seahub.notifications.models import UserNotification
|
||||
from seahub.utils import get_site_scheme_and_netloc, get_site_name
|
||||
from seahub.auth.models import SocialAuthUser
|
||||
from seahub.work_weixin.utils import work_weixin_notifications_check, \
|
||||
get_work_weixin_access_token, handler_work_weixin_api_response
|
||||
from seahub.work_weixin.settings import WORK_WEIXIN_NOTIFICATIONS_URL, \
|
||||
WORK_WEIXIN_PROVIDER, WORK_WEIXIN_UID_PREFIX, WORK_WEIXIN_AGENT_ID
|
||||
|
||||
# Get an instance of a logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# https://work.weixin.qq.com/api/doc#90000/90135/90236/
|
||||
# from social_django.models import UserSocialAuth
|
||||
|
||||
########## Utility Functions ##########
|
||||
def wrap_div(s):
|
||||
"""
|
||||
Replace <a ..>xx</a> to xx and wrap content with <div></div>.
|
||||
"""
|
||||
patt = '<a.*?>(.+?)</a>'
|
||||
|
||||
def repl(matchobj):
|
||||
return matchobj.group(1)
|
||||
|
||||
return '<div class="highlight">' + re.sub(patt, repl, s) + '</div>'
|
||||
|
||||
|
||||
class CommandLogMixin(object):
|
||||
def println(self, msg):
|
||||
self.stdout.write('[%s] %s\n' % (str(datetime.now()), msg))
|
||||
|
||||
def log_error(self, msg):
|
||||
logger.error(msg)
|
||||
self.println(msg)
|
||||
|
||||
def log_info(self, msg):
|
||||
logger.info(msg)
|
||||
self.println(msg)
|
||||
|
||||
def log_debug(self, msg):
|
||||
logger.debug(msg)
|
||||
self.println(msg)
|
||||
|
||||
|
||||
#######################################
|
||||
|
||||
class Command(BaseCommand, CommandLogMixin):
|
||||
""" send work weixin notifications
|
||||
"""
|
||||
|
||||
help = 'Send WeChat Work msg to user if he/she has unseen notices every '
|
||||
'period of time.'
|
||||
label = "notifications_send_wxwork_notices"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.log_debug('Start sending work weixin msg...')
|
||||
self.do_action()
|
||||
self.log_debug('Finish sending work weixin msg.\n')
|
||||
|
||||
def send_work_weixin_msg(self, uid, title, content):
|
||||
|
||||
self.log_info('Send wechat msg to user: %s, msg: %s' % (uid, content))
|
||||
|
||||
data = {
|
||||
"touser": uid,
|
||||
"agentid": WORK_WEIXIN_AGENT_ID,
|
||||
'msgtype': 'textcard',
|
||||
'textcard': {
|
||||
'title': title,
|
||||
'description': content,
|
||||
'url': self.detail_url,
|
||||
},
|
||||
}
|
||||
|
||||
api_response = requests.post(self.work_weixin_notifications_url, json=data)
|
||||
api_response_dic = handler_work_weixin_api_response(api_response)
|
||||
if api_response_dic:
|
||||
self.log_info(api_response_dic)
|
||||
else:
|
||||
self.log_error('can not get work weixin notifications API response')
|
||||
|
||||
def do_action(self):
|
||||
# check before start
|
||||
if not work_weixin_notifications_check():
|
||||
self.log_error('work weixin notifications settings check failed')
|
||||
return
|
||||
|
||||
access_token = get_work_weixin_access_token()
|
||||
if not access_token:
|
||||
self.log_error('can not get access_token')
|
||||
|
||||
self.work_weixin_notifications_url = WORK_WEIXIN_NOTIFICATIONS_URL + '?access_token=' + access_token
|
||||
self.detail_url = get_site_scheme_and_netloc().rstrip('/') + reverse('user_notification_list')
|
||||
site_name = get_site_name()
|
||||
|
||||
# start
|
||||
now = datetime.now()
|
||||
today = datetime.now().replace(hour=0).replace(minute=0).replace(
|
||||
second=0).replace(microsecond=0)
|
||||
|
||||
# 1. get all users who are connected work weixin
|
||||
socials = SocialAuthUser.objects.filter(provider=WORK_WEIXIN_PROVIDER, uid__contains=WORK_WEIXIN_UID_PREFIX)
|
||||
users = [(x.username, x.uid[len(WORK_WEIXIN_UID_PREFIX):]) for x in socials]
|
||||
self.log_info('Found %d users' % len(users))
|
||||
if not users:
|
||||
return
|
||||
|
||||
user_uid_map = {}
|
||||
for username, uid in users:
|
||||
user_uid_map[username] = uid
|
||||
|
||||
# 2. get previous time that command last runs
|
||||
try:
|
||||
cmd_last_check = CommandsLastCheck.objects.get(command_type=self.label)
|
||||
self.log_debug('Last check time is %s' % cmd_last_check.last_check)
|
||||
|
||||
last_check_dt = cmd_last_check.last_check
|
||||
|
||||
cmd_last_check.last_check = now
|
||||
cmd_last_check.save()
|
||||
except CommandsLastCheck.DoesNotExist:
|
||||
last_check_dt = today
|
||||
self.log_debug('Create new last check time: %s' % now)
|
||||
CommandsLastCheck(command_type=self.label, last_check=now).save()
|
||||
|
||||
# 3. get all unseen notices for those users
|
||||
qs = UserNotification.objects.filter(
|
||||
timestamp__gt=last_check_dt
|
||||
).filter(seen=False).filter(
|
||||
to_user__in=user_uid_map.keys()
|
||||
)
|
||||
self.log_info('Found %d notices' % qs.count())
|
||||
if qs.count() == 0:
|
||||
return
|
||||
|
||||
user_notices = {}
|
||||
for q in qs:
|
||||
if q.to_user not in user_notices:
|
||||
user_notices[q.to_user] = [q]
|
||||
else:
|
||||
user_notices[q.to_user].append(q)
|
||||
|
||||
# save current language
|
||||
cur_language = translation.get_language()
|
||||
# active zh-cn
|
||||
translation.activate('zh-cn')
|
||||
self.log_info('the language is set to zh-cn')
|
||||
|
||||
# 4. send msg to users
|
||||
for username, uid in users:
|
||||
notices = user_notices.get(username, [])
|
||||
count = len(notices)
|
||||
if count == 0:
|
||||
continue
|
||||
|
||||
title = ungettext(
|
||||
"\n"
|
||||
"You've got 1 new notice on %(site_name)s:\n",
|
||||
"\n"
|
||||
"You've got %(num)s new notices on %(site_name)s:\n",
|
||||
count
|
||||
) % {'num': count, 'site_name': site_name, }
|
||||
|
||||
content = ''.join([wrap_div(x.format_msg()) for x in notices])
|
||||
self.send_work_weixin_msg(uid, title, content)
|
||||
|
||||
# reset language
|
||||
translation.activate(cur_language)
|
||||
self.log_info('reset language success')
|
@ -51,6 +51,9 @@ class CommandLogMixin(object):
|
||||
#######################################
|
||||
|
||||
class Command(BaseCommand, CommandLogMixin):
|
||||
""" please use send_work_weixin_notifications.py
|
||||
"""
|
||||
|
||||
help = 'Send WeChat Work msg to user if he/she has unseen notices every '
|
||||
'period of time.'
|
||||
label = "notifications_send_wxwork_notices"
|
||||
|
@ -306,6 +306,8 @@ ENABLE_WATERMARK = False
|
||||
ENABLE_WORK_WEIXIN_OAUTH = False
|
||||
# allow seafile admin import user from work weixin
|
||||
ENABLE_WORK_WEIXIN_DEPARTMENTS = False
|
||||
# allow send unread msg to work weixin
|
||||
ENABLE_WORK_WEIXIN_NOTIFICATIONS = False
|
||||
|
||||
# allow user to clean library trash
|
||||
ENABLE_USER_CLEAN_TRASH = True
|
||||
|
34
seahub/templates/repo_snapshot_react.html
Normal file
34
seahub/templates/repo_snapshot_react.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends 'base_for_react.html' %}
|
||||
{% load seahub_tags i18n avatar_tags %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
|
||||
{% block sub_title %}{% trans "Snapshot" %} - {% endblock %}
|
||||
|
||||
{% block extra_style %}
|
||||
{% render_bundle 'repoSnapshot' 'css' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript">
|
||||
// overwrite the one in base_for_react.html
|
||||
window.app.pageOptions = {
|
||||
repoID: '{{repo.id}}',
|
||||
repoName: '{{repo.props.name|escapejs}}',
|
||||
|
||||
commitID: '{{current_commit.id}}',
|
||||
commitTime: '{{current_commit.props.ctime|tsstr_sec}}',
|
||||
commitDesc: '{{ current_commit.props.desc|translate_commit_desc|escapejs }}',
|
||||
commitRelativeTime: '{{ current_commit.props.ctime|translate_seahub_time }}',
|
||||
|
||||
{% if current_commit.props.creator_name %}
|
||||
showAuthor: true,
|
||||
authorAvatarURL: "{% avatar_url current_commit.props.creator_name 40 %}",
|
||||
authorName: '{{ current_commit.props.creator_name|escapejs }}',
|
||||
authorNickName: '{{ current_commit.props.creator_name|email2nickname|escapejs }}',
|
||||
{% endif %}
|
||||
|
||||
isRepoOwner: {% if is_repo_owner %} true {% else %} false {% endif %}
|
||||
};
|
||||
</script>
|
||||
{% render_bundle 'repoSnapshot' 'js' %}
|
||||
{% endblock %}
|
@ -14,7 +14,7 @@ from seahub.views.file import view_history_file, view_trash_file,\
|
||||
text_diff, view_raw_file, download_file, view_lib_file, \
|
||||
file_access, view_lib_file_via_smart_link, view_media_file_via_share_link, \
|
||||
view_media_file_via_public_wiki
|
||||
from seahub.views.repo import repo_history_view, view_shared_dir, \
|
||||
from seahub.views.repo import repo_history_view, repo_snapshot, view_shared_dir, \
|
||||
view_shared_upload_link, view_lib_as_wiki
|
||||
from notifications.views import notification_list
|
||||
from seahub.views.wiki import personal_wiki, personal_wiki_pages, \
|
||||
@ -55,6 +55,7 @@ from seahub.api2.endpoints.file_tag import FileTagView
|
||||
from seahub.api2.endpoints.file_tag import FileTagsView
|
||||
from seahub.api2.endpoints.repo_trash import RepoTrash
|
||||
from seahub.api2.endpoints.repo_commit_dir import RepoCommitDirView
|
||||
from seahub.api2.endpoints.repo_commit_revert import RepoCommitRevertView
|
||||
from seahub.api2.endpoints.deleted_repos import DeletedRepos
|
||||
from seahub.api2.endpoints.repo_history import RepoHistory
|
||||
from seahub.api2.endpoints.repo_set_password import RepoSetPassword
|
||||
@ -172,6 +173,7 @@ urlpatterns = [
|
||||
url(r'^repo/text_diff/(?P<repo_id>[-0-9a-f]{36})/$', text_diff, name='text_diff'),
|
||||
url(r'^repo/history/(?P<repo_id>[-0-9a-f]{36})/$', repo_history, name='repo_history'),
|
||||
url(r'^repo/history/view/(?P<repo_id>[-0-9a-f]{36})/$', repo_history_view, name='repo_history_view'),
|
||||
url(r'^repo/(?P<repo_id>[-0-9a-f]{36})/snapshot/$', repo_snapshot, name="repo_snapshot"),
|
||||
url(r'^repo/recycle/(?P<repo_id>[-0-9a-f]{36})/$', repo_recycle_view, name='repo_recycle_view'),
|
||||
url(r'^dir/recycle/(?P<repo_id>[-0-9a-f]{36})/$', dir_recycle_view, name='dir_recycle_view'),
|
||||
url(r'^repo/(?P<repo_id>[-0-9a-f]{36})/trash/$', repo_folder_trash, name="repo_folder_trash"),
|
||||
@ -336,6 +338,7 @@ urlpatterns = [
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/new_history/$', NewFileHistoryView.as_view(), name='api-v2.1-new-file-history-view'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/commits/(?P<commit_id>[0-9a-f]{40})/dir/$', RepoCommitDirView.as_view(), name='api-v2.1-repo-commit-dir'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/commits/(?P<commit_id>[0-9a-f]{40})/revert/$', RepoCommitRevertView.as_view(), name='api-v2.1-repo-commit-revert'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/dir/detail/$', DirDetailView.as_view(), name='api-v2.1-dir-detail-view'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/history/$', RepoHistory.as_view(), name='api-v2.1-repo-history'),
|
||||
|
@ -153,6 +153,50 @@ def repo_history_view(request, repo_id):
|
||||
'referer': referer,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def repo_snapshot(request, repo_id):
|
||||
"""View repo in history.
|
||||
"""
|
||||
repo = get_repo(repo_id)
|
||||
if not repo:
|
||||
raise Http404
|
||||
|
||||
username = request.user.username
|
||||
user_perm = check_folder_permission(request, repo.id, '/')
|
||||
if user_perm is None:
|
||||
return render_error(request, _(u'Permission denied'))
|
||||
|
||||
try:
|
||||
server_crypto = UserOptions.objects.is_server_crypto(username)
|
||||
except CryptoOptionNotSetError:
|
||||
# Assume server_crypto is ``False`` if this option is not set.
|
||||
server_crypto = False
|
||||
|
||||
reverse_url = reverse('lib_view', args=[repo_id, repo.name, ''])
|
||||
if repo.encrypted and \
|
||||
(repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)) \
|
||||
and not is_password_set(repo.id, username):
|
||||
return render(request, 'decrypt_repo_form.html', {
|
||||
'repo': repo,
|
||||
'next': get_next_url_from_request(request) or reverse_url,
|
||||
})
|
||||
|
||||
commit_id = request.GET.get('commit_id', None)
|
||||
if commit_id is None:
|
||||
return HttpResponseRedirect(reverse_url)
|
||||
current_commit = get_commit(repo.id, repo.version, commit_id)
|
||||
if not current_commit:
|
||||
current_commit = get_commit(repo.id, repo.version, repo.head_cmmt_id)
|
||||
|
||||
repo_owner = seafile_api.get_repo_owner(repo.id)
|
||||
is_repo_owner = True if username == repo_owner else False
|
||||
|
||||
return render(request, 'repo_snapshot_react.html', {
|
||||
'repo': repo,
|
||||
"is_repo_owner": is_repo_owner,
|
||||
'current_commit': current_commit,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def view_lib_as_wiki(request, repo_id, path):
|
||||
|
||||
|
@ -213,7 +213,7 @@ class WOPIFilesView(APIView):
|
||||
|
||||
# new file creation feature is not implemented on wopi host(seahub)
|
||||
# hide save as button on view/edit file page
|
||||
result['UserCanNotWriteRelative'] = False
|
||||
result['UserCanNotWriteRelative'] = True
|
||||
|
||||
return HttpResponse(json.dumps(result), status=200,
|
||||
content_type=json_content_type)
|
||||
|
@ -19,7 +19,7 @@ WORK_WEIXIN_DEPARTMENT_MEMBERS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENT_M
|
||||
WORK_WEIXIN_AGENT_ID = getattr(settings, 'WORK_WEIXIN_AGENT_ID', '')
|
||||
ENABLE_WORK_WEIXIN_OAUTH = getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False)
|
||||
WORK_WEIXIN_UID_PREFIX = WORK_WEIXIN_CORP_ID + '_'
|
||||
AUTO_UPDATE_WORK_WEIXIN_USER_INFO = getattr(settings, 'AUTO_UPDATE_WORK_WEIXIN_USER_INFO', False)
|
||||
WORK_WEIXIN_USER_INFO_AUTO_UPDATE = getattr(settings, 'WORK_WEIXIN_USER_INFO_AUTO_UPDATE', True)
|
||||
WORK_WEIXIN_AUTHORIZATION_URL = getattr(settings, 'WORK_WEIXIN_AUTHORIZATION_URL',
|
||||
'https://open.work.weixin.qq.com/wwopen/sso/qrConnect')
|
||||
WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL',
|
||||
@ -27,6 +27,11 @@ WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL
|
||||
WORK_WEIXIN_GET_USER_PROFILE_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_PROFILE_URL',
|
||||
'https://qyapi.weixin.qq.com/cgi-bin/user/get')
|
||||
|
||||
# # work weixin notifications
|
||||
ENABLE_WORK_WEIXIN_NOTIFICATIONS = getattr(settings, 'ENABLE_WORK_WEIXIN_NOTIFICATIONS', False)
|
||||
WORK_WEIXIN_NOTIFICATIONS_URL = getattr(settings, 'WORK_WEIXIN_NOTIFICATIONS_URL',
|
||||
'https://qyapi.weixin.qq.com/cgi-bin/message/send')
|
||||
|
||||
# # constants
|
||||
|
||||
WORK_WEIXIN_PROVIDER = 'work-weixin'
|
||||
|
@ -11,7 +11,8 @@ from seahub.work_weixin.settings import WORK_WEIXIN_CORP_ID, WORK_WEIXIN_AGENT_S
|
||||
WORK_WEIXIN_ACCESS_TOKEN_URL, ENABLE_WORK_WEIXIN_DEPARTMENTS, \
|
||||
WORK_WEIXIN_DEPARTMENTS_URL, WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, \
|
||||
ENABLE_WORK_WEIXIN_OAUTH, WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_AUTHORIZATION_URL, \
|
||||
WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL
|
||||
WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, \
|
||||
ENABLE_WORK_WEIXIN_NOTIFICATIONS, WORK_WEIXIN_NOTIFICATIONS_URL
|
||||
from seahub.profile.models import Profile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -23,6 +24,8 @@ WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY = 'WORK_WEIXIN_ACCESS_TOKEN'
|
||||
|
||||
|
||||
def get_work_weixin_access_token():
|
||||
""" get global work weixin access_token
|
||||
"""
|
||||
cache_key = normalize_cache_key(WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY)
|
||||
access_token = cache.get(cache_key, None)
|
||||
|
||||
@ -45,6 +48,8 @@ def get_work_weixin_access_token():
|
||||
|
||||
|
||||
def handler_work_weixin_api_response(response):
|
||||
""" handler work_weixin response and errcode
|
||||
"""
|
||||
try:
|
||||
response = response.json()
|
||||
except ValueError:
|
||||
@ -59,6 +64,8 @@ def handler_work_weixin_api_response(response):
|
||||
|
||||
|
||||
def work_weixin_base_check():
|
||||
""" work weixin base check
|
||||
"""
|
||||
if not WORK_WEIXIN_CORP_ID or not WORK_WEIXIN_AGENT_SECRET or not WORK_WEIXIN_ACCESS_TOKEN_URL:
|
||||
logger.error('work weixin base relevant settings invalid.')
|
||||
logger.error('WORK_WEIXIN_CORP_ID: %s' % WORK_WEIXIN_CORP_ID)
|
||||
@ -69,13 +76,14 @@ def work_weixin_base_check():
|
||||
|
||||
|
||||
def work_weixin_oauth_check():
|
||||
if not work_weixin_base_check():
|
||||
return False
|
||||
|
||||
""" use for work weixin login and profile bind
|
||||
"""
|
||||
if not ENABLE_WORK_WEIXIN_OAUTH:
|
||||
logger.error('work weixin oauth not enabled.')
|
||||
return False
|
||||
else:
|
||||
if not work_weixin_base_check():
|
||||
return False
|
||||
|
||||
if not WORK_WEIXIN_AGENT_ID \
|
||||
or not WORK_WEIXIN_GET_USER_INFO_URL \
|
||||
or not WORK_WEIXIN_AUTHORIZATION_URL \
|
||||
@ -91,13 +99,14 @@ def work_weixin_oauth_check():
|
||||
|
||||
|
||||
def admin_work_weixin_departments_check():
|
||||
if not work_weixin_base_check():
|
||||
return False
|
||||
|
||||
""" use for admin work weixin departments
|
||||
"""
|
||||
if not ENABLE_WORK_WEIXIN_DEPARTMENTS:
|
||||
logger.error('admin work weixin departments not enabled.')
|
||||
return False
|
||||
else:
|
||||
if not work_weixin_base_check():
|
||||
return False
|
||||
|
||||
if not WORK_WEIXIN_DEPARTMENTS_URL \
|
||||
or not WORK_WEIXIN_DEPARTMENT_MEMBERS_URL:
|
||||
logger.error('admin work weixin departments relevant settings invalid.')
|
||||
@ -108,12 +117,48 @@ def admin_work_weixin_departments_check():
|
||||
return True
|
||||
|
||||
|
||||
def work_weixin_notifications_check():
|
||||
""" use for send work weixin notifications
|
||||
"""
|
||||
if not ENABLE_WORK_WEIXIN_NOTIFICATIONS:
|
||||
return False
|
||||
else:
|
||||
if not work_weixin_base_check():
|
||||
return False
|
||||
|
||||
if not WORK_WEIXIN_AGENT_ID \
|
||||
or not WORK_WEIXIN_NOTIFICATIONS_URL:
|
||||
logger.error('work weixin notifications relevant settings invalid.')
|
||||
logger.error('WORK_WEIXIN_AGENT_ID: %s' % WORK_WEIXIN_AGENT_ID)
|
||||
logger.error('WORK_WEIXIN_NOTIFICATIONS_URL: %s' % WORK_WEIXIN_NOTIFICATIONS_URL)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_work_weixin_user_info(api_user):
|
||||
email = api_user.get('username')
|
||||
try:
|
||||
# update additional user info
|
||||
nickname = api_user.get("name", None)
|
||||
if nickname is not None:
|
||||
Profile.objects.add_or_update(email, nickname)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
""" update user profile from work weixin
|
||||
|
||||
use for work weixin departments, login, profile bind
|
||||
"""
|
||||
# update additional user info
|
||||
username = api_user.get('username')
|
||||
nickname = api_user.get('name')
|
||||
contact_email = api_user.get('contact_email')
|
||||
|
||||
# make sure the contact_email is unique
|
||||
if contact_email and Profile.objects.get_profile_by_contact_email(contact_email):
|
||||
logger.warning('contact email %s already exists' % contact_email)
|
||||
contact_email = ''
|
||||
|
||||
profile_kwargs = {}
|
||||
if nickname:
|
||||
profile_kwargs['nickname'] = nickname
|
||||
if contact_email:
|
||||
profile_kwargs['contact_email'] = contact_email
|
||||
|
||||
if profile_kwargs:
|
||||
try:
|
||||
Profile.objects.add_or_update(username, **profile_kwargs)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
@ -17,7 +17,7 @@ from seahub.base.accounts import User
|
||||
from seahub.work_weixin.settings import WORK_WEIXIN_AUTHORIZATION_URL, WORK_WEIXIN_CORP_ID, \
|
||||
WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_PROVIDER, \
|
||||
WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, WORK_WEIXIN_UID_PREFIX, \
|
||||
AUTO_UPDATE_WORK_WEIXIN_USER_INFO
|
||||
WORK_WEIXIN_USER_INFO_AUTO_UPDATE
|
||||
from seahub.work_weixin.utils import work_weixin_oauth_check, get_work_weixin_access_token, \
|
||||
handler_work_weixin_api_response, update_work_weixin_user_info
|
||||
from seahub.utils.auth import gen_user_virtual_id, VIRTUAL_ID_EMAIL_DOMAIN
|
||||
@ -100,8 +100,8 @@ def work_weixin_oauth_callback(request):
|
||||
return render_error(
|
||||
request, _('Error, new user registration is not allowed, please contact administrator.'))
|
||||
|
||||
if is_new_user or AUTO_UPDATE_WORK_WEIXIN_USER_INFO:
|
||||
# update user info
|
||||
# update user info
|
||||
if is_new_user or WORK_WEIXIN_USER_INFO_AUTO_UPDATE:
|
||||
user_info_data = {
|
||||
'access_token': access_token,
|
||||
'userid': user_id,
|
||||
@ -193,8 +193,8 @@ def work_weixin_oauth_connect_callback(request):
|
||||
|
||||
SocialAuthUser.objects.add(email, WORK_WEIXIN_PROVIDER, uid)
|
||||
|
||||
if AUTO_UPDATE_WORK_WEIXIN_USER_INFO:
|
||||
# update user info
|
||||
# update user info
|
||||
if WORK_WEIXIN_USER_INFO_AUTO_UPDATE:
|
||||
user_info_data = {
|
||||
'access_token': access_token,
|
||||
'userid': user_id,
|
||||
|
111
tests/api/endpoints/test_repo_commit_revert.py
Normal file
111
tests/api/endpoints/test_repo_commit_revert.py
Normal file
@ -0,0 +1,111 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from seaserv import seafile_api
|
||||
from seahub.test_utils import BaseTestCase
|
||||
|
||||
|
||||
class RepoCommitRevertTest(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user_name = self.user.username
|
||||
self.admin_name = self.admin.username
|
||||
|
||||
self.repo_id = self.repo.id
|
||||
self.repo_name = self.repo.repo_name
|
||||
|
||||
self.file_path = self.file
|
||||
self.file_name = os.path.basename(self.file_path)
|
||||
|
||||
self.enc_repo_id = self.enc_repo.id
|
||||
self.enc_repo_name = self.enc_repo
|
||||
|
||||
self.folder_path = self.folder
|
||||
self.folder_name = os.path.basename(self.folder.rstrip('/'))
|
||||
|
||||
def tearDown(self):
|
||||
self.remove_repo()
|
||||
self.remove_group()
|
||||
|
||||
def test_post(self):
|
||||
# delete a file first
|
||||
seafile_api.del_file(self.repo_id, '/', self.file_name, self.user_name)
|
||||
|
||||
self.login_as(self.user)
|
||||
|
||||
# get commit id form trash
|
||||
trash_url = reverse('api-v2.1-repo-trash', args=[self.repo_id])
|
||||
trash_resp = self.client.get(trash_url)
|
||||
self.assertEqual(200, trash_resp.status_code)
|
||||
trash_json_resp = json.loads(trash_resp.content)
|
||||
assert trash_json_resp['data'][0]['obj_name'] == self.file_name
|
||||
assert not trash_json_resp['data'][0]['is_dir']
|
||||
assert trash_json_resp['data'][0]['commit_id']
|
||||
commit_id = trash_json_resp['data'][0]['commit_id']
|
||||
|
||||
dir_url = reverse('api-v2.1-dir-view', args=[self.repo_id])
|
||||
url = reverse('api-v2.1-repo-commit-revert', args=[self.repo_id, commit_id])
|
||||
|
||||
# test can post
|
||||
resp = self.client.get(dir_url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
json_resp = json.loads(resp.content)
|
||||
assert len(json_resp['dirent_list']) == 1
|
||||
|
||||
resp = self.client.post(url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
resp = self.client.get(dir_url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
json_resp = json.loads(resp.content)
|
||||
assert len(json_resp['dirent_list']) == 2
|
||||
|
||||
# test_can_not_post_with_invalid_repo_permission
|
||||
self.logout()
|
||||
self.login_as(self.admin)
|
||||
|
||||
resp = self.client.post(url)
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
def test_enc_repo_post(self):
|
||||
# delete a file first
|
||||
seafile_api.del_file(self.enc_repo_id, '/', self.file_name, self.user_name)
|
||||
|
||||
self.login_as(self.user)
|
||||
|
||||
# get commit id form trash
|
||||
trash_url = reverse('api-v2.1-repo-trash', args=[self.enc_repo_id])
|
||||
trash_resp = self.client.get(trash_url)
|
||||
self.assertEqual(200, trash_resp.status_code)
|
||||
trash_json_resp = json.loads(trash_resp.content)
|
||||
assert trash_json_resp['data'][0]['obj_name'] == self.file_name
|
||||
assert not trash_json_resp['data'][0]['is_dir']
|
||||
assert trash_json_resp['data'][0]['commit_id']
|
||||
commit_id = trash_json_resp['data'][0]['commit_id']
|
||||
|
||||
dir_url = reverse('api-v2.1-dir-view', args=[self.enc_repo_id])
|
||||
url = reverse('api-v2.1-repo-commit-revert', args=[self.enc_repo_id, commit_id])
|
||||
|
||||
# test can not post without repo decrypted
|
||||
resp = self.client.post(url)
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
resp = self.client.get(dir_url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
json_resp = json.loads(resp.content)
|
||||
assert len(json_resp['dirent_list']) == 0
|
||||
|
||||
# test can post with repo decrypted
|
||||
decrypted_url = reverse('api-v2.1-repo-set-password', args=[self.enc_repo_id])
|
||||
resp = self.client.post(decrypted_url, data={'password': '123'})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
resp = self.client.post(url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
resp = self.client.get(dir_url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
json_resp = json.loads(resp.content)
|
||||
assert len(json_resp['dirent_list']) == 1
|
@ -994,6 +994,36 @@ class ReposAsyncBatchMoveItemView(BaseTestCase):
|
||||
resp = self.client.post(self.url, json.dumps(data), 'application/json')
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
def test_move_with_locked_file(self):
|
||||
|
||||
if not LOCAL_PRO_DEV_ENV:
|
||||
return
|
||||
|
||||
self.login_as(self.user)
|
||||
|
||||
# share admin's tmp repo to user with 'r' permission
|
||||
admin_repo_id = self.create_new_repo(self.admin_name)
|
||||
seafile_api.share_repo(admin_repo_id, self.admin_name,
|
||||
self.user_name, 'rw')
|
||||
|
||||
# admin lock file
|
||||
admin_file_name = randstring(6)
|
||||
seafile_api.post_empty_file(admin_repo_id, '/', admin_file_name,
|
||||
self.admin_name)
|
||||
seafile_api.lock_file(admin_repo_id, admin_file_name, self.admin_name, 0)
|
||||
|
||||
# user move locked file
|
||||
data = {
|
||||
"src_repo_id": admin_repo_id,
|
||||
"src_parent_dir": '/',
|
||||
"src_dirents":[admin_file_name],
|
||||
"dst_repo_id": self.dst_repo_id,
|
||||
"dst_parent_dir": '/',
|
||||
}
|
||||
resp = self.client.post(self.url, json.dumps(data), 'application/json')
|
||||
self.assertEqual(403, resp.status_code)
|
||||
json_resp = json.loads(resp.content)
|
||||
assert json_resp['error_msg'] == 'File %s is locked.' % admin_file_name
|
||||
|
||||
class ReposSyncBatchCopyItemView(BaseTestCase):
|
||||
|
||||
@ -1539,3 +1569,34 @@ class ReposSyncBatchMoveItemView(BaseTestCase):
|
||||
}
|
||||
resp = self.client.post(self.url, json.dumps(data), 'application/json')
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
def test_move_with_locked_file(self):
|
||||
|
||||
if not LOCAL_PRO_DEV_ENV:
|
||||
return
|
||||
|
||||
self.login_as(self.user)
|
||||
|
||||
# share admin's tmp repo to user with 'r' permission
|
||||
admin_repo_id = self.create_new_repo(self.admin_name)
|
||||
seafile_api.share_repo(admin_repo_id, self.admin_name,
|
||||
self.user_name, 'rw')
|
||||
|
||||
# admin lock file
|
||||
admin_file_name = randstring(6)
|
||||
seafile_api.post_empty_file(admin_repo_id, '/', admin_file_name,
|
||||
self.admin_name)
|
||||
seafile_api.lock_file(admin_repo_id, admin_file_name, self.admin_name, 0)
|
||||
|
||||
# user move locked file
|
||||
data = {
|
||||
"src_repo_id": admin_repo_id,
|
||||
"src_parent_dir": '/',
|
||||
"src_dirents":[admin_file_name],
|
||||
"dst_repo_id": self.dst_repo_id,
|
||||
"dst_parent_dir": '/',
|
||||
}
|
||||
resp = self.client.post(self.url, json.dumps(data), 'application/json')
|
||||
self.assertEqual(403, resp.status_code)
|
||||
json_resp = json.loads(resp.content)
|
||||
assert json_resp['error_msg'] == 'File %s is locked.' % admin_file_name
|
||||
|
Loading…
Reference in New Issue
Block a user