mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-01 23:38:37 +00:00
Repo invitation (#4264)
* add fake url * add SharedRepoInvitation model * add indexes * rm indexes * update Manager * add SharedRepoInvitations * add is_repo_admin * add try * fix path * update * add SharedRepoInvitation * add share in signup * rm InvitePeople icon * add inviter_name * add share invite dialog * add shared_queryset.delete * fix spell * rename to RepoShare * rename to RepoShare in py * add handleKeyDown * update error msg * add unittest * fix * add logger
This commit is contained in:
parent
4e1d766f74
commit
106f29be06
@ -1,9 +1,10 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
|
import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
|
||||||
import { gettext, username, canGenerateShareLink, canGenerateUploadLink } from '../../utils/constants';
|
import { gettext, username, canGenerateShareLink, canGenerateUploadLink, canInvitePeople } from '../../utils/constants';
|
||||||
import ShareToUser from './share-to-user';
|
import ShareToUser from './share-to-user';
|
||||||
import ShareToGroup from './share-to-group';
|
import ShareToGroup from './share-to-group';
|
||||||
|
import ShareToInvitePeople from './share-to-invite-people';
|
||||||
import GenerateShareLink from './generate-share-link';
|
import GenerateShareLink from './generate-share-link';
|
||||||
import GenerateUploadLink from './generate-upload-link';
|
import GenerateUploadLink from './generate-upload-link';
|
||||||
import InternalLink from './internal-link';
|
import InternalLink from './internal-link';
|
||||||
@ -117,6 +118,13 @@ class ShareDialog extends React.Component {
|
|||||||
{gettext('Share to group')}
|
{gettext('Share to group')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
{canInvitePeople &&
|
||||||
|
<NavItem>
|
||||||
|
<NavLink className={activeTab === 'invitePeople' ? 'active' : ''} onClick={this.toggle.bind(this, 'invitePeople')}>
|
||||||
|
{gettext('Invite People')}
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
</Nav>
|
</Nav>
|
||||||
@ -154,6 +162,11 @@ class ShareDialog extends React.Component {
|
|||||||
<TabPane tabId="shareToUser">
|
<TabPane tabId="shareToUser">
|
||||||
<ShareToUser itemType={this.props.itemType} isGroupOwnedRepo={this.props.isGroupOwnedRepo} itemPath={this.props.itemPath} repoID={this.props.repoID} isRepoOwner={this.state.isRepoOwner}/>
|
<ShareToUser itemType={this.props.itemType} isGroupOwnedRepo={this.props.isGroupOwnedRepo} itemPath={this.props.itemPath} repoID={this.props.repoID} isRepoOwner={this.state.isRepoOwner}/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
{canInvitePeople &&
|
||||||
|
<TabPane tabId="invitePeople">
|
||||||
|
<ShareToInvitePeople itemPath={this.props.itemPath} repoID={this.props.repoID}/>
|
||||||
|
</TabPane>
|
||||||
|
}
|
||||||
<TabPane tabId="shareToGroup">
|
<TabPane tabId="shareToGroup">
|
||||||
<ShareToGroup itemType={this.props.itemType} isGroupOwnedRepo={this.props.isGroupOwnedRepo} itemPath={this.props.itemPath} repoID={this.props.repoID} isRepoOwner={this.state.isRepoOwner}/>
|
<ShareToGroup itemType={this.props.itemType} isGroupOwnedRepo={this.props.isGroupOwnedRepo} itemPath={this.props.itemPath} repoID={this.props.repoID} isRepoOwner={this.state.isRepoOwner}/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
318
frontend/src/components/dialog/share-to-invite-people.js
Normal file
318
frontend/src/components/dialog/share-to-invite-people.js
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Button, Input } from 'reactstrap';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api.js';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import toaster from '../toast';
|
||||||
|
import Loading from '../loading';
|
||||||
|
import SharePermissionEditor from '../select-editor/share-permission-editor';
|
||||||
|
import '../../css/invitations.css';
|
||||||
|
|
||||||
|
class UserItem extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isOperationShow: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter = () => {
|
||||||
|
this.setState({ isOperationShow: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave = () => {
|
||||||
|
this.setState({ isOperationShow: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteShareItem = () => {
|
||||||
|
let item = this.props.item;
|
||||||
|
this.props.deleteShareItem(item.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeUserPermission = (permission) => {
|
||||||
|
let item = this.props.item;
|
||||||
|
this.props.onChangeUserPermission(item.token, permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let item = this.props.item;
|
||||||
|
let currentPermission = item.is_admin ? 'admin' : item.permission;
|
||||||
|
return (
|
||||||
|
<tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||||
|
<td className="name">{item.accepter}</td>
|
||||||
|
<td>
|
||||||
|
<SharePermissionEditor
|
||||||
|
isTextMode={true}
|
||||||
|
isEditIconShow={this.state.isOperationShow}
|
||||||
|
currentPermission={currentPermission}
|
||||||
|
permissions={this.props.permissions}
|
||||||
|
onPermissionChanged={this.onChangeUserPermission}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{moment(item.expire_time).format('YYYY-MM-DD')}</td>
|
||||||
|
<td className="name">{item.inviter_name}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'hide'}`}
|
||||||
|
onClick={this.deleteShareItem}
|
||||||
|
title={gettext('Delete')}
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserList extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let items = this.props.items;
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<UserItem
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
permissions={this.props.permissions}
|
||||||
|
deleteShareItem={this.props.deleteShareItem}
|
||||||
|
onChangeUserPermission={this.props.onChangeUserPermission}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
itemPath: PropTypes.string.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ShareToInvitePeople extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
errorMsg: '',
|
||||||
|
permission: 'r',
|
||||||
|
sharedItems: [],
|
||||||
|
emails: '',
|
||||||
|
isSubmitting: false,
|
||||||
|
};
|
||||||
|
this.permissions = ['rw', 'r'];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = (e) => {
|
||||||
|
let emails = e.target.value;
|
||||||
|
this.setState({
|
||||||
|
emails: emails,
|
||||||
|
});
|
||||||
|
if (this.state.errorMsg) {
|
||||||
|
this.setState({
|
||||||
|
errorMsg: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown = (e) => {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.shareAndInvite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const path = this.props.itemPath;
|
||||||
|
const repoID = this.props.repoID;
|
||||||
|
seafileAPI.listRepoShareInvitations(repoID, path).then((res) => {
|
||||||
|
if (res.data.length !== 0) {
|
||||||
|
this.setState({ sharedItems: res.data.repo_share_invitation_list });
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
const errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setPermission = (permission) => {
|
||||||
|
this.setState({ permission: permission });
|
||||||
|
}
|
||||||
|
|
||||||
|
onInvitePeople = (successArray) => {
|
||||||
|
successArray.push.apply(successArray, this.state.sharedItems);
|
||||||
|
this.setState({
|
||||||
|
sharedItems: successArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shareAndInvite = () => {
|
||||||
|
let emails = this.state.emails.trim();
|
||||||
|
if (!emails) {
|
||||||
|
this.setState({ errorMsg: gettext('It is required.') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let emailsArray = [];
|
||||||
|
emails = emails.split(',');
|
||||||
|
for (let i = 0, len = emails.length; i < len; i++) {
|
||||||
|
let email = emails[i].trim();
|
||||||
|
if (email) {
|
||||||
|
emailsArray.push(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emailsArray.length) {
|
||||||
|
this.setState({ errorMsg: gettext('Email is invalid.') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ isSubmitting: true });
|
||||||
|
|
||||||
|
const path = this.props.itemPath;
|
||||||
|
const repoID = this.props.repoID;
|
||||||
|
const permission = this.state.permission;
|
||||||
|
|
||||||
|
seafileAPI.addRepoShareInvitations(repoID, path, emailsArray, permission).then((res) => {
|
||||||
|
const success = res.data.success;
|
||||||
|
if (success.length) {
|
||||||
|
let successMsg = '';
|
||||||
|
if (success.length == 1) {
|
||||||
|
successMsg = gettext('Successfully invited %(email).')
|
||||||
|
.replace('%(email)', success[0].accepter);
|
||||||
|
} else {
|
||||||
|
successMsg = gettext('Successfully invited %(email) and %(num) other people.')
|
||||||
|
.replace('%(email)', success[0].accepter)
|
||||||
|
.replace('%(num)', success.length - 1);
|
||||||
|
}
|
||||||
|
toaster.success(successMsg);
|
||||||
|
this.onInvitePeople(success);
|
||||||
|
}
|
||||||
|
const failed = res.data.failed;
|
||||||
|
if (failed.length) {
|
||||||
|
for (let i = 0, len = failed.length; i < len; i++) {
|
||||||
|
let failedMsg = failed[i].email + ': ' + failed[i].error_msg;
|
||||||
|
toaster.danger(failedMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ isSubmitting: false });
|
||||||
|
}).catch((error) => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
this.setState({ isSubmitting: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteShareItem = (token) => {
|
||||||
|
const path = this.props.itemPath;
|
||||||
|
const repoID = this.props.repoID;
|
||||||
|
|
||||||
|
seafileAPI.deleteRepoShareInvitation(repoID, path, token).then(res => {
|
||||||
|
this.setState({
|
||||||
|
sharedItems: this.state.sharedItems.filter(item => { return item.token !== token; })
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
const errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeUserPermission = (token, permission) => {
|
||||||
|
const path = this.props.itemPath;
|
||||||
|
const repoID = this.props.repoID;
|
||||||
|
|
||||||
|
seafileAPI.updateRepoShareInvitation(repoID, path, token, permission).then(() => {
|
||||||
|
this.updateSharedItems(token, permission);
|
||||||
|
}).catch(error => {
|
||||||
|
const errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSharedItems = (token, permission) => {
|
||||||
|
let sharedItems = this.state.sharedItems.map(sharedItem => {
|
||||||
|
if (sharedItem.token === token) {
|
||||||
|
sharedItem.permission = permission;
|
||||||
|
}
|
||||||
|
return sharedItem;
|
||||||
|
});
|
||||||
|
this.setState({ sharedItems: sharedItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { sharedItems, isSubmitting } = this.state;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<table className="table-thead-hidden">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50%">{gettext('Invite People')}</th>
|
||||||
|
<th width="35%">{gettext('Permission')}</th>
|
||||||
|
<th width="15%">{''}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="emails"
|
||||||
|
placeholder={gettext('Emails, separated by \',\'')}
|
||||||
|
value={this.state.emails}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<SharePermissionEditor
|
||||||
|
isTextMode={false}
|
||||||
|
isEditIconShow={false}
|
||||||
|
currentPermission={this.state.permission}
|
||||||
|
permissions={this.permissions}
|
||||||
|
onPermissionChanged={this.setPermission}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Button onClick={this.shareAndInvite} disabled={isSubmitting}
|
||||||
|
>{isSubmitting ? <Loading /> : gettext('Submit')}</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{this.state.errorMsg.length > 0 &&
|
||||||
|
<tr key={'error'}>
|
||||||
|
<td colSpan={3}><p className="error">{this.state.errorMsg}</p></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="share-list-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="25%">{gettext('Email')}</th>
|
||||||
|
<th width="20%">{gettext('Permission')}</th>
|
||||||
|
<th width="20%">{gettext('Expiration')}</th>
|
||||||
|
<th width="20%">{gettext('Inviter')}</th>
|
||||||
|
<th width="15%">{''}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<UserList
|
||||||
|
items={sharedItems}
|
||||||
|
permissions={this.permissions}
|
||||||
|
deleteShareItem={this.deleteShareItem}
|
||||||
|
onChangeUserPermission={this.onChangeUserPermission}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareToInvitePeople.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ShareToInvitePeople;
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {gettext, isPro, canInvitePeople, siteRoot} from '../../utils/constants';
|
import {gettext, isPro, siteRoot} from '../../utils/constants';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { seafileAPI } from '../../utils/seafile-api.js';
|
import { seafileAPI } from '../../utils/seafile-api.js';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
@ -339,12 +339,6 @@ class ShareToUser extends React.Component {
|
|||||||
onChangeUserPermission={this.onChangeUserPermission}
|
onChangeUserPermission={this.onChangeUserPermission}
|
||||||
/>
|
/>
|
||||||
</table>
|
</table>
|
||||||
{canInvitePeople &&
|
|
||||||
<a href={siteRoot + 'invitations/'} className="invite-link-in-popup">
|
|
||||||
<i className="sf2-icon-invite invite-link-icon-in-popup"></i>
|
|
||||||
<span className="invite-link-icon-in-popup">{gettext('Invite People')}</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==1.11.23
|
Django==1.11.25
|
||||||
future
|
future
|
||||||
captcha
|
captcha
|
||||||
django-compressor
|
django-compressor
|
||||||
|
@ -16,6 +16,7 @@ from seahub.base.accounts import User
|
|||||||
from seahub.utils import is_valid_email
|
from seahub.utils import is_valid_email
|
||||||
from seahub.invitations.models import Invitation
|
from seahub.invitations.models import Invitation
|
||||||
from seahub.invitations.utils import block_accepter
|
from seahub.invitations.utils import block_accepter
|
||||||
|
from seahub.constants import GUEST_USER
|
||||||
|
|
||||||
json_content_type = 'application/json; charset=utf-8'
|
json_content_type = 'application/json; charset=utf-8'
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ class InvitationsView(APIView):
|
|||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
# Send invitation.
|
# Send invitation.
|
||||||
itype = request.data.get('type', '').lower()
|
itype = request.data.get('type', '').lower()
|
||||||
if not itype or itype != 'guest':
|
if not itype or itype != GUEST_USER:
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, 'type invalid.')
|
return api_error(status.HTTP_400_BAD_REQUEST, 'type invalid.')
|
||||||
|
|
||||||
accepter = request.data.get('accepter', '').lower()
|
accepter = request.data.get('accepter', '').lower()
|
||||||
@ -139,13 +140,13 @@ class InvitationsBatchView(APIView):
|
|||||||
|
|
||||||
i = Invitation.objects.add(inviter=request.user.username,
|
i = Invitation.objects.add(inviter=request.user.username,
|
||||||
accepter=accepter)
|
accepter=accepter)
|
||||||
|
result['success'].append(i.to_dict())
|
||||||
|
|
||||||
m = i.send_to(email=accepter)
|
m = i.send_to(email=accepter)
|
||||||
if m.status == STATUS.sent:
|
if m.status != STATUS.sent:
|
||||||
result['success'].append(i.to_dict())
|
|
||||||
else:
|
|
||||||
result['failed'].append({
|
result['failed'].append({
|
||||||
'email': accepter,
|
'email': accepter,
|
||||||
'error_msg': _('Internal Server Error'),
|
'error_msg': _('Failed to send email, email service is not properly configured, please contact administrator.'),
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(result)
|
return Response(result)
|
||||||
|
134
seahub/api2/endpoints/repo_share_invitation.py
Normal file
134
seahub/api2/endpoints/repo_share_invitation.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Copyright (c) 2012-2019 Seafile Ltd.
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
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 django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from seaserv import seafile_api
|
||||||
|
|
||||||
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
|
from seahub.api2.permissions import CanInviteGuest
|
||||||
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
|
from seahub.api2.utils import api_error
|
||||||
|
from seahub.invitations.models import Invitation, RepoShareInvitation
|
||||||
|
from post_office.models import STATUS
|
||||||
|
from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE
|
||||||
|
from seahub.share.utils import is_repo_admin
|
||||||
|
from seahub.utils import is_org_context
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
json_content_type = 'application/json; charset=utf-8'
|
||||||
|
|
||||||
|
|
||||||
|
class RepoShareInvitationView(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated, CanInviteGuest)
|
||||||
|
throttle_classes = (UserRateThrottle, )
|
||||||
|
|
||||||
|
def put(self, request, repo_id, format=None):
|
||||||
|
""" Update permission in repo share invitation.
|
||||||
|
"""
|
||||||
|
# argument check
|
||||||
|
path = request.data.get('path', None)
|
||||||
|
if not path:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.')
|
||||||
|
|
||||||
|
token = request.data.get('token', None)
|
||||||
|
if not token:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'token invalid.')
|
||||||
|
|
||||||
|
permission = request.data.get('permission', PERMISSION_READ)
|
||||||
|
if permission not in (PERMISSION_READ, PERMISSION_READ_WRITE):
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'permission invalid.')
|
||||||
|
|
||||||
|
# recourse check
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)
|
||||||
|
|
||||||
|
if seafile_api.get_dir_id_by_path(repo.id, path) is None:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path)
|
||||||
|
|
||||||
|
# permission check
|
||||||
|
username = request.user.username
|
||||||
|
if is_org_context(request):
|
||||||
|
repo_owner = seafile_api.get_org_repo_owner(repo_id)
|
||||||
|
else:
|
||||||
|
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||||
|
|
||||||
|
if username != repo_owner and not is_repo_admin(username, repo_id):
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
# mian
|
||||||
|
try:
|
||||||
|
shared_obj = RepoShareInvitation.objects.get_by_token_and_path(
|
||||||
|
token=token, repo_id=repo_id, path=path
|
||||||
|
)
|
||||||
|
if not shared_obj:
|
||||||
|
error_msg = 'repo share invitation not found.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
if shared_obj.permission == permission:
|
||||||
|
error_msg = 'repo share invitation already has %s premission.' % permission
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
shared_obj.permission = permission
|
||||||
|
shared_obj.save()
|
||||||
|
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})
|
||||||
|
|
||||||
|
def delete(self, request, repo_id, format=None):
|
||||||
|
""" Delete repo share invitation.
|
||||||
|
"""
|
||||||
|
# argument check
|
||||||
|
path = request.data.get('path', None)
|
||||||
|
if not path:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.')
|
||||||
|
|
||||||
|
token = request.data.get('token', None)
|
||||||
|
if not token:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'token invalid.')
|
||||||
|
|
||||||
|
# recourse check
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)
|
||||||
|
|
||||||
|
if seafile_api.get_dir_id_by_path(repo.id, path) is None:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path)
|
||||||
|
|
||||||
|
# permission check
|
||||||
|
username = request.user.username
|
||||||
|
if is_org_context(request):
|
||||||
|
repo_owner = seafile_api.get_org_repo_owner(repo_id)
|
||||||
|
else:
|
||||||
|
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||||
|
|
||||||
|
if username != repo_owner and not is_repo_admin(username, repo_id):
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
# mian
|
||||||
|
try:
|
||||||
|
shared_obj = RepoShareInvitation.objects.get_by_token_and_path(
|
||||||
|
token=token, repo_id=repo_id, path=path
|
||||||
|
)
|
||||||
|
if not shared_obj:
|
||||||
|
error_msg = 'repo share invitation not found.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
shared_obj.delete()
|
||||||
|
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})
|
213
seahub/api2/endpoints/repo_share_invitations.py
Normal file
213
seahub/api2/endpoints/repo_share_invitations.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
# Copyright (c) 2012-2019 Seafile Ltd.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from rest_framework import status
|
||||||
|
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 post_office.models import STATUS
|
||||||
|
|
||||||
|
from seaserv import seafile_api
|
||||||
|
|
||||||
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
|
from seahub.api2.permissions import CanInviteGuest
|
||||||
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
|
from seahub.api2.utils import api_error
|
||||||
|
from seahub.base.accounts import User
|
||||||
|
from seahub.utils import is_valid_email
|
||||||
|
from seahub.invitations.models import Invitation, RepoShareInvitation
|
||||||
|
from seahub.invitations.utils import block_accepter
|
||||||
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||||
|
from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE, GUEST_USER
|
||||||
|
from seahub.share.utils import is_repo_admin
|
||||||
|
from seahub.utils import is_org_context
|
||||||
|
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||||
|
|
||||||
|
json_content_type = 'application/json; charset=utf-8'
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class RepoShareInvitationsView(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated, CanInviteGuest)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request, repo_id, format=None):
|
||||||
|
""" List repo share invitations.
|
||||||
|
"""
|
||||||
|
# argument check
|
||||||
|
path = request.GET.get('path', None)
|
||||||
|
if not path:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.')
|
||||||
|
|
||||||
|
# recourse check
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if seafile_api.get_dir_id_by_path(repo.id, path) is None:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path)
|
||||||
|
|
||||||
|
# permission check
|
||||||
|
username = request.user.username
|
||||||
|
if is_org_context(request):
|
||||||
|
repo_owner = seafile_api.get_org_repo_owner(repo_id)
|
||||||
|
else:
|
||||||
|
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||||
|
|
||||||
|
if username != repo_owner and not is_repo_admin(username, repo_id):
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
# main
|
||||||
|
shared_list = list()
|
||||||
|
try:
|
||||||
|
shared_queryset = RepoShareInvitation.objects.list_by_repo_id_and_path(repo_id, path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
for obj in shared_queryset:
|
||||||
|
data = obj.invitation.to_dict()
|
||||||
|
data['permission'] = obj.permission
|
||||||
|
data['inviter_name'] = email2nickname(obj.invitation.inviter)
|
||||||
|
|
||||||
|
shared_list.append(data)
|
||||||
|
|
||||||
|
return Response({'repo_share_invitation_list': shared_list})
|
||||||
|
|
||||||
|
|
||||||
|
class RepoShareInvitationsBatchView(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated, CanInviteGuest)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request, repo_id, format=None):
|
||||||
|
""" Batch add repo share invitations.
|
||||||
|
"""
|
||||||
|
# argument check
|
||||||
|
path = request.data.get('path', None)
|
||||||
|
if not path:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.')
|
||||||
|
|
||||||
|
itype = request.data.get('type', '').lower()
|
||||||
|
if not itype or itype != GUEST_USER:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'type invalid.')
|
||||||
|
|
||||||
|
accepters = request.data.get('accepters', None)
|
||||||
|
if not accepters or not isinstance(accepters, list):
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'accepters invalid.')
|
||||||
|
|
||||||
|
permission = request.data.get('permission', PERMISSION_READ)
|
||||||
|
if permission not in (PERMISSION_READ, PERMISSION_READ_WRITE):
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'permission invalid.')
|
||||||
|
|
||||||
|
# recourse check
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)
|
||||||
|
|
||||||
|
if seafile_api.get_dir_id_by_path(repo.id, path) is None:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path)
|
||||||
|
|
||||||
|
# permission check
|
||||||
|
username = request.user.username
|
||||||
|
if is_org_context(request):
|
||||||
|
repo_owner = seafile_api.get_org_repo_owner(repo_id)
|
||||||
|
else:
|
||||||
|
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||||
|
|
||||||
|
if username != repo_owner and not is_repo_admin(username, repo_id):
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
# main
|
||||||
|
result = {}
|
||||||
|
result['failed'] = []
|
||||||
|
result['success'] = []
|
||||||
|
inviter_name = email2nickname(request.user.username)
|
||||||
|
|
||||||
|
try:
|
||||||
|
invitation_queryset = Invitation.objects.order_by('-invite_time').filter(
|
||||||
|
inviter=request.user.username, accept_time=None)
|
||||||
|
shared_queryset = RepoShareInvitation.objects.list_by_repo_id_and_path(
|
||||||
|
repo_id=repo_id, path=path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
for accepter in accepters:
|
||||||
|
|
||||||
|
if not accepter.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
accepter = accepter.lower()
|
||||||
|
|
||||||
|
if not is_valid_email(accepter):
|
||||||
|
result['failed'].append({
|
||||||
|
'email': accepter,
|
||||||
|
'error_msg': _('Email %s invalid.') % accepter
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if block_accepter(accepter):
|
||||||
|
result['failed'].append({
|
||||||
|
'email': accepter,
|
||||||
|
'error_msg': _('The email address is not allowed to be invited as a guest.')
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(accepter)
|
||||||
|
# user is active return exist
|
||||||
|
if user.is_active is True:
|
||||||
|
result['failed'].append({
|
||||||
|
'email': accepter,
|
||||||
|
'error_msg': _('User %s already exists.') % accepter
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if invitation_queryset.filter(accepter=accepter).exists():
|
||||||
|
invitation = invitation_queryset.filter(accepter=accepter)[0]
|
||||||
|
else:
|
||||||
|
invitation = Invitation.objects.add(
|
||||||
|
inviter=request.user.username, accepter=accepter)
|
||||||
|
|
||||||
|
if shared_queryset.filter(invitation=invitation).exists():
|
||||||
|
result['failed'].append({
|
||||||
|
'email': accepter,
|
||||||
|
'error_msg': _('This item has been shared to %s.') % accepter
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
RepoShareInvitation.objects.add(
|
||||||
|
invitation=invitation, repo_id=repo_id, path=path, permission=permission)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
result['failed'].append({
|
||||||
|
'email': accepter,
|
||||||
|
'error_msg': _('Internal Server Error'),
|
||||||
|
})
|
||||||
|
|
||||||
|
data = invitation.to_dict()
|
||||||
|
data['permission'] = permission
|
||||||
|
data['inviter_name'] = inviter_name
|
||||||
|
|
||||||
|
result['success'].append(data)
|
||||||
|
|
||||||
|
m = invitation.send_to(email=accepter)
|
||||||
|
if m.status != STATUS.sent:
|
||||||
|
result['failed'].append({
|
||||||
|
'email': accepter,
|
||||||
|
'error_msg': _('Failed to send email, email service is not properly configured, please contact administrator.'),
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response(result)
|
@ -11,6 +11,7 @@ from seahub.invitations.settings import INVITATIONS_TOKEN_AGE
|
|||||||
from seahub.utils import gen_token, get_site_name
|
from seahub.utils import gen_token, get_site_name
|
||||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||||
from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY
|
from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY
|
||||||
|
from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE
|
||||||
|
|
||||||
GUEST = 'Guest'
|
GUEST = 'Guest'
|
||||||
|
|
||||||
@ -102,3 +103,52 @@ class Invitation(models.Model):
|
|||||||
subject=subject,
|
subject=subject,
|
||||||
priority=MAIL_PRIORITY.now
|
priority=MAIL_PRIORITY.now
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RepoShareInvitationManager(models.Manager):
|
||||||
|
def add(self, invitation, repo_id, path, permission):
|
||||||
|
obj = self.model(
|
||||||
|
invitation=invitation,
|
||||||
|
repo_id=repo_id,
|
||||||
|
path=path,
|
||||||
|
permission=permission,
|
||||||
|
)
|
||||||
|
obj.save()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def list_by_repo_id_and_path(self, repo_id, path):
|
||||||
|
return self.select_related('invitation').filter(
|
||||||
|
invitation__expire_time__gte=timezone.now(),
|
||||||
|
invitation__accept_time=None,
|
||||||
|
repo_id=repo_id,
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_by_token_and_path(self, token, repo_id, path):
|
||||||
|
qs = self.select_related('invitation').filter(
|
||||||
|
invitation__token=token, repo_id=repo_id, path=path,
|
||||||
|
)
|
||||||
|
if qs.exists():
|
||||||
|
return qs[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_by_invitation(self, invitation):
|
||||||
|
return self.select_related('invitation').filter(invitation=invitation)
|
||||||
|
|
||||||
|
class RepoShareInvitation(models.Model):
|
||||||
|
PERMISSION_CHOICES = (
|
||||||
|
(PERMISSION_READ, 'read only'),
|
||||||
|
(PERMISSION_READ_WRITE, 'read and write')
|
||||||
|
)
|
||||||
|
|
||||||
|
invitation = models.ForeignKey(Invitation, on_delete=models.CASCADE, related_name='repo_share')
|
||||||
|
repo_id = models.CharField(max_length=36, db_index=True)
|
||||||
|
path = models.TextField()
|
||||||
|
permission = models.CharField(
|
||||||
|
max_length=50, choices=PERMISSION_CHOICES, default=PERMISSION_READ)
|
||||||
|
|
||||||
|
objects = RepoShareInvitationManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'repo_share_invitation'
|
||||||
|
@ -1,18 +1,26 @@
|
|||||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpResponseRedirect, Http404
|
from django.http import HttpResponseRedirect, Http404
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from seaserv import seafile_api
|
||||||
|
|
||||||
from seahub.auth import login as auth_login, authenticate
|
from seahub.auth import login as auth_login, authenticate
|
||||||
from seahub.auth import get_backends
|
from seahub.auth import get_backends
|
||||||
from seahub.base.accounts import User
|
from seahub.base.accounts import User
|
||||||
from seahub.constants import GUEST_USER
|
from seahub.constants import GUEST_USER
|
||||||
from seahub.invitations.models import Invitation
|
from seahub.invitations.models import Invitation, RepoShareInvitation
|
||||||
from seahub.invitations.signals import accept_guest_invitation_successful
|
from seahub.invitations.signals import accept_guest_invitation_successful
|
||||||
from seahub.settings import SITE_ROOT, NOTIFY_ADMIN_AFTER_REGISTRATION
|
from seahub.settings import SITE_ROOT, NOTIFY_ADMIN_AFTER_REGISTRATION
|
||||||
from registration.models import notify_admins_on_register_complete
|
from registration.models import notify_admins_on_register_complete
|
||||||
|
from seahub.utils import send_perm_audit_msg
|
||||||
|
from seahub.share.utils import share_dir_to_user
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def token_view(request, token):
|
def token_view(request, token):
|
||||||
"""Show form to let user set password.
|
"""Show form to let user set password.
|
||||||
@ -72,4 +80,36 @@ def token_view(request, token):
|
|||||||
if NOTIFY_ADMIN_AFTER_REGISTRATION:
|
if NOTIFY_ADMIN_AFTER_REGISTRATION:
|
||||||
notify_admins_on_register_complete(user.email)
|
notify_admins_on_register_complete(user.email)
|
||||||
|
|
||||||
|
# repo share invitation
|
||||||
|
try:
|
||||||
|
shared_queryset = RepoShareInvitation.objects.list_by_invitation(invitation=i)
|
||||||
|
accepter = i.accepter
|
||||||
|
|
||||||
|
for shared_obj in shared_queryset:
|
||||||
|
repo_id = shared_obj.repo_id
|
||||||
|
path = shared_obj.path
|
||||||
|
permission = shared_obj.permission
|
||||||
|
inviter = shared_obj.invitation.inviter
|
||||||
|
|
||||||
|
# recourse check
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
logger.warning('[ %s ] repo not found when [ %s ] accepts the invitation to share repo') % (repo_id, accepter)
|
||||||
|
continue
|
||||||
|
if seafile_api.get_dir_id_by_path(repo.id, path) is None:
|
||||||
|
logger.warning('[ %s ][ %s ] dir not found when [ %s ] accepts the invitation to share repo') % (repo_id, path, accepter)
|
||||||
|
continue
|
||||||
|
|
||||||
|
repo_owner = seafile_api.get_repo_owner(repo_id)
|
||||||
|
share_dir_to_user(repo, path, repo_owner,
|
||||||
|
inviter, accepter, permission, None)
|
||||||
|
|
||||||
|
send_perm_audit_msg('modify-repo-perm',
|
||||||
|
inviter, accepter, repo_id, path, permission)
|
||||||
|
|
||||||
|
# delete
|
||||||
|
shared_queryset.delete()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
return HttpResponseRedirect(SITE_ROOT)
|
return HttpResponseRedirect(SITE_ROOT)
|
||||||
|
@ -67,6 +67,8 @@ from seahub.api2.endpoints.query_copy_move_progress import QueryCopyMoveProgress
|
|||||||
from seahub.api2.endpoints.move_folder_merge import MoveFolderMergeView
|
from seahub.api2.endpoints.move_folder_merge import MoveFolderMergeView
|
||||||
from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchView
|
from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchView
|
||||||
from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView
|
from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView
|
||||||
|
from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView
|
||||||
|
from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView
|
||||||
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView
|
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView
|
||||||
from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView
|
from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView
|
||||||
from seahub.api2.endpoints.user_avatar import UserAvatarView
|
from seahub.api2.endpoints.user_avatar import UserAvatarView
|
||||||
@ -408,7 +410,9 @@ urlpatterns = [
|
|||||||
url(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()),
|
url(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()),
|
||||||
url(r'^api/v2.1/invitations/(?P<token>[a-f0-9]{32})/$', InvitationView.as_view()),
|
url(r'^api/v2.1/invitations/(?P<token>[a-f0-9]{32})/$', InvitationView.as_view()),
|
||||||
url(r'^api/v2.1/invitations/(?P<token>[a-f0-9]{32})/revoke/$', InvitationRevokeView.as_view()),
|
url(r'^api/v2.1/invitations/(?P<token>[a-f0-9]{32})/revoke/$', InvitationRevokeView.as_view()),
|
||||||
|
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/shared/invitations/$', RepoShareInvitationsView.as_view(), name="api-v2.1-repo-share-invitations"),
|
||||||
|
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/shared/invitations/batch/$', RepoShareInvitationsBatchView.as_view(), name="api-v2.1-repo-share-invitations-batch"),
|
||||||
|
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/shared/invitation/$', RepoShareInvitationView.as_view(), name="api-v2.1-repo-share-invitation"),
|
||||||
## user::avatar
|
## user::avatar
|
||||||
url(r'^api/v2.1/user-avatar/$', UserAvatarView.as_view(), name='api-v2.1-user-avatar'),
|
url(r'^api/v2.1/user-avatar/$', UserAvatarView.as_view(), name='api-v2.1-user-avatar'),
|
||||||
|
|
||||||
|
87
tests/api/endpoints/test_repo_share_invitation.py
Normal file
87
tests/api/endpoints/test_repo_share_invitation.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from seahub.base.accounts import UserPermissions
|
||||||
|
from seahub.invitations.models import Invitation, RepoShareInvitation
|
||||||
|
from seahub.test_utils import BaseTestCase
|
||||||
|
from seahub.api2.permissions import CanInviteGuest
|
||||||
|
from tests.common.utils import randstring
|
||||||
|
from seahub.base.accounts import User
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationsTest(BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.login_as(self.user)
|
||||||
|
self.username = self.user.username
|
||||||
|
self.repo_id = self.repo.id
|
||||||
|
self.url = reverse(
|
||||||
|
'api-v2.1-repo-share-invitation', args=[self.repo_id])
|
||||||
|
|
||||||
|
self.i = Invitation.objects.add(
|
||||||
|
inviter=self.username, accepter='3@qq.com')
|
||||||
|
assert len(Invitation.objects.all()) == 1
|
||||||
|
RepoShareInvitation.objects.add(self.i, self.repo_id, '/', 'r')
|
||||||
|
assert len(RepoShareInvitation.objects.all()) == 1
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_can_put(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
data = json.dumps({
|
||||||
|
'token': self.i.token,
|
||||||
|
'path': '/',
|
||||||
|
'permission': 'rw',
|
||||||
|
|
||||||
|
})
|
||||||
|
resp = self.client.put(self.url, data, 'application/json')
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_can_not_put_with_exist_permission(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
data = json.dumps({
|
||||||
|
'token': self.i.token,
|
||||||
|
'path': '/',
|
||||||
|
'permission': 'r',
|
||||||
|
|
||||||
|
})
|
||||||
|
resp = self.client.put(self.url, data, 'application/json')
|
||||||
|
self.assertEqual(400, resp.status_code)
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_can_delete(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
data = json.dumps({
|
||||||
|
'token': self.i.token,
|
||||||
|
'path': '/',
|
||||||
|
})
|
||||||
|
resp = self.client.delete(self.url, data, 'application/json')
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
|
||||||
|
assert len(RepoShareInvitation.objects.all()) == 0
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_can_not_delete_with_invalid_path(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
data = json.dumps({
|
||||||
|
'token': self.i.token,
|
||||||
|
'path': '/invalid_path',
|
||||||
|
})
|
||||||
|
resp = self.client.delete(self.url, data, 'application/json')
|
||||||
|
self.assertEqual(404, resp.status_code)
|
107
tests/api/endpoints/test_repo_share_invitations.py
Normal file
107
tests/api/endpoints/test_repo_share_invitations.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import json
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
|
from post_office.models import Email
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from seahub.base.accounts import UserPermissions
|
||||||
|
from seahub.invitations.models import Invitation, RepoShareInvitation
|
||||||
|
from seahub.test_utils import BaseTestCase
|
||||||
|
from seahub.api2.permissions import CanInviteGuest
|
||||||
|
|
||||||
|
|
||||||
|
class RepoShareInvitationsTest(BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.login_as(self.user)
|
||||||
|
self.username = self.user.username
|
||||||
|
self.repo_id = self.repo.id
|
||||||
|
self.url = reverse(
|
||||||
|
'api-v2.1-repo-share-invitations', args=[self.repo_id])
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_can_list(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
invitation_obj_1 = Invitation.objects.add(
|
||||||
|
inviter=self.username, accepter='1@qq.com')
|
||||||
|
invitation_obj_2 = Invitation.objects.add(
|
||||||
|
inviter=self.username, accepter='1@qq.com')
|
||||||
|
|
||||||
|
RepoShareInvitation.objects.add(invitation_obj_1, self.repo_id, '/', 'r')
|
||||||
|
RepoShareInvitation.objects.add(invitation_obj_2, self.repo_id, '/', 'rw')
|
||||||
|
|
||||||
|
resp = self.client.get(self.url + '?path=/')
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
json_resp = json.loads(resp.content)
|
||||||
|
assert len(json_resp.get('repo_share_invitation_list')) == 2
|
||||||
|
|
||||||
|
|
||||||
|
class RepoShareInvitationsBatchTest(BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.login_as(self.user)
|
||||||
|
self.username = self.user.username
|
||||||
|
self.repo_id = self.repo.id
|
||||||
|
self.url = reverse(
|
||||||
|
'api-v2.1-repo-share-invitations-batch', args=[self.repo_id])
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_can_add_with_batch(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
data = json.dumps({
|
||||||
|
'type': 'guest',
|
||||||
|
'accepters': ['1@qq.com', '2@qq.com'],
|
||||||
|
'path': '/',
|
||||||
|
'permission': 'r',
|
||||||
|
})
|
||||||
|
resp = self.client.post(self.url, data, 'application/json')
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
|
||||||
|
json_resp = json.loads(resp.content)
|
||||||
|
assert self.username == json_resp['success'][0]['inviter']
|
||||||
|
assert '1@qq.com' == json_resp['success'][0]['accepter']
|
||||||
|
assert '2@qq.com' == json_resp['success'][1]['accepter']
|
||||||
|
assert json_resp['success'][0]['expire_time'] is not None
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_with_invalid_path(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
data = json.dumps({
|
||||||
|
'type': 'guest',
|
||||||
|
'accepters': ['1@qq.com', '2@qq.com'],
|
||||||
|
'path': '/invalid_path',
|
||||||
|
'permission': 'r',
|
||||||
|
})
|
||||||
|
resp = self.client.post(self.url, data, 'application/json')
|
||||||
|
self.assertEqual(404, resp.status_code)
|
||||||
|
|
||||||
|
@patch.object(CanInviteGuest, 'has_permission')
|
||||||
|
@patch.object(UserPermissions, 'can_invite_guest')
|
||||||
|
def test_with_invalid_user(self, mock_can_invite_guest, mock_has_permission):
|
||||||
|
|
||||||
|
mock_can_invite_guest.return_val = True
|
||||||
|
mock_has_permission.return_val = True
|
||||||
|
|
||||||
|
self.logout()
|
||||||
|
self.login_as(self.admin)
|
||||||
|
|
||||||
|
data = json.dumps({
|
||||||
|
'type': 'guest',
|
||||||
|
'accepters': ['1@qq.com', '2@qq.com'],
|
||||||
|
'path': '/',
|
||||||
|
'permission': 'r',
|
||||||
|
})
|
||||||
|
resp = self.client.post(self.url, data, 'application/json')
|
||||||
|
self.assertEqual(403, resp.status_code)
|
||||||
|
self.logout
|
Loading…
Reference in New Issue
Block a user