mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-11 03:41:12 +00:00
Add department transfer function to the administrator interface (#6014)
* Add the function of transferring departments in the administrator interface * fix admin get all departments * update * Update departments.py * update * update * add department repo transfer * Update groups.py * update --------- Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com> Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
This commit is contained in:
@@ -1,19 +1,34 @@
|
|||||||
import React from 'react';
|
import React, {Fragment} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
Nav,
|
||||||
|
NavItem,
|
||||||
|
NavLink,
|
||||||
|
TabContent,
|
||||||
|
TabPane
|
||||||
|
} from 'reactstrap';
|
||||||
import makeAnimated from 'react-select/animated';
|
import makeAnimated from 'react-select/animated';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { gettext, isPro } from '../../utils/constants';
|
import {gettext, isPro, orgID} from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import UserSelect from '../user-select';
|
import UserSelect from '../user-select';
|
||||||
import { SeahubSelect } from '../common/select';
|
import { SeahubSelect } from '../common/select';
|
||||||
|
import '../../css/transfer-dialog.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
itemName: PropTypes.string.isRequired,
|
itemName: PropTypes.string.isRequired,
|
||||||
toggleDialog: PropTypes.func.isRequired,
|
toggleDialog: PropTypes.func.isRequired,
|
||||||
submit: PropTypes.func.isRequired,
|
submit: PropTypes.func.isRequired,
|
||||||
canTransferToDept: PropTypes.bool
|
canTransferToDept: PropTypes.bool,
|
||||||
|
isOrgAdmin: PropTypes.bool,
|
||||||
|
isSysAdmin: PropTypes.bool,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class TransferDialog extends React.Component {
|
class TransferDialog extends React.Component {
|
||||||
@@ -23,23 +38,54 @@ class TransferDialog extends React.Component {
|
|||||||
selectedOption: null,
|
selectedOption: null,
|
||||||
errorMsg: [],
|
errorMsg: [],
|
||||||
transferToUser: true,
|
transferToUser: true,
|
||||||
|
transferToGroup: false,
|
||||||
|
activeTab: 'transUser'
|
||||||
};
|
};
|
||||||
this.options = [];
|
this.options = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectChange = (option) => {
|
handleSelectChange = (option) => {
|
||||||
this.setState({selectedOption: option});
|
this.setState({ selectedOption: option });
|
||||||
};
|
};
|
||||||
|
|
||||||
submit = () => {
|
submit = () => {
|
||||||
let user = this.state.selectedOption;
|
let user = this.state.selectedOption;
|
||||||
this.props.submit(user);
|
this.props.submit(user);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (isPro) {
|
if (this.props.isOrgAdmin) {
|
||||||
|
seafileAPI.orgAdminListDepartments(orgID).then((res) => {
|
||||||
|
for (let i = 0; i < res.data.length; i++) {
|
||||||
|
let obj = {};
|
||||||
|
obj.value = res.data[i].name;
|
||||||
|
obj.email = res.data[i].email;
|
||||||
|
obj.label = res.data[i].name;
|
||||||
|
this.options.push(obj);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (this.props.isSysAdmin) {
|
||||||
|
seafileAPI.sysAdminListDepartments().then((res) => {
|
||||||
|
for (let i = 0; i < res.data.length; i++) {
|
||||||
|
let obj = {};
|
||||||
|
obj.value = res.data[i].name;
|
||||||
|
obj.email = res.data[i].email;
|
||||||
|
obj.label = res.data[i].name;
|
||||||
|
this.options.push(obj);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else{
|
||||||
seafileAPI.listDepartments().then((res) => {
|
seafileAPI.listDepartments().then((res) => {
|
||||||
for (let i = 0 ; i < res.data.length; i++) {
|
for (let i = 0; i < res.data.length; i++) {
|
||||||
let obj = {};
|
let obj = {};
|
||||||
obj.value = res.data[i].name;
|
obj.value = res.data[i].name;
|
||||||
obj.email = res.data[i].email;
|
obj.email = res.data[i].email;
|
||||||
@@ -56,49 +102,85 @@ class TransferDialog extends React.Component {
|
|||||||
onClick = () => {
|
onClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
transferToUser: !this.state.transferToUser,
|
transferToUser: !this.state.transferToUser,
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
toggle = (tab) => {
|
||||||
|
if (this.state.activeTab !== tab) {
|
||||||
|
this.setState({ activeTab: tab });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTransContent = () => {
|
||||||
|
let activeTab = this.state.activeTab;
|
||||||
let canTransferToDept = true;
|
let canTransferToDept = true;
|
||||||
if (this.props.canTransferToDept != undefined) {
|
if (this.props.canTransferToDept != undefined) {
|
||||||
canTransferToDept = this.props.canTransferToDept;
|
canTransferToDept = this.props.canTransferToDept;
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="transfer-dialog-side">
|
||||||
|
<Nav pills>
|
||||||
|
<NavItem role="tab" aria-selected={activeTab === 'transUser'} aria-controls="transfer-user-panel">
|
||||||
|
<NavLink className={activeTab === 'transUser' ? 'active' : ''} onClick={(this.toggle.bind(this, 'transUser'))} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||||
|
{gettext('Transfer to user')}
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
{isPro &&
|
||||||
|
<NavItem role="tab" aria-selected={activeTab === 'transDepart'} aria-controls="transfer-depart-panel">
|
||||||
|
<NavLink className={activeTab === 'transDepart' ? 'active' : ''} onClick={this.toggle.bind(this, 'transDepart')} tabIndex="0" onKeyDown={this.onTabKeyDown}>
|
||||||
|
{gettext('Transfer to department')}
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>}
|
||||||
|
</Nav>
|
||||||
|
</div>
|
||||||
|
<div className="transfer-dialog-main">
|
||||||
|
<TabContent activeTab={this.state.activeTab}>
|
||||||
|
<Fragment>
|
||||||
|
<TabPane tabId="transUser" role="tabpanel" id="transfer-user-panel">
|
||||||
|
<UserSelect
|
||||||
|
ref="userSelect"
|
||||||
|
isMulti={false}
|
||||||
|
className="reviewer-select"
|
||||||
|
placeholder={gettext('Select a user')}
|
||||||
|
onSelectChange={this.handleSelectChange}
|
||||||
|
/>
|
||||||
|
</TabPane>
|
||||||
|
{isPro && canTransferToDept &&
|
||||||
|
<TabPane tabId="transDepart" role="tabpanel" id="transfer-depart-panel">
|
||||||
|
<SeahubSelect
|
||||||
|
isClearable
|
||||||
|
maxMenuHeight={200}
|
||||||
|
hideSelectedOptions={true}
|
||||||
|
components={makeAnimated()}
|
||||||
|
placeholder={gettext('Select a department')}
|
||||||
|
options={this.options}
|
||||||
|
onChange={this.handleSelectChange}
|
||||||
|
value={this.state.selectedOption}
|
||||||
|
/>
|
||||||
|
</TabPane>}
|
||||||
|
</Fragment>
|
||||||
|
</TabContent>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render() {
|
||||||
|
|
||||||
|
|
||||||
const { itemName: repoName } = this.props;
|
const { itemName: repoName } = this.props;
|
||||||
let title = gettext('Transfer Library {library_name}');
|
let title = gettext('Transfer Library {library_name}');
|
||||||
title = title.replace('{library_name}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(repoName) + '</span>');
|
title = title.replace('{library_name}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(repoName) + '</span>');
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={true} toggle={this.props.toggleDialog}>
|
<Modal isOpen={true} style={{maxWidth: '720px'}} toggle={this.props.toggleDialog} className="transfer-dialog">
|
||||||
<ModalHeader toggle={this.props.toggleDialog}>
|
<ModalHeader toggle={this.props.toggleDialog}>
|
||||||
<span dangerouslySetInnerHTML={{__html: title}} className="d-flex mw-100"></span>
|
<span dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></span>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody className="transfer-dialog-content" role="tablist">
|
||||||
{this.state.transferToUser ?
|
{this.renderTransContent()}
|
||||||
<UserSelect
|
|
||||||
ref="userSelect"
|
|
||||||
isMulti={false}
|
|
||||||
className="reviewer-select"
|
|
||||||
placeholder={gettext('Select a user')}
|
|
||||||
onSelectChange={this.handleSelectChange}
|
|
||||||
/> :
|
|
||||||
<SeahubSelect
|
|
||||||
isClearable
|
|
||||||
maxMenuHeight={200}
|
|
||||||
hideSelectedOptions={true}
|
|
||||||
components={makeAnimated()}
|
|
||||||
placeholder={gettext('Select a department')}
|
|
||||||
options={this.options}
|
|
||||||
onChange={this.handleSelectChange}
|
|
||||||
value={this.state.selectedOption}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{isPro && canTransferToDept &&
|
|
||||||
<span role="button" tabIndex="0" className="action-link" onClick={this.onClick} onKeyDown={Utils.onKeyDown}>{this.state.transferToUser ?
|
|
||||||
gettext('Transfer to department'): gettext('Transfer to user')}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
|
<Button color="secondary" onClick={this.props.toggleDialog}>{gettext('Cancel')}</Button>
|
||||||
|
52
frontend/src/css/transfer-dialog.css
Normal file
52
frontend/src/css/transfer-dialog.css
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.transfer-dialog .transfer-dialog-content {
|
||||||
|
padding: 0;
|
||||||
|
min-height: 22.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.transfer-dialog .transfer-dialog-content {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-dialog-content .transfer-dialog-side {
|
||||||
|
flex-basis: 29%;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-dialog .nav .nav-item .nav-link {
|
||||||
|
padding: 0.3125rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.transfer-dialog-content .transfer-dialog-side {
|
||||||
|
padding: 12px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.transfer-dialog-side .nav {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.transfer-dialog-side .nav-pills .nav-item .nav-link {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.3125rem 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-dialog-content .transfer-dialog-main {
|
||||||
|
display: flex;
|
||||||
|
flex-basis: 78%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-dialog-content .transfer-dialog-main .tab-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-dialog-content .transfer-dialog-main .tab-pane {
|
||||||
|
height: 100%;
|
||||||
|
}
|
@@ -200,7 +200,8 @@ class RepoItem extends React.Component {
|
|||||||
highlight: false,
|
highlight: false,
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
isItemMenuShow: false,
|
isItemMenuShow: false,
|
||||||
isTransferDialogShow: false
|
isTransferDialogShow: false,
|
||||||
|
orgAdmin: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +292,7 @@ class RepoItem extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
let { repo } = this.props;
|
let { repo } = this.props;
|
||||||
|
|
||||||
let isOperationMenuShow = this.state.showMenu && !repo.isDepartmentRepo;
|
let isOperationMenuShow = this.state.showMenu;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||||
@@ -326,6 +327,7 @@ class RepoItem extends React.Component {
|
|||||||
itemName={repo.repoName}
|
itemName={repo.repoName}
|
||||||
submit={this.onTransferRepo}
|
submit={this.onTransferRepo}
|
||||||
toggleDialog={this.toggleTransfer}
|
toggleDialog={this.toggleTransfer}
|
||||||
|
isOrgAdmin={true}
|
||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
)}
|
)}
|
||||||
|
@@ -280,6 +280,12 @@ class Item extends Component {
|
|||||||
getOperations = () => {
|
getOperations = () => {
|
||||||
const { repo } = this.props;
|
const { repo } = this.props;
|
||||||
let operations = ['Delete', 'Transfer'];
|
let operations = ['Delete', 'Transfer'];
|
||||||
|
const index = repo.owner_email.indexOf('@seafile_group');
|
||||||
|
let isGroupOwnedRepo = index != -1;
|
||||||
|
if (isGroupOwnedRepo) {
|
||||||
|
operations = ['Transfer'];
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
if (!repo.encrypted) {
|
if (!repo.encrypted) {
|
||||||
operations.push('Share');
|
operations.push('Share');
|
||||||
}
|
}
|
||||||
@@ -319,7 +325,7 @@ class Item extends Component {
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{(!isGroupOwnedRepo && isOpIconShown) &&
|
{(isOpIconShown) &&
|
||||||
<OpMenu
|
<OpMenu
|
||||||
operations={this.getOperations()}
|
operations={this.getOperations()}
|
||||||
translateOperations={this.translateOperations}
|
translateOperations={this.translateOperations}
|
||||||
@@ -359,8 +365,8 @@ class Item extends Component {
|
|||||||
<TransferDialog
|
<TransferDialog
|
||||||
itemName={repo.name}
|
itemName={repo.name}
|
||||||
submit={this.onTransferRepo}
|
submit={this.onTransferRepo}
|
||||||
canTransferToDept={false}
|
|
||||||
toggleDialog={this.toggleTransferDialog}
|
toggleDialog={this.toggleTransferDialog}
|
||||||
|
isSysAdmin={true}
|
||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
}
|
}
|
||||||
|
@@ -11,8 +11,11 @@ from django.utils.translation import gettext as _
|
|||||||
from seaserv import seafile_api, ccnet_api
|
from seaserv import seafile_api, ccnet_api
|
||||||
from pysearpc import SearpcError
|
from pysearpc import SearpcError
|
||||||
|
|
||||||
|
from seahub.avatar.settings import GROUP_AVATAR_DEFAULT_SIZE
|
||||||
|
from seahub.avatar.templatetags.group_avatar_tags import get_default_group_avatar_url, api_grp_avatar_url
|
||||||
from seahub.base.accounts import User
|
from seahub.base.accounts import User
|
||||||
from seahub.base.templatetags.seahub_tags import email2nickname
|
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||||
|
from seahub.settings import CLOUD_MODE, MULTI_TENANCY
|
||||||
from seahub.utils import is_valid_username, is_pro_version
|
from seahub.utils import is_valid_username, is_pro_version
|
||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||||
from seahub.group.utils import is_group_member, is_group_admin, \
|
from seahub.group.utils import is_group_member, is_group_admin, \
|
||||||
@@ -351,3 +354,47 @@ class AdminSearchGroup(APIView):
|
|||||||
result.append(group_info)
|
result.append(group_info)
|
||||||
|
|
||||||
return Response({"group_list": result})
|
return Response({"group_list": result})
|
||||||
|
|
||||||
|
|
||||||
|
class AdminDepartments(APIView):
|
||||||
|
"""
|
||||||
|
List all departments
|
||||||
|
"""
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAdminUser,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
try:
|
||||||
|
all_groups = ccnet_api.list_all_departments()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
avatar_size = int(request.GET.get('avatar_size', GROUP_AVATAR_DEFAULT_SIZE))
|
||||||
|
except ValueError:
|
||||||
|
avatar_size = GROUP_AVATAR_DEFAULT_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for group in all_groups:
|
||||||
|
try:
|
||||||
|
avatar_url, is_default, date_uploaded = api_grp_avatar_url(group.id, avatar_size)
|
||||||
|
except:
|
||||||
|
avatar_url = get_default_group_avatar_url()
|
||||||
|
created_at = timestamp_to_isoformat_timestr(group.timestamp)
|
||||||
|
department_info = {
|
||||||
|
"id": group.id,
|
||||||
|
"email": '%s@seafile_group' % str(group.id),
|
||||||
|
"parent_group_id": group.parent_group_id,
|
||||||
|
"name": group.group_name,
|
||||||
|
"owner": group.creator_name,
|
||||||
|
"created_at": created_at,
|
||||||
|
"avatar_url": request.build_absolute_uri(avatar_url),
|
||||||
|
}
|
||||||
|
result.append(department_info)
|
||||||
|
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from django.template.defaultfilters import filesizeformat
|
from django.template.defaultfilters import filesizeformat
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
import seaserv
|
||||||
from seaserv import ccnet_api, seafile_api
|
from seaserv import ccnet_api, seafile_api
|
||||||
|
|
||||||
from seahub.api2.authentication import TokenAuthentication
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
@@ -26,6 +27,7 @@ from seahub.utils import is_valid_dirent_name, is_valid_email
|
|||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||||
|
|
||||||
from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner
|
from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner
|
||||||
|
from seahub.constants import PERMISSION_READ_WRITE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from seahub.settings import MULTI_TENANCY
|
from seahub.settings import MULTI_TENANCY
|
||||||
@@ -342,7 +344,7 @@ class AdminLibrary(APIView):
|
|||||||
|
|
||||||
new_owner = request.data.get('owner', None)
|
new_owner = request.data.get('owner', None)
|
||||||
if new_owner:
|
if new_owner:
|
||||||
if not is_valid_email(new_owner):
|
if not is_valid_email(new_owner) and '@seafile_group' not in new_owner:
|
||||||
error_msg = 'owner invalid.'
|
error_msg = 'owner invalid.'
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
@@ -419,7 +421,14 @@ class AdminLibrary(APIView):
|
|||||||
pub_repos = seafile_api.list_inner_pub_repos_by_owner(repo_owner)
|
pub_repos = seafile_api.list_inner_pub_repos_by_owner(repo_owner)
|
||||||
|
|
||||||
# transfer repo
|
# transfer repo
|
||||||
seafile_api.set_repo_owner(repo_id, new_owner)
|
if '@seafile_group' in new_owner:
|
||||||
|
group_id = int(new_owner.split('@')[0])
|
||||||
|
if seaserv.is_org_group(group_id):
|
||||||
|
error_msg = 'Can not transfer library to an organization department %s' % new_owner
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
seafile_api.transfer_repo_to_group(repo_id, group_id, PERMISSION_READ_WRITE)
|
||||||
|
else:
|
||||||
|
seafile_api.set_repo_owner(repo_id, new_owner)
|
||||||
|
|
||||||
# reshare repo to user
|
# reshare repo to user
|
||||||
for shared_user in shared_users:
|
for shared_user in shared_users:
|
||||||
|
@@ -13,7 +13,10 @@ from seahub.api2.throttling import UserRateThrottle
|
|||||||
from seahub.api2.authentication import TokenAuthentication
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
from seahub.api2.utils import api_error
|
from seahub.api2.utils import api_error
|
||||||
from seahub.api2.endpoints.admin.groups import AdminGroup as SysAdminGroup
|
from seahub.api2.endpoints.admin.groups import AdminGroup as SysAdminGroup
|
||||||
|
from seahub.avatar.settings import GROUP_AVATAR_DEFAULT_SIZE
|
||||||
|
from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, get_default_group_avatar_url
|
||||||
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
|
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
|
||||||
|
from seahub.utils.ccnet_db import CcnetDB
|
||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||||
|
|
||||||
from pysearpc import SearpcError
|
from pysearpc import SearpcError
|
||||||
@@ -202,3 +205,45 @@ class OrgAdminSearchGroup(APIView):
|
|||||||
groups_list.append(group)
|
groups_list.append(group)
|
||||||
|
|
||||||
return Response({'group_list': groups_list})
|
return Response({'group_list': groups_list})
|
||||||
|
|
||||||
|
|
||||||
|
class OrgAdminDepartments(APIView):
|
||||||
|
"""
|
||||||
|
List all departments of the current organization
|
||||||
|
"""
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request, org_id):
|
||||||
|
try:
|
||||||
|
db_api = CcnetDB()
|
||||||
|
departments = db_api.list_org_departments(int(org_id))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
avatar_size = int(request.GET.get('avatar_size', GROUP_AVATAR_DEFAULT_SIZE))
|
||||||
|
except ValueError:
|
||||||
|
avatar_size = GROUP_AVATAR_DEFAULT_SIZE
|
||||||
|
result = []
|
||||||
|
for group in departments:
|
||||||
|
try:
|
||||||
|
avatar_url, is_default, date_uploaded = api_grp_avatar_url(group.id, avatar_size)
|
||||||
|
except:
|
||||||
|
avatar_url = get_default_group_avatar_url()
|
||||||
|
created_at = timestamp_to_isoformat_timestr(group.timestamp)
|
||||||
|
department_info = {
|
||||||
|
"id": group.id,
|
||||||
|
"email": '%s@seafile_group' % str(group.id),
|
||||||
|
"parent_group_id": group.parent_group_id,
|
||||||
|
"name": group.group_name,
|
||||||
|
"owner": group.creator_name,
|
||||||
|
"created_at": created_at,
|
||||||
|
"avatar_url": request.build_absolute_uri(avatar_url),
|
||||||
|
}
|
||||||
|
result.append(department_info)
|
||||||
|
|
||||||
|
return Response(result)
|
||||||
|
@@ -19,11 +19,13 @@ from seahub.group.utils import group_id_to_name
|
|||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||||
from seahub.utils import is_valid_email
|
from seahub.utils import is_valid_email
|
||||||
from seahub.signals import repo_deleted
|
from seahub.signals import repo_deleted
|
||||||
|
from seahub.constants import PERMISSION_READ_WRITE
|
||||||
|
|
||||||
from seahub.organizations.views import is_org_repo, org_user_exists
|
from seahub.organizations.views import is_org_repo, org_user_exists
|
||||||
|
|
||||||
from pysearpc import SearpcError
|
from pysearpc import SearpcError
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -128,12 +130,11 @@ class OrgAdminRepo(APIView):
|
|||||||
"""Transfer an organization library
|
"""Transfer an organization library
|
||||||
"""
|
"""
|
||||||
new_owner = request.data.get('email', None)
|
new_owner = request.data.get('email', None)
|
||||||
|
|
||||||
if not new_owner:
|
if not new_owner:
|
||||||
error_msg = 'Email invalid.'
|
error_msg = 'Email invalid.'
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
if not is_valid_email(new_owner):
|
if not is_valid_email(new_owner) and not '@seafile_group' in new_owner:
|
||||||
error_msg = 'Email invalid.'
|
error_msg = 'Email invalid.'
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
@@ -143,9 +144,10 @@ class OrgAdminRepo(APIView):
|
|||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
# permission checking
|
# permission checking
|
||||||
if not org_user_exists(org_id, new_owner):
|
if '@seafile_group' not in new_owner:
|
||||||
error_msg = 'User %s not in org %s.' % (new_owner, org_id)
|
if not org_user_exists(org_id, new_owner):
|
||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
error_msg = 'User %s not in org %s.' % (new_owner, org_id)
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
repo = seafile_api.get_repo(repo_id)
|
repo = seafile_api.get_repo(repo_id)
|
||||||
if not repo:
|
if not repo:
|
||||||
@@ -166,8 +168,17 @@ class OrgAdminRepo(APIView):
|
|||||||
|
|
||||||
# get all pub repos
|
# get all pub repos
|
||||||
pub_repos = seafile_api.list_org_inner_pub_repos_by_owner(org_id, repo_owner)
|
pub_repos = seafile_api.list_org_inner_pub_repos_by_owner(org_id, repo_owner)
|
||||||
|
# transfer repo
|
||||||
seafile_api.set_org_repo_owner(org_id, repo_id, new_owner)
|
if '@seafile_group' in new_owner:
|
||||||
|
group_id = int(new_owner.split('@')[0])
|
||||||
|
if seaserv.is_org_group(group_id):
|
||||||
|
group_org_id = ccnet_api.get_org_id_by_group(group_id)
|
||||||
|
if org_id != group_org_id:
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
seafile_api.org_transfer_repo_to_group(repo_id, org_id, group_id, PERMISSION_READ_WRITE)
|
||||||
|
else:
|
||||||
|
seafile_api.set_org_repo_owner(org_id, repo_id, new_owner)
|
||||||
|
|
||||||
# reshare repo to user
|
# reshare repo to user
|
||||||
for shared_user in shared_users:
|
for shared_user in shared_users:
|
||||||
|
@@ -13,7 +13,7 @@ from .api.group_members import AdminGroupMembers, AdminGroupMember
|
|||||||
from .api.admin.users import OrgAdminUser, OrgAdminUsers, OrgAdminSearchUser, \
|
from .api.admin.users import OrgAdminUser, OrgAdminUsers, OrgAdminSearchUser, \
|
||||||
OrgAdminImportUsers, OrgAdminInviteUser
|
OrgAdminImportUsers, OrgAdminInviteUser
|
||||||
from .api.admin.user_set_password import OrgAdminUserSetPassword
|
from .api.admin.user_set_password import OrgAdminUserSetPassword
|
||||||
from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup
|
from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup, OrgAdminDepartments
|
||||||
from .api.admin.repos import OrgAdminRepos, OrgAdminRepo
|
from .api.admin.repos import OrgAdminRepos, OrgAdminRepo
|
||||||
from .api.admin.info import OrgAdminInfo
|
from .api.admin.info import OrgAdminInfo
|
||||||
from .api.admin.links import OrgAdminLinks, OrgAdminLink
|
from .api.admin.links import OrgAdminLinks, OrgAdminLink
|
||||||
@@ -95,5 +95,6 @@ urlpatterns = [
|
|||||||
path('admin/logs/file-access/', OrgAdminLogsFileAccess.as_view(), name='api-v2.1-org-admin-logs-file-access'),
|
path('admin/logs/file-access/', OrgAdminLogsFileAccess.as_view(), name='api-v2.1-org-admin-logs-file-access'),
|
||||||
path('admin/logs/file-update/', OrgAdminLogsFileUpdate.as_view(), name='api-v2.1-org-admin-logs-file-update'),
|
path('admin/logs/file-update/', OrgAdminLogsFileUpdate.as_view(), name='api-v2.1-org-admin-logs-file-update'),
|
||||||
path('admin/logs/repo-permission/', OrgAdminLogsPermAudit.as_view(), name='api-v2.1-org-admin-logs-repo-permission'),
|
path('admin/logs/repo-permission/', OrgAdminLogsPermAudit.as_view(), name='api-v2.1-org-admin-logs-repo-permission'),
|
||||||
|
path('<int:org_id>/admin/departments/', OrgAdminDepartments.as_view(), name='api-v2.1-org-admin-departments'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -150,7 +150,7 @@ from seahub.api2.endpoints.admin.system_library import AdminSystemLibrary, \
|
|||||||
AdminSystemLibraryUploadLink
|
AdminSystemLibraryUploadLink
|
||||||
from seahub.api2.endpoints.admin.default_library import AdminDefaultLibrary
|
from seahub.api2.endpoints.admin.default_library import AdminDefaultLibrary
|
||||||
from seahub.api2.endpoints.admin.trash_libraries import AdminTrashLibraries, AdminTrashLibrary
|
from seahub.api2.endpoints.admin.trash_libraries import AdminTrashLibraries, AdminTrashLibrary
|
||||||
from seahub.api2.endpoints.admin.groups import AdminGroups, AdminGroup, AdminSearchGroup
|
from seahub.api2.endpoints.admin.groups import AdminGroups, AdminGroup, AdminSearchGroup, AdminDepartments
|
||||||
from seahub.api2.endpoints.admin.group_libraries import AdminGroupLibraries, AdminGroupLibrary
|
from seahub.api2.endpoints.admin.group_libraries import AdminGroupLibraries, AdminGroupLibrary
|
||||||
from seahub.api2.endpoints.admin.group_members import AdminGroupMembers, AdminGroupMember
|
from seahub.api2.endpoints.admin.group_members import AdminGroupMembers, AdminGroupMember
|
||||||
from seahub.api2.endpoints.admin.shares import AdminShares
|
from seahub.api2.endpoints.admin.shares import AdminShares
|
||||||
@@ -635,6 +635,9 @@ urlpatterns = [
|
|||||||
re_path(r'^api/v2.1/admin/groups/(?P<group_id>\d+)/group-owned-libraries/$', AdminGroupOwnedLibraries.as_view(), name='api-v2.1-admin-group-owned-libraries'),
|
re_path(r'^api/v2.1/admin/groups/(?P<group_id>\d+)/group-owned-libraries/$', AdminGroupOwnedLibraries.as_view(), name='api-v2.1-admin-group-owned-libraries'),
|
||||||
re_path(r'^api/v2.1/admin/groups/(?P<group_id>\d+)/group-owned-libraries/(?P<repo_id>[-0-9a-f]{36})/$', AdminGroupOwnedLibrary.as_view(), name='api-v2.1-admin-owned-group-library'),
|
re_path(r'^api/v2.1/admin/groups/(?P<group_id>\d+)/group-owned-libraries/(?P<repo_id>[-0-9a-f]{36})/$', AdminGroupOwnedLibrary.as_view(), name='api-v2.1-admin-owned-group-library'),
|
||||||
|
|
||||||
|
## admin::departments
|
||||||
|
re_path(r'api/v2.1/admin/departments/$', AdminDepartments.as_view(), name='api-v2.1-admin-departments'),
|
||||||
|
|
||||||
## admin::shares
|
## admin::shares
|
||||||
re_path(r'^api/v2.1/admin/shares/$', AdminShares.as_view(), name='api-v2.1-admin-shares'),
|
re_path(r'^api/v2.1/admin/shares/$', AdminShares.as_view(), name='api-v2.1-admin-shares'),
|
||||||
|
|
||||||
|
@@ -22,3 +22,56 @@ def get_ccnet_db_name():
|
|||||||
error_msg = 'Failed to init ccnet db, only mysql db supported.'
|
error_msg = 'Failed to init ccnet db, only mysql db supported.'
|
||||||
return None, error_msg
|
return None, error_msg
|
||||||
return db_name, None
|
return db_name, None
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import configparser
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
class CcnetGroup(object):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.id = kwargs.get('group_id')
|
||||||
|
self.group_name = kwargs.get('group_name')
|
||||||
|
self.creator_name = kwargs.get('creator_name')
|
||||||
|
self.timestamp = kwargs.get('timestamp')
|
||||||
|
self.parent_group_id = kwargs.get('parent_group_id')
|
||||||
|
|
||||||
|
class CcnetDB:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.db_name = get_ccnet_db_name()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def list_org_departments(self, org_id):
|
||||||
|
sql = f"""
|
||||||
|
SELECT
|
||||||
|
g.group_id, group_name, creator_name, timestamp, type, parent_group_id
|
||||||
|
FROM
|
||||||
|
`{self.db_name}`.`OrgGroup` o
|
||||||
|
LEFT JOIN
|
||||||
|
`{self.db_name}`.`Group` g
|
||||||
|
ON o.group_id=g.group_id
|
||||||
|
WHERE
|
||||||
|
org_id={org_id} AND parent_group_id<>0;
|
||||||
|
"""
|
||||||
|
groups = []
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(sql)
|
||||||
|
for item in cursor.fetchall():
|
||||||
|
group_id = item[0]
|
||||||
|
group_name = item[1]
|
||||||
|
creator_name = item[2]
|
||||||
|
timestamp=item[3]
|
||||||
|
parent_group_id = item[5]
|
||||||
|
params = {
|
||||||
|
'group_id':group_id,
|
||||||
|
'group_name': group_name,
|
||||||
|
'creator_name': creator_name,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'parent_group_id': parent_group_id
|
||||||
|
}
|
||||||
|
group_obj = CcnetGroup(**params)
|
||||||
|
groups.append(group_obj)
|
||||||
|
return groups
|
||||||
|
Reference in New Issue
Block a user