1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-07 18:03:48 +00:00

import group members (#4814)

Co-authored-by: lian <lian@seafile.com>
This commit is contained in:
lian
2021-02-10 09:59:32 +08:00
committed by GitHub
parent 4173aa6540
commit 72678fbe8f
4 changed files with 274 additions and 23 deletions

View File

@@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
import { gettext, siteRoot } from '../../utils/constants';
const propTypes = {
toggleImportMembersDialog: PropTypes.func.isRequired,
importMembersInBatch: PropTypes.func.isRequired,
};
class ImportMembersDialog extends React.Component {
constructor(props) {
super(props);
this.fileInputRef = React.createRef();
this.state = {
errorMsg: ''
};
}
toggle = () => {
this.props.toggleImportMembersDialog();
}
openFileInput = () => {
this.fileInputRef.current.click();
}
uploadFile = (e) => {
// no file selected
if (!this.fileInputRef.current.files.length) {
return;
}
// check file extension
let fileName = this.fileInputRef.current.files[0].name;
if(fileName.substr(fileName.lastIndexOf('.') + 1) != 'xlsx') {
this.setState({
errorMsg: gettext('Please choose a .xlsx file.')
});
return;
}
const file = this.fileInputRef.current.files[0];
this.props.importMembersInBatch(file);
this.toggle();
}
render() {
let { errorMsg } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Import members from a .xlsx file')}</ModalHeader>
<ModalBody>
<p><a className="text-secondary small" href={`${siteRoot}api/v2.1/group-members-import-example/`}>{gettext('Download an example file')}</a></p>
<button className="btn btn-outline-primary" onClick={this.openFileInput}>{gettext('Upload file')}</button>
<input className="d-none" type="file" onChange={this.uploadFile} ref={this.fileInputRef} />
{errorMsg && <Alert color="danger">{errorMsg}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
</ModalFooter>
</Modal>
);
}
}
ImportMembersDialog.propTypes = propTypes;
export default ImportMembersDialog;

View File

@@ -18,7 +18,7 @@ import CreateDepartmentRepoDialog from '../../components/dialog/create-departmen
import DismissGroupDialog from '../../components/dialog/dismiss-group-dialog';
import RenameGroupDialog from '../../components/dialog/rename-group-dialog';
import TransferGroupDialog from '../../components/dialog/transfer-group-dialog';
// import ImportMembersDialog from '../../components/dialog/import-members-dialog';
import ImportMembersDialog from '../../components/dialog/import-members-dialog';
import ManageMembersDialog from '../../components/dialog/manage-members-dialog';
import LeaveGroupDialog from '../../components/dialog/leave-group-dialog';
import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view';
@@ -63,7 +63,7 @@ class GroupView extends React.Component {
showRenameGroupDialog: false,
showDismissGroupDialog: false,
showTransferGroupDialog: false,
// showImportMembersDialog: false,
showImportMembersDialog: false,
showManageMembersDialog: false,
groupMembers: [],
isShowDetails: false,
@@ -283,11 +283,24 @@ class GroupView extends React.Component {
});
}
// toggleImportMembersDialog= () => {
// this.setState({
// showImportMembersDialog: !this.state.showImportMembersDialog
// });
// }
toggleImportMembersDialog= () => {
this.setState({
showImportMembersDialog: !this.state.showImportMembersDialog
});
}
importMembersInBatch= (file) => {
toaster.notify(gettext('It may take some time, please wait.'));
seafileAPI.importGroupMembersViaFile(this.state.currentGroup.id, file).then((res) => {
res.data.failed.map(item => {
const msg = `${item.email}: ${item.error_msg}`;
toaster.danger(msg);
});
}).catch((error) => {
let errMsg = Utils.getErrorMsg(error);
toaster.danger(errMsg);
});
}
toggleManageMembersDialog = () => {
this.setState({
@@ -464,7 +477,7 @@ class GroupView extends React.Component {
}
{(this.state.isStaff || this.state.isOwner) &&
<ul className="sf-popover-list">
{/* <li><a href="#" className="sf-popover-item" onClick={this.toggleImportMembersDialog} >{gettext('Import Members')}</a></li> */}
<li><a href="#" className="sf-popover-item" onClick={this.toggleImportMembersDialog} >{gettext('Import Members')}</a></li>
<li><a href="#" className="sf-popover-item" onClick={this.toggleManageMembersDialog} >{gettext('Manage Members')}</a></li>
</ul>
}
@@ -596,13 +609,12 @@ class GroupView extends React.Component {
onGroupChanged={this.props.onGroupChanged}
/>
}
{/* this.state.showImportMembersDialog &&
{ this.state.showImportMembersDialog &&
<ImportMembersDialog
toggleImportMembersDialog={this.toggleImportMembersDialog}
groupID={this.props.groupID}
onGroupChanged={this.props.onGroupChanged}
importMembersInBatch={this.importMembersInBatch}
/>
*/}
}
{this.state.showManageMembersDialog &&
<ManageMembersDialog
toggleManageMembersDialog={this.toggleManageMembersDialog}

View File

@@ -1,6 +1,9 @@
# Copyright (c) 2012-2016 Seafile Ltd.
import logging
from io import BytesIO
from openpyxl import load_workbook
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from rest_framework.authentication import SessionAuthentication
@@ -9,16 +12,18 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
import seaserv
from seaserv import seafile_api, ccnet_api
from pysearpc import SearpcError
from seahub.api2.utils import api_error
from seahub.api2.endpoints.utils import is_org_user
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.avatar.settings import AVATAR_DEFAULT_SIZE
from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.utils import string2list, is_org_context
from seahub.utils import string2list, is_org_context, get_file_type_and_ext
from seahub.utils.ms_excel import write_xls
from seahub.utils.error_msg import file_type_error_msg
from seahub.base.accounts import User
from seahub.group.signals import add_user_to_group
from seahub.group.utils import is_group_member, is_group_admin, \
@@ -28,6 +33,7 @@ from .utils import api_check_group
logger = logging.getLogger(__name__)
class GroupMembers(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
@@ -40,8 +46,7 @@ class GroupMembers(APIView):
"""
try:
avatar_size = int(request.GET.get('avatar_size',
AVATAR_DEFAULT_SIZE))
avatar_size = int(request.GET.get('avatar_size', AVATAR_DEFAULT_SIZE))
except ValueError:
avatar_size = AVATAR_DEFAULT_SIZE
@@ -109,6 +114,9 @@ class GroupMembers(APIView):
if not ccnet_api.org_user_exists(org_id, email):
error_msg = _('User %s not found in organization.') % email2nickname(email)
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
elif is_org_user(email):
error_msg = _('User %s is an organization user.') % email
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
ccnet_api.group_add_member(group_id, username, email)
add_user_to_group.send(sender=None,
@@ -151,8 +159,7 @@ class GroupMember(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
try:
avatar_size = int(request.GET.get('avatar_size',
AVATAR_DEFAULT_SIZE))
avatar_size = int(request.GET.get('avatar_size', AVATAR_DEFAULT_SIZE))
except ValueError:
avatar_size = AVATAR_DEFAULT_SIZE
@@ -305,8 +312,7 @@ class GroupMembersBulk(APIView):
continue
# Can only invite organization users to group
if org_id and not \
seaserv.ccnet_threaded_rpc.org_user_exists(org_id, email):
if org_id and not ccnet_api.org_user_exists(org_id, email):
result['failed'].append({
'email': email,
'email_name': email_name,
@@ -314,13 +320,20 @@ class GroupMembersBulk(APIView):
})
continue
if not org_id and is_org_user(email):
result['failed'].append({
'email': email,
'email_name': email_name,
'error_msg': _('User %s is an organization user.') % email_name
})
continue
emails_need_add.append(email)
# Add user to group.
for email in emails_need_add:
try:
seaserv.ccnet_threaded_rpc.group_add_member(group_id,
username, email)
ccnet_api.group_add_member(group_id, username, email)
member_info = get_group_member_info(request, group_id, email)
result['success'].append(member_info)
except SearpcError as e:
@@ -335,3 +348,158 @@ class GroupMembersBulk(APIView):
group_id=group_id,
added_user=email)
return Response(result)
class GroupMembersImport(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def post(self, request, group_id):
""" Import members from xlsx file
Permission checking:
1. group admin or owner.
"""
xlsx_file = request.FILES.get('file', None)
if not xlsx_file:
error_msg = 'file can not be found.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
file_type, ext = get_file_type_and_ext(xlsx_file.name)
if ext != 'xlsx':
error_msg = file_type_error_msg(ext, 'xlsx')
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# recourse check
group_id = int(group_id)
group = ccnet_api.get_group(group_id)
if not group:
error_msg = _('Group does not exist')
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# check permission
# only group owner/admin can add group members
username = request.user.username
if not is_group_admin_or_owner(group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
content = xlsx_file.read()
try:
fs = BytesIO(content)
wb = load_workbook(filename=fs, read_only=True)
except Exception as e:
logger.error(e)
# example file is like:
# Email
# a@a.com
# b@b.com
rows = wb.worksheets[0].rows
records = []
# skip first row(head field).
next(rows)
for row in rows:
records.append([col.value for col in row])
emails_list = []
for record in records:
if record[0]:
email = record[0].strip().lower()
emails_list.append(email)
result = {}
result['failed'] = []
result['success'] = []
emails_need_add = []
org_id = None
if is_org_context(request):
org_id = request.user.org.org_id
for email in emails_list:
email_name = email2nickname(email)
try:
User.objects.get(email=email)
except User.DoesNotExist:
result['failed'].append({
'email': email,
'email_name': email_name,
'error_msg': 'User %s not found.' % email_name
})
continue
if is_group_member(group_id, email, in_structure=False):
result['failed'].append({
'email': email,
'email_name': email_name,
'error_msg': _('User %s is already a group member.') % email_name
})
continue
# Can only invite organization users to group
if org_id and not ccnet_api.org_user_exists(org_id, email):
result['failed'].append({
'email': email,
'email_name': email_name,
'error_msg': _('User %s not found in organization.') % email_name
})
continue
if not org_id and is_org_user(email):
result['failed'].append({
'email': email,
'email_name': email_name,
'error_msg': _('User %s is an organization user.') % email_name
})
continue
emails_need_add.append(email)
# Add user to group.
for email in emails_need_add:
try:
ccnet_api.group_add_member(group_id, username, email)
member_info = get_group_member_info(request, group_id, email)
result['success'].append(member_info)
except SearpcError as e:
logger.error(e)
result['failed'].append({
'email': email,
'error_msg': 'Internal Server Error'
})
add_user_to_group.send(sender=None,
group_staff=username,
group_id=group_id,
added_user=email)
return Response(result)
class GroupMembersImportExample(APIView):
throttle_classes = (UserRateThrottle, )
def get(self, request):
data_list = []
head = [_('Email')]
for i in range(5):
username = "test" + str(i) + "@example.com"
data_list.append([username])
wb = write_xls('sample', head, data_list)
if not wb:
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, _('Failed to export Excel'))
response = HttpResponse(content_type='application/ms-excel')
response['Content-Disposition'] = 'attachment; filename=members.xlsx'
wb.save(response)
return response

View File

@@ -34,7 +34,8 @@ from seahub.api2.endpoints.group_owned_libraries import GroupOwnedLibraries, \
from seahub.api2.endpoints.address_book.groups import AddressBookGroupsSubGroups
from seahub.api2.endpoints.address_book.members import AddressBookGroupsSearchMember
from seahub.api2.endpoints.group_members import GroupMembers, GroupMembersBulk, GroupMember
from seahub.api2.endpoints.group_members import GroupMembers, GroupMember, \
GroupMembersBulk, GroupMembersImport, GroupMembersImportExample
from seahub.api2.endpoints.search_group import SearchGroup
from seahub.api2.endpoints.share_links import ShareLinks, ShareLink, \
ShareLinkOnlineOfficeLock, ShareLinkDirents, ShareLinkSaveFileToRepo
@@ -290,6 +291,8 @@ urlpatterns = [
url(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'),
url(r'^api/v2.1/groups/(?P<group_id>\d+)/members/$', GroupMembers.as_view(), name='api-v2.1-group-members'),
url(r'^api/v2.1/groups/(?P<group_id>\d+)/members/bulk/$', GroupMembersBulk.as_view(), name='api-v2.1-group-members-bulk'),
url(r'^api/v2.1/groups/(?P<group_id>\d+)/members/import/$', GroupMembersImport.as_view(), name='api-v2.1-group-members-import'),
url(r'^api/v2.1/group-members-import-example/$', GroupMembersImportExample.as_view(), name='api-v2.1-group-members-import-example'),
url(r'^api/v2.1/groups/(?P<group_id>\d+)/members/(?P<email>[^/]+)/$', GroupMember.as_view(), name='api-v2.1-group-member'),
url(r'^api/v2.1/search-group/$', SearchGroup.as_view(), name='api-v2.1-search-group'),