mirror of
https://github.com/haiwen/seahub.git
synced 2025-05-10 08:55:02 +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
frontend/src/components/dialog
requirements.txtseahub
tests/api/endpoints
@ -1,9 +1,10 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 ShareToGroup from './share-to-group';
|
||||
import ShareToInvitePeople from './share-to-invite-people';
|
||||
import GenerateShareLink from './generate-share-link';
|
||||
import GenerateUploadLink from './generate-upload-link';
|
||||
import InternalLink from './internal-link';
|
||||
@ -117,6 +118,13 @@ class ShareDialog extends React.Component {
|
||||
{gettext('Share to group')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{canInvitePeople &&
|
||||
<NavItem>
|
||||
<NavLink className={activeTab === 'invitePeople' ? 'active' : ''} onClick={this.toggle.bind(this, 'invitePeople')}>
|
||||
{gettext('Invite People')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
</Nav>
|
||||
@ -154,6 +162,11 @@ class ShareDialog extends React.Component {
|
||||
<TabPane tabId="shareToUser">
|
||||
<ShareToUser itemType={this.props.itemType} isGroupOwnedRepo={this.props.isGroupOwnedRepo} itemPath={this.props.itemPath} repoID={this.props.repoID} isRepoOwner={this.state.isRepoOwner}/>
|
||||
</TabPane>
|
||||
{canInvitePeople &&
|
||||
<TabPane tabId="invitePeople">
|
||||
<ShareToInvitePeople itemPath={this.props.itemPath} repoID={this.props.repoID}/>
|
||||
</TabPane>
|
||||
}
|
||||
<TabPane tabId="shareToGroup">
|
||||
<ShareToGroup itemType={this.props.itemType} isGroupOwnedRepo={this.props.isGroupOwnedRepo} itemPath={this.props.itemPath} repoID={this.props.repoID} isRepoOwner={this.state.isRepoOwner}/>
|
||||
</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 PropTypes from 'prop-types';
|
||||
import {gettext, isPro, canInvitePeople, siteRoot} from '../../utils/constants';
|
||||
import {gettext, isPro, siteRoot} from '../../utils/constants';
|
||||
import { Button } from 'reactstrap';
|
||||
import { seafileAPI } from '../../utils/seafile-api.js';
|
||||
import { Utils } from '../../utils/utils';
|
||||
@ -339,12 +339,6 @@ class ShareToUser extends React.Component {
|
||||
onChangeUserPermission={this.onChangeUserPermission}
|
||||
/>
|
||||
</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>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django==1.11.23
|
||||
Django==1.11.25
|
||||
future
|
||||
captcha
|
||||
django-compressor
|
||||
|
@ -16,6 +16,7 @@ from seahub.base.accounts import User
|
||||
from seahub.utils import is_valid_email
|
||||
from seahub.invitations.models import Invitation
|
||||
from seahub.invitations.utils import block_accepter
|
||||
from seahub.constants import GUEST_USER
|
||||
|
||||
json_content_type = 'application/json; charset=utf-8'
|
||||
|
||||
@ -38,7 +39,7 @@ class InvitationsView(APIView):
|
||||
def post(self, request, format=None):
|
||||
# Send invitation.
|
||||
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.')
|
||||
|
||||
accepter = request.data.get('accepter', '').lower()
|
||||
@ -139,13 +140,13 @@ class InvitationsBatchView(APIView):
|
||||
|
||||
i = Invitation.objects.add(inviter=request.user.username,
|
||||
accepter=accepter)
|
||||
result['success'].append(i.to_dict())
|
||||
|
||||
m = i.send_to(email=accepter)
|
||||
if m.status == STATUS.sent:
|
||||
result['success'].append(i.to_dict())
|
||||
else:
|
||||
if m.status != STATUS.sent:
|
||||
result['failed'].append({
|
||||
'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)
|
||||
|
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.timeutils import datetime_to_isoformat_timestr
|
||||
from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY
|
||||
from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE
|
||||
|
||||
GUEST = 'Guest'
|
||||
|
||||
@ -102,3 +103,52 @@ class Invitation(models.Model):
|
||||
subject=subject,
|
||||
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.
|
||||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
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 get_backends
|
||||
from seahub.base.accounts import 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.settings import SITE_ROOT, NOTIFY_ADMIN_AFTER_REGISTRATION
|
||||
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):
|
||||
"""Show form to let user set password.
|
||||
@ -72,4 +80,36 @@ def token_view(request, token):
|
||||
if NOTIFY_ADMIN_AFTER_REGISTRATION:
|
||||
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)
|
||||
|
@ -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.invitations import InvitationsView, InvitationsBatchView
|
||||
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.repo_file_uploaded_bytes import RepoFileUploadedBytesView
|
||||
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/(?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/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
|
||||
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