- {gettext('Invite People')}
+
+ {gettext('Invite People')}
- {this.state.errorMsg}
+ {this.state.errorMsg}
-
-
+
+
);
}
}
-const InvitePeopleDialogPropTypes = {
- toggleInvitePeopleDialog: PropTypes.func.isRequired,
- isInvitePeopleDialogOpen: PropTypes.bool.isRequired,
- onInvitePeople: PropTypes.func.isRequired,
-};
-
InvitePeopleDialog.propTypes = InvitePeopleDialogPropTypes;
-export default InvitePeopleDialog;
\ No newline at end of file
+export default InvitePeopleDialog;
diff --git a/frontend/src/css/invitations.css b/frontend/src/css/invitations.css
index c79a33abe8..34d67ad870 100644
--- a/frontend/src/css/invitations.css
+++ b/frontend/src/css/invitations.css
@@ -21,3 +21,9 @@
cursor: pointer;
vertical-align: middle;
}
+
+.submit-btn .loading-icon {
+ margin: 1px auto;
+ width: 21px;
+ height: 21px;
+}
diff --git a/frontend/src/pages/invitations/invitations-view.js b/frontend/src/pages/invitations/invitations-view.js
index bc453082e0..fa225472ac 100644
--- a/frontend/src/pages/invitations/invitations-view.js
+++ b/frontend/src/pages/invitations/invitations-view.js
@@ -1,103 +1,149 @@
-import React, {Fragment} from 'react';
+import React, { Component, Fragment } from 'react';
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 InvitePeopleDialog from '../../components/dialog/invite-people-dialog';
-import {seafileAPI} from '../../utils/seafile-api';
-import {Table} from 'reactstrap';
+import InvitationRevokeDialog from '../../components/dialog/invitation-revoke-dialog';
import Loading from '../../components/loading';
-import moment from 'moment';
import toaster from '../../components/toast';
import EmptyTip from '../../components/empty-tip';
import '../../css/invitations.css';
-class InvitationsListItem extends React.Component {
+if (!canInvitePeople) {
+ location.href = siteRoot;
+}
+
+class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
- isOperationShow: false,
+ isOpIconShown: false,
+ isRevokeDialogOpen: false
};
}
- onMouseEnter = (event) => {
- event.preventDefault();
+ onMouseEnter = () => {
this.setState({
- isOperationShow: true,
- });
- }
-
- onMouseOver = () => {
- this.setState({
- isOperationShow: true,
+ isOpIconShown: true
});
}
onMouseLeave = () => {
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() {
+ const { isOpIconShown, deleted, isRevokeDialogOpen } = this.state;
+
+ if (deleted) {
+ return null;
+ }
+
const invitationItem = this.props.invitation;
- const acceptIcon = ;
- const deleteOperation = ;
+ const operation = invitationItem.accept_time ?
+
+ :
+
+ ;
+
return (
-
- {invitationItem.accepter} |
- {moment(invitationItem.invite_time).format('YYYY-MM-DD')} |
- {moment(invitationItem.expire_time).format('YYYY-MM-DD')} |
- {invitationItem.accept_time && acceptIcon} |
- {!invitationItem.accept_time && deleteOperation} |
-
+
+
+ {invitationItem.accepter} |
+ {moment(invitationItem.invite_time).format('YYYY-MM-DD')} |
+ {moment(invitationItem.expire_time).format('YYYY-MM-DD')} |
+ {invitationItem.accept_time && } |
+ {isOpIconShown && operation} |
+
+ {isRevokeDialogOpen &&
+
+ }
+
);
}
}
-const InvitationsListItemPropTypes = {
+const ItemPropTypes = {
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) {
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() {
- const invitationsListItems = this.props.invitationsList.map((invitation, index) => {
- return (
- );
- });
+ const {
+ loading, errorMsg, invitationsList
+ } = this.props.data;
+
+ if (loading) {
+ return ;
+ }
+
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ if (!invitationsList.length) {
+ return (
+
+ {gettext('You have not invited any people.')}
+
+ );
+ }
return (
-
+
{gettext('Email')} |
@@ -108,100 +154,74 @@ class InvitationsListView extends React.Component {
- {invitationsListItems}
+ {invitationsList.map((invitation, index) => {
+ return (
+
+ );
+ })}
-
+
);
}
}
-const InvitationsListViewPropTypes = {
- invitationsList: PropTypes.array.isRequired,
- onDeleteInvitation: PropTypes.func.isRequired,
-};
-
-InvitationsListView.propTypes = InvitationsListViewPropTypes;
-
class InvitationsView extends React.Component {
constructor(props) {
super(props);
this.state = {
- isInvitePeopleDialogOpen: false,
+ loading: true,
+ errorMsg: '',
invitationsList: [],
- isLoading: true,
- permissionDeniedMsg: '',
- showEmptyTip: false,
+ isInvitePeopleDialogOpen: false
};
}
- listInvitations = () => {
+ componentDidMount() {
seafileAPI.listInvitations().then((res) => {
this.setState({
invitationsList: res.data,
- showEmptyTip: true,
- isLoading: false,
+ loading: false
});
}).catch((error) => {
- this.setState({
- isLoading: false,
- });
- if (error.response){
- if (error.response.status === 403){
- let permissionDeniedMsg = gettext('Permission error');
+ if (error.response) {
+ if (error.response.status == 403) {
this.setState({
- permissionDeniedMsg: permissionDeniedMsg,
- });
- } else{
- toaster.danger(error.response.data.detail || gettext('Error'), {duration: 3});
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
}
} else {
- toaster.danger(gettext('Please check the network.'), {duration: 3});
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
}
- });
+ });
}
onInvitePeople = (invitationsArray) => {
- invitationsArray.push.apply(invitationsArray,this.state.invitationsList);
+ invitationsArray.push.apply(invitationsArray, this.state.invitationsList);
this.setState({
invitationsList: invitationsArray,
});
}
- onDeleteInvitation = (index) => {
- this.state.invitationsList.splice(index, 1);
- this.setState({
- invitationsList: this.state.invitationsList,
- });
- }
-
- componentDidMount() {
- this.listInvitations();
- }
-
toggleInvitePeopleDialog = () => {
this.setState({
isInvitePeopleDialogOpen: !this.state.isInvitePeopleDialogOpen
});
}
- emptyTip = () => {
- return (
-
- {gettext('You have not invited any people.')}
-
- );
- }
-
- handlePermissionDenied = () => {
- window.location = siteRoot;
- return(
-
- {this.state.permissionDeniedMsg}
-
- );
- }
-
render() {
return (
@@ -216,22 +236,14 @@ class InvitationsView extends React.Component {
{gettext('Invite People')}
- {this.state.isLoading && }
- {(!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}
- />}
+
{this.state.isInvitePeopleDialogOpen &&
}
diff --git a/media/css/sf_font3/iconfont.css b/media/css/sf_font3/iconfont.css
index f858ebce99..fe3e68aaa4 100644
--- a/media/css/sf_font3/iconfont.css
+++ b/media/css/sf_font3/iconfont.css
@@ -1,10 +1,10 @@
@font-face {font-family: "sf3-font";
- src: url('iconfont.eot?t=1562919437059'); /* IE9 */
- src: url('iconfont.eot?t=1562919437059#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('iconfont.woff?t=1562919437059') format('woff'),
- url('iconfont.ttf?t=1562919437059') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
- url('iconfont.svg?t=1562919437059#sf3-font') format('svg'); /* iOS 4.1- */
+ src: url('iconfont.eot?t=1564546243284'); /* IE9 */
+ src: url('iconfont.eot?t=1564546243284#iefix') format('embedded-opentype'), /* IE6-IE8 */
+ 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=1564546243284') format('woff'),
+ url('iconfont.ttf?t=1564546243284') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
+ url('iconfont.svg?t=1564546243284#sf3-font') format('svg'); /* iOS 4.1- */
}
.sf3-font {
@@ -35,3 +35,7 @@
content: "\e657";
}
+.sf3-font-cancel-invitation:before {
+ content: "\e661";
+}
+
diff --git a/media/css/sf_font3/iconfont.eot b/media/css/sf_font3/iconfont.eot
index 3748273478..7563a84236 100644
Binary files a/media/css/sf_font3/iconfont.eot and b/media/css/sf_font3/iconfont.eot differ
diff --git a/media/css/sf_font3/iconfont.js b/media/css/sf_font3/iconfont.js
index 76f5196568..018147f11f 100644
--- a/media/css/sf_font3/iconfont.js
+++ b/media/css/sf_font3/iconfont.js
@@ -1 +1 @@
-!function(h){var c,e='',t=(c=document.getElementsByTagName("script"))[c.length-1].getAttribute("data-injectcss");if(t&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}!function(c){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(c,0);else{var t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()};document.addEventListener("DOMContentLoaded",t,!1)}else document.attachEvent&&(n=c,o=h.document,l=!1,(i=function(){try{o.documentElement.doScroll("left")}catch(c){return void setTimeout(i,50)}e()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,e())});function e(){l||(l=!0,n())}var n,o,l,i}(function(){var c,t;(c=document.createElement("div")).innerHTML=e,e=null,(t=c.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",function(c,t){t.firstChild?function(c,t){t.parentNode.insertBefore(c,t)}(c,t.firstChild):t.appendChild(c)}(t,document.body))})}(window);
\ No newline at end of file
+!function(h){var c,e='',t=(c=document.getElementsByTagName("script"))[c.length-1].getAttribute("data-injectcss");if(t&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}!function(c){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(c,0);else{var t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()};document.addEventListener("DOMContentLoaded",t,!1)}else document.attachEvent&&(n=c,o=h.document,l=!1,(i=function(){try{o.documentElement.doScroll("left")}catch(c){return void setTimeout(i,50)}e()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,e())});function e(){l||(l=!0,n())}var n,o,l,i}(function(){var c,t;(c=document.createElement("div")).innerHTML=e,e=null,(t=c.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",function(c,t){t.firstChild?function(c,t){t.parentNode.insertBefore(c,t)}(c,t.firstChild):t.appendChild(c)}(t,document.body))})}(window);
\ No newline at end of file
diff --git a/media/css/sf_font3/iconfont.svg b/media/css/sf_font3/iconfont.svg
index 05920c8701..112d096f78 100644
--- a/media/css/sf_font3/iconfont.svg
+++ b/media/css/sf_font3/iconfont.svg
@@ -35,6 +35,9 @@ Created by iconfont
+
+
+
diff --git a/media/css/sf_font3/iconfont.ttf b/media/css/sf_font3/iconfont.ttf
index 05c8fb3953..916f7ac43d 100644
Binary files a/media/css/sf_font3/iconfont.ttf and b/media/css/sf_font3/iconfont.ttf differ
diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff
index e8aa715e1d..14c28f5dd2 100644
Binary files a/media/css/sf_font3/iconfont.woff and b/media/css/sf_font3/iconfont.woff differ
diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2
index f15e9bf543..a563978679 100644
Binary files a/media/css/sf_font3/iconfont.woff2 and b/media/css/sf_font3/iconfont.woff2 differ
diff --git a/seahub/api2/endpoints/invitation.py b/seahub/api2/endpoints/invitation.py
index 21b945294a..676cc28d93 100644
--- a/seahub/api2/endpoints/invitation.py
+++ b/seahub/api2/endpoints/invitation.py
@@ -1,17 +1,25 @@
# Copyright (c) 2012-2016 Seafile Ltd.
+import logging
+
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
+from django.utils.translation import ugettext as _
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.permissions import CanInviteGuest
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
from seahub.invitations.models import Invitation
+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'
def invitation_owner_check(func):
@@ -50,3 +58,63 @@ class InvitationView(APIView):
return Response({
}, 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})
diff --git a/seahub/api2/endpoints/invitations.py b/seahub/api2/endpoints/invitations.py
index cc885907ca..a3422bd62b 100644
--- a/seahub/api2/endpoints/invitations.py
+++ b/seahub/api2/endpoints/invitations.py
@@ -59,14 +59,13 @@ class InvitationsView(APIView):
_('%s is already invited.') % accepter)
try:
- User.objects.get(accepter)
- user_exists = True
+ user = User.objects.get(accepter)
+ # 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:
- user_exists = False
-
- if user_exists:
- return api_error(status.HTTP_400_BAD_REQUEST,
- _('User %s already exists.') % accepter)
+ pass
i = Invitation.objects.add(inviter=request.user.username,
accepter=accepter)
@@ -127,22 +126,26 @@ class InvitationsBatchView(APIView):
continue
try:
- User.objects.get(accepter)
- result['failed'].append({
- 'email': accepter,
- '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:
+ user = User.objects.get(accepter)
+ # user is active return exist
+ if user.is_active is True:
result['failed'].append({
'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)
diff --git a/seahub/invitations/models.py b/seahub/invitations/models.py
index 8bb34d94ba..6e6d0b364d 100644
--- a/seahub/invitations/models.py
+++ b/seahub/invitations/models.py
@@ -31,6 +31,13 @@ class InvitationManager(models.Manager):
def delete_all_expire_invitation(self):
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):
INVITE_TYPE_CHOICES = (
(GUEST, _('Guest')),
diff --git a/seahub/invitations/templates/invitations/invitation_revoke_email.html b/seahub/invitations/templates/invitations/invitation_revoke_email.html
new file mode 100644
index 0000000000..6a9c2fb04e
--- /dev/null
+++ b/seahub/invitations/templates/invitations/invitation_revoke_email.html
@@ -0,0 +1,18 @@
+{% extends 'email_base.html' %}
+
+{% load i18n %}
+
+{% block email_con %}
+
+{% autoescape off %}
+
+{% trans "Hi," %}
+
+
+{% blocktrans %}{{ inviter }} revoke your access to {{ site_name }}.{% endblocktrans %}
+
+
+
+{% endautoescape %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/seahub/invitations/views.py b/seahub/invitations/views.py
index 273d13e941..8283ed526b 100644
--- a/seahub/invitations/views.py
+++ b/seahub/invitations/views.py
@@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404, render
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.base.accounts import User
from seahub.constants import GUEST_USER
@@ -21,36 +21,55 @@ def token_view(request, token):
if i.is_expired():
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':
passwd = request.POST.get('password', '')
if not passwd:
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
try:
- User.objects.get(email=i.accepter)
- messages.error(request, _('A user with this email already exists.'))
+ 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.'))
+ 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:
- # Create user, set that user as guest, and log user in.
- u = User.objects.create_user(email=i.accepter, password=passwd,
- is_active=True)
- User.objects.update_role(u.username, GUEST_USER)
-
- i.accept() # Update invitaion accept time.
-
+ # Create user, set that user as guest.
+ user = User.objects.create_user(
+ email=i.accepter, password=passwd, is_active=True)
+ User.objects.update_role(user.username, GUEST_USER)
for backend in get_backends():
- u.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
- auth_login(request, u)
+ user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
- # send signal to notify inviter
- accept_guest_invitation_successful.send(
- sender=None, invitation_obj=i)
+ # Update invitation accept time.
+ i.accept()
- # send email to notify admin
- if NOTIFY_ADMIN_AFTER_REGISTRATION:
- notify_admins_on_register_complete(u.email)
+ # login
+ auth_login(request, user)
- 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', {
- 'iv': i,
- })
+ # send email to notify admin
+ if NOTIFY_ADMIN_AFTER_REGISTRATION:
+ notify_admins_on_register_complete(user.email)
+
+ return HttpResponseRedirect(SITE_ROOT)
diff --git a/seahub/urls.py b/seahub/urls.py
index 36ee196059..3067c024da 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -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.move_folder_merge import MoveFolderMergeView
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.user_enabled_modules import UserEnabledModulesView
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/batch/$', InvitationsBatchView.as_view()),
url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/$', InvitationView.as_view()),
+ url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/revoke/$', InvitationRevokeView.as_view()),
## user::avatar
url(r'^api/v2.1/user-avatar/$', UserAvatarView.as_view(), name='api-v2.1-user-avatar'),
diff --git a/tests/api/endpoints/test_invitation.py b/tests/api/endpoints/test_invitation.py
index f748e3f6be..a448c63b97 100644
--- a/tests/api/endpoints/test_invitation.py
+++ b/tests/api/endpoints/test_invitation.py
@@ -5,6 +5,9 @@ from seahub.base.accounts import UserPermissions
from seahub.invitations.models import Invitation
from seahub.test_utils import BaseTestCase
from seahub.api2.permissions import CanInviteGuest
+from tests.common.utils import randstring
+from seahub.base.accounts import User
+from django.core.urlresolvers import reverse
class InvitationsTest(BaseTestCase):
@@ -69,3 +72,98 @@ class InvitationsTest(BaseTestCase):
self.assertEqual(204, resp.status_code)
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