diff --git a/frontend/config/webpack.entry.js b/frontend/config/webpack.entry.js
index 9b88e763b9..52f6070493 100644
--- a/frontend/config/webpack.entry.js
+++ b/frontend/config/webpack.entry.js
@@ -39,6 +39,7 @@ const entryFiles = {
search: '/pages/search',
uploadLink: '/pages/upload-link',
subscription: '/subscription.js',
+ institutionAdmin: '/pages/institution-admin/index.js'
};
const getEntries = (isEnvDevelopment) => {
diff --git a/frontend/src/pages/institution-admin/api/index.js b/frontend/src/pages/institution-admin/api/index.js
new file mode 100644
index 0000000000..76693a4200
--- /dev/null
+++ b/frontend/src/pages/institution-admin/api/index.js
@@ -0,0 +1,122 @@
+import axios from 'axios';
+import cookie from 'react-cookies';
+import { siteRoot } from '../../../utils/constants';
+
+class InstAdminAPI {
+
+ init({ server, username, password, token }) {
+ this.server = server;
+ this.username = username;
+ this.password = password;
+ this.token = token; //none
+ if (this.token && this.server) {
+ this.req = axios.create({
+ baseURL: this.server,
+ headers: { 'Authorization': 'Token ' + this.token },
+ });
+ }
+ return this;
+ }
+
+ initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
+ if (siteRoot && siteRoot.charAt(siteRoot.length-1) === '/') {
+ var server = siteRoot.substring(0, siteRoot.length-1);
+ this.server = server;
+ } else {
+ this.server = siteRoot;
+ }
+
+ this.req = axios.create({
+ headers: {
+ 'X-CSRFToken': xcsrfHeaders,
+ }
+ });
+ return this;
+ }
+
+ _sendPostRequest(url, form) {
+ if (form.getHeaders) {
+ return this.req.post(url, form, {
+ headers:form.getHeaders()
+ });
+ } else {
+ return this.req.post(url, form);
+ }
+ }
+
+ getToken() {
+ const url = this.server + '/api2/auth-token/';
+ axios.post(url, {
+ username: this.username,
+ password: this.password
+ }).then((response) => {
+ this.token = response.data;
+ return this.token;
+ });
+ }
+
+ listInstitutionUsers(page, perPage) {
+ const url = this.server + '/api/v2.1/institutions/admin/users/';
+ let params = {
+ };
+ if (page != undefined) {
+ params.page = page;
+ }
+ if (perPage != undefined) {
+ params.per_page = perPage;
+ }
+ return this.req.get(url);
+ }
+
+ searchInstitutionUsers(q) {
+ const url = this.server + '/api/v2.1/institutions/admin/search-user/';
+ const params = {
+ q: q
+ };
+ return this.req.get(url, {params: params});
+ }
+
+ deleteInstitutionUser(email) {
+ const url = this.server + '/api/v2.1/institutions/admin/users/' + encodeURIComponent(email) + '/';
+ return this.req.delete(url);
+ }
+
+ getInstitutionUserInfo(email) {
+ const url = this.server + '/api/v2.1/institutions/admin/users/' + encodeURIComponent(email) + '/';
+ return this.req.get(url);
+ }
+
+ setInstitutionUserQuote(email, quota) {
+ const url = this.server + '/api/v2.1/institutions/admin/users/' + encodeURIComponent(email) + '/';
+ const data = {
+ quota_total: quota
+ };
+ return this.req.put(url, data);
+ }
+
+ listInstitutionUserRepos(email) {
+ const url = this.server + '/api/v2.1/institutions/admin/users/' + encodeURIComponent(email) + '/libraries/';
+ return this.req.get(url);
+ }
+
+ listInstitutionUserGroups(email) {
+ const url = this.server + '/api/v2.1/institutions/admin/users/' + encodeURIComponent(email) + '/groups/';
+ return this.req.get(url);
+ }
+
+ updateInstitutionUserStatus(email, is_active) {
+ const url = this.server + '/api/v2.1/institutions/admin/users/' + encodeURIComponent(email) + '/';
+ const data = {
+ is_active: is_active ? 'true' : 'false',
+ };
+ return this.req.put(url, data);
+ }
+
+}
+
+
+const instAdminAPI = new InstAdminAPI();
+const xcsrfHeaders = cookie.load('sfcsrftoken');
+instAdminAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
+
+export default instAdminAPI;
diff --git a/frontend/src/pages/institution-admin/index.js b/frontend/src/pages/institution-admin/index.js
new file mode 100644
index 0000000000..863de069f1
--- /dev/null
+++ b/frontend/src/pages/institution-admin/index.js
@@ -0,0 +1,32 @@
+import React, { useState } from 'react';
+import ReactDom from 'react-dom';
+import MediaQuery from 'react-responsive';
+import { Modal } from 'reactstrap';
+import SidePanel from './side-panel';
+import MainPanel from './main-panel';
+
+import '../../css/layout.css';
+import '../../css/toolbar.css';
+
+export default function Institutions() {
+
+ const [isSidePanelClosed, setIsSidePanelClosed] = useState(false);
+
+ const toggleSidePanel = () => {
+ setIsSidePanelClosed(!isSidePanelClosed);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+ReactDom.render( , document.getElementById('wrapper'));
diff --git a/frontend/src/pages/institution-admin/main-panel.js b/frontend/src/pages/institution-admin/main-panel.js
new file mode 100644
index 0000000000..5cb73d51d8
--- /dev/null
+++ b/frontend/src/pages/institution-admin/main-panel.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import MainPanelTopbar from '../sys-admin/main-panel-topbar';
+import UserList from './user-list';
+import { Router, navigate } from '@gatsbyjs/reach-router';
+import UserContent from './user-content';
+import UsersNav from './users-nav';
+import UserInfo from './user-content/user-info';
+import UserRepos from './user-content/user-repos';
+import UserGroups from './user-content/user-groups';
+import { gettext, siteRoot } from '../../utils/constants';
+import Search from '../sys-admin/search';
+import UserListSearch from './user-list-search';
+
+export default function MainPanel(props) {
+
+
+ const searchItems = (keyword) => {
+ navigate(`${siteRoot}inst/useradmin/search/?query=${encodeURIComponent(keyword)}`);
+ };
+
+ const getSearch = () => {
+ // offer 'Search' for 'DB' & 'LDAPImported' users
+ return ;
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/institution-admin/side-panel.js b/frontend/src/pages/institution-admin/side-panel.js
new file mode 100644
index 0000000000..11e6892ab0
--- /dev/null
+++ b/frontend/src/pages/institution-admin/side-panel.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@gatsbyjs/reach-router';
+import Logo from '../../components/logo';
+import { gettext, siteRoot, institutionName } from '../../utils/constants';
+
+const propTypes = {
+ isSidePanelClosed: PropTypes.bool.isRequired,
+ onCloseSidePanel: PropTypes.func.isRequired,
+};
+
+class SidePanel extends React.Component {
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
{institutionName}
+
+
+
+
+ {gettext('Users')}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+SidePanel.propTypes = propTypes;
+
+export default SidePanel;
diff --git a/frontend/src/pages/institution-admin/user-content/index.js b/frontend/src/pages/institution-admin/user-content/index.js
new file mode 100644
index 0000000000..52f5b1b7c1
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-content/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@gatsbyjs/reach-router';
+import { gettext, siteRoot } from '../../../utils/constants';
+
+const NAV_ITEMS = [
+ {name: 'info', urlPart: '', text: gettext('Info')},
+ {name: 'owned-repos', urlPart: 'owned-libraries', text: gettext('Owned Libraries')},
+ {name: 'groups', urlPart: 'groups', text: gettext('Groups')}
+];
+
+const UserContent = ({ children, ...rest }) => {
+ const nav = rest['*'];
+ const username = rest.email;
+ return (
+ <>
+
+ {NAV_ITEMS.map((item, index) => {
+ return (
+
+ {item.text}
+
+ );
+ })}
+
+
+ {children}
+
+ >
+ );
+};
+
+UserContent.propTypes = {
+ children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
+};
+
+export default UserContent;
\ No newline at end of file
diff --git a/frontend/src/pages/institution-admin/user-content/user-group-item.js b/frontend/src/pages/institution-admin/user-content/user-group-item.js
new file mode 100644
index 0000000000..590ff884bb
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-content/user-group-item.js
@@ -0,0 +1,49 @@
+import React, { useCallback, useState } from 'react';
+import PropTypes from 'prop-types';
+import { gettext, siteRoot } from '../../../utils/constants';
+import moment from 'moment';
+import { Link } from '@gatsbyjs/reach-router';
+
+const UserGroupItem = ({ group }) => {
+
+ const [highlight, setHighlight] = useState(false);
+
+ const handleMouseEnter = useCallback(() => {
+ setHighlight(true);
+ }, []);
+
+ const handleMouseLeave = useCallback(() => {
+ setHighlight(false);
+ }, []);
+
+ const getRoleText = useCallback((group) => {
+ let roleText;
+ if (group.is_admin) {
+ roleText = gettext('Admin');
+ return roleText;
+ }
+
+ if (group.is_owner) {
+ roleText = gettext('Owner');
+ return roleText;
+ }
+
+ roleText = gettext('Member');
+ return roleText;
+ }, []);
+
+ return (
+
+ {group.name}
+ {getRoleText(group)}
+ {moment(group.created_at).format('YYYY-MM-DD HH:mm')}
+
+
+ );
+};
+
+UserGroupItem.propTypes = {
+ group: PropTypes.object,
+};
+
+export default UserGroupItem;
diff --git a/frontend/src/pages/institution-admin/user-content/user-groups.js b/frontend/src/pages/institution-admin/user-content/user-groups.js
new file mode 100644
index 0000000000..9d3ba47edc
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-content/user-groups.js
@@ -0,0 +1,52 @@
+import { useParams } from '@gatsbyjs/reach-router';
+import React, { useEffect, useState } from 'react';
+import { gettext } from '../../../utils/constants';
+import Loading from '../../../components/loading';
+import EmptyTip from '../../../components/empty-tip';
+import UserGroupItem from './user-group-item';
+import instAdminAPI from '../api';
+
+export default function UsersGroups() {
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [groups, setGroups] = useState(null);
+ const params = useParams();
+
+ useEffect(() => {
+ instAdminAPI.listInstitutionUserGroups(decodeURIComponent(params.email)).then(res => {
+ const { groups_list } = res.data;
+ setGroups(groups_list);
+ setIsLoading(false);
+ });
+ }, [params.email]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (groups.length === 0) {
+ return (
+
+ {gettext('This user has not created or joined any groups')}
+
+ );
+ }
+
+ return (
+
+
+
+ {gettext('Name')}
+ {gettext('Role')}
+ {gettext('Create At')}
+ {gettext('Operations')}
+
+
+
+ {groups.map((group) => {
+ return ;
+ })}
+
+
+ );
+}
diff --git a/frontend/src/pages/institution-admin/user-content/user-info.js b/frontend/src/pages/institution-admin/user-content/user-info.js
new file mode 100644
index 0000000000..d9f972045a
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-content/user-info.js
@@ -0,0 +1,72 @@
+import { useParams } from '@gatsbyjs/reach-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import { Utils } from '../../../utils/utils';
+import { gettext } from '../../../utils/constants';
+import Loading from '../../../components/loading';
+import SetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
+import instAdminAPI from '../api';
+
+export default function UserInfo() {
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [user, setUser] = useState(null);
+ const [isShowEditDialog, setIsShowEditDialog] = useState(false);
+ const params = useParams();
+
+ useEffect(() => {
+ instAdminAPI.getInstitutionUserInfo(decodeURIComponent(params.email)).then(res => {
+ const user = res.data;
+ setUser(user);
+ setIsLoading(false);
+ });
+ }, [params.email]);
+
+ const toggleSetQuotaDialog = useCallback(() => {
+ setIsShowEditDialog(!isShowEditDialog);
+ }, [isShowEditDialog]);
+
+ const updateQuota = useCallback((quote) => {
+ instAdminAPI.setInstitutionUserQuote(user.email, quote).then(res => {
+ // convert value to mb
+ const newUser = {...user, quota_total: quote * 1000 * 1000};
+ setUser(newUser);
+ });
+ }, [user]);
+
+ if (isLoading) {
+ return ;
+ }
+
+
+ return (
+ <>
+
+ {gettext('Avatar')}
+
+
+
+
+ {gettext('Email')}
+ {user.email}
+
+ {gettext('Name')}
+
+ {user.name || '--'}
+
+
+ {gettext('Space Used / Quota')}
+
+ {`${Utils.bytesToSize(user.quota_usage)} / ${user.quota_total > 0 ? Utils.bytesToSize(user.quota_total) : '--'}`}
+
+
+
+
+ {isShowEditDialog && (
+
+ )}
+ >
+ );
+}
diff --git a/frontend/src/pages/institution-admin/user-content/user-repo-item.js b/frontend/src/pages/institution-admin/user-content/user-repo-item.js
new file mode 100644
index 0000000000..f2a92f2124
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-content/user-repo-item.js
@@ -0,0 +1,50 @@
+import React, { useCallback, useState } from 'react';
+import { Link } from '@gatsbyjs/reach-router';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import { Utils } from '../../../utils/utils';
+import { enableSysAdminViewRepo, gettext, isPro, siteRoot } from '../../../utils/constants';
+
+const UserRepoItem = ({ repo }) => {
+
+ const [highlight, setHighlight] = useState(false);
+
+ const handleMouseEnter = useCallback(() => {
+ setHighlight(true);
+ }, []);
+
+ const handleMouseLeave = useCallback(() => {
+ setHighlight(false);
+ }, []);
+
+ const renderRepoName = (repo) => {
+ if (repo.name) {
+ if (isPro && enableSysAdminViewRepo && !repo.encrypted) {
+ return {repo.name};
+ } else {
+ return repo.name;
+ }
+ } else {
+ return gettext('Broken ({repo_id_placeholder})').replace('{repo_id_placeholder}', repo.id);
+ }
+ };
+
+ const iconUrl = Utils.getLibIconUrl(repo);
+ const iconTitle = Utils.getLibIconTitle(repo);
+
+ return (
+
+
+ {renderRepoName(repo)}
+ {Utils.bytesToSize(repo.size)}
+ {moment(repo.last_modified).fromNow()}
+
+
+ );
+};
+
+UserRepoItem.propTypes = {
+ repo: PropTypes.object,
+};
+
+export default UserRepoItem;
diff --git a/frontend/src/pages/institution-admin/user-content/user-repos.js b/frontend/src/pages/institution-admin/user-content/user-repos.js
new file mode 100644
index 0000000000..06d28032b3
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-content/user-repos.js
@@ -0,0 +1,53 @@
+import { useParams } from '@gatsbyjs/reach-router';
+import React, { useEffect, useState } from 'react';
+import { gettext } from '../../../utils/constants';
+import Loading from '../../../components/loading';
+import EmptyTip from '../../../components/empty-tip';
+import instAdminAPI from '../api';
+import UserRepoItem from './user-repo-item';
+
+export default function UserRepos() {
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [repos, setRepos] = useState(null);
+ const params = useParams();
+
+ useEffect(() => {
+ instAdminAPI.listInstitutionUserRepos(decodeURIComponent(params.email)).then(res => {
+ const { repo_list } = res.data;
+ setRepos(repo_list);
+ setIsLoading(false);
+ });
+ }, [params.email]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (repos.length === 0) {
+ return (
+
+ {gettext('No libraries')}
+
+ );
+ }
+
+ return (
+
+
+
+
+ {gettext('Name')}
+ {gettext('Size')}
+ {gettext('Last Update')}
+ {/* Operations */}
+
+
+
+ {repos.map((repo) => {
+ return ;
+ })}
+
+
+ );
+}
diff --git a/frontend/src/pages/institution-admin/user-list-search/index.js b/frontend/src/pages/institution-admin/user-list-search/index.js
new file mode 100644
index 0000000000..8a85a1a414
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-list-search/index.js
@@ -0,0 +1,100 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { useLocation } from '@gatsbyjs/reach-router';
+import Loading from '../../../components/loading';
+import { gettext } from '../../../utils/constants';
+import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
+import UserItem from '../user-list/user-item';
+import instAdminAPI from '../api';
+
+const UserListSearch = () => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [userList, setUserList] = useState([]);
+ const [deleteUser, setDeleteUser] = useState(null);
+ const [deleteMessage, setDeleteMessage] = useState('');
+ const [isShowDeleteUserDialog, setIsShowDeleteDialog] = useState(false);
+
+ const location = useLocation();
+
+ useEffect(() => {
+ const params = new URLSearchParams(location.search);
+ const q = params.get('query');
+ instAdminAPI.searchInstitutionUsers(q).then(res => {
+ const { user_list } = res.data;
+ setUserList(user_list);
+ setIsLoading(false);
+ });
+ }, [location.search]);
+
+ const deleteInstUserToggle = useCallback((user) => {
+ if (user) {
+ const deleteMessage = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', user.name);
+ setDeleteUser(user);
+ setDeleteMessage(deleteMessage);
+ }
+ setIsShowDeleteDialog(!isShowDeleteUserDialog);
+ }, [isShowDeleteUserDialog]);
+
+ const deleteInstUser = useCallback(() => {
+ instAdminAPI.deleteInstitutionUser(deleteUser.email).then(res => {
+ const newUserList = userList.filter(item => item.email !== deleteUser.email);
+ setUserList(newUserList);
+ });
+ }, [deleteUser?.email, userList]);
+
+ const updateInstUserStatus = useCallback((user) => {
+ const is_active = user.is_active ? false : true;
+ instAdminAPI.updateInstitutionUserStatus(user.email, is_active).then(res => {
+ const newUserList = userList.map(item => {
+ if (item.email === user.email) {
+ item.is_active = is_active;
+ }
+ return item;
+ });
+ setUserList(newUserList);
+ });
+ }, [userList]);
+
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {gettext('Email')} / {gettext('Name')} / {gettext('Contact Email')}
+ {gettext('Status')}
+ {gettext('Space Used')}
+ {gettext('Create At')} / {gettext('Last Login')}
+ {gettext('Operations')}
+
+
+
+ {userList.map((user) => {
+ return (
+
+ );
+ })}
+
+
+ {isShowDeleteUserDialog && (
+
+ )}
+
+ );
+};
+
+export default UserListSearch;
diff --git a/frontend/src/pages/institution-admin/user-list/index.js b/frontend/src/pages/institution-admin/user-list/index.js
new file mode 100644
index 0000000000..5258c79e4c
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-list/index.js
@@ -0,0 +1,156 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import Loading from '../../../components/loading';
+import { gettext } from '../../../utils/constants';
+import Paginator from '../../../components/paginator';
+import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
+import UserItem from './user-item';
+import instAdminAPI from '../api';
+
+const UserList = ({ onUserLinkClick }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [userList, setUserList] = useState([]);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [curPerPage, setCurPrePage] = useState(25);
+ const [hasNextPage, setHasNextPage] = useState(true);
+ const [deleteUser, setDeleteUser] = useState(null);
+ const [deleteMessage, setDeleteMessage] = useState('');
+ const [isShowDeleteUserDialog, setIsShowDeleteDialog] = useState(false);
+
+ useEffect(() => {
+ instAdminAPI.listInstitutionUsers(currentPage, curPerPage).then(res => {
+ const { user_list, total_count } = res.data;
+ setUserList(user_list);
+ if (user_list.length >= total_count) {
+ setHasNextPage(false);
+ }
+ setIsLoading(false);
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const getPreviousPage = useCallback(() => {
+ if (curPerPage === 1) return;
+ const newPage = currentPage - 1;
+ setCurrentPage(newPage);
+ instAdminAPI.listInstitutionUsers(newPage, curPerPage).then(res => {
+ const { user_list, total_count } = res.data;
+ setUserList(user_list);
+ if (newPage * curPerPage >= total_count) {
+ setHasNextPage(false);
+ }
+ });
+ }, [curPerPage, currentPage]);
+
+ const getNextPage = useCallback(() => {
+ if (!hasNextPage) return;
+ const newPage = currentPage + 1;
+ setCurrentPage(newPage);
+ instAdminAPI.listInstitutionUsers(newPage, curPerPage).then(res => {
+ const { user_list, total_count } = res.data;
+ setUserList(user_list);
+ if (newPage * curPerPage >= total_count) {
+ setHasNextPage(false);
+ }
+ });
+ }, [curPerPage, currentPage, hasNextPage]);
+
+ const resetPerPage = useCallback((perPage) => {
+ setCurPrePage(perPage);
+ instAdminAPI.listInstitutionUsers(1, perPage).then(res => {
+ const { user_list, total_count } = res.data;
+ setUserList(user_list);
+ if (1 * curPerPage >= total_count) {
+ setHasNextPage(false);
+ }
+ });
+ }, [curPerPage]);
+
+ const deleteInstUserToggle = useCallback((user) => {
+ if (user) {
+ const deleteMessage = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', user.name);
+ setDeleteUser(user);
+ setDeleteMessage(deleteMessage);
+ }
+ setIsShowDeleteDialog(!isShowDeleteUserDialog);
+ }, [isShowDeleteUserDialog]);
+
+ const deleteInstUser = useCallback(() => {
+ instAdminAPI.deleteInstitutionUser(deleteUser.email).then(res => {
+ const newUserList = userList.filter(item => item.email !== deleteUser.email);
+ setUserList(newUserList);
+ });
+ }, [deleteUser?.email, userList]);
+
+ const updateInstUserStatus = useCallback((user) => {
+ const is_active = user.is_active ? false : true;
+ instAdminAPI.updateInstitutionUserStatus(user.email, is_active).then(res => {
+ const newUserList = userList.map(item => {
+ if (item.email === user.email) {
+ item.is_active = is_active;
+ }
+ return item;
+ });
+ setUserList(newUserList);
+ });
+ }, [userList]);
+
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {gettext('Email')} / {gettext('Name')} / {gettext('Contact Email')}
+ {gettext('Status')}
+ {gettext('Space Used')}
+ {gettext('Create At')} / {gettext('Last Login')}
+ {gettext('Operations')}
+
+
+
+ {userList.map((user) => {
+ return (
+
+ );
+ })}
+
+
+ {hasNextPage && (
+
+ )}
+ {isShowDeleteUserDialog && (
+
+ )}
+
+ );
+};
+
+UserList.propTypes = {
+ onUserLinkClick: PropTypes.func,
+};
+
+export default UserList;
diff --git a/frontend/src/pages/institution-admin/user-list/user-item.js b/frontend/src/pages/institution-admin/user-list/user-item.js
new file mode 100644
index 0000000000..2150878ed6
--- /dev/null
+++ b/frontend/src/pages/institution-admin/user-list/user-item.js
@@ -0,0 +1,98 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '@gatsbyjs/reach-router';
+import Selector from '../../../components/single-selector';
+import { gettext, username } from '../../../utils/constants';
+import { Utils } from '../../../utils/utils';
+import moment from 'moment';
+
+const OPERATIONS =[
+ {
+ value: 'active',
+ text: gettext('active'),
+ is_active: true,
+ isSelected: false,
+ },
+ {
+ value: 'inactive',
+ text: gettext('inactive'),
+ is_active: false,
+ isSelected: false,
+ },
+];
+
+const UserItem = ({ user, deleteInstUser, updateInstUserStatus }) => {
+ const [highlight, setHighlight] = useState(false);
+ const [isOpIconShow, setIsOpIconSHow] = useState(false);
+
+ const operations = OPERATIONS.map(item => {
+ if (user.is_active === item.is_active) {
+ item.isSelected = true;
+ } else {
+ item.isSelected = false;
+ }
+ return item;
+ });
+ const currentSelection = operations.find(item => item.isSelected);
+
+ const handleMouseEnter = () => {
+ setHighlight(true);
+ setIsOpIconSHow(true);
+ };
+
+ const handleMouseLeave = () => {
+ setHighlight(false);
+ setIsOpIconSHow(false);
+ };
+
+ const updateStatus = () => {
+ updateInstUserStatus(user);
+ };
+
+ const deleteCurrentUser = () => {
+ deleteInstUser(user);
+ };
+
+ return (
+
+
+ {user.email}
+
+ {user.name}
+
+ {user.contact_email}
+
+
+
+
+
+ {`${Utils.bytesToSize(user.quota_usage)} / ${user.quota_total > 0 ? Utils.bytesToSize(user.quota_total) : '--'}`}
+
+
+ {`${user.create_time ? moment(user.create_time).format('YYYY-MM-DD HH:mm') : '--'} /`}
+
+ {`${user.last_login ? moment(user.last_login).fromNow() : '--'}`}
+
+ {`${user.last_access_time ? moment(user.last_access_time).fromNow() : '--'}`}
+
+
+ {isOpIconShow && !user.is_institution_admin && !user.is_system_admin && user.email !== username && (
+ {gettext('Delete')}
+ )}
+
+
+ );
+};
+
+UserItem.propTypes = {
+ user: PropTypes.object.isRequired,
+ deleteInstUser: PropTypes.func.isRequired,
+ updateInstUserStatus: PropTypes.func.isRequired,
+};
+
+export default UserItem;
diff --git a/frontend/src/pages/institution-admin/users-nav/index.js b/frontend/src/pages/institution-admin/users-nav/index.js
new file mode 100644
index 0000000000..a08a94301b
--- /dev/null
+++ b/frontend/src/pages/institution-admin/users-nav/index.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Link } from '@gatsbyjs/reach-router';
+import { siteRoot, gettext } from '../../../utils/constants';
+import { getNavMessage } from '../utils';
+
+const UsersNav = () => {
+ const { username } = getNavMessage(window.location.href);
+
+ return (
+
+
+
{gettext('Users')}{username ? ' / ' + username : ''}
+
+
+ );
+};
+
+export default UsersNav;
diff --git a/frontend/src/pages/institution-admin/utils/index.js b/frontend/src/pages/institution-admin/utils/index.js
new file mode 100644
index 0000000000..a4fdb5445e
--- /dev/null
+++ b/frontend/src/pages/institution-admin/utils/index.js
@@ -0,0 +1,8 @@
+export const getNavMessage = (url) => {
+ const paths = url.split('/');
+ const adminIdx = paths.findIndex(item => item ==='useradmin');
+ const email = paths[adminIdx + 1];
+ const nav = paths[adminIdx + 2];
+ const username = decodeURIComponent(email);
+ return { username, nav };
+};
\ No newline at end of file
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 0bb26943f1..4fdae8c652 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -165,3 +165,6 @@ export const enableDingtalk = window.sysadmin ? window.sysadmin.pageOptions.enab
export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : '';
export const haveLDAP = window.sysadmin ? window.sysadmin.pageOptions.haveLDAP : '';
export const enableShareLinkReportAbuse = window.sysadmin ? window.sysadmin.pageOptions.enable_share_link_report_abuse : '';
+
+// institution admin
+export const institutionName = window.app ? window.app.pageOptions.institutionName : '';
diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css
index cc819859d4..c4453b7a28 100644
--- a/media/css/seahub_react.css
+++ b/media/css/seahub_react.css
@@ -1268,3 +1268,10 @@ a.table-sort-op:hover {
text-overflow: ellipsis;
white-space: nowrap;
}
+
+.word-break-all {
+ /* overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap; */
+ word-break: break-all;
+}
diff --git a/seahub/institutions/api_urls.py b/seahub/institutions/api_urls.py
new file mode 100644
index 0000000000..d6eba34aa9
--- /dev/null
+++ b/seahub/institutions/api_urls.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2012-2016 Seafile Ltd.
+from django.urls import path
+
+from .api_views import InstAdminUsers, InstAdminUser, \
+ InstAdminSearchUser, InstAdminUserLibraries, InstAdminUserGroups
+
+urlpatterns = [
+ path('admin/users/', InstAdminUsers.as_view(), name='api-v2.1-inst-admin-users'),
+ path('admin/search-user/',
+ InstAdminSearchUser.as_view(), name='api-v2.1-inst-admin-search-user'),
+ path('admin/users//', InstAdminUser.as_view(), name='api-v2.1-inst-admin-user'),
+ path('admin/users//libraries/',
+ InstAdminUserLibraries.as_view(), name='api-v2.1-inst-admin-user-libraries'),
+ path('admin/users//groups/',
+ InstAdminUserGroups.as_view(), name='api-v2.1-inst-admin-user-groups'),
+]
diff --git a/seahub/institutions/api_views.py b/seahub/institutions/api_views.py
new file mode 100644
index 0000000000..ee985da323
--- /dev/null
+++ b/seahub/institutions/api_views.py
@@ -0,0 +1,434 @@
+# Copyright (c) 2012-2016 Seafile Ltd.
+import logging
+
+from rest_framework import status
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework.permissions import BasePermission
+from rest_framework.authentication import SessionAuthentication
+
+from django.conf import settings
+from django.utils.translation import gettext as _
+
+from seaserv import ccnet_api, seafile_api
+
+from seahub.api2.utils import api_error
+from seahub.api2.permissions import IsProVersion
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.authentication import TokenAuthentication
+
+from seahub.base.accounts import User
+from seahub.base.models import UserLastLogin
+from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
+from seahub.profile.models import Profile
+from seahub.institutions.models import InstitutionAdmin
+from seahub.institutions.utils import get_institution_available_quota
+from seahub.utils import inactive_user
+from seahub.utils.timeutils import datetime_to_isoformat_timestr
+from seahub.utils.timeutils import timestamp_to_isoformat_timestr
+from seahub.utils.file_size import get_file_size_unit
+from seahub.utils.licenseparse import user_number_over_limit
+from seahub.avatar.templatetags.avatar_tags import api_avatar_url
+
+
+logger = logging.getLogger(__name__)
+
+
+class IsInstAdmin(BasePermission):
+ """
+ Check whether is inst admin
+ """
+
+ def has_permission(self, request, *args, **kwargs):
+
+ # permission check
+ if not getattr(settings, 'MULTI_INSTITUTION', False):
+ return False
+
+ username = request.user.username
+
+ try:
+ inst_admin = InstitutionAdmin.objects.get(user=username)
+ except InstitutionAdmin.DoesNotExist:
+ inst_admin = False
+
+ if not inst_admin:
+ return False
+
+ inst = inst_admin.institution
+ profile = Profile.objects.get_profile_by_user(username)
+ if profile and profile.institution != inst.name:
+ return False
+
+ return True
+
+
+class InstAdminUsers(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsProVersion, IsInstAdmin)
+
+ def get(self, request):
+
+ """List users in institution.
+ """
+
+ username = request.user.username
+ inst_admin = InstitutionAdmin.objects.get(user=username)
+ inst = inst_admin.institution
+
+ # get user list
+ try:
+ current_page = int(request.GET.get('page', '1'))
+ per_page = int(request.GET.get('per_page', '100'))
+ except ValueError:
+ current_page = 1
+ per_page = 100
+
+ offset = per_page * (current_page - 1)
+ inst_users = Profile.objects.filter(institution=inst.name)[offset:offset + per_page]
+
+ admin_users = InstitutionAdmin.objects.filter(institution=inst)
+ admin_emails = [user.user for user in admin_users]
+
+ last_logins = UserLastLogin.objects.filter(username__in=[x.user for x in inst_users])
+
+ result = []
+ for user in inst_users:
+
+ email = user.user
+
+ user_info = {}
+ user_info['email'] = email
+ user_info['name'] = email2nickname(email)
+ user_info['contact_email'] = email2contact_email(email)
+ user_info['is_institution_admin'] = email in admin_emails
+ user_info['avatar_url'], _, _ = api_avatar_url(email, 72)
+
+ try:
+ user_obj = User.objects.get(email=email)
+ user_info['is_active'] = user_obj.is_active
+ user_info['is_system_admin'] = user_obj.is_staff
+ user_info['create_time'] = timestamp_to_isoformat_timestr(user_obj.ctime)
+ except User.DoesNotExist:
+ user_info['is_active'] = ''
+ user_info['is_system_admin'] = ''
+ user_info['create_time'] = ''
+
+ user_info['last_login'] = ''
+ for last_login in last_logins:
+ if last_login.username == email:
+ last_login_time = last_login.last_login
+ user_info['last_login'] = datetime_to_isoformat_timestr(last_login_time)
+
+ try:
+ user_info['quota_total'] = seafile_api.get_user_quota(email)
+ user_info['quota_usage'] = seafile_api.get_user_self_usage(email)
+ except Exception as e:
+ logger.error(e)
+ user_info['quota_total'] = -1
+ user_info['quota_usage'] = -1
+
+ result.append(user_info)
+
+ return Response({
+ 'user_list': result,
+ 'total_count': inst_users.count()
+ })
+
+
+class InstAdminUser(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsProVersion, IsInstAdmin)
+
+ def put(self, request, email):
+
+ """ Update user info in institution.
+ """
+
+ username = request.user.username
+ inst_admin = InstitutionAdmin.objects.get(user=username)
+ inst = inst_admin.institution
+
+ try:
+ user_obj = User.objects.get(email=email)
+ except User.DoesNotExist:
+ error_msg = f'User {email} not found.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ profile = Profile.objects.get_profile_by_user(email)
+ if not profile or \
+ profile.institution != inst.name:
+ error_msg = f'User {email} not found in {inst.name}.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # set user quota
+ quota_total_mb = request.data.get("quota_total", None)
+ if quota_total_mb is not None:
+ try:
+ quota_total_mb = int(quota_total_mb)
+ except ValueError:
+ error_msg = _("Must be an integer that is greater than or equal to 0.")
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if quota_total_mb < 0:
+ error_msg = _("Space quota is too low (minimum value is 0).")
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ quota = quota_total_mb * get_file_size_unit('MB')
+ available_quota = get_institution_available_quota(inst)
+ if available_quota is not None:
+ # None means has unlimit quota
+ if available_quota == 0 or available_quota < quota:
+ error_msg = _(f"Failed to set quota: maximum quota is {available_quota} MB")
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ seafile_api.set_user_quota(email, quota)
+
+ is_active = request.data.get("is_active", None)
+ if is_active is not None:
+
+ is_active = is_active.lower()
+ if is_active not in ('true', 'false'):
+ error_msg = "is_active invalid."
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if is_active == 'true':
+
+ if user_number_over_limit(new_users=1):
+ error_msg = _("The number of users exceeds the limit.")
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ else:
+ # del tokens and personal repo api tokens
+ try:
+ inactive_user(email)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ user_obj.is_active = is_active == 'true'
+ user_obj.save()
+
+ return Response({'success': True})
+
+ def get(self, request, email):
+
+ """ Get user info in institution.
+ """
+
+ username = request.user.username
+ inst_admin = InstitutionAdmin.objects.get(user=username)
+ inst = inst_admin.institution
+
+ # get user info
+ try:
+ user_obj = User.objects.get(email=email)
+ except User.DoesNotExist:
+ error_msg = f'User {email} not found.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ profile = Profile.objects.get_profile_by_user(email)
+ if not profile or \
+ profile.institution != inst.name:
+ error_msg = f'User {email} not found in {inst.name}.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ user_info = {}
+ user_info['email'] = email
+ user_info['name'] = email2nickname(email)
+ user_info['contact_email'] = email2contact_email(email)
+ user_info['is_active'] = user_obj.is_active
+ user_info['avatar_url'], _, _ = api_avatar_url(email, 72)
+ try:
+ user_info['quota_total'] = seafile_api.get_user_quota(email)
+ user_info['quota_usage'] = seafile_api.get_user_self_usage(email)
+ except Exception as e:
+ logger.error(e)
+ user_info['quota_total'] = -1
+ user_info['quota_usage'] = -1
+
+ return Response(user_info)
+
+ def delete(self, request, email):
+
+ """ Delete user in institution.
+ """
+
+ # delete user
+ try:
+ user = User.objects.get(email=email)
+ except User.DoesNotExist:
+ error_msg = f'User {email} not found.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if user.is_staff:
+ error_msg = f'User {email} is system administrator.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ user.delete()
+ return Response({'success': True})
+
+
+class InstAdminSearchUser(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsProVersion, IsInstAdmin)
+
+ def get(self, request):
+
+ """Search user in institution.
+ """
+
+ username = request.user.username
+ inst_admin = InstitutionAdmin.objects.get(user=username)
+ inst = inst_admin.institution
+
+ # search user
+ q = request.GET.get('q', '').lower()
+ if not q:
+ error_msg = 'q invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ inst_users = Profile.objects.filter(institution=inst.name)
+ admin_users = InstitutionAdmin.objects.filter(institution=inst)
+ admin_emails = [user.user for user in admin_users]
+ last_logins = UserLastLogin.objects.filter(username__in=[x.user for x in inst_users])
+
+ result = []
+ for user in inst_users:
+
+ email = user.user
+
+ if q not in email and \
+ q not in email2nickname(email) and \
+ q not in email2contact_email(email):
+ continue
+
+ user_info = {}
+ user_info['email'] = email
+ user_info['name'] = email2nickname(email)
+ user_info['contact_email'] = email2contact_email(email)
+ user_info['is_institution_admin'] = email in admin_emails
+ user_info['avatar_url'], _, _ = api_avatar_url(email, 72)
+
+ try:
+ user_obj = User.objects.get(email=email)
+ user_info['is_active'] = user_obj.is_active
+ user_info['is_system_admin'] = user_obj.is_staff
+ user_info['create_time'] = timestamp_to_isoformat_timestr(user_obj.ctime)
+ except User.DoesNotExist:
+ user_info['is_active'] = ''
+ user_info['is_system_admin'] = ''
+ user_info['create_time'] = ''
+
+ user_info['last_login'] = ''
+ for last_login in last_logins:
+ if last_login.username == email:
+ last_login_time = last_login.last_login
+ user_info['last_login'] = datetime_to_isoformat_timestr(last_login_time)
+
+ try:
+ user_info['quota_total'] = seafile_api.get_user_quota(email)
+ user_info['quota_usage'] = seafile_api.get_user_self_usage(email)
+ except Exception as e:
+ logger.error(e)
+ user_info['quota_total'] = -1
+ user_info['quota_usage'] = -1
+
+ result.append(user_info)
+
+ return Response({'user_list': result})
+
+
+class InstAdminUserLibraries(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsProVersion, IsInstAdmin)
+
+ def get(self, request, email):
+
+ """Get user repos.
+ """
+
+ try:
+ User.objects.get(email=email)
+ except User.DoesNotExist:
+ error_msg = f'User {email} not found.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ username = request.user.username
+ inst_admin = InstitutionAdmin.objects.get(user=username)
+ inst = inst_admin.institution
+ profile = Profile.objects.get_profile_by_user(email)
+ if not profile or \
+ profile.institution != inst.name:
+ error_msg = f'User {email} not found in {inst.name}.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ repo_info_list = []
+ owned_repos = seafile_api.get_owned_repo_list(email)
+
+ for repo in owned_repos:
+
+ if repo.is_virtual:
+ continue
+
+ repo_info = {}
+ repo_info['id'] = repo.repo_id
+ repo_info['name'] = repo.repo_name
+ repo_info['size'] = repo.size
+ repo_info['last_modified'] = timestamp_to_isoformat_timestr(repo.last_modified)
+
+ repo_info_list.append(repo_info)
+
+ return Response({"repo_list": repo_info_list})
+
+
+class InstAdminUserGroups(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsProVersion, IsInstAdmin)
+
+ def get(self, request, email):
+
+ """Get user repos.
+ """
+
+ try:
+ User.objects.get(email=email)
+ except User.DoesNotExist:
+ error_msg = f'User {email} not found.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ username = request.user.username
+ inst_admin = InstitutionAdmin.objects.get(user=username)
+ inst = inst_admin.institution
+
+ profile = Profile.objects.get_profile_by_user(email)
+ if not profile or \
+ profile.institution != inst.name:
+ error_msg = f'User {email} not found in {inst.name}.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ group_info_list = []
+ groups = ccnet_api.get_groups(email)
+
+ for group in groups:
+
+ group_info = {}
+ group_info['id'] = group.id
+ group_info['name'] = group.group_name
+ group_info['is_owner'] = group.creator_name == email
+ group_info['is_admin'] = 1 == ccnet_api.check_group_staff(group.id, email)
+ group_info['created_at'] = timestamp_to_isoformat_timestr(group.timestamp)
+
+ group_info_list.append(group_info)
+
+ return Response({"groups_list": group_info_list})
diff --git a/seahub/institutions/templates/institutions/admin.html b/seahub/institutions/templates/institutions/admin.html
new file mode 100644
index 0000000000..111afb30c4
--- /dev/null
+++ b/seahub/institutions/templates/institutions/admin.html
@@ -0,0 +1,27 @@
+{% extends 'base_for_react.html' %}
+{% load render_bundle from webpack_loader %}
+{% load seahub_tags %}
+
+{% block extra_style %}
+{% render_bundle 'institutionAdmin' 'css' %}
+{% endblock %}
+
+{% block extra_script %}
+
+{% render_bundle 'institutionAdmin' 'js' %}
+{% endblock %}
diff --git a/seahub/institutions/templates/institutions/base.html b/seahub/institutions/templates/institutions/base.html
deleted file mode 100644
index 619f437a88..0000000000
--- a/seahub/institutions/templates/institutions/base.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "base.html" %}
-{% load i18n %}
-
-{% block main_class %}d-flex ovhd{% endblock %}
-
-{% block admin_link %}
-{% trans "Exit admin panel" %}
-{% endblock %}
-
-{% block main_content %}
-
-
-
- {% block left_panel %}
-
{{ request.user.institution.name }}
-
-
-
- {% endblock %}
-
-
-
- {% block right_panel %}{% endblock %}
-
-
-{% endblock %}
diff --git a/seahub/institutions/templates/institutions/info.html b/seahub/institutions/templates/institutions/info.html
deleted file mode 100644
index c9a55b32d0..0000000000
--- a/seahub/institutions/templates/institutions/info.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends "institutions/base.html" %}
-{% load seahub_tags i18n %}
-
-{% block cur_info %}tab-cur{% endblock %}
-
-{% block right_panel %}
-{% trans "Info" %}
-
-
- {% trans "Name" %}
- {{ inst.name }}
-
- {% trans "Libraries" %}
- {{repos_count}}
-
- {% trans "Activated Users" %} / {% trans "Total Users" %}
-
- {% if active_users_count %}{{ active_users_count }}{% else %}--{% endif %}
- /
- {% if users_count %}{{ users_count }}{% else %}--{% endif %}
-
-
- {% trans "Groups" %}
- {{groups_count}}
-
-{% endblock %}
-
diff --git a/seahub/institutions/templates/institutions/user_info.html b/seahub/institutions/templates/institutions/user_info.html
deleted file mode 100644
index b73c892a11..0000000000
--- a/seahub/institutions/templates/institutions/user_info.html
+++ /dev/null
@@ -1,174 +0,0 @@
-{% extends "institutions/base.html" %}
-{% load i18n avatar_tags seahub_tags %}
-{% load static %}
-
-{% block right_panel %}
-
- Users
- /
- {{ email }}
-
-
-
-
-
-
-
- {% trans "Avatar" %}
- {% avatar email 48 %}
-
- {% trans "Email" %}
- {{ email }}
-
- {% if profile %}
- {% trans "Name" context "true name" %}
- {{ profile.nickname }}
- {% endif %}
-
- {% if d_profile %}
- {% trans "Department" %}
- {{ d_profile.department }}
-
- {% trans "Telephone" %}
- {{ d_profile.telephone }}
- {% endif %}
-
- {% trans "Space Used" %}
- {{ space_usage|seahub_filesizeformat }} {% if space_quota > 0 %} / {{ space_quota|seahub_filesizeformat }} {% endif %} {% trans "Set Quota" %}
-
-
-
-
-
-
- {% if owned_repos %}
-
-
-
- {% trans "Name" %}
- {% trans "Size"%}
- {% trans "Last Update"%}
- {% trans "Operations" %}
-
-
- {% for repo in owned_repos %}
-
- {% if repo.encrypted %}
-
- {% else %}
-
- {% endif %}
-
- {% if not repo.name %}
- Broken ({{repo.id}})
- {% else %}
- {% if repo.encrypted %}
- {{ repo.name }}
- {% elif enable_sys_admin_view_repo %}
- {{ repo.name }}
- {% else %}
- {{ repo.name }}
- {% endif %}
- {% endif %}
-
- {{ repo.size|filesizeformat }}
- {{ repo.last_modify|translate_seahub_time }}
-
-
-
- {% endfor %}
-
- {% else %}
-
-
{% trans "This user has not created any libraries" %}
-
- {% endif %}
-
-
-
- {% if personal_groups %}
-
-
- {% trans "Name" %}
- {% trans "Role" %}
- {% trans "Create At" %}
- {% trans "Operations" %}
-
- {% for group in personal_groups %}
-
- {{ group.group_name }}
- {{ group.role }}
- {{ group.timestamp|tsstr_sec }}
-
-
- {% endfor %}
-
- {% else %}
-
-
{% trans "This user has not created or joined any groups" %}
-
- {% endif %}
-
-
-
-{% endblock %}
-
-
-{% block extra_script %}
-
-
-{% endblock %}
diff --git a/seahub/institutions/templates/institutions/useradmin.html b/seahub/institutions/templates/institutions/useradmin.html
deleted file mode 100644
index 4b3289eac4..0000000000
--- a/seahub/institutions/templates/institutions/useradmin.html
+++ /dev/null
@@ -1,148 +0,0 @@
-{% extends "institutions/base.html" %}
-{% load seahub_tags i18n %}
-{% block cur_users %}tab-cur{% endblock %}
-
-
-{% block right_panel %}
-
-
-{% if users %}
-
-
- {% trans "Email" %} / {% trans "Name" %} / {% trans "Contact Email" %}
- {% trans "Status" %}
- {% trans "Space Used" %}
- {% trans "Create At / Last Login" %}
- {% trans "Operations" %}
-
-
- {% for user in users %}
-
-
- {{ user.email }}
- {% if user.name %} {{ user.name }}{% endif %}
- {% if user.contact_email %} {{ user.contact_email }}{% endif %}
-
-
-
- {% if user.is_active %}
- {% trans "Active" %}
- {% else %}
- {% trans "Inactive" %}
- {% endif %}
- {% if not user.is_self and not user.is_staff and not user.is_institution_admin %}
-
- {% endif %}
-
-
- {% trans "Active" %}
- {% trans "Inactive"%}
-
-
-
- {{ user.space_usage|seahub_filesizeformat }} {% if user.space_quota > 0 %} / {{ user.space_quota|seahub_filesizeformat }} {% endif %}
-
-
- {% if user.source == "DB" %}
- {{ user.ctime|tsstr_sec }} /
- {% else %}
- -- /
- {% endif %}
- {% if user.last_login %}{{user.last_login|translate_seahub_time}} {% else %} -- {% endif %}
-
-
- {% if not user.is_self and not user.is_staff and not user.is_institution_admin %}
- {% trans "Delete" %}
- {% endif %}
-
-
- {% endfor %}
-
-
-{% include "snippets/admin_paginator.html" %}
-{% else %}
-{% trans "Empty" %}
-{% endif %}
-
-
-
{% trans "Activating..., please wait" %}
-
-
-{% endblock %}
-
-{% block extra_script %}
-
-{% endblock %}
diff --git a/seahub/institutions/templates/institutions/useradmin_search.html b/seahub/institutions/templates/institutions/useradmin_search.html
deleted file mode 100644
index 55f2761c2f..0000000000
--- a/seahub/institutions/templates/institutions/useradmin_search.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{% extends "institutions/base.html" %}
-{% load seahub_tags i18n %}
-{% block cur_users %}tab-cur{% endblock %}
-
-
-{% block right_panel %}
-{% trans "Search User"%}
-
-
-{% trans "Result"%}
-
-{% if users %}
-
-
- {% trans "Email" %} / {% trans "Name" %} / {% trans "Contact Email" %}
- {% trans "Status" %}
- {% trans "Space Used" %}
- {% trans "Create At / Last Login" %}
- {% trans "Operations" %}
-
-
- {% for user in users %}
-
-
- {{ user.email }}
- {% if user.name %} {{ user.name }}{% endif %}
- {% if user.contact_email %} {{ user.contact_email }}{% endif %}
-
-
-
- {% if user.is_active %}
- {% trans "Active" %}
- {% else %}
- {% trans "Inactive" %}
- {% endif %}
-
-
-
- {{ user.space_usage|seahub_filesizeformat }} {% if user.space_quota > 0 %} / {{ user.space_quota|seahub_filesizeformat }} {% endif %}
-
-
- {% if user.source == "DB" %}
- {{ user.ctime|tsstr_sec }} /
- {% else %}
- -- /
- {% endif %}
- {% if user.last_login %}{{user.last_login|translate_seahub_time}} {% else %} -- {% endif %}
-
-
- {% if not user.is_self and not user.is_staff and not user.is_institution_admin %}
- {% trans "Delete" %}
- {% endif %}
-
-
- {% endfor %}
-
-
-{% else %}
-{% trans "Empty" %}
-{% endif %}
-
-{% endblock %}
-
-{% block extra_script %}
-
-{% endblock %}
diff --git a/seahub/institutions/urls.py b/seahub/institutions/urls.py
index 7c295dd0bc..dc9e29b487 100644
--- a/seahub/institutions/urls.py
+++ b/seahub/institutions/urls.py
@@ -1,15 +1,18 @@
# Copyright (c) 2012-2016 Seafile Ltd.
from django.urls import path
-from .views import (info, useradmin, user_info, user_remove, useradmin_search,
+from .views import (info, useradmin_react_fake_view, useradmin, user_info, user_remove, useradmin_search,
user_toggle_status, user_set_quota)
urlpatterns = [
path('info/', info, name="info"),
- path('useradmin/', useradmin, name="useradmin"),
+ path('useradmin/', useradmin_react_fake_view, name="useradmin"),
+ path('useradmin/search/', useradmin_react_fake_view, name="useradmin_search"),
+ path('useradmin//', useradmin_react_fake_view, name='useraedmin_email'),
+ path('useradmin//owned-libraries/', useradmin_react_fake_view, name='useradmin_libraries'),
+ path('useradmin//groups/', useradmin_react_fake_view, name='useradmin_groups'),
path('useradmin/info//', user_info, name='user_info'),
path('useradmin/remove//', user_remove, name='user_remove'),
- path('useradmin/search/', useradmin_search, name="useradmin_search"),
path('useradmin/set_quota//', user_set_quota, name='user_set_quota'),
path('useradmin/toggle_status//', user_toggle_status, name='user_toggle_status'),
]
diff --git a/seahub/institutions/views.py b/seahub/institutions/views.py
index 7389215b0e..9bb6dd9dc1 100644
--- a/seahub/institutions/views.py
+++ b/seahub/institutions/views.py
@@ -54,6 +54,16 @@ def info(request):
'inst': inst,
})
+@inst_admin_required
+def useradmin_react_fake_view(request, **kwargs):
+ """List users in the institution.
+ """
+ # Make sure page request is an int. If not, deliver first page.
+ inst = request.user.institution
+
+ return render(request, 'institutions/admin.html', {
+ 'institution': inst.name,
+ })
@inst_admin_required
def useradmin(request):
diff --git a/seahub/urls.py b/seahub/urls.py
index 2e46b98b26..69f658904e 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -892,6 +892,11 @@ if getattr(settings, 'MULTI_TENANCY', False):
path('org/', include('seahub.organizations.urls')),
]
+if getattr(settings, 'MULTI_INSTITUTION', False):
+ urlpatterns += [
+ re_path(r'^api/v2.1/institutions/', include('seahub.institutions.api_urls')),
+ ]
+
if getattr(settings, 'ENABLE_SHIB_LOGIN', False):
urlpatterns += [
re_path(r'^shib-complete/', TemplateView.as_view(template_name='shibboleth/complete.html'), name="shib_complete"),
diff --git a/tests/seahub/institutions/test_views.py b/tests/seahub/institutions/test_views.py
deleted file mode 100644
index 834ca4ab9d..0000000000
--- a/tests/seahub/institutions/test_views.py
+++ /dev/null
@@ -1,128 +0,0 @@
-from django.core import mail
-from django.conf import settings
-from django.urls import reverse
-from django.test import override_settings
-
-from seahub.base.accounts import User
-from seahub.institutions.models import Institution, InstitutionAdmin
-from seahub.institutions.utils import is_institution_admin
-from seahub.profile.models import Profile
-from seahub.test_utils import BaseTestCase
-
-settings.MIDDLEWARE.append(
- 'seahub.institutions.middleware.InstitutionMiddleware',
-)
-
-
-class InstTestBase(BaseTestCase):
- def setUp(self):
- self.inst = Institution.objects.create(name='inst_test')
-
- assert len(Profile.objects.all()) == 0
- p = Profile.objects.add_or_update(self.user.username, '')
- p.institution = self.inst.name
- p.save()
-
- p = Profile.objects.add_or_update(self.admin.username, '')
- p.institution = self.inst.name
- p.save()
- assert len(Profile.objects.all()) == 2
-
- InstitutionAdmin.objects.create(institution=self.inst,
- user=self.user.username)
-
-class InfoTest(InstTestBase):
- @override_settings(
- MIDDLEWARE=settings.MIDDLEWARE,
- MULTI_INSTITUTION=True
- )
- def test_can_render(self):
- self.login_as(self.user)
-
- resp = self.client.get(reverse('institutions:info'))
- self.assertEqual(200, resp.status_code)
- assert resp.context['inst'] == self.inst
-
-
-class UseradminTest(InstTestBase):
- @override_settings(
- MIDDLEWARE=settings.MIDDLEWARE,
- MULTI_INSTITUTION=True
- )
- def test_can_list(self):
- self.login_as(self.user)
- resp = self.client.get(reverse('institutions:useradmin'))
- self.assertEqual(200, resp.status_code)
- assert resp.context['inst'] == self.inst
- assert len(resp.context['users']) == 2
-
-
-class UseradminSearchTest(InstTestBase):
- @override_settings(
- MIDDLEWARE=settings.MIDDLEWARE,
- MULTI_INSTITUTION=True
- )
- def test_can_search(self):
- self.login_as(self.user)
- resp = self.client.get(reverse('institutions:useradmin_search') + '?q=@')
- self.assertEqual(200, resp.status_code)
- assert resp.context['inst'] == self.inst
- assert len(resp.context['users']) == 2
- assert resp.context['q'] == '@'
-
-
-class UserToggleStatusTest(InstTestBase):
- @override_settings(
- MIDDLEWARE=settings.MIDDLEWARE,
- MULTI_INSTITUTION=True
- )
- def test_can_activate(self):
- self.login_as(self.user)
- self.assertEqual(len(mail.outbox), 0)
-
- old_passwd = self.admin.enc_password
- resp = self.client.post(
- reverse('institutions:user_toggle_status', args=[self.admin.username]),
- {'s': 1},
- HTTP_X_REQUESTED_WITH='XMLHttpRequest'
- )
- self.assertEqual(200, resp.status_code)
- self.assertContains(resp, '"success": true')
-
- u = User.objects.get(email=self.admin.username)
- assert u.is_active is True
- assert u.enc_password == old_passwd
- self.assertEqual(len(mail.outbox), 1)
-
- @override_settings(
- MIDDLEWARE=settings.MIDDLEWARE,
- MULTI_INSTITUTION=True
- )
- def test_can_deactivate(self):
- self.login_as(self.user)
-
- old_passwd = self.admin.enc_password
- resp = self.client.post(
- reverse('institutions:user_toggle_status', args=[self.admin.username]),
- {'s': 0},
- HTTP_X_REQUESTED_WITH='XMLHttpRequest'
- )
- self.assertEqual(200, resp.status_code)
- self.assertContains(resp, '"success": true')
-
- u = User.objects.get(email=self.admin.username)
- assert u.is_active is False
- assert u.enc_password == old_passwd
-
-
-class UserIsAdminTest(InstTestBase):
- @override_settings(
- MIDDLEWARE=settings.MIDDLEWARE,
- MULTI_INSTITUTION=True
- )
-
- def test_is_institution_admin(self):
- assert is_institution_admin(self.user.username) == True
- assert is_institution_admin(self.admin.username) == False
- assert is_institution_admin(self.user.username, self.inst) == True
- assert is_institution_admin(self.admin.username, self.inst) == False