1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-06 17:33:18 +00:00

add dep admin transfer dep repo

This commit is contained in:
孙永强
2024-10-15 15:39:37 +08:00
committed by r350178982
parent a362b786a8
commit e0d48c9fff
9 changed files with 249 additions and 17 deletions

View File

@@ -28,6 +28,8 @@ const propTypes = {
canTransferToDept: PropTypes.bool, canTransferToDept: PropTypes.bool,
isOrgAdmin: PropTypes.bool, isOrgAdmin: PropTypes.bool,
isSysAdmin: PropTypes.bool, isSysAdmin: PropTypes.bool,
isDepAdminTransfer: PropTypes.bool,
}; };
const TRANS_USER = 'transUser'; const TRANS_USER = 'transUser';
@@ -41,7 +43,7 @@ class TransferDialog extends React.Component {
errorMsg: [], errorMsg: [],
transferToUser: true, transferToUser: true,
transferToGroup: false, transferToGroup: false,
activeTab: TRANS_USER activeTab: !this.props.isDepAdminTransfer ? TRANS_USER : TRANS_DEPART
}; };
this.options = []; this.options = [];
} }
@@ -131,16 +133,18 @@ class TransferDialog extends React.Component {
<Fragment> <Fragment>
<div className="transfer-dialog-side"> <div className="transfer-dialog-side">
<Nav pills> <Nav pills>
<NavItem role="tab" aria-selected={activeTab === TRANS_USER} aria-controls="transfer-user-panel"> {!this.props.isDepAdminTransfer &&
<NavLink <NavItem role="tab" aria-selected={activeTab === TRANS_USER} aria-controls="transfer-user-panel">
className={activeTab === TRANS_USER ? 'active' : ''} <NavLink
onClick={(this.toggle.bind(this, TRANS_USER))} className={activeTab === TRANS_USER ? 'active' : ''}
tabIndex="0" onClick={(this.toggle.bind(this, TRANS_USER))}
onKeyDown={this.onTabKeyDown} tabIndex="0"
> onKeyDown={this.onTabKeyDown}
{gettext('Transfer to user')} >
</NavLink> {gettext('Transfer to user')}
</NavItem> </NavLink>
</NavItem>
}
{isPro && {isPro &&
<NavItem role="tab" aria-selected={activeTab === TRANS_DEPART} aria-controls="transfer-depart-panel"> <NavItem role="tab" aria-selected={activeTab === TRANS_DEPART} aria-controls="transfer-depart-panel">
<NavLink <NavLink

View File

@@ -15,12 +15,14 @@ import ResetEncryptedRepoPasswordDialog from '../../components/dialog/reset-encr
import LibOldFilesAutoDelDialog from '../../components/dialog/lib-old-files-auto-del-dialog'; import LibOldFilesAutoDelDialog from '../../components/dialog/lib-old-files-auto-del-dialog';
import Rename from '../rename'; import Rename from '../rename';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { userAPI } from '../../utils/user-api';
import LibHistorySettingDialog from '../dialog/lib-history-setting-dialog'; import LibHistorySettingDialog from '../dialog/lib-history-setting-dialog';
import toaster from '../toast'; import toaster from '../toast';
import RepoAPITokenDialog from '../dialog/repo-api-token-dialog'; import RepoAPITokenDialog from '../dialog/repo-api-token-dialog';
import RepoShareAdminDialog from '../dialog/repo-share-admin-dialog'; import RepoShareAdminDialog from '../dialog/repo-share-admin-dialog';
import RepoMonitoredIcon from '../../components/repo-monitored-icon'; import RepoMonitoredIcon from '../../components/repo-monitored-icon';
import { GRID_MODE, LIST_MODE } from '../dir-view-mode/constants'; import { GRID_MODE, LIST_MODE } from '../dir-view-mode/constants';
import TransferDialog from '../dialog/transfer-dialog';
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@@ -37,6 +39,7 @@ const propTypes = {
onItemDelete: PropTypes.func, onItemDelete: PropTypes.func,
onMonitorRepo: PropTypes.func, onMonitorRepo: PropTypes.func,
onContextMenu: PropTypes.func.isRequired, onContextMenu: PropTypes.func.isRequired,
onTransferRepo: PropTypes.func
}; };
class SharedRepoListItem extends React.Component { class SharedRepoListItem extends React.Component {
@@ -55,6 +58,7 @@ class SharedRepoListItem extends React.Component {
isHistorySettingDialogShow: false, isHistorySettingDialogShow: false,
isDeleteDialogShow: false, isDeleteDialogShow: false,
isAPITokenDialogShow: false, isAPITokenDialogShow: false,
isTransferDialogShow: false,
isRepoShareAdminDialogOpen: false, isRepoShareAdminDialogOpen: false,
isRepoDeleted: false, isRepoDeleted: false,
isChangePasswordDialogShow: false, isChangePasswordDialogShow: false,
@@ -166,6 +170,9 @@ class SharedRepoListItem extends React.Component {
case 'Rename': case 'Rename':
this.onItemRenameToggle(); this.onItemRenameToggle();
break; break;
case 'Transfer':
this.onTransferToggle();
break;
case 'Folder Permission': case 'Folder Permission':
this.onItemFolderPermissionToggle(); this.onItemFolderPermissionToggle();
break; break;
@@ -231,6 +238,33 @@ class SharedRepoListItem extends React.Component {
}); });
}; };
onTransferToggle = () => {
this.setState({ isTransferDialogShow: !this.state.isTransferDialogShow });
};
onTransferRepo = (user) => {
let repoID = this.props.repo.repo_id;
let groupID = this.props.currentGroup.id;
let email = null;
if (Array.isArray(user)) {
email = user[0].email;
} else {
email = user.email;
}
userAPI.depAdminTransferRepo(repoID, groupID, email).then(res => {
this.props.onTransferRepo(repoID, groupID, email);
let message = gettext('Successfully transferred the library.');
toaster.success(message);
}).catch(error => {
if (error.response) {
toaster.danger(error.response.data.error_msg || gettext('Error'), { duration: 3 });
} else {
toaster.danger(gettext('Failed. Please check the network.'), { duration: 3 });
}
});
this.onTransferToggle();
};
onRenameConfirm = (name) => { onRenameConfirm = (name) => {
this.props.onItemRename(this.props.repo, name); this.props.onItemRename(this.props.repo, name);
this.onRenameCancel(); this.onRenameCancel();
@@ -322,6 +356,9 @@ class SharedRepoListItem extends React.Component {
case 'Rename': case 'Rename':
translateResult = gettext('Rename'); translateResult = gettext('Rename');
break; break;
case 'Transfer':
translateResult = gettext('Transfer');
break;
case 'Folder Permission': case 'Folder Permission':
translateResult = gettext('Folder Permission'); translateResult = gettext('Folder Permission');
break; break;
@@ -389,7 +426,7 @@ class SharedRepoListItem extends React.Component {
if (isStaff) { if (isStaff) {
if (repo.owner_email == currentGroup.id + '@seafile_group') { if (repo.owner_email == currentGroup.id + '@seafile_group') {
this.isDeparementOnwerGroupMember = true; this.isDeparementOnwerGroupMember = true;
operations = ['Rename']; operations = ['Rename', 'Transfer'];
if (folderPermEnabled) { if (folderPermEnabled) {
operations.push('Folder Permission'); operations.push('Folder Permission');
} }
@@ -822,6 +859,17 @@ class SharedRepoListItem extends React.Component {
/> />
</ModalPortal> </ModalPortal>
)} )}
{this.state.isTransferDialogShow && (
<ModalPortal>
<TransferDialog
itemName={repo.repo_name}
submit={this.onTransferRepo}
canTransferToDept={true}
toggleDialog={this.onTransferToggle}
isDepAdminTransfer={true}
/>
</ModalPortal>
)}
</Fragment> </Fragment>
); );
} }

View File

@@ -131,6 +131,7 @@ class SharedRepoListView extends React.Component {
isItemFreezed={this.state.isItemFreezed} isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem} onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem} onUnfreezedItem={this.onUnfreezedItem}
onTransferRepo={this.props.onTransferRepo}
onItemUnshare={this.props.onItemUnshare} onItemUnshare={this.props.onItemUnshare}
onItemDelete={this.props.onItemDelete} onItemDelete={this.props.onItemDelete}
onItemRename={this.props.onItemRename} onItemRename={this.props.onItemRename}

View File

@@ -14,7 +14,9 @@ const propTypes = {
inAllLibs: PropTypes.bool, inAllLibs: PropTypes.bool,
currentViewMode: PropTypes.string, currentViewMode: PropTypes.string,
group: PropTypes.object.isRequired, group: PropTypes.object.isRequired,
updateGroup: PropTypes.func.isRequired updateGroup: PropTypes.func.isRequired,
onTransferRepo: PropTypes.func.isRequired
}; };
@@ -139,6 +141,7 @@ class GroupItem extends React.Component {
onItemDelete={this.onItemDelete} onItemDelete={this.onItemDelete}
onItemRename={this.onItemRename} onItemRename={this.onItemRename}
onMonitorRepo={this.onMonitorRepo} onMonitorRepo={this.onMonitorRepo}
onTransferRepo={this.props.onTransferRepo}
currentViewMode={currentViewMode} currentViewMode={currentViewMode}
/> />
} }

View File

@@ -149,6 +149,31 @@ class Libraries extends Component {
this.setState({ repoList: repoList }); this.setState({ repoList: repoList });
}; };
onGroupTransferRepo = (repoID, oldGroupID, newOwner) => {
let newGroupID = parseInt(newOwner.split('@')[0]);
let repoToMove = null;
const updatedGroups = this.state.groupList.map(group => {
if (group.id === oldGroupID) {
group.repos = group.repos.filter(repo => {
if (repo.repo_id === repoID) {
repoToMove = repo;
return false;
}
return true;
});
}
return group;
});
if (repoToMove) {
updatedGroups.forEach(group => {
if (group.id === newGroupID) {
group.repos.push(repoToMove);
}
});
}
this.setState({ groupList: updatedGroups });
};
onRenameRepo = (repo, newName) => { onRenameRepo = (repo, newName) => {
let repoList = this.state.repoList.map(item => { let repoList = this.state.repoList.map(item => {
if (item.repo_id === repo.repo_id) { if (item.repo_id === repo.repo_id) {
@@ -326,6 +351,7 @@ class Libraries extends Component {
inAllLibs={true} inAllLibs={true}
group={group} group={group}
updateGroup={this.updateGroup} updateGroup={this.updateGroup}
onTransferRepo={this.onGroupTransferRepo}
currentViewMode={currentViewMode} currentViewMode={currentViewMode}
/> />
); );

View File

@@ -227,7 +227,13 @@ class RepoItem extends React.Component {
onTransferRepo = (user) => { onTransferRepo = (user) => {
let repo = this.props.repo; let repo = this.props.repo;
seafileAPI.orgAdminTransferOrgRepo(orgID, repo.repoID, user.email).then(res => { let email = null;
if (Array.isArray(user)) {
email = user[0].email;
} else {
email = user.email;
}
seafileAPI.orgAdminTransferOrgRepo(orgID, repo.repoID, email).then(res => {
this.props.transferRepoItem(repo.repoID, user); this.props.transferRepoItem(repo.repoID, user);
let msg = gettext('Successfully transferred the library.'); let msg = gettext('Successfully transferred the library.');
toaster.success(msg); toaster.success(msg);

View File

@@ -47,6 +47,13 @@ class UserAPI {
const url = this.server + '/api/v2.1/query-io-status/?task_id=' + task_id; const url = this.server + '/api/v2.1/query-io-status/?task_id=' + task_id;
return this.req.get(url); return this.req.get(url);
} }
depAdminTransferRepo(repo_id, group_id, email) {
const url = this.server + '/api/v2.1/groups/' + group_id + '/group-owned-libraries/' + repo_id + '/transfer/';
const formData = new FormData();
formData.append('email', email);
return this.req.put(url, formData);
}
} }
let userAPI = new UserAPI(); let userAPI = new UserAPI();

View File

@@ -9,7 +9,7 @@ from rest_framework.views import APIView
from rest_framework import status from rest_framework import status
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from seaserv import seafile_api, ccnet_api from seaserv import seafile_api, ccnet_api, seafserv_threaded_rpc
from constance import config from constance import config
@@ -25,11 +25,12 @@ from seahub.base.templatetags.seahub_tags import email2nickname, \
email2contact_email email2contact_email
from seahub.base.accounts import User from seahub.base.accounts import User
from seahub.organizations.models import OrgAdminSettings, DISABLE_ORG_ENCRYPTED_LIBRARY from seahub.organizations.models import OrgAdminSettings, DISABLE_ORG_ENCRYPTED_LIBRARY
from seahub.organizations.views import org_user_exists
from seahub.signals import repo_created from seahub.signals import repo_created
from seahub.group.utils import is_group_admin from seahub.group.utils import is_group_admin
from seahub.utils import is_valid_dirent_name, is_org_context, \ from seahub.utils import is_valid_dirent_name, is_org_context, \
is_pro_version, normalize_dir_path, is_valid_username, \ is_pro_version, normalize_dir_path, is_valid_username, \
send_perm_audit_msg, is_valid_org_id send_perm_audit_msg, is_valid_org_id, is_valid_email
from seahub.utils.repo import ( from seahub.utils.repo import (
get_library_storages, get_repo_owner, get_available_repo_perms get_library_storages, get_repo_owner, get_available_repo_perms
) )
@@ -1406,3 +1407,137 @@ class GroupOwnedLibraryUserShareInLibrary(APIView):
repo_id, '/', permission) repo_id, '/', permission)
return Response({'success': True}) return Response({'success': True})
class GroupOwnedLibraryTransferLibrary(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, IsProVersion)
throttle_classes = (UserRateThrottle,)
@api_check_group
@add_org_context
def put(self, request, group_id, repo_id, org_id):
""" Transfer a library.
Permission checking:
1. is group admin;
"""
# argument check
new_owner = request.data.get('email', None)
if not new_owner:
error_msg = 'Email invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if '@seafile_group' not in new_owner:
error_msg = 'Email invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if org_id:
org_id = int(org_id)
if not ccnet_api.get_org_by_id(org_id):
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
if org_id:
repo_owner = seafile_api.get_org_repo_owner(repo_id)
else:
repo_owner = seafile_api.get_repo_owner(repo_id)
cur_group_id = int(group_id)
username = request.user.username
if not is_group_admin(cur_group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# preparation before transfer repo
pub_repos = []
if org_id:
# get repo shared to user/group list
shared_users = seafile_api.list_org_repo_shared_to(org_id,
repo_owner,
repo_id)
shared_groups = seafile_api.list_org_repo_shared_group(org_id,
repo_owner,
repo_id)
# get all org pub repos
pub_repos = seafile_api.list_org_inner_pub_repos_by_owner(
org_id, repo_owner)
else:
# get repo shared to user/group list
shared_users = seafile_api.list_repo_shared_to(
repo_owner, repo_id)
shared_groups = seafile_api.list_repo_shared_group_by_user(
repo_owner, repo_id)
# get all pub repos
if not request.cloud_mode:
pub_repos = seafile_api.list_inner_pub_repos_by_owner(repo_owner)
# transfer repo
try:
if org_id:
group_id = int(new_owner.split('@')[0])
seafile_api.org_transfer_repo_to_group(repo_id, org_id, group_id, PERMISSION_READ_WRITE)
else:
if ccnet_api.get_orgs_by_user(new_owner):
# can not transfer library to organization user %s.
error_msg = 'Email %s invalid.' % new_owner
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
else:
group_id = int(new_owner.split('@')[0])
seafile_api.transfer_repo_to_group(repo_id, group_id, PERMISSION_READ_WRITE)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
# reshare repo to user
for shared_user in shared_users:
shared_username = shared_user.user
if new_owner == shared_username:
continue
if org_id:
seafserv_threaded_rpc.org_add_share(org_id, repo_id,
new_owner, shared_username, shared_user.perm)
else:
seafile_api.share_repo(repo_id, new_owner,
shared_username, shared_user.perm)
# reshare repo to group
for shared_group in shared_groups:
shared_group_id = shared_group.group_id
if shared_group_id == cur_group_id:
continue
if org_id:
seafile_api.add_org_group_repo(repo_id, org_id,
shared_group_id, new_owner, shared_group.perm)
else:
seafile_api.set_group_repo(repo_id, shared_group_id,
new_owner, shared_group.perm)
# check if current repo is pub-repo
# if YES, reshare current repo to public
for pub_repo in pub_repos:
if repo_id != pub_repo.id:
continue
if org_id:
seafile_api.set_org_inner_pub_repo(org_id, repo_id,
pub_repo.permission)
else:
seafserv_threaded_rpc.set_inner_pub_repo(
repo_id, pub_repo.permission)
break
return Response({'success': True})

View File

@@ -37,7 +37,8 @@ from seahub.api2.endpoints.group_libraries import GroupLibraries, GroupLibrary
from seahub.api2.endpoints.group_owned_libraries import GroupOwnedLibraries, \ from seahub.api2.endpoints.group_owned_libraries import GroupOwnedLibraries, \
GroupOwnedLibrary, GroupOwnedLibraryUserFolderPermission, \ GroupOwnedLibrary, GroupOwnedLibraryUserFolderPermission, \
GroupOwnedLibraryGroupFolderPermission, GroupOwnedLibraryUserShare, \ GroupOwnedLibraryGroupFolderPermission, GroupOwnedLibraryUserShare, \
GroupOwnedLibraryGroupShare, GroupOwnedLibraryUserShareInLibrary GroupOwnedLibraryGroupShare, GroupOwnedLibraryUserShareInLibrary, \
GroupOwnedLibraryTransferLibrary
from seahub.api2.endpoints.address_book.groups import AddressBookGroupsSubGroups from seahub.api2.endpoints.address_book.groups import AddressBookGroupsSubGroups
from seahub.api2.endpoints.address_book.members import AddressBookGroupsSearchMember from seahub.api2.endpoints.address_book.members import AddressBookGroupsSearchMember
@@ -353,6 +354,7 @@ urlpatterns = [
re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/libraries/(?P<repo_id>[-0-9a-f]{36})/$', GroupLibrary.as_view(), name='api-v2.1-group-library'), re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/libraries/(?P<repo_id>[-0-9a-f]{36})/$', GroupLibrary.as_view(), name='api-v2.1-group-library'),
re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/group-owned-libraries/$', GroupOwnedLibraries.as_view(), name='api-v2.1-group-owned-libraries'), re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/group-owned-libraries/$', GroupOwnedLibraries.as_view(), name='api-v2.1-group-owned-libraries'),
re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/group-owned-libraries/(?P<repo_id>[-0-9a-f]{36})/$', GroupOwnedLibrary.as_view(), name='api-v2.1-owned-group-library'), re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/group-owned-libraries/(?P<repo_id>[-0-9a-f]{36})/$', GroupOwnedLibrary.as_view(), name='api-v2.1-owned-group-library'),
re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/group-owned-libraries/(?P<repo_id>[-0-9a-f]{36})/transfer/$', GroupOwnedLibraryTransferLibrary.as_view(), name='api-v2.1-group-owned-group-library-transfer'),
re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/members/$', GroupMembers.as_view(), name='api-v2.1-group-members'), re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/members/$', GroupMembers.as_view(), name='api-v2.1-group-members'),
re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/search-member/$', GroupSearchMember.as_view(), name='api-v2.1-group-search-member'), re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/search-member/$', GroupSearchMember.as_view(), name='api-v2.1-group-search-member'),
re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/members/bulk/$', GroupMembersBulk.as_view(), name='api-v2.1-group-members-bulk'), re_path(r'^api/v2.1/groups/(?P<group_id>\d+)/members/bulk/$', GroupMembersBulk.as_view(), name='api-v2.1-group-members-bulk'),