diff --git a/frontend/src/components/dialog/import-work-weixin-department-dialog.js b/frontend/src/components/dialog/import-work-weixin-department-dialog.js new file mode 100644 index 0000000000..9b0e1ae0b0 --- /dev/null +++ b/frontend/src/components/dialog/import-work-weixin-department-dialog.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import Loading from '../loading'; + +const propTypes = { + importDepartmentDialogToggle: PropTypes.func.isRequired, + onImportDepartmentSubmit: PropTypes.func.isRequired, + departmentsCount: PropTypes.number.isRequired, + membersCount: PropTypes.number.isRequired, + departmentName: PropTypes.string.isRequired, +}; + +class ImportWorkWeixinDepartmentDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading : false, + }; + } + + toggle = () => { + this.props.importDepartmentDialogToggle(null); + }; + + handleSubmit = () => { + this.props.onImportDepartmentSubmit(); + this.setState({ isLoading : true }); + }; + + render() { + const { departmentsCount, membersCount, departmentName } = this.props; + return ( + + + {'导入部门 '}{departmentName} + + +

{'将要导入 '}{departmentsCount}{' 个部门,其中包括 '}{membersCount}{' 个成员'}

+ {this.state.isLoading && } +
+ + + + +
+ ); + } +} + +ImportWorkWeixinDepartmentDialog.propTypes = propTypes; + +export default ImportWorkWeixinDepartmentDialog; diff --git a/frontend/src/components/dirent-detail/dirent-details.js b/frontend/src/components/dirent-detail/dirent-details.js index 615f9570f0..05904d8e6a 100644 --- a/frontend/src/components/dirent-detail/dirent-details.js +++ b/frontend/src/components/dirent-detail/dirent-details.js @@ -238,7 +238,7 @@ class DirentDetail extends React.Component { diff --git a/frontend/src/css/work-weixin-departments.css b/frontend/src/css/work-weixin-departments.css index 9771bcd857..9a0d08d6fa 100644 --- a/frontend/src/css/work-weixin-departments.css +++ b/frontend/src/css/work-weixin-departments.css @@ -23,8 +23,9 @@ height: 140px; } .dir-content-nav { - position: fixed; + position: absolute; overflow: hidden; + width: 24%; } .dir-content-nav:hover { overflow: auto; @@ -42,6 +43,8 @@ } .tree-node-inner { position: relative; + display: flex; + padding-right: 1rem; } .tree-node-inner i { position: absolute; diff --git a/frontend/src/pages/lib-content-view/lib-content-container.js b/frontend/src/pages/lib-content-view/lib-content-container.js index f7f929735f..d8e5ebba76 100644 --- a/frontend/src/pages/lib-content-view/lib-content-container.js +++ b/frontend/src/pages/lib-content-view/lib-content-container.js @@ -133,11 +133,19 @@ class LibContentContainer extends React.Component { } onItemDelete = (dirent) => { - let currentDirent = this.state; - if (currentDirent && dirent.name === currentDirent.name) { + this.checkCurrentDirent(dirent); + this.props.onItemDelete(dirent); + } + + onItemMove = (destRepo, dirent, selectedPath, currentPath) => { + this.checkCurrentDirent(dirent); + this.props.onItemMove(destRepo, dirent, selectedPath, currentPath); + } + + checkCurrentDirent = (deletedDirent) => { + if (deletedDirent.name === this.state.currentDirent.name) { this.setState({currentDirent: null}); } - this.props.onItemDelete(dirent); } onItemsScroll = (e) => { @@ -212,7 +220,7 @@ class LibContentContainer extends React.Component { onItemSelected={this.onItemSelected} onItemDelete={this.onItemDelete} onItemRename={this.props.onItemRename} - onItemMove={this.props.onItemMove} + onItemMove={this.onItemMove} onItemCopy={this.props.onItemCopy} onDirentClick={this.onDirentClick} updateDirent={this.props.updateDirent} @@ -246,7 +254,7 @@ class LibContentContainer extends React.Component { onAddFile={this.props.onAddFile} onItemClick={this.onItemClick} onItemDelete={this.props.onItemDelete} - onItemMove={this.props.onItemMove} + onItemMove={this.onItemMove} onItemCopy={this.props.onItemCopy} updateDirent={this.props.updateDirent} onAddFolder={this.props.onAddFolder} @@ -304,7 +312,7 @@ class LibContentContainer extends React.Component { onItemSelected={this.onItemSelected} onItemDelete={this.onItemDelete} onItemRename={this.props.onItemRename} - onItemMove={this.props.onItemMove} + onItemMove={this.onItemMove} onItemCopy={this.props.onItemCopy} onDirentClick={this.onDirentClick} updateDirent={this.props.updateDirent} diff --git a/frontend/src/pages/sys-admin/work-weixin-departments.js b/frontend/src/pages/sys-admin/work-weixin-departments.js index d7383023b9..984a4cee85 100644 --- a/frontend/src/pages/sys-admin/work-weixin-departments.js +++ b/frontend/src/pages/sys-admin/work-weixin-departments.js @@ -2,10 +2,12 @@ import React, { Component, Fragment } from 'react'; import { Button } from 'reactstrap'; import _ from 'lodash'; import { seafileAPI } from '../../utils/seafile-api'; -import { gettext, siteRoot } from '../../utils/constants'; +import { gettext, siteRoot, isPro } from '../../utils/constants'; import toaster from '../../components/toast'; import Account from '../../components/common/account'; import { WorkWeixinDepartmentMembersList, WorkWeixinDepartmentsTreePanel } from './work-weixin'; +import ImportWorkWeixinDepartmentDialog from '../../components/dialog/import-work-weixin-department-dialog'; + import '../../css/work-weixin-departments.css'; class WorkWeixinDepartments extends Component { @@ -22,6 +24,10 @@ class WorkWeixinDepartments extends Component { newUsersTempObj: {}, isCheckedAll: false, canCheckUserIds: [], + isImportDepartmentDialogShow: false, + importDepartment: null, + importDepartmentChildrenCount: 0, + importDepartmentMembersCount: 0, }; } @@ -50,13 +56,20 @@ class WorkWeixinDepartments extends Component { }); }; - getWorkWeixinDepartmentsList = () => { - seafileAPI.adminListWorkWeixinDepartments().then((res) => { - let departmentsTree = this.getDepartmentsTree(res.data.department); - this.setState({ - isTreeLoading: false, - departmentsTree: departmentsTree, - }); + getWorkWeixinDepartmentsList = (departmentID) => { + seafileAPI.adminListWorkWeixinDepartments(departmentID).then((res) => { + if (!departmentID) { + let departmentsTree = this.getDepartmentsTree(res.data.department); + this.setState({ + isTreeLoading: false, + departmentsTree: departmentsTree, + }); + } else { + this.setState({ + importDepartmentChildrenCount: res.data.department.length, + importDepartmentMembersCount: this.state.membersTempObj[departmentID].length, + }); + } }).catch((error) => { this.handleError(error); this.setState({ @@ -213,6 +226,59 @@ class WorkWeixinDepartments extends Component { }); } + importDepartmentDialogToggle = (importDepartment) => { + this.setState({ + isImportDepartmentDialogShow: !this.state.isImportDepartmentDialogShow, + importDepartment: importDepartment, + }, () => { + if (importDepartment) { + this.getWorkWeixinDepartmentsList(importDepartment.id); + } + }); + }; + + onImportDepartmentSubmit = () => { + let importDepartment = this.state.importDepartment; + if (!importDepartment) return; + seafileAPI.adminImportWorkWeixinDepartment(importDepartment.id).then((res) => { + this.setState({ + isMembersListLoading: true, + checkedDepartmentId: importDepartment.id, + membersTempObj: {}, + membersList: [], + newUsersTempObj: {}, + isCheckedAll: false, + canCheckUserIds: [], + }); + this.getWorkWeixinDepartmentMembersList(importDepartment.id); + this.importDepartmentDialogToggle(null); + if (res.data.success) { + this.handleImportDepartmentSubmitSuccess(res.data.success); + } + if (res.data.failed) { + this.handleImportDepartmentSubmitFailed(res.data.failed); + } + }).catch((error) => { + this.handleError(error); + }); + }; + + handleImportDepartmentSubmitSuccess = (successes) => { + for (let i = 0, len = successes.length; i < len; i++) { + let success = successes[i]; + let successMsg = success.type === 'department' ? '部门 ' + success.department_name + ' 导入成功' : success.api_user_name + ' 导入成功' ; + toaster.success(successMsg, { duration: 3 }); + } + }; + + handleImportDepartmentSubmitFailed = (fails) => { + for (let i = 0, len = fails.length; i < len; i++) { + let fail = fails[i]; + let failName = fail.type === 'department' ? fail.department_name : fail.api_user_name; + toaster.danger(failName + ' ' + fail.msg, { duration: 3} ); + } + }; + handleError = (e) => { if (e.response) { toaster.danger(e.response.data.error_msg || e.response.data.detail || gettext('Error'), {duration: 3}); @@ -222,7 +288,7 @@ class WorkWeixinDepartments extends Component { } componentDidMount() { - this.getWorkWeixinDepartmentsList(); + this.getWorkWeixinDepartmentsList(null); } renderNav() { @@ -242,6 +308,8 @@ class WorkWeixinDepartments extends Component { } render() { + const { isImportDepartmentDialogShow, isTreeLoading, importDepartment, importDepartmentChildrenCount, importDepartmentMembersCount } = this.state; + let canImportDepartment = !!(isPro && isImportDepartmentDialogShow && !isTreeLoading && importDepartment); return ( {this.renderNav()} @@ -256,6 +324,7 @@ class WorkWeixinDepartments extends Component { isTreeLoading={this.state.isTreeLoading} onChangeDepartment={this.onChangeDepartment} checkedDepartmentId={this.state.checkedDepartmentId} + importDepartmentDialogToggle={this.importDepartmentDialogToggle} />
+ {canImportDepartment && + + }
); } diff --git a/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-node.js b/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-node.js index 1d54a8c3b4..9f2f0e1971 100644 --- a/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-node.js +++ b/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-node.js @@ -1,6 +1,8 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { gettext, isPro } from '../../../utils/constants'; const WorkWeixinDepartmentsTreeNodePropTypes = { index: PropTypes.number, @@ -8,6 +10,7 @@ const WorkWeixinDepartmentsTreeNodePropTypes = { isChildrenShow: PropTypes.bool.isRequired, onChangeDepartment: PropTypes.func.isRequired, checkedDepartmentId: PropTypes.number.isRequired, + importDepartmentDialogToggle: PropTypes.func.isRequired, }; class WorkWeixinDepartmentsTreeNode extends Component { @@ -16,6 +19,8 @@ class WorkWeixinDepartmentsTreeNode extends Component { super(props); this.state = { isChildrenShow: false, + dropdownOpen: false, + active: false, }; } @@ -27,6 +32,18 @@ class WorkWeixinDepartmentsTreeNode extends Component { }); }; + dropdownToggle = () => { + this.setState({ dropdownOpen: !this.state.dropdownOpen }); + }; + + onMouseEnter = () => { + this.setState({ active: true }); + }; + + onMouseLeave = () => { + this.setState({ active: false }); + }; + componentDidMount() { if (this.props.index === 0) { this.setState({ isChildrenShow: true }); @@ -44,6 +61,7 @@ class WorkWeixinDepartmentsTreeNode extends Component { isChildrenShow={this.state.isChildrenShow} onChangeDepartment={this.props.onChangeDepartment} checkedDepartmentId={this.props.checkedDepartmentId} + importDepartmentDialogToggle={this.props.importDepartmentDialogToggle} /> ); }); @@ -63,9 +81,37 @@ class WorkWeixinDepartmentsTreeNode extends Component { return ( {isChildrenShow && -
this.props.onChangeDepartment(department.id)}> +
this.props.onChangeDepartment(department.id)} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > this.toggleChildren(e)}>{' '} {department.name} + {isPro && + + + + + {'导入部门'} + + + }
} {this.state.isChildrenShow && diff --git a/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-panel.js b/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-panel.js index 080d6e39a5..fb41302648 100644 --- a/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-panel.js +++ b/frontend/src/pages/sys-admin/work-weixin/work-weixin-departments-tree-panel.js @@ -8,6 +8,7 @@ const WorkWeixinDepartmentsTreePanelPropTypes = { departmentsTree: PropTypes.array.isRequired, onChangeDepartment: PropTypes.func.isRequired, checkedDepartmentId: PropTypes.number.isRequired, + importDepartmentDialogToggle: PropTypes.func.isRequired, }; class WorkWeixinDepartmentsTreePanel extends Component { @@ -33,6 +34,7 @@ class WorkWeixinDepartmentsTreePanel extends Component { isChildrenShow={true} onChangeDepartment={this.props.onChangeDepartment} checkedDepartmentId={this.props.checkedDepartmentId} + importDepartmentDialogToggle={this.props.importDepartmentDialogToggle} /> ); })} diff --git a/seahub/api2/endpoints/admin/work_weixin.py b/seahub/api2/endpoints/admin/work_weixin.py index b248a97f27..ae318045a4 100644 --- a/seahub/api2/endpoints/admin/work_weixin.py +++ b/seahub/api2/endpoints/admin/work_weixin.py @@ -5,6 +5,7 @@ import logging import requests import json +from seaserv import seafile_api, ccnet_api from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAdminUser from rest_framework.response import Response @@ -13,6 +14,8 @@ from rest_framework import status from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error +from seahub.api2.permissions import IsProVersion + from seahub.work_weixin.utils import handler_work_weixin_api_response, \ get_work_weixin_access_token, admin_work_weixin_departments_check, \ update_work_weixin_user_info @@ -21,10 +24,13 @@ from seahub.work_weixin.settings import WORK_WEIXIN_DEPARTMENTS_URL, \ from seahub.base.accounts import User from seahub.utils.auth import gen_user_virtual_id from seahub.auth.models import SocialAuthUser +from seahub.group.utils import validate_group_name logger = logging.getLogger(__name__) WORK_WEIXIN_DEPARTMENT_FIELD = 'department' WORK_WEIXIN_DEPARTMENT_MEMBERS_FIELD = 'userlist' +DEPARTMENT_OWNER = 'system admin' + # # uid = corpid + '_' + userid # from social_django.models import UserSocialAuth @@ -149,7 +155,6 @@ def _handler_work_weixin_user_data(api_user, social_auth_queryset): def _import_user_from_work_weixin(email, api_user): - api_user['username'] = email uid = WORK_WEIXIN_UID_PREFIX + api_user.get('userid') try: @@ -204,3 +209,235 @@ class AdminWorkWeixinUsersBatch(APIView): failed.append(error_data) return Response({'success': success, 'failed': failed}) + + +class AdminWorkWeixinDepartmentsImport(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser,) + + def _list_departments_from_work_weixin(self, access_token, department_id): + data = { + 'access_token': access_token, + 'id': department_id, + } + api_response = requests.get(WORK_WEIXIN_DEPARTMENTS_URL, params=data) + api_response_dic = handler_work_weixin_api_response(api_response) + + if not api_response_dic: + logger.error('can not get work weixin departments response') + return None + + if WORK_WEIXIN_DEPARTMENT_FIELD not in api_response_dic: + logger.error(json.dumps(api_response_dic)) + logger.error('can not get department list in work weixin departments response') + return None + + return api_response_dic[WORK_WEIXIN_DEPARTMENT_FIELD] + + def _list_department_members_from_work_weixin(self, access_token, department_id): + data = { + 'access_token': access_token, + 'department_id': department_id, + 'fetch_child': 1, + } + api_response = requests.get(WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, params=data) + api_response_dic = handler_work_weixin_api_response(api_response) + + if not api_response_dic: + logger.error('can not get work weixin department members response') + return None + + if WORK_WEIXIN_DEPARTMENT_MEMBERS_FIELD not in api_response_dic: + logger.error(json.dumps(api_response_dic)) + logger.error('can not get userlist in work weixin department members response') + return None + + return api_response_dic[WORK_WEIXIN_DEPARTMENT_MEMBERS_FIELD] + + def _admin_check_group_name_conflict(self, new_group_name): + checked_groups = ccnet_api.search_groups(new_group_name, -1, -1) + + for g in checked_groups: + if g.group_name == new_group_name: + return True, g + + return False, None + + def _api_department_success_msg(self, department_obj_id, department_obj_name, group_id): + return { + 'type': 'department', + 'department_id': department_obj_id, + 'department_name': department_obj_name, + 'group_id': group_id, + } + + def _api_department_failed_msg(self, department_obj_id, department_obj_name, msg): + return { + 'type': 'department', + 'department_id': department_obj_id, + 'department_name': department_obj_name, + 'msg': msg, + } + + def _api_user_success_msg(self, email, api_user_name, department_obj_id, group_id): + return { + 'type': 'user', + 'email': email, + 'api_user_name': api_user_name, + 'department_id': department_obj_id, + 'group_id': group_id, + } + + def _api_user_failed_msg(self, email, api_user_name, department_obj_id, msg): + return { + 'type': 'user', + 'email': email, + 'api_user_name': api_user_name, + 'department_id': department_obj_id, + 'msg': msg, + } + + def post(self, request): + """import department from work weixin + + permission: IsProVersion + """ + # argument check + department_id = request.data.get('work_weixin_department_id') + try: + department_id = int(department_id) + except Exception as e: + logger.error(e) + error_msg = 'work_weixin_department_ids invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # is pro version and work weixin check + if not IsProVersion or not admin_work_weixin_departments_check(): + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + access_token = get_work_weixin_access_token() + if not access_token: + logger.error('can not get work weixin access_token') + error_msg = '获取企业微信组织架构失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # list departments from work weixin + api_department_list = self._list_departments_from_work_weixin(access_token, department_id) + if api_department_list is None: + error_msg = '获取企业微信组织架构失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # list department members from work weixin + api_user_list = self._list_department_members_from_work_weixin(access_token, department_id) + if api_user_list is None: + error_msg = '获取企业微信组织架构成员失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # main + success = list() + failed = list() + department_map_to_group_dict = dict() + + for index, department_obj in enumerate(api_department_list): + # check department argument + new_group_name = department_obj.get('name') + department_obj_id = department_obj.get('id') + if department_obj_id is None or not new_group_name or not validate_group_name(new_group_name): + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '部门参数错误') + failed.append(failed_msg) + continue + + # check parent group + if index == 0: + parent_group_id = -1 + else: + parent_department_id = department_obj.get('parentid') + parent_group_id = department_map_to_group_dict.get(parent_department_id) + + if parent_group_id is None: + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '父级部门不存在') + failed.append(failed_msg) + continue + + # check department exist by group name + exist, exist_group = self._admin_check_group_name_conflict(new_group_name) + if exist: + department_map_to_group_dict[department_obj_id] = exist_group.id + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '部门已存在') + failed.append(failed_msg) + continue + + # import department + try: + group_id = ccnet_api.create_group( + new_group_name, DEPARTMENT_OWNER, parent_group_id=parent_group_id) + + seafile_api.set_group_quota(group_id, -2) + + department_map_to_group_dict[department_obj_id] = group_id + success_msg = self._api_department_success_msg( + department_obj_id, new_group_name, group_id) + success.append(success_msg) + except Exception as e: + logger.error(e) + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '部门导入失败') + failed.append(failed_msg) + + # todo filter ccnet User database + social_auth_queryset = SocialAuthUser.objects.filter( + provider=WORK_WEIXIN_PROVIDER, uid__contains=WORK_WEIXIN_UID_PREFIX) + + # import api_user + for api_user in api_user_list: + uid = WORK_WEIXIN_UID_PREFIX + api_user.get('userid', '') + api_user['contact_email'] = api_user['email'] + api_user_name = api_user.get('name') + + # determine the user exists + if social_auth_queryset.filter(uid=uid).exists(): + email = social_auth_queryset.get(uid=uid).username + else: + # create user + email = gen_user_virtual_id() + create_user_success = _import_user_from_work_weixin(email, api_user) + if not create_user_success: + failed_msg = self._api_user_failed_msg( + '', api_user_name, department_id, '导入用户失败') + failed.append(failed_msg) + continue + + # bind user to department + api_user_department_list = api_user.get('department') + for department_obj_id in api_user_department_list: + group_id = department_map_to_group_dict.get(department_obj_id) + if group_id is None: + # the api_user also exist in the brother department which not import + continue + + if ccnet_api.is_group_user(group_id, email): + failed_msg = self._api_user_failed_msg( + email, api_user_name, department_obj_id, '部门成员已存在') + failed.append(failed_msg) + continue + + try: + ccnet_api.group_add_member(group_id, DEPARTMENT_OWNER, email) + success_msg = self._api_user_success_msg( + email, api_user_name, department_obj_id, group_id) + success.append(success_msg) + except Exception as e: + logger.error(e) + failed_msg = self._api_user_failed_msg( + email, api_user_name, department_id, '导入部门成员失败') + failed.append(failed_msg) + + return Response({ + 'success': success, + 'failed': failed, + }) diff --git a/seahub/urls.py b/seahub/urls.py index a212d7113b..27ff07831f 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -145,7 +145,7 @@ from seahub.api2.endpoints.admin.user_activities import UserActivitiesView from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords from seahub.api2.endpoints.admin.notifications import AdminNotificationsView from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \ - AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch + AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch, AdminWorkWeixinDepartmentsImport from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView urlpatterns = [ @@ -567,6 +567,7 @@ urlpatterns = [ url(r'^api/v2.1/admin/work-weixin/departments/$', AdminWorkWeixinDepartments.as_view(), name='api-v2.1-admin-work-weixin-departments'), url(r'^api/v2.1/admin/work-weixin/departments/(?P\d+)/members/$', AdminWorkWeixinDepartmentMembers.as_view(), name='api-v2.1-admin-work-weixin-department-members'), url(r'^api/v2.1/admin/work-weixin/users/batch/$', AdminWorkWeixinUsersBatch.as_view(), name='api-v2.1-admin-work-weixin-users'), + url(r'^api/v2.1/admin/work-weixin/departments/import/$', AdminWorkWeixinDepartmentsImport.as_view(), name='api-v2.1-admin-work-weixin-department-import'), ### system admin ### url(r'^sysadmin/$', sysadmin, name='sysadmin'),