1
0
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:
lian 2019-06-19 12:11:40 +08:00
commit 26f70c61d9
33 changed files with 1160 additions and 55 deletions

View File

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

View File

@ -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"],

View File

@ -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",

View File

@ -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",

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = () => {

View File

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

View 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%;
}

View File

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

View 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')
);

View File

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

View File

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

View File

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

View 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})

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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