mirror of
https://github.com/haiwen/seahub.git
synced 2025-04-27 19:05:16 +00:00
optimize code (#6066)
* optimize code * add new inst web api * update * update * optimize code * set user is_active * optimize code * optimize code * optimize code * optimize code * rm unused test code * delete test code --------- Co-authored-by: lian <imwhatiam123@gmail.com>
This commit is contained in:
parent
6d1bb9039d
commit
eda0171d66
@ -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) => {
|
||||
|
122
frontend/src/pages/institution-admin/api/index.js
Normal file
122
frontend/src/pages/institution-admin/api/index.js
Normal file
@ -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;
|
32
frontend/src/pages/institution-admin/index.js
Normal file
32
frontend/src/pages/institution-admin/index.js
Normal file
@ -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 (
|
||||
<>
|
||||
<div id="main">
|
||||
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={toggleSidePanel} />
|
||||
<MainPanel toggleSidePanel={toggleSidePanel} />
|
||||
</div>
|
||||
<MediaQuery query="(max-width: 767.8px)">
|
||||
<Modal zIndex="1030" isOpen={!isSidePanelClosed} toggle={toggleSidePanel} contentClassName="d-none"></Modal>
|
||||
</MediaQuery>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDom.render(<Institutions />, document.getElementById('wrapper'));
|
47
frontend/src/pages/institution-admin/main-panel.js
Normal file
47
frontend/src/pages/institution-admin/main-panel.js
Normal file
@ -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 <Search placeholder={gettext('Search users')} submit={searchItems} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='main-panel'>
|
||||
<MainPanelTopbar search={getSearch()} {...props}></MainPanelTopbar>
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<Router>
|
||||
<UsersNav path={siteRoot + '/inst/useradmin/*'} />
|
||||
</Router>
|
||||
<Router style={{ display: 'flex', width: '100%', height: '100%', flexDirection: 'column' }}>
|
||||
<UserList path={siteRoot + '/inst/useradmin'} />
|
||||
<UserListSearch path={siteRoot + '/inst/useradmin/search'} />
|
||||
<UserContent path={siteRoot + '/inst/useradmin/:email'}>
|
||||
<UserInfo path="/" />
|
||||
<UserRepos path='/owned-libraries' />
|
||||
<UserGroups path="/groups" />
|
||||
</UserContent>
|
||||
</Router>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
42
frontend/src/pages/institution-admin/side-panel.js
Normal file
42
frontend/src/pages/institution-admin/side-panel.js
Normal file
@ -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 (
|
||||
<div className={`side-panel ${this.props.isSidePanelClosed ? '' : 'left-zero'}`}>
|
||||
<div className="side-panel-north">
|
||||
<Logo onCloseSidePanel={this.props.onCloseSidePanel}/>
|
||||
</div>
|
||||
<div className="side-panel-center">
|
||||
<div className="side-nav">
|
||||
<div className="side-nav-con">
|
||||
<h3 className="sf-heading">{institutionName}</h3>
|
||||
<ul className="nav nav-pills flex-column nav-container">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link ellipsis active" to={`${siteRoot}inst/useradmin/`}>
|
||||
<span className="sf2-icon-info" aria-hidden="true"></span>
|
||||
<span className="nav-text">{gettext('Users')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SidePanel.propTypes = propTypes;
|
||||
|
||||
export default SidePanel;
|
37
frontend/src/pages/institution-admin/user-content/index.js
Normal file
37
frontend/src/pages/institution-admin/user-content/index.js
Normal file
@ -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 (
|
||||
<>
|
||||
<ul className="nav border-bottom mx-4">
|
||||
{NAV_ITEMS.map((item, index) => {
|
||||
return (
|
||||
<li className="nav-item mr-2" key={index}>
|
||||
<Link to={`${siteRoot}inst/useradmin/${encodeURIComponent(username)}/${item.urlPart}`} className={`nav-link ${nav == item.urlPart ? ' active' : ''}`}>{item.text}</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="cur-view-content">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UserContent.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
};
|
||||
|
||||
export default UserContent;
|
@ -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 (
|
||||
<tr className={highlight ? 'tr-highlight' : ''} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<td><Link to={`${siteRoot}sys/groups/${group.id}/libraries/`}>{group.name}</Link></td>
|
||||
<td>{getRoleText(group)}</td>
|
||||
<td>{moment(group.created_at).format('YYYY-MM-DD HH:mm')}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
UserGroupItem.propTypes = {
|
||||
group: PropTypes.object,
|
||||
};
|
||||
|
||||
export default UserGroupItem;
|
@ -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 <Loading />;
|
||||
}
|
||||
|
||||
if (groups.length === 0) {
|
||||
return (
|
||||
<EmptyTip>
|
||||
<h2>{gettext('This user has not created or joined any groups')}</h2>
|
||||
</EmptyTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30%">{gettext('Name')}</th>
|
||||
<th width="30%">{gettext('Role')}</th>
|
||||
<th width="25%">{gettext('Create At')}</th>
|
||||
<th width="15%">{gettext('Operations')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groups.map((group) => {
|
||||
return <UserGroupItem key={group.id} group={group} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
@ -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 <Loading />;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<dl className="m-0">
|
||||
<dt className="info-item-heading">{gettext('Avatar')}</dt>
|
||||
<dd className="info-item-content">
|
||||
<img src={user.avatar_url} alt={user.name} width="80" className="rounded" />
|
||||
</dd>
|
||||
|
||||
<dt className="info-item-heading">{gettext('Email')}</dt>
|
||||
<dd className="info-item-content">{user.email}</dd>
|
||||
|
||||
<dt className="info-item-heading">{gettext('Name')}</dt>
|
||||
<dd className="info-item-content">
|
||||
{user.name || '--'}
|
||||
</dd>
|
||||
|
||||
<dt className="info-item-heading">{gettext('Space Used / Quota')}</dt>
|
||||
<dd className="info-item-content">
|
||||
{`${Utils.bytesToSize(user.quota_usage)} / ${user.quota_total > 0 ? Utils.bytesToSize(user.quota_total) : '--'}`}
|
||||
<span
|
||||
title={gettext('Edit')}
|
||||
className="fa fa-pencil-alt attr-action-icon"
|
||||
onClick={toggleSetQuotaDialog}>
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
{isShowEditDialog && (
|
||||
<SetQuotaDialog updateQuota={updateQuota} toggle={toggleSetQuotaDialog}/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 <Link to={`${siteRoot}sys/libraries/${repo.id}/`}>{repo.name}</Link>;
|
||||
} 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 (
|
||||
<tr key={repo.id} className={highlight ? 'tr-highlight' : ''} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
|
||||
<td>{renderRepoName(repo)}</td>
|
||||
<td>{Utils.bytesToSize(repo.size)}</td>
|
||||
<td>{moment(repo.last_modified).fromNow()}</td>
|
||||
<td data-id={repo.id} data-name={repo.name}></td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
UserRepoItem.propTypes = {
|
||||
repo: PropTypes.object,
|
||||
};
|
||||
|
||||
export default UserRepoItem;
|
@ -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 <Loading />;
|
||||
}
|
||||
|
||||
if (repos.length === 0) {
|
||||
return (
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No libraries')}</h2>
|
||||
</EmptyTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%"></th>
|
||||
<th width="35%">{gettext('Name')}</th>
|
||||
<th width="30%">{gettext('Size')}</th>
|
||||
<th width="25%">{gettext('Last Update')}</th>
|
||||
<th width="5%">{/* Operations */}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{repos.map((repo) => {
|
||||
return <UserRepoItem key={repo.id} repo={repo} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
100
frontend/src/pages/institution-admin/user-list-search/index.js
Normal file
100
frontend/src/pages/institution-admin/user-list-search/index.js
Normal file
@ -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 <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cur-view-content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="36%">{gettext('Email')} / {gettext('Name')} / {gettext('Contact Email')}</th>
|
||||
<th width="12%">{gettext('Status')}</th>
|
||||
<th width="16%">{gettext('Space Used')}</th>
|
||||
<th width="22%">{gettext('Create At')} / {gettext('Last Login')}</th>
|
||||
<th width="14%">{gettext('Operations')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userList.map((user) => {
|
||||
return (
|
||||
<UserItem
|
||||
key={user.email}
|
||||
user={user}
|
||||
deleteInstUser={deleteInstUserToggle}
|
||||
updateInstUserStatus={updateInstUserStatus}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{isShowDeleteUserDialog && (
|
||||
<CommonOperationConfirmationDialog
|
||||
title={gettext('Delete User')}
|
||||
message={deleteMessage}
|
||||
executeOperation={deleteInstUser}
|
||||
confirmBtnText={gettext('Delete')}
|
||||
toggleDialog={deleteInstUserToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserListSearch;
|
156
frontend/src/pages/institution-admin/user-list/index.js
Normal file
156
frontend/src/pages/institution-admin/user-list/index.js
Normal file
@ -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 <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cur-view-content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="36%">{gettext('Email')} / {gettext('Name')} / {gettext('Contact Email')}</th>
|
||||
<th width="12%">{gettext('Status')}</th>
|
||||
<th width="16%">{gettext('Space Used')}</th>
|
||||
<th width="22%">{gettext('Create At')} / {gettext('Last Login')}</th>
|
||||
<th width="14%">{gettext('Operations')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userList.map((user) => {
|
||||
return (
|
||||
<UserItem
|
||||
key={user.email}
|
||||
user={user}
|
||||
onUserLinkClick={onUserLinkClick}
|
||||
deleteInstUser={deleteInstUserToggle}
|
||||
updateInstUserStatus={updateInstUserStatus}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{hasNextPage && (
|
||||
<Paginator
|
||||
hasNextPage={hasNextPage}
|
||||
currentPage={currentPage}
|
||||
curPerPage={curPerPage}
|
||||
gotoNextPage={getNextPage}
|
||||
gotoPreviousPage={getPreviousPage}
|
||||
resetPerPage={resetPerPage}
|
||||
/>
|
||||
)}
|
||||
{isShowDeleteUserDialog && (
|
||||
<CommonOperationConfirmationDialog
|
||||
title={gettext('Delete User')}
|
||||
message={deleteMessage}
|
||||
executeOperation={deleteInstUser}
|
||||
confirmBtnText={gettext('Delete')}
|
||||
toggleDialog={deleteInstUserToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserList.propTypes = {
|
||||
onUserLinkClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default UserList;
|
98
frontend/src/pages/institution-admin/user-list/user-item.js
Normal file
98
frontend/src/pages/institution-admin/user-list/user-item.js
Normal file
@ -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 (
|
||||
<tr className={`${highlight ? 'hl' : ''}`} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<td>
|
||||
<Link to={`/inst/useradmin/${encodeURIComponent(user.email)}`}>{user.email}</Link>
|
||||
<br/>
|
||||
{user.name}
|
||||
<br/>
|
||||
{user.contact_email}
|
||||
</td>
|
||||
<td>
|
||||
<Selector
|
||||
isDropdownToggleShown={highlight}
|
||||
currentSelectedOption={currentSelection}
|
||||
options={operations}
|
||||
selectOption={updateStatus}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{`${Utils.bytesToSize(user.quota_usage)} / ${user.quota_total > 0 ? Utils.bytesToSize(user.quota_total) : '--'}`}
|
||||
</td>
|
||||
<td>
|
||||
{`${user.create_time ? moment(user.create_time).format('YYYY-MM-DD HH:mm') : '--'} /`}
|
||||
<br />
|
||||
{`${user.last_login ? moment(user.last_login).fromNow() : '--'}`}
|
||||
<br />
|
||||
{`${user.last_access_time ? moment(user.last_access_time).fromNow() : '--'}`}
|
||||
</td>
|
||||
<td>
|
||||
{isOpIconShow && !user.is_institution_admin && !user.is_system_admin && user.email !== username && (
|
||||
<span className='sf-link' onClick={deleteCurrentUser}>{gettext('Delete')}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
UserItem.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
deleteInstUser: PropTypes.func.isRequired,
|
||||
updateInstUserStatus: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UserItem;
|
18
frontend/src/pages/institution-admin/users-nav/index.js
Normal file
18
frontend/src/pages/institution-admin/users-nav/index.js
Normal file
@ -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 (
|
||||
<div>
|
||||
<div className="cur-view-path">
|
||||
<h3 className="sf-heading word-break-all"><Link className='sf-link' to={`${siteRoot}inst/useradmin/`}>{gettext('Users')}</Link>{username ? ' / ' + username : ''}</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersNav;
|
8
frontend/src/pages/institution-admin/utils/index.js
Normal file
8
frontend/src/pages/institution-admin/utils/index.js
Normal file
@ -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 };
|
||||
};
|
@ -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 : '';
|
||||
|
@ -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;
|
||||
}
|
||||
|
16
seahub/institutions/api_urls.py
Normal file
16
seahub/institutions/api_urls.py
Normal file
@ -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/<str:email>/', InstAdminUser.as_view(), name='api-v2.1-inst-admin-user'),
|
||||
path('admin/users/<str:email>/libraries/',
|
||||
InstAdminUserLibraries.as_view(), name='api-v2.1-inst-admin-user-libraries'),
|
||||
path('admin/users/<str:email>/groups/',
|
||||
InstAdminUserGroups.as_view(), name='api-v2.1-inst-admin-user-groups'),
|
||||
]
|
434
seahub/institutions/api_views.py
Normal file
434
seahub/institutions/api_views.py
Normal file
@ -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})
|
27
seahub/institutions/templates/institutions/admin.html
Normal file
27
seahub/institutions/templates/institutions/admin.html
Normal file
@ -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 %}
|
||||
<script type="text/javascript">
|
||||
// overwrite the one in base_for_react.html
|
||||
window.app.pageOptions = {
|
||||
server: '{{ service_url }}',
|
||||
username: '{{ user.username|escapejs }}',
|
||||
userNickName: '{{request.user.username|email2nickname|escapejs}}',
|
||||
institutionName: '{{institution}}',
|
||||
users: '{{users}}'
|
||||
};
|
||||
|
||||
window.app.userInfo = {
|
||||
username: '{{ user.username }}',
|
||||
name: '{{ user.username|email2nickname|escapejs }}',
|
||||
contact_email: '{{ user.username|email2contact_email }}',
|
||||
}
|
||||
</script>
|
||||
{% render_bundle 'institutionAdmin' 'js' %}
|
||||
{% endblock %}
|
@ -1,32 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block main_class %}d-flex ovhd{% endblock %}
|
||||
|
||||
{% block admin_link %}
|
||||
<a href="{{ SITE_ROOT }}" title="{% trans "Exit admin panel" %}" class="item">{% trans "Exit admin panel" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_content %}
|
||||
<div class="row flex-1 d-flex">
|
||||
<div class="side-nav side-tabnav col-md-3">
|
||||
<a href="#" title="{% trans "Close" %}" aria-label="{% trans "Close" %}" class="sf2-icon-x1 sf-popover-close op-icon hidden-md-up js-close-side-nav fright"></a>
|
||||
{% block left_panel %}
|
||||
<h3 class="hd">{{ request.user.institution.name }}</h3>
|
||||
<ul class="side-tabnav-tabs">
|
||||
<li class="tab {% block cur_users %}{% endblock %}">
|
||||
<a href="{% url "institutions:useradmin" %}"><span class="sf2-icon-user"></span>{% trans "Users" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form action="{% url 'institutions:useradmin_search' %}" method="get" class="side-search-form">
|
||||
<input type="text" name="q" class="input" value="{{q}}" placeholder="{% trans "Search users" %}" />
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div id="right-panel" class="col-md-9 ov-auto flex-1">
|
||||
{% block right_panel %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,27 +0,0 @@
|
||||
{% extends "institutions/base.html" %}
|
||||
{% load seahub_tags i18n %}
|
||||
|
||||
{% block cur_info %}tab-cur{% endblock %}
|
||||
|
||||
{% block right_panel %}
|
||||
<h3 class="hd">{% trans "Info" %}</h3>
|
||||
|
||||
<dl>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ inst.name }}</dd>
|
||||
|
||||
<dt>{% trans "Libraries" %}</dt>
|
||||
<dd>{{repos_count}}</dd>
|
||||
|
||||
<dt>{% trans "Activated Users" %} / {% trans "Total Users" %}</dt>
|
||||
<dd>
|
||||
{% if active_users_count %}{{ active_users_count }}{% else %}--{% endif %}
|
||||
/
|
||||
{% if users_count %}{{ users_count }}{% else %}--{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>{% trans "Groups" %}</dt>
|
||||
<dd>{{groups_count}}</dd>
|
||||
</dl>
|
||||
{% endblock %}
|
||||
|
@ -1,174 +0,0 @@
|
||||
{% extends "institutions/base.html" %}
|
||||
{% load i18n avatar_tags seahub_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block right_panel %}
|
||||
<p class="path-bar">
|
||||
<a class="normal" href="{% url "institutions:useradmin" %}">Users</a>
|
||||
<span class="path-split">/</span>
|
||||
{{ email }}
|
||||
</p>
|
||||
|
||||
<div id="tabs" class="tab-tabs">
|
||||
<div class="hd ovhd">
|
||||
<ul class="tab-tabs-nav fleft">
|
||||
<li class="tab"><a href="#profile" class="a">{% trans "Profile" %}</a></li>
|
||||
<li class="tab"><a href="#owned" class="a">{% trans "Owned Libs" %}</a></li>
|
||||
<li class="tab"><a href="#user-admin-groups" class="a">{% trans "Groups" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="profile">
|
||||
<dl>
|
||||
<dt>{% trans "Avatar" %}</dt>
|
||||
<dd>{% avatar email 48 %}</dd>
|
||||
|
||||
<dt>{% trans "Email" %}</dt>
|
||||
<dd>{{ email }}</dd>
|
||||
|
||||
{% if profile %}
|
||||
<dt>{% trans "Name" context "true name" %}</dt>
|
||||
<dd>{{ profile.nickname }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if d_profile %}
|
||||
<dt>{% trans "Department" %}</dt>
|
||||
<dd>{{ d_profile.department }}</dd>
|
||||
|
||||
<dt>{% trans "Telephone" %}</dt>
|
||||
<dd>{{ d_profile.telephone }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>{% trans "Space Used" %}</dt>
|
||||
<dd>{{ space_usage|seahub_filesizeformat }} {% if space_quota > 0 %} / {{ space_quota|seahub_filesizeformat }} {% endif %} <a href="#" class="sf-btn-link" style="margin-left:20px;" id="set-quota">{% trans "Set Quota" %}</a></dd>
|
||||
</dl>
|
||||
|
||||
<form id="set-quota-form" method="post" class="hide">{% csrf_token %}
|
||||
<h3>{% trans "Set Quota" %}</h3>
|
||||
<input type="hidden" name="email" value="{{ email }}" />
|
||||
<input type="text" name="space_quota" class="input" /> MB
|
||||
<p class="tip">{% trans "Available quota:" %} {{ available_quota|seahub_filesizeformat}}</p>
|
||||
<p class="error hide"></p>
|
||||
<input type="submit" value="{% trans "Submit" %}" class="submit" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="owned">
|
||||
{% if owned_repos %}
|
||||
<table class="repo-list">
|
||||
<tr>
|
||||
<th width="4%"><!--icon--></th>
|
||||
<th width="35%">{% trans "Name" %}</th>
|
||||
<th width="16%">{% trans "Size"%}</th>
|
||||
<th width="25%">{% trans "Last Update"%}</th>
|
||||
<th width="20%">{% trans "Operations" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for repo in owned_repos %}
|
||||
<tr>
|
||||
{% if repo.encrypted %}
|
||||
<td><img src="{{MEDIA_URL}}img/lib/48/lib-encrypted.png" width="24" title="{% trans "Encrypted"%}" alt="{% trans "library icon" %}" /></td>
|
||||
{% else %}
|
||||
<td><img src="{{MEDIA_URL}}img/lib/48/lib.png" width="24" title="{% trans "Read-Write" %}" alt="{% trans "library icon" %}" /></td>
|
||||
{% endif %}
|
||||
|
||||
{% if not repo.name %}
|
||||
<td>Broken ({{repo.id}})</td>
|
||||
{% else %}
|
||||
{% if repo.encrypted %}
|
||||
<td>{{ repo.name }}</td>
|
||||
{% elif enable_sys_admin_view_repo %}
|
||||
<td><a href="{{ SITE_ROOT }}sysadmin/#libs/{{ repo.id }}/">{{ repo.name }}</a></td>
|
||||
{% else %}
|
||||
<td>{{ repo.name }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<td>{{ repo.size|filesizeformat }}</td>
|
||||
<td>{{ repo.last_modify|translate_seahub_time }}</td>
|
||||
<td data-id="{{ repo.props.id }}" data-name="{{repo.name}}">
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-tips">
|
||||
<h2 class="alc">{% trans "This user has not created any libraries" %}</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="user-admin-groups">
|
||||
{% if personal_groups %}
|
||||
<table>
|
||||
<tr>
|
||||
<th width="30%">{% trans "Name" %}</th>
|
||||
<th width="30%">{% trans "Role" %}</th>
|
||||
<th width="25%">{% trans "Create At" %}</th>
|
||||
<th width="15%">{% trans "Operations" %}</th>
|
||||
</tr>
|
||||
{% for group in personal_groups %}
|
||||
<tr>
|
||||
<td><a href="{{ SITE_ROOT }}sysadmin/#groups/{{ group.id }}/libs/">{{ group.group_name }}</a></td>
|
||||
<td>{{ group.role }}</td>
|
||||
<td>{{ group.timestamp|tsstr_sec }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-tips">
|
||||
<h2 class="alc">{% trans "This user has not created or joined any groups" %}</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript" src="{% static "scripts/lib/jquery-ui.min.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
$('#tabs').tabs({cookie:{expires:1}});
|
||||
|
||||
$('#set-quota').on('click', function() {
|
||||
$("#set-quota-form").modal({appendTo: "#main"});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#set-quota-form').on('submit', function() {
|
||||
var form = $('#set-quota-form'),
|
||||
form_id = form.attr('id'),
|
||||
space_quota = $('input[name="space_quota"]', form).val();
|
||||
|
||||
if (!$.trim(space_quota)) {
|
||||
apply_form_error(form_id, "{% trans "Space Quota can't be empty" %}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = { 'email': $('input[name="email"]', form).val(), 'space_quota': space_quota };
|
||||
|
||||
var sb_btn = $(this);
|
||||
disable(sb_btn);
|
||||
$.ajax({
|
||||
url: '{% url 'institutions:user_set_quota' email %}',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
beforeSend: prepareCSRFToken,
|
||||
data: data,
|
||||
success: function(data) {
|
||||
location.reload(true);
|
||||
},
|
||||
error: function(xhr, textStatus, errorThrown) {
|
||||
var error_msg = prepareAjaxErrorMsg(xhr);
|
||||
apply_form_error(form_id, error_msg);
|
||||
enable(sb_btn);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,148 +0,0 @@
|
||||
{% extends "institutions/base.html" %}
|
||||
{% load seahub_tags i18n %}
|
||||
{% block cur_users %}tab-cur{% endblock %}
|
||||
|
||||
|
||||
{% block right_panel %}
|
||||
<div class="tabnav ovhd">
|
||||
<ul class="tabnav-tabs fleft">
|
||||
<li class="tabnav-tab tabnav-tab-cur"><a href="{% url 'institutions:useradmin' %}">{% trans "Users" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if users %}
|
||||
<table>
|
||||
<tr>
|
||||
<th width="36%">{% trans "Email" %} / {% trans "Name" %} / {% trans "Contact Email" %}</th>
|
||||
<th width="12%">{% trans "Status" %}</th>
|
||||
<th width="16%">{% trans "Space Used" %}</th>
|
||||
<th width="22%">{% trans "Create At / Last Login" %}</th>
|
||||
<th width="14%">{% trans "Operations" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for user in users %}
|
||||
<tr data-userid="{{user.email}}">
|
||||
<td>
|
||||
<a href="{% url 'institutions:user_info' user.email %}">{{ user.email }}</a>
|
||||
{% if user.name %}<br />{{ user.name }}{% endif %}
|
||||
{% if user.contact_email %}<br />{{ user.contact_email }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="user-status">
|
||||
{% if user.is_active %}
|
||||
<span class="user-status-cur-value">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="user-status-cur-value">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
{% if not user.is_self and not user.is_staff and not user.is_institution_admin %}
|
||||
<span title="{% trans "Edit"%}" class="user-status-edit-icon sf2-icon-edit op-icon vh"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<select name="permission" class="user-status-select hide">
|
||||
<option value="1" {%if user.is_active %}selected="selected"{% endif %}>{% trans "Active" %}</option>
|
||||
<option value="0" {%if not user.is_active %}selected="selected"{% endif %}>{% trans "Inactive"%}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td style="font-size:11px;">
|
||||
<p> {{ user.space_usage|seahub_filesizeformat }} {% if user.space_quota > 0 %} / {{ user.space_quota|seahub_filesizeformat }} {% endif %} </p>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.source == "DB" %}
|
||||
{{ user.ctime|tsstr_sec }} /<br />
|
||||
{% else %}
|
||||
-- /
|
||||
{% endif %}
|
||||
{% if user.last_login %}{{user.last_login|translate_seahub_time}} {% else %} -- {% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not user.is_self and not user.is_staff and not user.is_institution_admin %}
|
||||
<a href="#" class="remove-user-btn op vh" data-url="{% url 'institutions:user_remove' user.email %}" data-target="{{ user.email }}">{% trans "Delete" %}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% include "snippets/admin_paginator.html" %}
|
||||
{% else %}
|
||||
<p>{% trans "Empty" %}</p>
|
||||
{% endif %}
|
||||
|
||||
<div id="activate-msg" class="hide">
|
||||
<p>{% trans "Activating..., please wait" %}</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript">
|
||||
addConfirmTo($('.remove-user-btn'), {
|
||||
'title':"{% trans "Delete User" %}",
|
||||
'con':"{% trans "Are you sure you want to delete %s ?" %}",
|
||||
'post': true // post request
|
||||
});
|
||||
|
||||
$('.user-status-edit-icon, .user-role-edit-icon').on('click', function() {
|
||||
$(this).parent().addClass('hide');
|
||||
$(this).parent().next().removeClass('hide'); // show 'select'
|
||||
});
|
||||
$('.user-status-select, .user-role-select').on('change', function() {
|
||||
var select = $(this),
|
||||
select_val = select.val(),
|
||||
uid = select.parents('tr').attr('data-userid'),
|
||||
$select_prev = $(this).prev('.user-status, .user-role'), // .user-status, .user-role
|
||||
url, data;
|
||||
|
||||
if (select.hasClass('user-status-select')) {
|
||||
url = "{{ SITE_ROOT }}inst/useradmin/toggle_status/" + uid + "/";
|
||||
data = {'s': select_val};
|
||||
} else {
|
||||
url = "{{ SITE_ROOT }}inst/useradmin/toggle_role/" + uid + "/";
|
||||
data = {'r': select_val};
|
||||
}
|
||||
|
||||
if (select_val == 1) {
|
||||
// show activating popup
|
||||
$('#activate-msg').modal();
|
||||
$('#simplemodal-container').css({'height':'auto'});
|
||||
}
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
cache: false,
|
||||
beforeSend: prepareCSRFToken,
|
||||
success: function(data) {
|
||||
if (data['email_sent']) {
|
||||
feedback("{% trans "Edit succeeded, an email has been sent." %}", 'success');
|
||||
} else if (data['email_sent'] === false) {
|
||||
feedback("{% trans "Edit succeeded, but failed to send email, please check your email configuration." %}", 'success');
|
||||
} else {
|
||||
feedback("{% trans "Edit succeeded" %}", 'success');
|
||||
}
|
||||
$('.user-status-cur-value', $select_prev).html(select.children('option[value="' +select.val() + '"]').text());
|
||||
select.addClass('hide');
|
||||
$select_prev.removeClass('hide');
|
||||
$.modal.close();
|
||||
},
|
||||
error: function() {
|
||||
feedback("{% trans "Edit failed." %}", 'error');
|
||||
select.addClass('hide');
|
||||
$select_prev.removeClass('hide');
|
||||
$.modal.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
// select shows, but the user doesn't select a value, or doesn't change the permission, click other place to hide the select
|
||||
$(document).on('click', function(e) {
|
||||
var target = e.target || event.srcElement;
|
||||
// target can't be edit-icon
|
||||
if (!$('.user-status-edit-icon, .user-status-select').is(target)) {
|
||||
$('.user-status').removeClass('hide');
|
||||
$('.user-status-select').addClass('hide');
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,77 +0,0 @@
|
||||
{% extends "institutions/base.html" %}
|
||||
{% load seahub_tags i18n %}
|
||||
{% block cur_users %}tab-cur{% endblock %}
|
||||
|
||||
|
||||
{% block right_panel %}
|
||||
<h3>{% trans "Search User"%}</h3>
|
||||
|
||||
<form id="search-user-form" method="get" action=".">
|
||||
<label>{% trans "Email" %}</label><br />
|
||||
<input type="text" name="q" class="input" value="{{q}}" /><br />
|
||||
<input type="submit" value="{% trans "Submit" %}" class="submit" />
|
||||
</form>
|
||||
<h3>{% trans "Result"%}</h3>
|
||||
|
||||
{% if users %}
|
||||
<table>
|
||||
<tr>
|
||||
<th width="36%">{% trans "Email" %} / {% trans "Name" %} / {% trans "Contact Email" %}</th>
|
||||
<th width="12%">{% trans "Status" %}</th>
|
||||
<th width="16%">{% trans "Space Used" %}</th>
|
||||
<th width="22%">{% trans "Create At / Last Login" %}</th>
|
||||
<th width="14%">{% trans "Operations" %}</th>
|
||||
</tr>
|
||||
|
||||
{% for user in users %}
|
||||
<tr data-userid="{{user.email}}">
|
||||
<td>
|
||||
<a href="{% url 'institutions:user_info' user.email %}">{{ user.email }}</a>
|
||||
{% if user.name %}<br />{{ user.name }}{% endif %}
|
||||
{% if user.contact_email %}<br />{{ user.contact_email }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="user-status">
|
||||
{% if user.is_active %}
|
||||
<span class="user-status-cur-value">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="user-status-cur-value">{% trans "Inactive" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-size:11px;">
|
||||
<p> {{ user.space_usage|seahub_filesizeformat }} {% if user.space_quota > 0 %} / {{ user.space_quota|seahub_filesizeformat }} {% endif %} </p>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.source == "DB" %}
|
||||
{{ user.ctime|tsstr_sec }} /<br />
|
||||
{% else %}
|
||||
-- /
|
||||
{% endif %}
|
||||
{% if user.last_login %}{{user.last_login|translate_seahub_time}} {% else %} -- {% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not user.is_self and not user.is_staff and not user.is_institution_admin %}
|
||||
<a href="#" class="remove-user-btn op vh" data-url="{% url 'institutions:user_remove' user.email %}" data-target="{{ user.email }}">{% trans "Delete" %}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<p>{% trans "Empty" %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript">
|
||||
addConfirmTo($('.remove-user-btn'), {
|
||||
'title':"{% trans "Delete User" %}",
|
||||
'con':"{% trans "Are you sure you want to delete %s ?" %}",
|
||||
'post': true // post request
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
@ -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/<str:email>/', useradmin_react_fake_view, name='useraedmin_email'),
|
||||
path('useradmin/<str:email>/owned-libraries/', useradmin_react_fake_view, name='useradmin_libraries'),
|
||||
path('useradmin/<str:email>/groups/', useradmin_react_fake_view, name='useradmin_groups'),
|
||||
path('useradmin/info/<str:email>/', user_info, name='user_info'),
|
||||
path('useradmin/remove/<str:email>/', user_remove, name='user_remove'),
|
||||
path('useradmin/search/', useradmin_search, name="useradmin_search"),
|
||||
path('useradmin/set_quota/<str:email>/', user_set_quota, name='user_set_quota'),
|
||||
path('useradmin/toggle_status/<str:email>/', user_toggle_status, name='user_toggle_status'),
|
||||
]
|
||||
|
@ -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):
|
||||
|
@ -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"),
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user