1
0
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:
杨顺强 2024-05-17 15:26:04 +08:00 committed by GitHub
parent 6d1bb9039d
commit eda0171d66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1445 additions and 589 deletions

View File

@ -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) => {

View 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;

View 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'));

View 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>
);
}

View 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;

View 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;

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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}/>
)}
</>
);
}

View File

@ -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;

View File

@ -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>
);
}

View 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;

View 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;

View 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;

View 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;

View 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 };
};

View File

@ -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 : '';

View File

@ -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;
}

View 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'),
]

View 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})

View 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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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'),
]

View File

@ -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):

View File

@ -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"),

View File

@ -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