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:
66
frontend/src/components/dialog/invitation-revoke-dialog.js
Normal file
66
frontend/src/components/dialog/invitation-revoke-dialog.js
Normal 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;
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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,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') 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
@@ -35,6 +35,9 @@ Created by iconfont
|
|||||||
<glyph glyph-name="sort" unicode="" 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="" 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="" 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.
@@ -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})
|
||||||
|
@@ -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)
|
||||||
|
@@ -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')),
|
||||||
|
@@ -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 %}
|
@@ -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)
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user