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 ( + <> + +
+ {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 ( + + + + + + + + + + + {groups.map((group) => { + return ; + })} + +
{gettext('Name')}{gettext('Role')}{gettext('Create At')}{gettext('Operations')}
+ ); +} 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')}
+
+ {user.name} +
+ +
{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 ( + + {iconTitle} + {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 ( + + + + + + + + + + + + {repos.map((repo) => { + return ; + })} + +
{gettext('Name')}{gettext('Size')}{gettext('Last Update')}{/* Operations */}
+ ); +} 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 ( +
+ + + + + + + + + + + + {userList.map((user) => { + return ( + + ); + })} + +
{gettext('Email')} / {gettext('Name')} / {gettext('Contact Email')}{gettext('Status')}{gettext('Space Used')}{gettext('Create At')} / {gettext('Last Login')}{gettext('Operations')}
+ {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 ( +
+ + + + + + + + + + + + {userList.map((user) => { + return ( + + ); + })} + +
{gettext('Email')} / {gettext('Name')} / {gettext('Contact Email')}{gettext('Status')}{gettext('Space Used')}{gettext('Create At')} / {gettext('Last Login')}{gettext('Operations')}
+ {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" %}
-
- -
{% csrf_token %} -

{% trans "Set Quota" %}

- - MB -

{% trans "Available quota:" %} {{ available_quota|seahub_filesizeformat}}

-

- -
-
- -
- {% if owned_repos %} - - - - - - - - - - {% for repo in owned_repos %} - - {% if repo.encrypted %} - - {% else %} - - {% endif %} - - {% if not repo.name %} - - {% else %} - {% if repo.encrypted %} - - {% elif enable_sys_admin_view_repo %} - - {% else %} - - {% endif %} - {% endif %} - - - - - - {% endfor %} -
{% trans "Name" %}{% trans "Size"%}{% trans "Last Update"%}{% trans "Operations" %}
{% trans {% trans Broken ({{repo.id}}){{ repo.name }}{{ repo.name }}{{ repo.name }}{{ repo.size|filesizeformat }}{{ repo.last_modify|translate_seahub_time }} -
- {% else %} -
-

{% trans "This user has not created any libraries" %}

-
- {% endif %} -
- -
- {% if personal_groups %} - - - - - - - - {% for group in personal_groups %} - - - - - - - {% endfor %} -
{% trans "Name" %}{% trans "Role" %}{% trans "Create At" %}{% trans "Operations" %}
{{ group.group_name }}{{ group.role }}{{ group.timestamp|tsstr_sec }}
- {% 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 %} - - - - - - - - - - {% for user in users %} - - - - - - - - {% endfor %} -
{% trans "Email" %} / {% trans "Name" %} / {% trans "Contact Email" %}{% trans "Status" %}{% trans "Space Used" %}{% trans "Create At / Last Login" %}{% trans "Operations" %}
- {{ 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 %} -
- -
-

{{ 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 %} -
- -{% 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 %} - - - - - - - - - - {% for user in users %} - - - - - - - - {% endfor %} -
{% trans "Email" %} / {% trans "Name" %} / {% trans "Contact Email" %}{% trans "Status" %}{% trans "Space Used" %}{% trans "Create At / Last Login" %}{% trans "Operations" %}
- {{ 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 %} -
- -{% 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