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

Revoke invitation (#3899)

This commit is contained in:
sniper-py
2019-08-05 20:46:59 +08:00
committed by Daniel Pan
parent c7933c7f63
commit c462709b4f
18 changed files with 549 additions and 239 deletions

View File

@@ -0,0 +1,66 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import Loading from '../loading';
import toaster from '../toast';
const propTypes = {
accepter: PropTypes.string.isRequired,
token: PropTypes.string.isRequired,
revokeInvitation: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired
};
class InvitationRevokeDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
isSubmitting: false
};
}
onRevokeInvitation = () => {
this.setState({
isSubmitting: true,
});
seafileAPI.revokeInvitation(this.props.token).then((res) => {
this.props.revokeInvitation();
this.props.toggleDialog();
const msg = gettext('Successfully revoked access of user {placeholder}.').replace('{placeholder}', this.props.accepter);
toaster.success(msg);
}).catch((error) => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
this.props.toggleDialog();
});
};
render() {
const { accepter, toggleDialog } = this.props;
const { isSubmitting } = this.state;
const email = '<span class="op-target">' + Utils.HTMLescape(this.props.accepter) + '</span>';
const content = gettext('Are you sure to revoke access of user {placeholder} ?').replace('{placeholder}', email);
return (
<Modal isOpen={true} toggle={toggleDialog}>
<ModalHeader toggle={toggleDialog}>{gettext('Revoke Access')}</ModalHeader>
<ModalBody>
<p dangerouslySetInnerHTML={{__html: content}}></p>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={toggleDialog}>{gettext('Cancel')}</Button>
<Button className="submit-btn" color="primary" onClick={this.onRevokeInvitation} disabled={isSubmitting}>{isSubmitting ? <Loading /> : gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
InvitationRevokeDialog.propTypes = propTypes;
export default InvitationRevokeDialog;

View File

@@ -1,9 +1,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {gettext} from '../../utils/constants'; import { Utils } from '../../utils/utils';
import {seafileAPI} from '../../utils/seafile-api'; import { gettext } from '../../utils/constants';
import {Modal, ModalHeader, ModalBody, ModalFooter, Input, Button} from 'reactstrap'; import { seafileAPI } from '../../utils/seafile-api';
import { Modal, ModalHeader, ModalBody, ModalFooter, Input, Button } from 'reactstrap';
import toaster from '../toast'; import toaster from '../toast';
import Loading from '../loading';
const InvitePeopleDialogPropTypes = {
onInvitePeople: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired,
};
class InvitePeopleDialog extends React.Component { class InvitePeopleDialog extends React.Component {
@@ -12,11 +19,12 @@ class InvitePeopleDialog extends React.Component {
this.state = { this.state = {
emails: '', emails: '',
errorMsg: '', errorMsg: '',
isSubmitting: false
}; };
} }
handleEmailsChange = (event) => { handleInputChange = (e) => {
let emails = event.target.value; let emails = e.target.value;
this.setState({ this.setState({
emails: emails emails: emails
}); });
@@ -36,91 +44,88 @@ class InvitePeopleDialog extends React.Component {
handleSubmitInvite = () => { handleSubmitInvite = () => {
let emails = this.state.emails.trim(); let emails = this.state.emails.trim();
if (!emails) {
this.setState({
errorMsg: gettext('It is required.')
});
return false;
}
let emailsArray = []; let emailsArray = [];
emails = emails.split(','); emails = emails.split(',');
for (let i = 0; i < emails.length; i++) { for (let i = 0, len = emails.length; i < len; i++) {
let email = emails[i].trim(); let email = emails[i].trim();
if (email) { if (email) {
emailsArray.push(email); emailsArray.push(email);
} }
} }
if (emailsArray.length) {
seafileAPI.invitePeople(emailsArray).then((res) => { if (!emailsArray.length) {
this.setState({ this.setState({
emails: '', errorMsg: gettext('Email is invalid.')
});
this.props.toggleInvitePeopleDialog();
// success messages
let successMsg = '';
if (res.data.success.length === 1) {
successMsg = gettext('Successfully invited %(email).')
.replace('%(email)', res.data.success[0].accepter);
} else if(res.data.success.length > 1) {
successMsg = gettext('Successfully invited %(email) and %(num) other people.')
.replace('%(email)', res.data.success[0].accepter)
.replace('%(num)', res.data.success.length - 1);
}
if (successMsg) {
toaster.success(successMsg, {duration: 2});
this.props.onInvitePeople(res.data.success);
}
// failed messages
if (res.data.failed.length) {
for (let i = 0; i< res.data.failed.length; i++){
let failedMsg = res.data.failed[i].email + ': ' + res.data.failed[i].error_msg;
toaster.danger(failedMsg, {duration: 3});}
}
}).catch((error) => {
this.props.toggleInvitePeopleDialog();
if (error.response){
toaster.danger(error.response.data.detail || gettext('Error'), {duration: 3});
} else {
toaster.danger(gettext('Please check the network.'), {duration: 3});
}
}); });
} else { return false;
if (this.state.emails){
this.setState({
errorMsg: gettext('Email is invalid.')
});
} else {
this.setState({
errorMsg: gettext('It is required.')
});
}
} }
this.setState({
isSubmitting: true
});
seafileAPI.invitePeople(emailsArray).then((res) => {
this.props.toggleDialog();
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.props.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);
}
}
}).catch((error) => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
this.props.toggleDialog();
});
} }
render() { render() {
const { isSubmitting } = this.state;
return ( return (
<Modal isOpen={this.props.isInvitePeopleDialogOpen} toggle={this.props.toggleInvitePeopleDialog}> <Modal isOpen={true} toggle={this.props.toggleDialog}>
<ModalHeader toggle={this.props.toggleInvitePeopleDialog}>{gettext('Invite People')}</ModalHeader> <ModalHeader toggle={this.props.toggleDialog}>{gettext('Invite People')}</ModalHeader>
<ModalBody> <ModalBody>
<label htmlFor="emails">{gettext('Emails')}</label> <label htmlFor="emails">{gettext('Emails')}</label>
<Input <Input
type="text" id="emails" type="text"
id="emails"
placeholder={gettext('Emails, separated by \',\'')} placeholder={gettext('Emails, separated by \',\'')}
value={this.state.emails} value={this.state.emails}
onChange={this.handleEmailsChange} onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
/> />
<span className="error">{this.state.errorMsg}</span> <p className="error mt-2">{this.state.errorMsg}</p>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="secondary" onClick={this.props.toggleInvitePeopleDialog}>{gettext('Cancel')}</Button> <Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmitInvite}>{gettext('Submit')}</Button> <Button className="submit-btn" color="primary" onClick={this.handleSubmitInvite} disabled={isSubmitting}>{isSubmitting ? <Loading /> : gettext('Submit')}</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
); );
} }
} }
const InvitePeopleDialogPropTypes = {
toggleInvitePeopleDialog: PropTypes.func.isRequired,
isInvitePeopleDialogOpen: PropTypes.bool.isRequired,
onInvitePeople: PropTypes.func.isRequired,
};
InvitePeopleDialog.propTypes = InvitePeopleDialogPropTypes; InvitePeopleDialog.propTypes = InvitePeopleDialogPropTypes;
export default InvitePeopleDialog; export default InvitePeopleDialog;

View File

@@ -21,3 +21,9 @@
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
} }
.submit-btn .loading-icon {
margin: 1px auto;
width: 21px;
height: 21px;
}

View File

@@ -1,103 +1,149 @@
import React, {Fragment} from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {gettext, siteRoot} from '../../utils/constants'; import moment from 'moment';
import { gettext, siteRoot, loginUrl, canInvitePeople } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import InvitationsToolbar from '../../components/toolbar/invitations-toolbar'; import InvitationsToolbar from '../../components/toolbar/invitations-toolbar';
import InvitePeopleDialog from '../../components/dialog/invite-people-dialog'; import InvitePeopleDialog from '../../components/dialog/invite-people-dialog';
import {seafileAPI} from '../../utils/seafile-api'; import InvitationRevokeDialog from '../../components/dialog/invitation-revoke-dialog';
import {Table} from 'reactstrap';
import Loading from '../../components/loading'; import Loading from '../../components/loading';
import moment from 'moment';
import toaster from '../../components/toast'; import toaster from '../../components/toast';
import EmptyTip from '../../components/empty-tip'; import EmptyTip from '../../components/empty-tip';
import '../../css/invitations.css'; import '../../css/invitations.css';
class InvitationsListItem extends React.Component { if (!canInvitePeople) {
location.href = siteRoot;
}
class Item extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isOperationShow: false, isOpIconShown: false,
isRevokeDialogOpen: false
}; };
} }
onMouseEnter = (event) => { onMouseEnter = () => {
event.preventDefault();
this.setState({ this.setState({
isOperationShow: true, isOpIconShown: true
});
}
onMouseOver = () => {
this.setState({
isOperationShow: true,
}); });
} }
onMouseLeave = () => { onMouseLeave = () => {
this.setState({ this.setState({
isOperationShow: false, isOpIconShown: false
});
}
deleteItem = () => {
// make the icon avoid being clicked repeatedly
this.setState({
isOpIconShown: false
});
const token = this.props.invitation.token;
seafileAPI.deleteInvitation(token).then((res) => {
this.setState({deleted: true});
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
this.setState({
isOpIconShown: true
});
});
}
revokeItem = () => {
this.setState({deleted: true});
}
toggleRevokeDialog = () => {
this.setState({
isRevokeDialogOpen: !this.state.isRevokeDialogOpen
}); });
} }
render() { render() {
const { isOpIconShown, deleted, isRevokeDialogOpen } = this.state;
if (deleted) {
return null;
}
const invitationItem = this.props.invitation; const invitationItem = this.props.invitation;
const acceptIcon = <i className="sf2-icon-tick invite-accept-icon"></i>; const operation = invitationItem.accept_time ?
const deleteOperation = <i className="action-icon sf2-icon-x3" <i
title={gettext('Delete')} className="action-icon sf3-font sf3-font-cancel-invitation"
style={!this.state.isOperationShow ? {opacity: 0} : {}} title={gettext('Revoke Access')}
onClick={this.props.onItemDelete.bind(this, invitationItem.token, this.props.index)}></i>; onClick={this.toggleRevokeDialog}>
</i> :
<i
className="action-icon sf2-icon-x3"
title={gettext('Delete')}
onClick={this.deleteItem}>
</i>;
return ( return (
<tr onMouseEnter={this.onMouseEnter} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}> <Fragment>
<td>{invitationItem.accepter}</td> <tr onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td>{moment(invitationItem.invite_time).format('YYYY-MM-DD')}</td> <td>{invitationItem.accepter}</td>
<td>{moment(invitationItem.expire_time).format('YYYY-MM-DD')}</td> <td>{moment(invitationItem.invite_time).format('YYYY-MM-DD')}</td>
<td>{invitationItem.accept_time && acceptIcon}</td> <td>{moment(invitationItem.expire_time).format('YYYY-MM-DD')}</td>
<td>{!invitationItem.accept_time && deleteOperation}</td> <td>{invitationItem.accept_time && <i className="sf2-icon-tick invite-accept-icon"></i>}</td>
</tr> <td>{isOpIconShown && operation}</td>
</tr>
{isRevokeDialogOpen &&
<InvitationRevokeDialog
accepter={invitationItem.accepter}
token={invitationItem.token}
revokeInvitation={this.revokeItem}
toggleDialog={this.toggleRevokeDialog}
/>
}
</Fragment>
); );
} }
} }
const InvitationsListItemPropTypes = { const ItemPropTypes = {
invitation: PropTypes.object.isRequired, invitation: PropTypes.object.isRequired,
onItemDelete: PropTypes.func.isRequired,
}; };
InvitationsListItem.propTypes = InvitationsListItemPropTypes; Item.propTypes = ItemPropTypes;
class InvitationsListView extends React.Component { class Content extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
} }
onItemDelete = (token, index) => {
seafileAPI.deleteInvitation(token).then((res) => {
this.props.onDeleteInvitation(index);
toaster.success(gettext('Successfully deleted 1 item.'), {duration: 1});
}).catch((error) => {
if (error.response){
toaster.danger(error.response.data.detail || gettext('Error'), {duration: 3});
} else {
toaster.danger(gettext('Please check the network.'), {duration: 3});
}
});
}
render() { render() {
const invitationsListItems = this.props.invitationsList.map((invitation, index) => { const {
return ( loading, errorMsg, invitationsList
<InvitationsListItem } = this.props.data;
key={index}
onItemDelete={this.onItemDelete} if (loading) {
invitation={invitation} return <Loading />;
index={index} }
/>);
}); if (errorMsg) {
return <p className="error text-center mt-2">{errorMsg}</p>;
}
if (!invitationsList.length) {
return (
<EmptyTip>
<h2>{gettext('You have not invited any people.')}</h2>
</EmptyTip>
);
}
return ( return (
<Table hover> <table className="table-hover">
<thead> <thead>
<tr> <tr>
<th width="25%">{gettext('Email')}</th> <th width="25%">{gettext('Email')}</th>
@@ -108,100 +154,74 @@ class InvitationsListView extends React.Component {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{invitationsListItems} {invitationsList.map((invitation, index) => {
return (
<Item
key={index}
invitation={invitation}
/>
);
})}
</tbody> </tbody>
</Table> </table>
); );
} }
} }
const InvitationsListViewPropTypes = {
invitationsList: PropTypes.array.isRequired,
onDeleteInvitation: PropTypes.func.isRequired,
};
InvitationsListView.propTypes = InvitationsListViewPropTypes;
class InvitationsView extends React.Component { class InvitationsView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isInvitePeopleDialogOpen: false, loading: true,
errorMsg: '',
invitationsList: [], invitationsList: [],
isLoading: true, isInvitePeopleDialogOpen: false
permissionDeniedMsg: '',
showEmptyTip: false,
}; };
} }
listInvitations = () => { componentDidMount() {
seafileAPI.listInvitations().then((res) => { seafileAPI.listInvitations().then((res) => {
this.setState({ this.setState({
invitationsList: res.data, invitationsList: res.data,
showEmptyTip: true, loading: false
isLoading: false,
}); });
}).catch((error) => { }).catch((error) => {
this.setState({ if (error.response) {
isLoading: false, if (error.response.status == 403) {
});
if (error.response){
if (error.response.status === 403){
let permissionDeniedMsg = gettext('Permission error');
this.setState({ this.setState({
permissionDeniedMsg: permissionDeniedMsg, loading: false,
}); errorMsg: gettext('Permission denied')
} else{ });
toaster.danger(error.response.data.detail || gettext('Error'), {duration: 3}); location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
} }
} else { } else {
toaster.danger(gettext('Please check the network.'), {duration: 3}); this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
} }
}); });
} }
onInvitePeople = (invitationsArray) => { onInvitePeople = (invitationsArray) => {
invitationsArray.push.apply(invitationsArray,this.state.invitationsList); invitationsArray.push.apply(invitationsArray, this.state.invitationsList);
this.setState({ this.setState({
invitationsList: invitationsArray, invitationsList: invitationsArray,
}); });
} }
onDeleteInvitation = (index) => {
this.state.invitationsList.splice(index, 1);
this.setState({
invitationsList: this.state.invitationsList,
});
}
componentDidMount() {
this.listInvitations();
}
toggleInvitePeopleDialog = () => { toggleInvitePeopleDialog = () => {
this.setState({ this.setState({
isInvitePeopleDialogOpen: !this.state.isInvitePeopleDialogOpen isInvitePeopleDialogOpen: !this.state.isInvitePeopleDialogOpen
}); });
} }
emptyTip = () => {
return (
<EmptyTip>
<h2>{gettext('You have not invited any people.')}</h2>
</EmptyTip>
);
}
handlePermissionDenied = () => {
window.location = siteRoot;
return(
<div className="error mt-6 text-center">
<span>{this.state.permissionDeniedMsg}</span>
</div>
);
}
render() { render() {
return ( return (
<Fragment> <Fragment>
@@ -216,22 +236,14 @@ class InvitationsView extends React.Component {
<h3 className="sf-heading">{gettext('Invite People')}</h3> <h3 className="sf-heading">{gettext('Invite People')}</h3>
</div> </div>
<div className="cur-view-content"> <div className="cur-view-content">
{this.state.isLoading && <Loading/>} <Content data={this.state} />
{(!this.state.isLoading && this.state.permissionDeniedMsg !== '') && this.handlePermissionDenied() }
{(!this.state.isLoading && this.state.showEmptyTip && this.state.invitationsList.length === 0) && this.emptyTip()}
{(!this.state.isLoading && this.state.invitationsList.length !== 0) &&
< InvitationsListView
invitationsList={this.state.invitationsList}
onDeleteInvitation={this.onDeleteInvitation}
/>}
</div> </div>
</div> </div>
</div> </div>
{this.state.isInvitePeopleDialogOpen && {this.state.isInvitePeopleDialogOpen &&
<InvitePeopleDialog <InvitePeopleDialog
toggleInvitePeopleDialog={this.toggleInvitePeopleDialog}
isInvitePeopleDialogOpen={this.state.isInvitePeopleDialogOpen}
onInvitePeople={this.onInvitePeople} onInvitePeople={this.onInvitePeople}
toggleDialog={this.toggleInvitePeopleDialog}
/> />
} }
</Fragment> </Fragment>

View File

@@ -1,10 +1,10 @@
@font-face {font-family: "sf3-font"; @font-face {font-family: "sf3-font";
src: url('iconfont.eot?t=1562919437059'); /* IE9 */ src: url('iconfont.eot?t=1564546243284'); /* IE9 */
src: url('iconfont.eot?t=1562919437059#iefix') format('embedded-opentype'), /* IE6-IE8 */ src: url('iconfont.eot?t=1564546243284#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAXIAAsAAAAADBgAAAV6AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDOAqLFIhzATYCJAMYCw4ABCAFhG0HXRseChGVpDuR/UzI5P6wvAd1auyv0yQR3c8g2tLZEzh2sRf8VS5JyxMxh5jo0Tq0jkfErQbAfftlu6bW3SBMhNnuztJ89b+87Pb8yYY2HqdRCiFBOBoggIDsH8de/nVe5/nuvn+wQs8IzbKmD7gbWERblPBmBUxTnIlt9giyeQWz4DN5lcuBAKBIgQxSo1YDNyQIkNsRAKRf756dIVX0EHqwBJJKxRwqgPwDERL3O3cYwN/J55NHcBIJ4CDykFdq0qNmN1S5S+625JVvCtrxCpTLWQH4RwE8ABmAAJB+rL0H+EwqJ3LKZQsoB0BSa3HAXe6u+27Lb9+IQO42F75KXAKFFv/yOPAQIAJEBSnXQtmAEiJwlxgZEgUHGaBoDi14KNzQQoCiJcogAgBUF9wkGSC3Ac4MqZRpGw1E8JAgGJGRkGgytdJBIkCiVh5qTNXh93Rk/gWAMp9S30JCvItaV9nRMWo7DO2rOffdBgZMbMEKW3TDumX22MKcuQcLQi0D1sjyvMACi2XRodx5y6ZuzPIFWkQUU5DNOZAfXGQ2L1xpj7W8fyDg3l5j9mJbMGha4HeEyaRVhWFzyNfj9Okmm+XTZzzRLfluBOeY/GHGclvO2cPOtZy70LH6dCk6NycUSKRu/U4hYrE4HOGYeWPxnWeLlpXfdq72upJ7zlQMLrFFZwDz1phM084WX1rp5NkWe3sSBgYKStTDsh6GNSeShcnHXfq1J1PEaac8szdbheiWbMO8bQ4xtjVnKAVrCUa6EYqXjS4ihPaoS7nyFykL2EjiXUj1vvlA20s8d3/fQrvoDsymc7favZMsLcPzmD/fE528eHVBCyE6f4yyhen827xbc7SzF7fWzcu8mxHdsqVd5l1Ktb7FKy+Z9m2uAQYzll5bGybJrT14pF8v9lfAtP8iUb757KQRNY90614lyM2mTveZpk9IvRkZ0YxIpqveSU+PQz1a0LXTQ8/lr9fiZ9szIxm/O6vNkYfX8vy0Ia3N8tOeHj3M/bpBvv45od70ot9+J7TpxXrqH8tznoGZ9emM4jV2/fmdtp2OlVjOd+H5w578D2dSG6CBpXSj+LaNJTfdtb/z82uhZn4G/51YVm7W3Val25RN89jEgybDb32d51bNutfb2evZD0PW7XVordUPZVh1jfmBngylUh31b7gwJ1A3HNacdKtA2cnb/6mh2a6p8Y+7Rd1K04uoQoumK8KDgc+uyzvXvXZ9w3Xu8a7ruwqiBqKftWFo/ubrm3W7Ns/SE8P+Uv9H0gXcov9N4qXeER+rWp+ypl2G+EqzQXFqref3Lff2tmJW57Z7uwwvOjsOd5ed8t2QB/YEa0Vrgit+UMV/Xpb9a+yg+JI5ly7lsHEJpbYNuum6Zd1meWW3vcQ4yDV5hQfw7bF7vCXRbKc6qyC5lX8p0y5tBrFdxUcdDRXeSGz1WrrQqKvO8X3bCjDjBRZCcBDYnMD2wOrSgzSoxjkhlAgNfxRFZQHNNhyAogA7kwbi/V1KaRoth/1RmgoOnGrJAJ4kCyFgZBAxLlCRVAaq0P3RTAJ1RKYBUOBLAGH0DRy9bcAz+kEImF8Qmf2DirEjUC26OiWT05BdK5whkv+kIcjKOseyjds8LX1A9zo7LYsS4gt0G0JUF1U9dY8Veok9treuMWNilYXuxNUwz0JJZUSwojdLl7Jk7g5FkGWXqU6BYyAi3mdEPwjEincymaFYK298/APovJo52lPzYH+BEfIBqye1QgXEvXwdVfNROMh+02kYRRlhv1gsyB1ZYVZxQRK/0QgCU+hnRJOL0unFY+XFSyqXf1kKNCDvpIojPBGISFREDXZzfkY+SydnvM37adJhQ/6O0Ds7uhhPLLqcNlHb7QAAAA==') format('woff2'), url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAakAAsAAAAADUAAAAZVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDTgqNDIpEATYCJAMcCxAABCAFhG0HcRsdC1GUTlKX7EeCbdtghVejLpYjGdqh2RIKfanCHUG19m/PHv69D+wAyLMlRYlNRREID2RsjIrREYaNeFdLtwmtC8WN/+8uitZUquTb3uXyZX82mzYepzECjZAgHJLhvzN7k9kUb6JFvKLqoEJ2CmfbHjRpndw5rrKigzvX+Dr//bHSzfG1xXwbVmCFXvH/38i6aMuwpgM+OoxoixQsOz8wOsDTkM5mYsGHbwAuJ5BNc3TOH96GsK/E7YFOT6xJYT/lVdawQuuoSw4txTdkbXqfZoGv7vvjP+zHPklV4LteP74v4OoX8itFiR5hp6Ewri+M3lEU2ARK4l1p9IWtYNt0KJftsu0cyFpJ+iV6Dh+JH+37v8d6HYPYckTlrN9guyUhKdp/eJWaqHHUD+ZdreZXR9k53IUd8ABuBfAQbiXwBG4VcArsrNKbnoOYA2kP5BOYh9U+leLuC5TDXNrWjsncesI4t7I2IpWHL7+Fit+FwqY47sEUVQAQlRDKqxFEVrMtrQlV2Y6pwLp+l323k0omVtXE1XV1NPD01Z7l075qsZKjbfRWVrHZNTNeFQ23ut3lSpGWYKqwsikfVQ2LVd3M0xe8v1IpGEwvreWqVMwqBV+DXG/x07DUctxo3NjrbzQJdX0+AqAqYyo0GOYlLhvDLOLyan6rMRKWe6qVdlAg30mNsNl8vkbP6g4cNoc1xA1YsjoixkwJqjqurgiAijYm87Y5sD5x2SwaP4hgAAMQQETcE4bT25YcrG4shtPalx3Jt1eEpb0cK12fB71igE/W93sWQoCJUUYkCARHW65BEIhnbaTkDxFV2AeIrBrS5JUA7FjHcx+XV/PIAmUpLO/nya6zxZoKTOEj1N2obfUVWekqPyb6MKpiQNbvSSmt3UatCOleoOsbijfI6iFFXnuWuk+MRoHJJLRYxGbzziCZGsJMGOiqlltTgwSqzY4llgKzWVSnI6xiNO7QyFhqO4FIlZUEUyjQKRVbLISuaGWRwnL7/3I6oGNGNEpmKvad/htP639r+y+A299rdeiWUvtzGXOSgmQVqRSGCkybXiK5Ra46V61beO6yEJ/BRbDdoH7j/98fNqU8N63rtdDUMv+zmcKLXc7bG41CHGcdkwD/O/+M5RrCrl5D4Kafc1dfiCMJT7rlwaLA9JGvgik7qVhQI7ofRWeFPn+anPJBPjtqvc2ODWsEcGRy35szqwwfVzSYHOO/uYCbLLnZU4F9Mc2kXz0aamkpeXw49NDr89KOcT6FkzbjyqFuQE8KXYnE7NVXwU9lyhyNRmr9VsqYG4Pfpq8dXJv+bWeLnERDGCRgmIFgKXDy9R1+55w/7nTdIb0YuTPiq6MjtJKuQp/eO73Ukd4SGkKfjPzB2KrKFvuBSV7XO4K/UjivsE37pfIo7JT1aorwWt/j8a0YJ3Tg8Qj97Z3QPWcFMTeDpU95tpwEjm24zamEb9/FfP3JKZuISNe+DZ9Lso0cOHUv/D5ngP2ex30H7Ev2SpBLlxCJYF+6KCF5Ll6M7+viJfcDf+c/KIMfkgwG0ociVGtTDV9S/va0y6SlMjasTRKSTkuRLWAXSreQkBS3PSXlUg4yJK9+z8nfkP/7K5xRkJOXk4NbS17+lpOfnW90FVrbbm1KGvgc8DVRAgUA/P8CfYyynWsQFaOTwe1H36FudQNqRPd+fTeTx8OLg23vZ/H/bfeRXvLTRHy10j1DCWgIvoMY2fhlzZUIH9FxcF9TlsWV4XorA5Hf+G7tBxKyjK9pctTv+sgEGfk/IBMxJI0pKLQWqSVzEyqdQ6i1jiDb8Hh0ZwzbEmUf1n0wCMM+IRn0AwrDvqkl8wcqk/6gNhxbkN2E7Sk7y2EP0xhTwUJlG2VzarRsJYeFOnZ95z6aRZVy0uw4Y+S5LagwMDjbeMQGeR17zJcmEtFKM9XqgV0Nq4pUyzTDXLypSNvzg/CyPb2cajh0hqGUGMmCktkoWjnS0DuJQ69pMTf39X3IWKikuKbfSXQMCchi+UrIE4D6iBpSv/uiRcslIyLtsKZUmZE6kIflwqgSVaK05RvNoJzwTJcot3r81niairyjJPXtbINB8Ds2kaKIMqqoo4lW90jSrEK3IkMXeDofkFu2c3RXmE9T2dpXFNs4cb1dSCwXedrkWLm2WVpJ5fJ5AAAAAAA=') format('woff2'),
url('iconfont.woff?t=1562919437059') format('woff'), url('iconfont.woff?t=1564546243284') format('woff'),
url('iconfont.ttf?t=1562919437059') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ url('iconfont.ttf?t=1564546243284') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('iconfont.svg?t=1562919437059#sf3-font') format('svg'); /* iOS 4.1- */ url('iconfont.svg?t=1564546243284#sf3-font') format('svg'); /* iOS 4.1- */
} }
.sf3-font { .sf3-font {
@@ -35,3 +35,7 @@
content: "\e657"; content: "\e657";
} }
.sf3-font-cancel-invitation:before {
content: "\e661";
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,9 @@ Created by iconfont
<glyph glyph-name="sort" unicode="&#58967;" d="M763.345455-65.939394c-31.030303 0-55.854545 21.721212-55.854546 58.957576V585.69697l-99.29697-114.812122c-12.412121-15.515152-24.824242-18.618182-40.339394-18.618181-31.030303 0-58.957576 27.927273-58.957575 58.957575 0 15.515152 3.10303 31.030303 15.515151 43.442425l189.284849 232.727272c18.618182 9.309091 31.030303 15.515152 46.545454 15.515152s31.030303-6.206061 43.442424-18.618182l192.387879-232.727273c9.309091-12.412121 12.412121-27.927273 12.412121-43.442424 0-31.030303-24.824242-58.957576-55.854545-58.957576-15.515152 0-31.030303 6.206061-43.442424 18.618182L812.993939 585.69697v-595.781818c6.206061-34.133333-12.412121-55.854545-49.648484-55.854546zM15.515152 678.787879c0-34.133333 27.927273-62.060606 58.957575-62.060606h316.509091c31.030303 0 58.957576 27.927273 58.957576 62.060606s-27.927273 62.060606-58.957576 62.060606H74.472727C40.339394 740.848485 15.515152 712.921212 15.515152 678.787879z m58.957575-372.363637h378.569697c31.030303 0 58.957576 27.927273 58.957576 62.060606s-27.927273 62.060606-58.957576 62.060607H74.472727c-31.030303 0-58.957576-27.927273-58.957575-62.060607 3.10303-34.133333 27.927273-62.060606 58.957575-62.060606z m-3.10303-310.30303h446.836364c31.030303 0 55.854545 27.927273 55.854545 62.060606s-24.824242 62.060606-55.854545 62.060606H71.369697c-31.030303 0-55.854545-27.927273-55.854545-62.060606 3.10303-34.133333 27.927273-62.060606 55.854545-62.060606z" horiz-adv-x="1024" /> <glyph glyph-name="sort" unicode="&#58967;" d="M763.345455-65.939394c-31.030303 0-55.854545 21.721212-55.854546 58.957576V585.69697l-99.29697-114.812122c-12.412121-15.515152-24.824242-18.618182-40.339394-18.618181-31.030303 0-58.957576 27.927273-58.957575 58.957575 0 15.515152 3.10303 31.030303 15.515151 43.442425l189.284849 232.727272c18.618182 9.309091 31.030303 15.515152 46.545454 15.515152s31.030303-6.206061 43.442424-18.618182l192.387879-232.727273c9.309091-12.412121 12.412121-27.927273 12.412121-43.442424 0-31.030303-24.824242-58.957576-55.854545-58.957576-15.515152 0-31.030303 6.206061-43.442424 18.618182L812.993939 585.69697v-595.781818c6.206061-34.133333-12.412121-55.854545-49.648484-55.854546zM15.515152 678.787879c0-34.133333 27.927273-62.060606 58.957575-62.060606h316.509091c31.030303 0 58.957576 27.927273 58.957576 62.060606s-27.927273 62.060606-58.957576 62.060606H74.472727C40.339394 740.848485 15.515152 712.921212 15.515152 678.787879z m58.957575-372.363637h378.569697c31.030303 0 58.957576 27.927273 58.957576 62.060606s-27.927273 62.060606-58.957576 62.060607H74.472727c-31.030303 0-58.957576-27.927273-58.957575-62.060607 3.10303-34.133333 27.927273-62.060606 58.957575-62.060606z m-3.10303-310.30303h446.836364c31.030303 0 55.854545 27.927273 55.854545 62.060606s-24.824242 62.060606-55.854545 62.060606H71.369697c-31.030303 0-55.854545-27.927273-55.854545-62.060606 3.10303-34.133333 27.927273-62.060606 55.854545-62.060606z" horiz-adv-x="1024" />
<glyph glyph-name="cancel-invitation" unicode="&#58977;" d="M457.6 864c-131.2 0-236.8-105.6-236.8-236.8s105.6-236.8 236.8-236.8 236.8 105.6 236.8 236.8S588.8 864 457.6 864z m64-614.4l32 44.8c0 12.8-9.6 25.6-25.6 25.6h-192c-156.8 0-284.8-128-284.8-284.8v-86.4c0-25.6 22.4-44.8 44.8-44.8h425.6c12.8 0 25.6 9.6 25.6 25.6l-32 44.8c-3.2 6.4-25.6 64-25.6 124.8 6.4 67.2 19.2 124.8 32 150.4z m256-339.2c-112 0-201.6 92.8-201.6 201.6s92.8 201.6 201.6 201.6 201.6-92.8 201.6-201.6-89.6-201.6-201.6-201.6z m112 272c9.6 9.6 9.6 22.4 0 28.8-9.6 9.6-19.2 9.6-28.8 0l-73.6-76.8-80 76.8c-9.6 9.6-19.2 9.6-28.8 0-9.6-9.6-9.6-22.4 0-28.8l73.6-76.8-73.6-73.6c-9.6-9.6-9.6-22.4 0-28.8 9.6-9.6 19.2-9.6 28.8 0l73.6 76.8 73.6-76.8c9.6-9.6 19.2-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8l-73.6 76.8c6.4-6.4 80 73.6 80 73.6z" horiz-adv-x="1024" />
</font> </font>

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,17 +1,25 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import logging
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import status
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.utils.translation import ugettext as _
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.permissions import CanInviteGuest from seahub.api2.permissions import CanInviteGuest
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
from seahub.invitations.models import Invitation from seahub.invitations.models import Invitation
from seahub.base.accounts import User
from post_office.models import STATUS
from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY
from seahub.utils import get_site_name
logger = logging.getLogger(__name__)
json_content_type = 'application/json; charset=utf-8' json_content_type = 'application/json; charset=utf-8'
def invitation_owner_check(func): def invitation_owner_check(func):
@@ -50,3 +58,63 @@ class InvitationView(APIView):
return Response({ return Response({
}, status=204) }, status=204)
class InvitationRevokeView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, CanInviteGuest)
throttle_classes = (UserRateThrottle, )
def post(self, request, token, format=None):
"""Revoke invitation when the accepter successfully creates an account.
And set the account to inactive.
"""
# recourse check
invitation = Invitation.objects.get_by_token(token)
if not invitation:
error_msg = "Invitation not found."
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
if request.user.username != invitation.inviter:
error_msg = "Permission denied."
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
if invitation.accept_time is None:
error_msg = "The email address didn't accept the invitation."
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
email = invitation.accepter
inviter = invitation.inviter
try:
user = User.objects.get(email)
except User.DoesNotExist:
error_msg = 'User %s not found.' % email
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# set the account to inactive.
user.freeze_user()
# delete the invitation.
invitation.delete()
# send email
site_name = get_site_name()
subject = _('%(user)s revoke your access to %(site_name)s.') % {
'user': inviter, 'site_name': site_name}
context = {
'inviter': inviter,
'site_name': site_name,
}
m = send_html_email_with_dj_template(
email, dj_template='invitations/invitation_revoke_email.html',
subject=subject,
context=context,
priority=MAIL_PRIORITY.now
)
if m.status != STATUS.sent:
logger.warning('send revoke access email to %s failed')
return Response({'success': True})

View File

@@ -59,14 +59,13 @@ class InvitationsView(APIView):
_('%s is already invited.') % accepter) _('%s is already invited.') % accepter)
try: try:
User.objects.get(accepter) user = User.objects.get(accepter)
user_exists = True # user is active return exist
if user.is_active is True:
return api_error(status.HTTP_400_BAD_REQUEST,
_('User %s already exists.') % accepter)
except User.DoesNotExist: except User.DoesNotExist:
user_exists = False pass
if user_exists:
return api_error(status.HTTP_400_BAD_REQUEST,
_('User %s already exists.') % accepter)
i = Invitation.objects.add(inviter=request.user.username, i = Invitation.objects.add(inviter=request.user.username,
accepter=accepter) accepter=accepter)
@@ -127,22 +126,26 @@ class InvitationsBatchView(APIView):
continue continue
try: try:
User.objects.get(accepter) user = User.objects.get(accepter)
result['failed'].append({ # user is active return exist
'email': accepter, if user.is_active is True:
'error_msg': _('User %s already exists.') % accepter
})
continue
except User.DoesNotExist:
i = Invitation.objects.add(inviter=request.user.username,
accepter=accepter)
m = i.send_to(email=accepter)
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': _('User %s already exists.') % accepter
}) })
continue
except User.DoesNotExist:
pass
i = Invitation.objects.add(inviter=request.user.username,
accepter=accepter)
m = i.send_to(email=accepter)
if m.status == STATUS.sent:
result['success'].append(i.to_dict())
else:
result['failed'].append({
'email': accepter,
'error_msg': _('Internal Server Error'),
})
return Response(result) return Response(result)

View File

@@ -31,6 +31,13 @@ class InvitationManager(models.Manager):
def delete_all_expire_invitation(self): def delete_all_expire_invitation(self):
super(InvitationManager, self).filter(expire_time__lte=timezone.now()).delete() super(InvitationManager, self).filter(expire_time__lte=timezone.now()).delete()
def get_by_token(self, token):
qs = self.filter(token=token)
if qs.count() > 0:
return qs[0]
return None
class Invitation(models.Model): class Invitation(models.Model):
INVITE_TYPE_CHOICES = ( INVITE_TYPE_CHOICES = (
(GUEST, _('Guest')), (GUEST, _('Guest')),

View File

@@ -0,0 +1,18 @@
{% extends 'email_base.html' %}
{% load i18n %}
{% block email_con %}
{% autoescape off %}
<p style="color:#121214;font-size:14px;">{% trans "Hi," %}</p>
<p style="font-size:14px;color:#434144;">
{% blocktrans %}{{ inviter }} revoke your access to {{ site_name }}.{% endblocktrans %}
</p>
{% endautoescape %}
{% endblock %}

View File

@@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404, render
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from seahub.auth import login as auth_login 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
@@ -21,36 +21,55 @@ def token_view(request, token):
if i.is_expired(): if i.is_expired():
raise Http404 raise Http404
if request.method == 'GET':
try:
user = User.objects.get(email=i.accepter)
if user.is_active is True:
# user is active return exist
messages.error(request, _('A user with this email already exists.'))
except User.DoesNotExist:
pass
return render(request, 'invitations/token_view.html', {'iv': i, })
if request.method == 'POST': if request.method == 'POST':
passwd = request.POST.get('password', '') passwd = request.POST.get('password', '')
if not passwd: if not passwd:
return HttpResponseRedirect(request.META.get('HTTP_REFERER')) return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
try: try:
User.objects.get(email=i.accepter) user = User.objects.get(email=i.accepter)
messages.error(request, _('A user with this email already exists.')) if user.is_active is True:
# user is active return exist
messages.error(request, _('A user with this email already exists.'))
return render(request, 'invitations/token_view.html', {'iv': i, })
else:
# user is inactive then set active and new password
user.set_password(passwd)
user.is_active = True
user.save()
user = authenticate(username=user.username, password=passwd)
except User.DoesNotExist: except User.DoesNotExist:
# Create user, set that user as guest, and log user in. # Create user, set that user as guest.
u = User.objects.create_user(email=i.accepter, password=passwd, user = User.objects.create_user(
is_active=True) email=i.accepter, password=passwd, is_active=True)
User.objects.update_role(u.username, GUEST_USER) User.objects.update_role(user.username, GUEST_USER)
i.accept() # Update invitaion accept time.
for backend in get_backends(): for backend in get_backends():
u.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
auth_login(request, u)
# send signal to notify inviter # Update invitation accept time.
accept_guest_invitation_successful.send( i.accept()
sender=None, invitation_obj=i)
# send email to notify admin # login
if NOTIFY_ADMIN_AFTER_REGISTRATION: auth_login(request, user)
notify_admins_on_register_complete(u.email)
return HttpResponseRedirect(SITE_ROOT) # send signal to notify inviter
accept_guest_invitation_successful.send(
sender=None, invitation_obj=i)
return render(request, 'invitations/token_view.html', { # send email to notify admin
'iv': i, if NOTIFY_ADMIN_AFTER_REGISTRATION:
}) notify_admins_on_register_complete(user.email)
return HttpResponseRedirect(SITE_ROOT)

View File

@@ -69,7 +69,7 @@ from seahub.api2.endpoints.copy_move_task import CopyMoveTaskView
from seahub.api2.endpoints.query_copy_move_progress import QueryCopyMoveProgressView from seahub.api2.endpoints.query_copy_move_progress import QueryCopyMoveProgressView
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 from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView from seahub.api2.endpoints.notifications import NotificationsView, NotificationView
from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView
from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView
@@ -386,6 +386,7 @@ urlpatterns = [
url(r'^api/v2.1/invitations/$', InvitationsView.as_view()), url(r'^api/v2.1/invitations/$', InvitationsView.as_view()),
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()),
## 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'),

View File

@@ -5,6 +5,9 @@ from seahub.base.accounts import UserPermissions
from seahub.invitations.models import Invitation from seahub.invitations.models import Invitation
from seahub.test_utils import BaseTestCase from seahub.test_utils import BaseTestCase
from seahub.api2.permissions import CanInviteGuest 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): class InvitationsTest(BaseTestCase):
@@ -69,3 +72,98 @@ class InvitationsTest(BaseTestCase):
self.assertEqual(204, resp.status_code) self.assertEqual(204, resp.status_code)
assert len(Invitation.objects.all()) == 0 assert len(Invitation.objects.all()) == 0
class InvitationRevokeTest(BaseTestCase):
def setUp(self):
self.login_as(self.user)
self.username = self.user.username
self.tmp_username = 'user_%s@test.com' % randstring(4)
# add invitation
self.i = Invitation.objects.add(inviter=self.username, accepter=self.tmp_username)
self.endpoint = '/api/v2.1/invitations/' + self.i.token + '/revoke/'
assert len(Invitation.objects.all()) == 1
# accept invitation
self.i.accept()
self.tmp_user = self.create_user(self.tmp_username, is_staff=False)
assert self.tmp_user.is_active is True
def tearDown(self):
self.remove_user(self.tmp_username)
@patch.object(CanInviteGuest, 'has_permission')
@patch.object(UserPermissions, 'can_invite_guest')
def test_can_post(self, mock_can_invite_guest, mock_has_permission):
mock_can_invite_guest.return_val = True
mock_has_permission.return_val = True
resp = self.client.post(self.endpoint)
self.assertEqual(200, resp.status_code)
tmp_user = User.objects.get(self.tmp_username)
assert len(Invitation.objects.all()) == 0
assert tmp_user.is_active is False
@patch.object(CanInviteGuest, 'has_permission')
@patch.object(UserPermissions, 'can_invite_guest')
def test_can_invite_again_after_revoke(self, mock_can_invite_guest, mock_has_permission):
mock_can_invite_guest.return_val = True
mock_has_permission.return_val = True
# revoke
resp = self.client.post(self.endpoint)
self.assertEqual(200, resp.status_code)
tmp_user = User.objects.get(self.tmp_username)
assert len(Invitation.objects.all()) == 0
assert tmp_user.is_active is False
# invite again
invite_endpoint = '/api/v2.1/invitations/'
resp = self.client.post(invite_endpoint, {
'type': 'guest',
'accepter': self.tmp_username,
})
self.assertEqual(201, resp.status_code)
assert len(Invitation.objects.all()) == 1
@patch.object(CanInviteGuest, 'has_permission')
@patch.object(UserPermissions, 'can_invite_guest')
def test_can_invite_batch_again_and_accept_again_after_revoke(self, mock_can_invite_guest, mock_has_permission):
mock_can_invite_guest.return_val = True
mock_has_permission.return_val = True
# revoke
resp = self.client.post(self.endpoint)
self.assertEqual(200, resp.status_code)
tmp_user = User.objects.get(self.tmp_username)
assert len(Invitation.objects.all()) == 0
assert tmp_user.is_active is False
# invite again
invite_batch_endpoint = '/api/v2.1/invitations/batch/'
resp = self.client.post(invite_batch_endpoint, {
'type': 'guest',
'accepter': [self.tmp_username, ],
})
self.assertEqual(200, resp.status_code)
assert len(Invitation.objects.all()) == 1
# accept again
self.logout()
iv = Invitation.objects.all()[0]
token_endpoint = reverse('invitations:token_view', args=[iv.token])
assert iv.accept_time is None
resp = self.client.post(token_endpoint, {
'password': 'passwd'
})
self.assertEqual(302, resp.status_code)
assert Invitation.objects.get(pk=iv.pk).accept_time is not None
tmp_user_accept = User.objects.get(self.tmp_username)
assert tmp_user_accept.is_active is True