From c462709b4f4a1b089f60bb6b48d0498c53081ad1 Mon Sep 17 00:00:00 2001 From: sniper-py <38058090+sniper-py@users.noreply.github.com> Date: Mon, 5 Aug 2019 20:46:59 +0800 Subject: [PATCH] Revoke invitation (#3899) --- .../dialog/invitation-revoke-dialog.js | 66 +++++ .../components/dialog/invite-people-dialog.js | 133 ++++----- frontend/src/css/invitations.css | 6 + .../src/pages/invitations/invitations-view.js | 258 +++++++++--------- media/css/sf_font3/iconfont.css | 16 +- media/css/sf_font3/iconfont.eot | Bin 3264 -> 3560 bytes media/css/sf_font3/iconfont.js | 2 +- media/css/sf_font3/iconfont.svg | 3 + media/css/sf_font3/iconfont.ttf | Bin 3096 -> 3392 bytes media/css/sf_font3/iconfont.woff | Bin 1956 -> 2172 bytes media/css/sf_font3/iconfont.woff2 | Bin 1480 -> 1700 bytes seahub/api2/endpoints/invitation.py | 68 +++++ seahub/api2/endpoints/invitations.py | 47 ++-- seahub/invitations/models.py | 7 + .../invitations/invitation_revoke_email.html | 18 ++ seahub/invitations/views.py | 63 +++-- seahub/urls.py | 3 +- tests/api/endpoints/test_invitation.py | 98 +++++++ 18 files changed, 549 insertions(+), 239 deletions(-) create mode 100644 frontend/src/components/dialog/invitation-revoke-dialog.js create mode 100644 seahub/invitations/templates/invitations/invitation_revoke_email.html diff --git a/frontend/src/components/dialog/invitation-revoke-dialog.js b/frontend/src/components/dialog/invitation-revoke-dialog.js new file mode 100644 index 0000000000..c703c154b1 --- /dev/null +++ b/frontend/src/components/dialog/invitation-revoke-dialog.js @@ -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 = '' + Utils.HTMLescape(this.props.accepter) + ''; + const content = gettext('Are you sure to revoke access of user {placeholder} ?').replace('{placeholder}', email); + + return ( + + {gettext('Revoke Access')} + + + + + {gettext('Cancel')} + {isSubmitting ? : gettext('Submit')} + + + ); + } +} + +InvitationRevokeDialog.propTypes = propTypes; + +export default InvitationRevokeDialog; diff --git a/frontend/src/components/dialog/invite-people-dialog.js b/frontend/src/components/dialog/invite-people-dialog.js index d8e54fa4e9..0c6deb4bcc 100644 --- a/frontend/src/components/dialog/invite-people-dialog.js +++ b/frontend/src/components/dialog/invite-people-dialog.js @@ -1,9 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {gettext} from '../../utils/constants'; -import {seafileAPI} from '../../utils/seafile-api'; -import {Modal, ModalHeader, ModalBody, ModalFooter, Input, Button} from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Input, Button } from 'reactstrap'; import toaster from '../toast'; +import Loading from '../loading'; + +const InvitePeopleDialogPropTypes = { + onInvitePeople: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired, +}; class InvitePeopleDialog extends React.Component { @@ -12,11 +19,12 @@ class InvitePeopleDialog extends React.Component { this.state = { emails: '', errorMsg: '', + isSubmitting: false }; } - handleEmailsChange = (event) => { - let emails = event.target.value; + handleInputChange = (e) => { + let emails = e.target.value; this.setState({ emails: emails }); @@ -36,91 +44,88 @@ class InvitePeopleDialog extends React.Component { handleSubmitInvite = () => { let emails = this.state.emails.trim(); + if (!emails) { + this.setState({ + errorMsg: gettext('It is required.') + }); + return false; + } + let emailsArray = []; - emails = emails.split(','); - for (let i = 0; i < emails.length; i++) { + emails = emails.split(','); + for (let i = 0, len = emails.length; i < len; i++) { let email = emails[i].trim(); if (email) { emailsArray.push(email); } } - if (emailsArray.length) { - seafileAPI.invitePeople(emailsArray).then((res) => { - this.setState({ - emails: '', - }); - 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}); - } + + if (!emailsArray.length) { + this.setState({ + errorMsg: gettext('Email is invalid.') }); - } else { - if (this.state.emails){ - this.setState({ - errorMsg: gettext('Email is invalid.') - }); - } else { - this.setState({ - errorMsg: gettext('It is required.') - }); - } + return false; } + + 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() { + const { isSubmitting } = this.state; return ( - - {gettext('Invite People')} + + {gettext('Invite People')} {gettext('Emails')} - {this.state.errorMsg} + {this.state.errorMsg} - {gettext('Cancel')} - {gettext('Submit')} + {gettext('Cancel')} + {isSubmitting ? : gettext('Submit')} ); } } -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,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') 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 3748273478e3240b64eb716d5a23170551fa9af8..7563a842365b1002cbc46bf4dc236f7ad346523c 100644 GIT binary patch delta 723 zcmYLFO=uHQ5T1E&cVC*OyJ>728%vFU$-(r8Hcc+l77?YP3ze2CQZY-iHlacfbu9~8tKk& zZ|{=eD`hN04bkBDiNB*foynC}dXO04K0(joQp%9;iEA`pr`nq{Rtgw^ZTf$d>R{f; znXg|4eq07{&jGMpVX0V(yx)EX;Q2^`IbZ$QO&SO^$GJEWv2^J0Pe5|oor z01K_@f84gEK+<_YrO)P!JS31op+b3u9DUUG*gClCAT6)zl}39kgWM=>r4u{2!x)=m zd;H?oob*(#(94>ee4i(w6;47wOp>LFq8I(681F|OkA}|Ljm};iJ~0#<3HM`o6w$#r zsZ=}?i-toQhC@Q>IF1DVt}*m?#beO{9E#zdxz5Z>)50Uy3`YeC*w zLAf2>v$q&4&CC=TTMh`Jsfj676C#mt_=KhkG0}3KCmkEk*GzN$_w$I`%WJE%9o?!Y zA}$RKPyMO$B6)BJvCZX`HBHwwO?Z{bglLylRo7LO+Pb}V3Li9J>sQMIrU_)UL&*wg24PZ{}?x!9M@m$N0KlwHa@Lw^BOYL1Nn delta 422 zcmYLFJxD@P7(MsC`>y{=)B`ODp+;LBD$21Sk|=U9^5IVozBF<$2x@2uT9RAP7J<;z zWP*c*Q$ur0OGLw?p`oFMmh5{v^uf8`Ip00!8@~6kZbWwdK-gcGagofVje6qbU=d*Z z0M@gXS!w>XLZm+k?XA*YVQ0oV1hiW~KP%?VoYCEAlS*x%JTJ#?#9o;sq{PpGN*9zcS0Jv2t?^bU<@7@4kn+zK& z$0p__oRtJ4ok?znX20PRGytBLvw{EKyBE+Bq}|}ol*-9T^}w!JN)>rbUGU*lsXqk@ z(E2P!-AmRI1g6RF9^`Fqui2?y53rfNI$+L9%DBl-HzdX>E1(mVYDoK4QB7ANAw5fd sCtXa9i@6|)HfwXv)BF)=*2KBq)P1KsAQ@*G6HI54xl|)CMVqMp0DityI{*Lx 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 05c8fb395397d0b7e5c091d1b0fc222bf594b33c..916f7ac43d63b3c9483dfe08e4f18058b38879fe 100644 GIT binary patch delta 707 zcmYLFOK1~O6utMo%)De0XOh@9HI!JbCJWOP`bjpDf{4=4V5K2`sF)!gn_zx5A+#cN zp$#Z5ltFYM+O7mQE^48RA^~?r#6>r5imSnm;6lNT#ybPz9nN{@-Z}R$^PUZM!;ePR z<^k{y0OX5Ry|H-kf+T(?d|axm-Uwd1?E>JR0OW@yLtpIw_U0}bz7Ucn8i)gapY$%_ zM5)?Z8A4Kk`$R*PdQq3|iL12VrQToFR~i_GZF)aQ{cugM8n0f4e_RAm764eaQE#^5 z@3x-;_&$(f`SI?^v;aGia5L+fOMI?XvfT8Np4| z%`V2xuNczX&J>^l+);5l6|%O$1!u{xIQz_ER?fO)>AfC+h1UE(ZO2j}`Fx<#V{1kk zE|5T`MmBZ!%WN>O=5w5v9j41OOFTVf=@dt>txoOW4r6SA?eTM4Z}}5xjef1S!S{Iq zyl@;6FiV*#iU@|%m`Wf|O-4`IgZ>Ck92-wg#S$1xBRZTSmx_mzld-6Vv8a&yjv`TL zU>ZXMspRAsjwf-?=rGnXjCID=PY$cT?&>DTa1dWB+A!~5Ln(kt{uX1cT&~I3a##pW z&CIEq5SfglXEara8P6GBaAG)LH;m5j=W)f)H&^q0M^s;2oFAK*`_tu3%HR&;A-7-B zv}{(>gkPS`h=8Q3*{rJ4IBVZKRS*tf>w@P2(*!c)=t2o)6zDcB?`SAc)N4hfGFq;+ M%PqZCuGgHUzwoq+>;M1& delta 409 zcmYLDJ4gdT6r8trm;a|R5L1W{%h+1@FBL=(#WV?e#KaUi6ER2xK?@7ZNEGZu5Ui}u zK#C}-tt@P8L@ch+!a@r{h;vEd!t9%wH}CCs66f)H@?aj2eE_RD%dFTx%_#K;(b_ES z6}Bg=LqNL)jMHM?+=#UUF=k(hu_6uci*`)jCPs=@wH6mL$P=%4shl&7BYlALSJV;9 ztW{(XP4?r|!&|16zrIL(Edl;HKv|XYPW9&V?hOdG7|5QqlQSN;8;MwE>U}Lb`Hi4X z19)D{h5u{!UWYbI?Spb%9(T^u7?Mj#H#(9kP#T0rqzG=2QZED)=(_O~YoO=r7^n92 z*Ez~ranju`;E1!_XY9I9_?Qp%nBuGs!(LZm@h|k+e6X>rO-CqN(o#xg)B|8YYG;O> X4b)ZGBT6*U#So8p8^57Z_k+M6(8W(m diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff index e8aa715e1dc6cf8dd86789cab5c5b6b03cdd71c0..14c28f5dd280d9d14c1ce0f25c1a3b92406a5b9e 100644 GIT binary patch delta 1715 zcmV;k22A;+5Bv}mcTYw}00961000Pl01E&B000d@krY3FQ)6vmZ~y=SfB*mhdH?_c z&Sc)60B3A@W&i*I`~Uz1NB{r^j1paX1ZZVpWB>pLL;wH)F8}}lHWlr!gJ@`FVE_OI zbN~PV9smFUBnRFDiD+$fcmMzflmGw#5&!@I92@}v0Bmn#VE_OIr~m)}5C8xG5Cwz> zT5Vx?Z~y={2DktK03QGV03ZPx0JUymZDjxe2G{@q0Zae@0&Su%}(z}WrD$NP~dhRL*O z)qb9!qWM?fDt>R`&^CylMP3^?kd$786Wj; zEXclyptT|tA6&uhli3Qz!K-c^-sbeepuXkNPIBg z`=9kTujz)N|C52~!`%BO{r7(uxsRuJ5BCo6d@cynoDh+EYDs)>6S#7-y_@t~qRs$5x5@%f4Kz@q&u|pD7YpQV z#x>g!1^LBef*3el6%0c)u{BAi@O}RA&_BBL=^1}(-grq>3c%EkEJ8WoFcnG2NfdG& z27;`bwrmLX?(x+Vesea==W_XQw&0oNq^d|ch0QLvkNV*#FA)^ly=}J5P7#GnlP)0; zg{83MmAuG{LTKT>8#ufc?zb@n2VDc97LLV02)_zB z?J9p=-T%V1D;rrRd~!dfll?Nn#l6)Bk~8_||K`L9CsTMbZBiovNsyga3X@V8LFAQC zjqyqFGRVNtvO|alzSnZa8kh%&(>Hc^7q6##yU_VaH?F52+_-VW(CKYM-+Mtf>;vrK z=UseKb7!ZhU4_nWdUxllR;01Th z6b5l7oUq>jTcbTNY!Q%es~->iprs+*A?$l7j{P9(0|WY7xdE?8)>PRAQM#$xQ)O#g zg=Wt>pR4@-t6PSXW!EjIJC){R@Xo|tE4#F1!B^!*qntkBG->m6W12p9aVigPS+RfR zJ6tmFSVvtj-`@M8Ql~#w+UqD$^%AtoMT{QU|?WpSjoW200K-v%msuD z4FAD=1^^mY0nh+=oMn$qP6D$N21fx_ZNV`PfFKM2z*bO*| delta 1476 zcmX9;c{J1u6#mUvGxkgkAu%$;vlhm_JWEu!LuJYH z9yMcMqezVGL-BYt%n2uj_kMc!obTLwzx$o@-GA=gldO=CiFdNI17H9YLKOhwFpHw- zG8UkNKbaT-0I+0;Aw#*jakoi=6q*nS)tdmocNYKzvLj8%$wAkN0Z>hWblMOMkW3L| z5JW;;71S$2Knut$i zC73MG6h0yFnU=1H7MavKAS_Fh3AM3i~?WF4;BH zJ_5)D@(;j(k9DFZl)}&S=_AQHqP9-K8p>IV{BJoW)`pvdEsg2R3#hrM*O#oZGk_Q> zW4ubqFm>%ktqn-XO1j>7^M#A$ufEy9YcBvd&2PgAYAS4Nd!JsVsqV;~ z`YG^SV(!U<;r-g#dQ8&+ma~|!dINKiD3;>FY`&G05yAT2=kM-i$rUbQi<7)4!s(!-o;Bxf?O}ehUSo)(7%NlDhrP* zrPwm+`}W?p!6VBrRX|ed*dO|yDvncuwNhs;{mb;w4|yYHk8hYm=a{?1f;eKWGDbwf z17E#br5@Zf;RH4-wy>yp{{h_*HqH9n8kGxHL~qx)954HPE~F{D2JLg&MuzrG_@SPg z7u!=GT+JMaNceU$)a=T;Opke`REJs2E@7>~8y`KIJ2+%so)JMF{UULX{;~8a<~vx; zdHMNSkC*$Xk4N684|q}9m9Axb7kwC5WlFaR8NMAk;9jbI;wF-&I})&^uHfma>Wl<0 z%S&dT?Fy93nd(BAoN$iER^Rngb+1YPGtV#U`=V$V~<%u&T)2T2T?$S#p z_cAS#5T-L*sqGqBaC*>apBfG^OT}2SJTxvD%YjRcA+Kym^zAL{GN-cN$hh+ zj3mYH;LA1l##Xj_Px-ll%*?&^wrZ&J!rd6O__xCFA)fE8XWiv2&Y5m`$PZ#D}e{->eTD#B;P={3j+44 zaOgupb0UF2fD!F5Vm$yX9*~1>{xD>kVa;$j+zbBthv?9h;Lqp~Jj!PZ8*ZEsbb_7t zC-@};T7v7znRFWc5uHlUp=Z*IQcRwpb+KxALp2LCUmg8AbW_!Dm_{7_FH?OZm4-AU YT~(^8W*a8&39hXwEd>TVT$eoiA37X`TL1t6 diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2 index f15e9bf5433fd78bfb729b31f77579771624c70f..a5639786791efe39150d7a9d65154a6b7bfc5470 100644 GIT binary patch delta 1691 zcmV;M24wlj3#1JgcTYw#00961000K001E&B000d@000J6kr*C-P6~|-ibMf60we<* z3lIPVAO(bN2XPx63sIC#QkU#Uf^FMig;%34mLnP3q1h4&eW}77LACa8&mR8059|QQ zvn54|O+^t14`ghNiqR2~JHk&pcr!fMJkRTkA z;lDF`li3t+qD8!aqUxYRb_!?P9yDp4+&QkYqJulv_&WdnvC_@)wZ(iJR$zr*@&Dh* zx@gO=rUO1YjA)A#EIU6iI>6JA&Sql4hi?EbCy-5Zbj}~%hOn>1?SW2DtVv-%yKZsxe z0}wz&+!Kb=+m;-xZY+k=+Ls;4w^xLvy&NlDp9j5SfKS%l4Qm8&WOCn|Gph_AS2UO$9A=3SEiB?A2GnPi4v+jYewy zR?3(ggWsmF9bB9KZD)&|L_-)PV3>h_U@36&^&NiaoImb#-66dgxijLcqa(DEu0o&Q zJH6DA-Vz!@pN#y$*s97F`+-Sb*E@p0QaG=$P5Y(tDD0H=YKrh$-{bME7$6-$9)Iv zOZG~TOP3IdU|+f@A$cxdjPL8>CHDh==RYWeACe3P(uX2cYf}whQvUX|%cP|;Hmpq& zl1@vJEnpW)w~&w&w>>50QVudCuYS+T-;n?NE1Xe~lb4f&TT5R4mXn{IZ*&z}+iq=2 z8u%RWHBkZu0Q~;~eT=f5YY-KGqm#kyN8h1tbpvWd_kMjhlgGo0gKh6;@&9f2kzVrC zM0~ZDJZlSsU>q0DxE{9wT>X$@OX4*4BHZAAJ%;^2*W-h8T11R!@jA1CG!U=lm4JK z99xjxgzYIi%V7`G7*oMQWgBI4YP4)6hlM(Jedj)!MU`^W>^NiOxh<$LFgV+I#0GgC zd(4+6646RC)#`y=4XcV&%QVAWys1dr^MmlRJ-wW2ICO@glo-i^lG!L)InvNOi9@ex zF*o=1eaKj-q`0QvNpuW)3CLpkO2`v{`iL4*zx$#^%OxWs?Xad)nUOk>hvmYk5>=wK zA6x%p)yn+a002R4GX($u delta 1470 zcmV;v1ws0x4af@^cTYw#00961000HZ01E&B000aa000Ggkr*C-I0}mth;sop0we<% z3l0DTAO(bN2VEN;3K5m0JCXfN$mIU8ya#n^?61=#5#7%q+S1t*ICilY{8cVV%M&q& znCQ_uw2nu_tp>n-`(?YP*4-dXgxT)S(tP#5yzKV;$%Zx_r%?(aK{y&9fPm~D$6o$* zUg!DlzJIVn&j`(bvZfEX8(2hJl<;N+OjDeRZT1MVc?GlJGkKNEfdHULfecckRs%OA z2$0(m0Hj~X{94dhP0}&GUJ9iks-^u5bM{p7e;2`plS8CFuW;ay1 zOYXMhm2V2#@d}j7SpoP*0XzU1fB=wwti1>LOe!aG%4G|GC<&e_AxwVb=qvHEl=IyWa2NThE4P9b7{u{a#zn0aIIoo9=dmEU%*_PUaL z&QuI8u|;P9^VXP5)6N!OTIuB37JGXV1_laB)WfnKhBYUWg~`WV`n4xh#A&BIvo~9X z=$355yloCKwl!xc1#1aLx)F+(jV>aD_NYs_@)s!!*hu1CNcFz?K-)_^cYohPyXX$g zrgOJ{+P#x3EyMGeKi?Cbym)m%3n4mxjIxF4{M)=+bF{M;x7N+eyqgi-vZY<-T}rjS z#VaqF_HC{K1~ZmkTN@_Htvz_8UoZAoz_kA&QNH=?q!Bfbbn9Lv$jzos_nD?oNWB>u z(Tqr@tKLaZk3&6L(6!S;&*iVz;%D14BN@McJFCr+hu8A_v>~m{^3$FkJOLB9Su zq2BbO-+m{w>BV~LAIo!|ftmH`jN&zR{rR1?osN}Q&UfMY!=C&PXHpxWfu(e#__mED zH{G?r^Yd$=W_||$omf_G*4!1loE(t|Xp#yKIB2n~M}MP&iaHXML|q5wOS z2IBo)N@*G`hy78S3J#oV$pD@t3n5@+5HT)LkyHk%(EZU&0(C^D0Z`ye0AchE9KCJ8 zGx|XYm|qZ?{ez0JBTy~6Iwh0SkX3A;`))$FhyvJS}}d_c}W*ixT1sbQ?lc z7garVj}>}}J+`;5F=Jwth3>?E)iBRONo7Q^qBl#IvSjWKMUZ8esZN1oAR^vpL_Y|` zigz-Zp;#+#eEb9GyqY=MQ}bYdff4cnt0%QW1;o949aZyDILLm}X&6NrVZT@`$Q@Z> zR&hZRztIQ+Q|Mn#bf2gODdOtSqd~D@1%+&JQ0XUR74GSbACo0K1(NOyv_S* Y(qTjXj?g 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
{this.state.errorMsg}
{errorMsg}
6d@cynoDh+EYDs)>6S#7-y_@t~qRs$5x5@%f4Kz@q&u|pD7YpQV z#x>g!1^LBef*3el6%0c)u{BAi@O}RA&_BBL=^1}(-grq>3c%EkEJ8WoFcnG2NfdG& z27;`bwrmLX?(x+Vesea==W_XQw&0oNq^d|ch0QLvkNV*#FA)^ly=}J5P7#GnlP)0; zg{83MmAuG{LTKT>8#ufc?zb@n2VDc97LLV02)_zB z?J9p=-T%V1D;rrRd~!dfll?Nn#l6)Bk~8_||K`L9CsTMbZBiovNsyga3X@V8LFAQC zjqyqFGRVNtvO|alzSnZa8kh%&(>Hc^7q6##yU_VaH?F52+_-VW(CKYM-+Mtf>;vrK z=UseKb7!ZhU4_nWdUxllR;01Th z6b5l7oUq>jTcbTNY!Q%es~->iprs+*A?$l7j{P9(0|WY7xdE?8)>PRAQM#$xQ)O#g zg=Wt>pR4@-t6PSXW!EjIJC){R@Xo|tE4#F1!B^!*qntkBG->m6W12p9aVigPS+RfR zJ6tmFSVvtj-`@M8Ql~#w+UqD$^%AtoMT{QU|?WpSjoW200K-v%msuD z4FAD=1^^mY0nh+=oMn$qP6D$N21fx_ZNV`PfFKM2z*bO*| delta 1476 zcmX9;c{J1u6#mUvGxkgkAu%$;vlhm_JWEu!LuJYH z9yMcMqezVGL-BYt%n2uj_kMc!obTLwzx$o@-GA=gldO=CiFdNI17H9YLKOhwFpHw- zG8UkNKbaT-0I+0;Aw#*jakoi=6q*nS)tdmocNYKzvLj8%$wAkN0Z>hWblMOMkW3L| z5JW;;71S$2Knut$i zC73MG6h0yFnU=1H7MavKAS_Fh3AM3i~?WF4;BH zJ_5)D@(;j(k9DFZl)}&S=_AQHqP9-K8p>IV{BJoW)`pvdEsg2R3#hrM*O#oZGk_Q> zW4ubqFm>%ktqn-XO1j>7^M#A$ufEy9YcBvd&2PgAYAS4Nd!JsVsqV;~ z`YG^SV(!U<;r-g#dQ8&+ma~|!dINKiD3;>FY`&G05yAT2=kM-i$rUbQi<7)4!s(!-o;Bxf?O}ehUSo)(7%NlDhrP* zrPwm+`}W?p!6VBrRX|ed*dO|yDvncuwNhs;{mb;w4|yYHk8hYm=a{?1f;eKWGDbwf z17E#br5@Zf;RH4-wy>yp{{h_*HqH9n8kGxHL~qx)954HPE~F{D2JLg&MuzrG_@SPg z7u!=GT+JMaNceU$)a=T;Opke`REJs2E@7>~8y`KIJ2+%so)JMF{UULX{;~8a<~vx; zdHMNSkC*$Xk4N684|q}9m9Axb7kwC5WlFaR8NMAk;9jbI;wF-&I})&^uHfma>Wl<0 z%S&dT?Fy93nd(BAoN$iER^Rngb+1YPGtV#U`=V$V~<%u&T)2T2T?$S#p z_cAS#5T-L*sqGqBaC*>apBfG^OT}2SJTxvD%YjRcA+Kym^zAL{GN-cN$hh+ zj3mYH;LA1l##Xj_Px-ll%*?&^wrZ&J!rd6O__xCFA)fE8XWiv2&Y5m`$PZ#D}e{->eTD#B;P={3j+44 zaOgupb0UF2fD!F5Vm$yX9*~1>{xD>kVa;$j+zbBthv?9h;Lqp~Jj!PZ8*ZEsbb_7t zC-@};T7v7znRFWc5uHlUp=Z*IQcRwpb+KxALp2LCUmg8AbW_!Dm_{7_FH?OZm4-AU YT~(^8W*a8&39hXwEd>TVT$eoiA37X`TL1t6 diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2 index f15e9bf5433fd78bfb729b31f77579771624c70f..a5639786791efe39150d7a9d65154a6b7bfc5470 100644 GIT binary patch delta 1691 zcmV;M24wlj3#1JgcTYw#00961000K001E&B000d@000J6kr*C-P6~|-ibMf60we<* z3lIPVAO(bN2XPx63sIC#QkU#Uf^FMig;%34mLnP3q1h4&eW}77LACa8&mR8059|QQ zvn54|O+^t14`ghNiqR2~JHk&pcr!fMJkRTkA z;lDF`li3t+qD8!aqUxYRb_!?P9yDp4+&QkYqJulv_&WdnvC_@)wZ(iJR$zr*@&Dh* zx@gO=rUO1YjA)A#EIU6iI>6JA&Sql4hi?EbCy-5Zbj}~%hOn>1?SW2DtVv-%yKZsxe z0}wz&+!Kb=+m;-xZY+k=+Ls;4w^xLvy&NlDp9j5SfKS%l4Qm8&WOCn|Gph_AS2UO$9A=3SEiB?A2GnPi4v+jYewy zR?3(ggWsmF9bB9KZD)&|L_-)PV3>h_U@36&^&NiaoImb#-66dgxijLcqa(DEu0o&Q zJH6DA-Vz!@pN#y$*s97F`+-Sb*E@p0QaG=$P5Y(tDD0H=YKrh$-{bME7$6-$9)Iv zOZG~TOP3IdU|+f@A$cxdjPL8>CHDh==RYWeACe3P(uX2cYf}whQvUX|%cP|;Hmpq& zl1@vJEnpW)w~&w&w>>50QVudCuYS+T-;n?NE1Xe~lb4f&TT5R4mXn{IZ*&z}+iq=2 z8u%RWHBkZu0Q~;~eT=f5YY-KGqm#kyN8h1tbpvWd_kMjhlgGo0gKh6;@&9f2kzVrC zM0~ZDJZlSsU>q0DxE{9wT>X$@OX4*4BHZAAJ%;^2*W-h8T11R!@jA1CG!U=lm4JK z99xjxgzYIi%V7`G7*oMQWgBI4YP4)6hlM(Jedj)!MU`^W>^NiOxh<$LFgV+I#0GgC zd(4+6646RC)#`y=4XcV&%QVAWys1dr^MmlRJ-wW2ICO@glo-i^lG!L)InvNOi9@ex zF*o=1eaKj-q`0QvNpuW)3CLpkO2`v{`iL4*zx$#^%OxWs?Xad)nUOk>hvmYk5>=wK zA6x%p)yn+a002R4GX($u delta 1470 zcmV;v1ws0x4af@^cTYw#00961000HZ01E&B000aa000Ggkr*C-I0}mth;sop0we<% z3l0DTAO(bN2VEN;3K5m0JCXfN$mIU8ya#n^?61=#5#7%q+S1t*ICilY{8cVV%M&q& znCQ_uw2nu_tp>n-`(?YP*4-dXgxT)S(tP#5yzKV;$%Zx_r%?(aK{y&9fPm~D$6o$* zUg!DlzJIVn&j`(bvZfEX8(2hJl<;N+OjDeRZT1MVc?GlJGkKNEfdHULfecckRs%OA z2$0(m0Hj~X{94dhP0}&GUJ9iks-^u5bM{p7e;2`plS8CFuW;ay1 zOYXMhm2V2#@d}j7SpoP*0XzU1fB=wwti1>LOe!aG%4G|GC<&e_AxwVb=qvHEl=IyWa2NThE4P9b7{u{a#zn0aIIoo9=dmEU%*_PUaL z&QuI8u|;P9^VXP5)6N!OTIuB37JGXV1_laB)WfnKhBYUWg~`WV`n4xh#A&BIvo~9X z=$355yloCKwl!xc1#1aLx)F+(jV>aD_NYs_@)s!!*hu1CNcFz?K-)_^cYohPyXX$g zrgOJ{+P#x3EyMGeKi?Cbym)m%3n4mxjIxF4{M)=+bF{M;x7N+eyqgi-vZY<-T}rjS z#VaqF_HC{K1~ZmkTN@_Htvz_8UoZAoz_kA&QNH=?q!Bfbbn9Lv$jzos_nD?oNWB>u z(Tqr@tKLaZk3&6L(6!S;&*iVz;%D14BN@McJFCr+hu8A_v>~m{^3$FkJOLB9Su zq2BbO-+m{w>BV~LAIo!|ftmH`jN&zR{rR1?osN}Q&UfMY!=C&PXHpxWfu(e#__mED zH{G?r^Yd$=W_||$omf_G*4!1loE(t|Xp#yKIB2n~M}MP&iaHXML|q5wOS z2IBo)N@*G`hy78S3J#oV$pD@t3n5@+5HT)LkyHk%(EZU&0(C^D0Z`ye0AchE9KCJ8 zGx|XYm|qZ?{ez0JBTy~6Iwh0SkX3A;`))$FhyvJS}}d_c}W*ixT1sbQ?lc z7garVj}>}}J+`;5F=Jwth3>?E)iBRONo7Q^qBl#IvSjWKMUZ8esZN1oAR^vpL_Y|` zigz-Zp;#+#eEb9GyqY=MQ}bYdff4cnt0%QW1;o949aZyDILLm}X&6NrVZT@`$Q@Z> zR&hZRztIQ+Q|Mn#bf2gODdOtSqd~D@1%+&JQ0XUR74GSbACo0K1(NOyv_S* Y(qTjXj?g 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
{% trans "Hi," %}
+{% blocktrans %}{{ inviter }} revoke your access to {{ site_name }}.{% endblocktrans %} +