1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-05-10 08:55:02 +00:00

Repo invitation ()

* 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:
sniper-py 2019-11-18 17:55:17 +08:00 committed by Daniel Pan
parent 4e1d766f74
commit 106f29be06
12 changed files with 977 additions and 16 deletions

View File

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

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

View File

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

View File

@ -1,4 +1,4 @@
Django==1.11.23
Django==1.11.25
future
captcha
django-compressor

View File

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

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

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

View File

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

View File

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

View File

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

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

View 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