- this.tabItemClick('invitations')}>
+ this.tabItemClick(e, 'invitations')}>
{gettext('Invite People')}
diff --git a/frontend/src/components/org-admin-group-nav.js b/frontend/src/components/org-admin-group-nav.js
new file mode 100644
index 0000000000..ae0d728b1a
--- /dev/null
+++ b/frontend/src/components/org-admin-group-nav.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@reach/router';
+import { siteRoot, gettext } from '../utils/constants';
+
+const propTypes = {
+ groupID: PropTypes.string.isRequired,
+ currentItem: PropTypes.string.isRequired
+};
+
+class OrgAdminGroupNav extends React.Component {
+
+ render() {
+ const { groupID, currentItem } = this.props;
+ const urlBase = `${siteRoot}org/groupadmin/${groupID}/`;
+ return (
+
+
+ -
+ {gettext('Group Info')}
+
+ -
+ {gettext('Libraries')}
+
+ -
+ {gettext('Members')}
+
+
+
+ );
+ }
+}
+
+OrgAdminGroupNav.propTypes = propTypes;
+
+export default OrgAdminGroupNav;
diff --git a/frontend/src/components/org-admin-user-nav.js b/frontend/src/components/org-admin-user-nav.js
new file mode 100644
index 0000000000..3e15d5ab1d
--- /dev/null
+++ b/frontend/src/components/org-admin-user-nav.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@reach/router';
+import { siteRoot, gettext } from '../utils/constants';
+
+const propTypes = {
+ email: PropTypes.string.isRequired,
+ currentItem: PropTypes.string.isRequired
+};
+
+class OrgAdminUserNav extends React.Component {
+
+ render() {
+ const { email, currentItem } = this.props;
+ const urlBase = `${siteRoot}org/useradmin/info/${encodeURIComponent(email)}/`;
+ return (
+
+
+ -
+ {gettext('Profile')}
+
+ -
+ {gettext('Owned Libraries')}
+
+ -
+ {gettext('Shared Libraries')}
+
+
+
+ );
+ }
+}
+
+OrgAdminUserNav.propTypes = propTypes;
+
+export default OrgAdminUserNav;
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/css/org-admin-user.css b/frontend/src/css/org-admin-user.css
new file mode 100644
index 0000000000..9ebdeb8ced
--- /dev/null
+++ b/frontend/src/css/org-admin-user.css
@@ -0,0 +1,3 @@
+.cur-view-path.org-admin-user-nav {
+ padding: 0 16px 1px;
+}
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/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js
index ae71320bc5..a62a2ebad4 100644
--- a/frontend/src/pages/org-admin/index.js
+++ b/frontend/src/pages/org-admin/index.js
@@ -5,7 +5,13 @@ import { Router } from '@reach/router';
import { siteRoot } from '../../utils/constants';
import SidePanel from './side-panel';
import OrgUsers from './org-users';
+import OrgUserProfile from './org-user-profile';
+import OrgUserRepos from './org-user-repos';
+import OrgUserSharedRepos from './org-user-shared-repos';
import OrgGroups from './org-groups';
+import OrgGroupInfo from './org-group-info';
+import OrgGroupRepos from './org-group-repos';
+import OrgGroupMembers from './org-group-members';
import OrgLibraries from './org-libraries';
import OrgInfo from './org-info';
import OrgLinks from './org-links';
@@ -35,10 +41,14 @@ class Org extends React.Component {
componentDidMount() {
let href = window.location.href.split('/');
let currentTab = href[href.length - 2];
- if (currentTab == 'useradmin') {
+
+ if (location.href.indexOf(`${siteRoot}org/useradmin`) != -1) {
currentTab = 'users';
}
- if (currentTab > 0) {
+ if (location.href.indexOf(`${siteRoot}org/groupadmin`) != -1) {
+ currentTab = 'groupadmin';
+ }
+ if (location.href.indexOf(`${siteRoot}org/departmentadmin`) != -1) {
currentTab = 'departmentadmin';
}
this.setState({currentTab: currentTab});
@@ -61,7 +71,13 @@ class Org extends React.Component {
-
+
+
+
+
+
+
+
@@ -69,9 +85,9 @@ class Org extends React.Component {
-
-
-
+
+
+
diff --git a/frontend/src/pages/org-admin/org-department-item.js b/frontend/src/pages/org-admin/org-department-item.js
index 09766b82b8..9f81356140 100644
--- a/frontend/src/pages/org-admin/org-department-item.js
+++ b/frontend/src/pages/org-admin/org-department-item.js
@@ -59,7 +59,7 @@ class OrgDepartmentItem extends React.Component {
}
listOrgGroupRepo = (groupID) => {
- seafileAPI.orgAdminListDepartGroupRepos(orgID, groupID).then(res => {
+ seafileAPI.orgAdminListGroupRepos(orgID, groupID).then(res => {
this.setState({ repos: res.data.libraries });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
@@ -392,7 +392,7 @@ class MemberItem extends React.Component {
onChangeUserRole = (role) => {
let isAdmin = role === 'Admin' ? true : false;
- seafileAPI.orgAdminSetDepartGroupUserRole(orgID, this.props.groupID, this.props.member.email, isAdmin).then((res) => {
+ seafileAPI.orgAdminSetGroupMemberRole(orgID, this.props.groupID, this.props.member.email, isAdmin).then((res) => {
this.props.onMemberChanged();
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
diff --git a/frontend/src/pages/org-admin/org-group-info.js b/frontend/src/pages/org-admin/org-group-info.js
new file mode 100644
index 0000000000..57209c43f1
--- /dev/null
+++ b/frontend/src/pages/org-admin/org-group-info.js
@@ -0,0 +1,105 @@
+import React, { Component, Fragment } from 'react';
+import { Link } from '@reach/router';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext, loginUrl, siteRoot } from '../../utils/constants';
+import Loading from '../../components/loading';
+import OrgAdminGroupNav from '../../components/org-admin-group-nav';
+import MainPanelTopbar from './main-panel-topbar';
+
+import '../../css/org-admin-user.css';
+
+const { orgID } = window.org.pageOptions;
+
+class OrgGroupInfo extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: ''
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.orgAdminGetGroup(orgID, this.props.groupID).then((res) => {
+ this.setState(Object.assign({
+ loading: false
+ }, res.data));
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ };
+ }
+
+ render() {
+ const {
+ loading, errorMsg,
+ group_name, creator_email, creator_name
+ } = this.props.data;
+
+ if (loading) {
+ return ;
+ }
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ return (
+
+ - {gettext('Name')}
+ - {group_name}
+
+ - {gettext('Creator')}
+ -
+ {creator_name}
+
+
+ );
+ }
+}
+
+export default OrgGroupInfo;
diff --git a/frontend/src/pages/org-admin/org-group-members.js b/frontend/src/pages/org-admin/org-group-members.js
new file mode 100644
index 0000000000..342feb390b
--- /dev/null
+++ b/frontend/src/pages/org-admin/org-group-members.js
@@ -0,0 +1,146 @@
+import React, { Component, Fragment } from 'react';
+import { Link } from '@reach/router';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext, loginUrl, siteRoot } from '../../utils/constants';
+import Loading from '../../components/loading';
+import OrgAdminGroupNav from '../../components/org-admin-group-nav';
+import MainPanelTopbar from './main-panel-topbar';
+
+import '../../css/org-admin-user.css';
+
+const { orgID } = window.org.pageOptions;
+
+class OrgGroupMembers extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: ''
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.orgAdminListGroupMembers(orgID, this.props.groupID).then((res) => {
+ this.setState(Object.assign({
+ loading: false
+ }, res.data));
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {
+ loading, errorMsg, members
+ } = this.props.data;
+
+ if (loading) {
+ return ;
+ }
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ return (
+
+
+
+
+ |
+ {gettext('Name')} |
+ {gettext('Role')} |
+
+
+
+ {members.map((item, index) => {
+ return ;
+ })}
+
+
+
+ );
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ getRoleText() {
+ switch (this.props.data.role) {
+ case 'Owner':
+ return gettext('Owner');
+ case 'Admin':
+ return gettext('Admin');
+ case 'Member':
+ return gettext('Member');
+ }
+ }
+
+ render() {
+ const item = this.props.data;
+ return (
+
+
+
+
+ |
+
+ {item.name}
+ |
+
+ {this.getRoleText()}
+ |
+
+
+ );
+ }
+}
+
+export default OrgGroupMembers;
diff --git a/frontend/src/pages/org-admin/org-group-repos.js b/frontend/src/pages/org-admin/org-group-repos.js
new file mode 100644
index 0000000000..90f8d25132
--- /dev/null
+++ b/frontend/src/pages/org-admin/org-group-repos.js
@@ -0,0 +1,195 @@
+import React, { Component, Fragment } from 'react';
+import { Link } from '@reach/router';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext, loginUrl, siteRoot } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+import Loading from '../../components/loading';
+import toaster from '../../components/toast';
+import OrgAdminGroupNav from '../../components/org-admin-group-nav';
+import DeleteRepoDialog from '../../components/dialog/delete-repo-dialog';
+import MainPanelTopbar from './main-panel-topbar';
+
+import '../../css/org-admin-user.css';
+
+const { orgID } = window.org.pageOptions;
+
+class OrgGroupRepos extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: ''
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.orgAdminListGroupRepos(orgID, this.props.groupID).then((res) => {
+ this.setState(Object.assign({
+ loading: false
+ }, res.data));
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {
+ loading, errorMsg, libraries
+ } = this.props.data;
+
+ if (loading) {
+ return ;
+ }
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ return (
+
+
+
+
+ {/*icon*/} |
+ {gettext('Name')} |
+ {gettext('Size')} |
+ {gettext('Shared By')} |
+ |
+
+
+
+ {libraries.map((item, index) => {
+ return ;
+ })}
+
+
+
+ );
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false,
+ deleted: false,
+ isDeleteRepoDialogOpen: false
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({
+ isOpIconShown: true
+ });
+ }
+
+ handleMouseOut = () => {
+ this.setState({
+ isOpIconShown: false
+ });
+ }
+
+ handleDeleteIconClick = (e) => {
+ e.preventDefault();
+ this.toggleDeleteRepoDialog();
+ }
+
+ toggleDeleteRepoDialog = () => {
+ this.setState({
+ isDeleteRepoDialogOpen: !this.state.isDeleteRepoDialogOpen
+ });
+ }
+
+ deleteRepo = () => {
+ const repo = this.props.data;
+ seafileAPI.deleteOrgRepo(orgID, repo.repo_id).then((res) => {
+ this.setState({
+ deleted: true
+ });
+ const msg = gettext('Successfully deleted {name}.').replace('{name}', repo.name);
+ toaster.success(msg);
+ }).catch((error) => {
+ const errorMsg = Utils.getErrorMsg(error);
+ toaster.danger(errorMsg);
+ });
+ }
+
+ render() {
+ const { deleted, isOpIconShown, isDeleteRepoDialogOpen } = this.state;
+ const repo = this.props.data;
+
+ if (deleted) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ |
+ {repo.name} |
+ {Utils.bytesToSize(repo.size)} |
+ {repo.shared_by_name} |
+
+
+ |
+
+ {isDeleteRepoDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+export default OrgGroupRepos;
diff --git a/frontend/src/pages/org-admin/org-user-item.js b/frontend/src/pages/org-admin/org-user-item.js
index 039c3dda56..305fa12030 100644
--- a/frontend/src/pages/org-admin/org-user-item.js
+++ b/frontend/src/pages/org-admin/org-user-item.js
@@ -56,12 +56,15 @@ class UserItem extends React.Component {
toggleResetPW = () => {
const email = this.props.user.email;
+ toaster.success(gettext('Resetting user\'s password, please wait for a moment.'));
seafileAPI.resetOrgUserPassword(orgID, email).then(res => {
let msg;
msg = gettext('Successfully reset password to %(passwd)s for user %(user)s.');
msg = msg.replace('%(passwd)s', res.data.new_password);
msg = msg.replace('%(user)s', email);
- toaster.success(msg);
+ toaster.success(msg, {
+ duration: 15
+ });
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
diff --git a/frontend/src/pages/org-admin/org-user-profile.js b/frontend/src/pages/org-admin/org-user-profile.js
new file mode 100644
index 0000000000..99c2e43a50
--- /dev/null
+++ b/frontend/src/pages/org-admin/org-user-profile.js
@@ -0,0 +1,202 @@
+import React, { Component, Fragment } from 'react';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext, loginUrl } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+import Loading from '../../components/loading';
+import OrgAdminUserNav from '../../components/org-admin-user-nav';
+import SetOrgUserName from '../../components/dialog/set-org-user-name';
+import SetOrgUserContactEmail from '../../components/dialog/set-org-user-contact-email';
+import SetOrgUserQuota from '../../components/dialog/set-org-user-quota';
+import MainPanelTopbar from './main-panel-topbar';
+
+import '../../css/org-admin-user.css';
+
+const { orgID, orgName } = window.org.pageOptions;
+
+class OrgUserProfile extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: ''
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.getOrgUserInfo(orgID, this.props.email).then((res) => {
+ this.setState(Object.assign({
+ loading: false
+ }, res.data));
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ updateName = (name) => {
+ this.setState({
+ name: name
+ });
+ }
+
+ updateContactEmail = (contactEmail) => {
+ this.setState({
+ contact_email: contactEmail
+ });
+ }
+
+ updateQuota = (quota) => {
+ this.setState({
+ quota_total: quota
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isSetNameDialogOpen: false,
+ isSetContactEmailDialogOpen: false,
+ isSetQuotaDialogOpen: false
+ };
+ }
+
+ toggleSetNameDialog = () => {
+ this.setState({
+ isSetNameDialogOpen: !this.state.isSetNameDialogOpen
+ });
+ }
+
+ toggleSetContactEmailDialog = () => {
+ this.setState({
+ isSetContactEmailDialogOpen: !this.state.isSetContactEmailDialogOpen
+ });
+ }
+
+ toggleSetQuotaDialog = () => {
+ this.setState({
+ isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen
+ });
+ }
+
+ render() {
+ const {
+ loading, errorMsg,
+ avatar_url, email, contact_email,
+ name, quota_total, quota_usage
+ } = this.props.data;
+ const { isSetNameDialogOpen, isSetContactEmailDialogOpen, isSetQuotaDialogOpen } = this.state;
+
+ if (loading) {
+ return ;
+ }
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ return (
+
+
+ - {gettext('Avatar')}
+ -
+
+
+
+ - ID
+ - {email}
+
+ - {gettext('Name')}
+ -
+ {name || '--'}
+
+
+
+ - {gettext('Contact Email')}
+ -
+ {contact_email || '--'}
+
+
+
+ - {gettext('Organization')}
+ - {orgName}
+
+ - {gettext('Space Used / Quota')}
+ -
+ {`${Utils.bytesToSize(quota_usage)}${quota_total > 0 ? ' / ' + Utils.bytesToSize(quota_total) : ''}`}
+
+
+
+ {isSetNameDialogOpen &&
+
+ }
+ {isSetContactEmailDialogOpen &&
+
+ }
+ {isSetQuotaDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+export default OrgUserProfile;
diff --git a/frontend/src/pages/org-admin/org-user-repos.js b/frontend/src/pages/org-admin/org-user-repos.js
new file mode 100644
index 0000000000..fb8f4ba87b
--- /dev/null
+++ b/frontend/src/pages/org-admin/org-user-repos.js
@@ -0,0 +1,195 @@
+import React, { Component, Fragment } from 'react';
+import moment from 'moment';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext, loginUrl } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+import Loading from '../../components/loading';
+import toaster from '../../components/toast';
+import OrgAdminUserNav from '../../components/org-admin-user-nav';
+import DeleteRepoDialog from '../../components/dialog/delete-repo-dialog';
+import MainPanelTopbar from './main-panel-topbar';
+
+import '../../css/org-admin-user.css';
+
+const { orgID } = window.org.pageOptions;
+
+class OrgUserOwnedRepos extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: ''
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.getOrgUserOwnedRepos(orgID, this.props.email).then((res) => {
+ this.setState(Object.assign({
+ loading: false
+ }, res.data));
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {
+ loading, errorMsg, repo_list
+ } = this.props.data;
+
+ if (loading) {
+ return ;
+ }
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ return (
+
+
+
+
+ {/*icon*/} |
+ {gettext('Name')} |
+ {gettext('Size')} |
+ {gettext('Last Update')} |
+ |
+
+
+
+ {repo_list.map((item, index) => {
+ return ;
+ })}
+
+
+
+ );
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false,
+ deleted: false,
+ isDeleteRepoDialogOpen: false
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({
+ isOpIconShown: true
+ });
+ }
+
+ handleMouseOut = () => {
+ this.setState({
+ isOpIconShown: false
+ });
+ }
+
+ handleDeleteIconClick = (e) => {
+ e.preventDefault();
+ this.toggleDeleteRepoDialog();
+ }
+
+ toggleDeleteRepoDialog = () => {
+ this.setState({
+ isDeleteRepoDialogOpen: !this.state.isDeleteRepoDialogOpen
+ });
+ }
+
+ deleteRepo = () => {
+ const repo = this.props.data;
+ seafileAPI.deleteOrgRepo(orgID, repo.repo_id).then((res) => {
+ this.setState({
+ deleted: true
+ });
+ const msg = gettext('Successfully deleted {name}.').replace('{name}', repo.repo_name);
+ toaster.success(msg);
+ }).catch((error) => {
+ const errorMsg = Utils.getErrorMsg(error);
+ toaster.danger(errorMsg);
+ });
+ }
+
+ render() {
+ const { deleted, isOpIconShown, isDeleteRepoDialogOpen } = this.state;
+ const repo = this.props.data;
+
+ if (deleted) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ |
+ {repo.repo_name} |
+ {Utils.bytesToSize(repo.size)} |
+ {moment(repo.last_modified).format('YYYY-MM-DD')} |
+
+
+ |
+
+ {isDeleteRepoDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+export default OrgUserOwnedRepos;
diff --git a/frontend/src/pages/org-admin/org-user-shared-repos.js b/frontend/src/pages/org-admin/org-user-shared-repos.js
new file mode 100644
index 0000000000..2c698d705b
--- /dev/null
+++ b/frontend/src/pages/org-admin/org-user-shared-repos.js
@@ -0,0 +1,132 @@
+import React, { Component, Fragment } from 'react';
+import moment from 'moment';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext, loginUrl } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+import Loading from '../../components/loading';
+import OrgAdminUserNav from '../../components/org-admin-user-nav';
+import MainPanelTopbar from './main-panel-topbar';
+
+import '../../css/org-admin-user.css';
+
+const { orgID } = window.org.pageOptions;
+
+class OrgUserSharedRepos extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: ''
+ };
+ }
+
+ componentDidMount() {
+ seafileAPI.getOrgUserBesharedRepos(orgID, this.props.email).then((res) => {
+ this.setState(Object.assign({
+ loading: false
+ }, res.data));
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {
+ loading, errorMsg, repo_list
+ } = this.props.data;
+
+ if (loading) {
+ return ;
+ }
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+
+ return (
+
+
+
+ {/*icon*/} |
+ {gettext('Name')} |
+ {gettext('Owner')} |
+ {gettext('Size')} |
+ {gettext('Last Update')} |
+
+
+
+ {repo_list.map((item, index) => {
+ return ;
+ })}
+
+
+ );
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const repo = this.props.data;
+ return (
+
+
+
+ |
+ {repo.repo_name} |
+ {repo.owner_name} |
+ {Utils.bytesToSize(repo.size)} |
+ {moment(repo.last_modified).format('YYYY-MM-DD')} |
+
+ );
+ }
+}
+
+export default OrgUserSharedRepos;
diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js
index a44aa3389a..c60602255d 100644
--- a/frontend/src/utils/utils.js
+++ b/frontend/src/utils/utils.js
@@ -983,6 +983,13 @@ export const Utils = {
}
return false;
+ },
+
+ registerGlobalVariable(namespace, key, value) {
+ if (!window[namespace]) {
+ window[namespace] = {};
+ }
+ window[namespace][key] = value;
}
};
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/api2/views.py b/seahub/api2/views.py
index e136bc3da5..7d5ab86e79 100644
--- a/seahub/api2/views.py
+++ b/seahub/api2/views.py
@@ -999,7 +999,7 @@ class Repos(APIView):
storage_id = request.data.get("storage_id", None)
if storage_id and storage_id not in [s['storage_id'] for s in storages]:
error_msg = 'storage_id invalid.'
- return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ return None, api_error(status.HTTP_400_BAD_REQUEST, error_msg)
repo_id = seafile_api.create_repo(repo_name,
repo_desc, username, passwd,
@@ -1043,7 +1043,7 @@ class Repos(APIView):
salt = request.data.get('salt', '')
if not salt:
error_msg = 'salt invalid.'
- return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ return None, api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if len(magic) != 64 or len(random_key) != 96 or enc_version < 0:
return None, api_error(status.HTTP_400_BAD_REQUEST,
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/share/utils.py b/seahub/share/utils.py
index 68ad86f110..a88eecbafd 100644
--- a/seahub/share/utils.py
+++ b/seahub/share/utils.py
@@ -12,30 +12,40 @@ logger = logging.getLogger(__name__)
def is_repo_admin(username, repo_id):
- repo_owner = seafile_api.get_repo_owner(repo_id)
-
+ # repo is shared to user with admin permission
try:
- if '@seafile_group' in repo_owner:
- # is group owned repo
- group_id = int(repo_owner.split('@')[0])
+ user_share_permission = ExtraSharePermission.objects. \
+ get_user_permission(repo_id, username)
+ if user_share_permission == PERMISSION_ADMIN:
+ return True
+
+ # get all groups that repo is shared to with admin permission
+ group_ids = ExtraGroupsSharePermission.objects.get_admin_groups_by_repo(repo_id)
+ for group_id in group_ids:
if is_group_admin(group_id, username):
return True
- else:
- user_share_permission = ExtraSharePermission.objects.\
- get_user_permission(repo_id, username)
- if user_share_permission == PERMISSION_ADMIN:
- return True
-
- # get all groups that repo is shared to with admin permission
- group_ids = ExtraGroupsSharePermission.objects.get_admin_groups_by_repo(repo_id)
- for group_id in group_ids:
- if is_group_admin(group_id, username):
- return True
- return False
except Exception as e:
logger.error(e)
return False
+ repo_owner = seafile_api.get_repo_owner(repo_id) or seafile_api.get_org_repo_owner(repo_id)
+ if not repo_owner:
+ logger.error('repo %s owner is None' % repo_id)
+ return False
+
+ # repo owner
+ if username == repo_owner:
+ return True
+
+ # user is department admin
+ if '@seafile_group' in repo_owner:
+ # is group owned repo
+ group_id = int(repo_owner.split('@')[0])
+ if is_group_admin(group_id, username):
+ return True
+
+ return False
+
def share_dir_to_user(repo, path, owner, share_from, share_to, permission, org_id=None):
# Share repo or subdir to user with permission(r, rw, admin).
extra_share_permission = ''
diff --git a/seahub/urls.py b/seahub/urls.py
index 7cc7a9f8f0..311d52c9cb 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
@@ -407,6 +407,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