diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6f15d100e6..e35f235cdc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -716,9 +716,9 @@ } }, "@seafile/resumablejs": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@seafile/resumablejs/-/resumablejs-1.1.15.tgz", - "integrity": "sha512-DJPhjBRLENONdDNaaKRckWWtwXvoqfJcRdSk01FbjmZ3DWpbIwebd/vfuB4qJwm0R2j2qzdEVhgxe6wDYS/n9A==" + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@seafile/resumablejs/-/resumablejs-1.1.16.tgz", + "integrity": "sha512-8rBbmAEuuwOAGHYGCtEzpx+bxAcGS+V30otMmhRe7bPAdh4E57RWgCa8x7pkzHGFlY1t5d+ILz1gojvPVMYQig==" }, "@seafile/seafile-calendar": { "version": "0.0.12", diff --git a/frontend/package.json b/frontend/package.json index e248d32f91..ce18e25c36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,7 @@ "dependencies": { "@reach/router": "^1.2.0", "@seafile/react-image-lightbox": "0.0.1", - "@seafile/resumablejs": "^1.1.15", + "@seafile/resumablejs": "1.1.16", "@seafile/seafile-calendar": "0.0.12", "@seafile/seafile-editor": "^0.3.26", "MD5": "^1.3.0", diff --git a/frontend/src/components/dialog/create-repo-dialog.js b/frontend/src/components/dialog/create-repo-dialog.js index 4951833640..7928fac024 100644 --- a/frontend/src/components/dialog/create-repo-dialog.js +++ b/frontend/src/components/dialog/create-repo-dialog.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Select from 'react-select'; import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap'; -import { gettext, enableEncryptedLibrary, repoPasswordMinLength, storages } from '../../utils/constants'; +import { gettext, enableEncryptedLibrary, repoPasswordMinLength, storages, libraryTemplates } from '../../utils/constants'; const propTypes = { libraryType: PropTypes.string.isRequired, @@ -22,6 +22,7 @@ class CreateRepoDialog extends React.Component { errMessage: '', permission: 'rw', storage_id: storages.length ? storages[0].id : '', + library_template: libraryTemplates.length ? libraryTemplates[0] : '', isSubmitBtnActive: false, }; this.newInput = React.createRef(); @@ -121,6 +122,10 @@ class CreateRepoDialog extends React.Component { this.setState({storage_id: selectedItem.value}); } + handlelibraryTemplatesInputChange = (selectedItem) => { + this.setState({library_template: selectedItem.value}); + } + onEncrypted = (e) => { let isChecked = e.target.checked; this.setState({ @@ -162,6 +167,11 @@ class CreateRepoDialog extends React.Component { repo.storage_id = storage_id; } + const library_template = this.state.library_template; + if (library_template) { + repo.library_template = library_template; + } + return repo; } @@ -192,6 +202,19 @@ class CreateRepoDialog extends React.Component { /> )} + + {libraryTemplates.length > 0 && ( + + + + + + + ); + } +} + +export default WebAPIAuthToken; diff --git a/frontend/src/markdown-editor.js b/frontend/src/markdown-editor.js index 89888f0e81..5e6a52c89f 100644 --- a/frontend/src/markdown-editor.js +++ b/frontend/src/markdown-editor.js @@ -25,7 +25,7 @@ const { repoID, repoName, filePath, fileName, mode, draftID, isDraft, hasDraft, const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config; const userInfo = window.app.userInfo; const userName = userInfo.username; -let dirPath = '/'; +let dirPath = Utils.getDirName(filePath); function getImageFileNameWithTimestamp() { var d = Date.now(); @@ -143,7 +143,8 @@ class EditorApi { } getFiles() { - return seafileAPI.listDir(repoID, dirPath, { recursive: true} ).then((response) => { + const rootPath = '/'; + return seafileAPI.listDir(repoID, rootPath, { recursive: true} ).then((response) => { var files = response.data.dirent_list.map((item) => { return { name: item.name, diff --git a/frontend/src/pages/org-admin/org-user-item.js b/frontend/src/pages/org-admin/org-user-item.js index a0f526579b..847a8738c1 100644 --- a/frontend/src/pages/org-admin/org-user-item.js +++ b/frontend/src/pages/org-admin/org-user-item.js @@ -77,16 +77,16 @@ class UserItem extends React.Component { } changeStatus = (st) => { - let statusCode; + let isActive; if (st == 'active') { - statusCode = 1; + isActive = 'true'; } else { - statusCode = 0; + isActive = 'false'; } - seafileAPI.orgAdminChangeOrgUserStatus(this.props.user.id, statusCode).then(res => { + seafileAPI.orgAdminChangeOrgUserStatus(orgID, this.props.user.email, isActive).then(res => { this.setState({ - currentStatus: statusCode == 1 ? 'active' : 'inactive', + currentStatus: isActive == 'true' ? 'active' : 'inactive', highlight: false, showMenu: false, }); diff --git a/frontend/src/pages/sys-admin/departments/department-detail.js b/frontend/src/pages/sys-admin/departments/department-detail.js index 3d4c128894..87d1fe9f5d 100644 --- a/frontend/src/pages/sys-admin/departments/department-detail.js +++ b/frontend/src/pages/sys-admin/departments/department-detail.js @@ -2,6 +2,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import { Link } from '@reach/router'; +import Paginator from '../../../components/paginator'; import { seafileAPI } from '../../../utils/seafile-api'; import { Utils } from '../../../utils/utils.js'; import toaster from '../../../components/toast'; @@ -35,6 +36,11 @@ class DepartmentDetail extends React.Component { isItemFreezed: false, ancestorGroups: [], members: [], + membersErrorMsg: '', + membersPageInfo: { + }, + membersPage: 1, + membersPerPage: 25, deletedMember: {}, isShowAddMemberDialog: false, showDeleteMemberDialog: false, @@ -54,13 +60,15 @@ class DepartmentDetail extends React.Component { componentDidMount() { const groupID = this.props.groupID; this.listGroupRepo(groupID); - this.listMembers(groupID); + this.getDepartmentInfo(groupID); + this.listMembers(groupID, this.state.membersPage, this.state.membersPerPage); } componentWillReceiveProps(nextProps) { if (this.props.groupID !== nextProps.groupID) { this.listGroupRepo(nextProps.groupID); - this.listMembers(nextProps.groupID); + this.getDepartmentInfo(nextProps.groupID); + this.listMembers(nextProps.groupID, this.state.membersPage, this.state.membersPerPage); } } @@ -73,10 +81,9 @@ class DepartmentDetail extends React.Component { }); } - listMembers = (groupID) => { + getDepartmentInfo = (groupID) => { seafileAPI.sysAdminGetDepartmentInfo(groupID, true).then(res => { this.setState({ - members: res.data.members, groups: res.data.groups, ancestorGroups: res.data.ancestor_groups, groupName: res.data.name, @@ -87,6 +94,34 @@ class DepartmentDetail extends React.Component { }); } + listMembers = (groupID, page, perPage) => { + seafileAPI.sysAdminListGroupMembers(groupID, page, perPage).then((res) => { + this.setState({ + members: res.data.members, + membersPageInfo: res.data.page_info + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + this.setState({membersErrorMsg: errMessage}); + }); + } + + getPreviousPageList = () => { + this.listMembers(this.props.groupID, this.state.membersPageInfo.current_page - 1, this.state.membersPerPage); + } + + getNextPageList = () => { + this.listMembers(this.props.groupID, this.state.membersPageInfo.current_page + 1, this.state.membersPerPage); + } + + resetPerPage = (perPage) => { + this.setState({ + membersPerPage: perPage + }, () => { + this.listMembers(this.props.groupID, 1, perPage); + }); + } + listSubDepartGroups = (groupID) => { seafileAPI.sysAdminGetDepartmentInfo(groupID, true).then(res => { this.setState({ groups: res.data.groups }); @@ -114,7 +149,7 @@ class DepartmentDetail extends React.Component { } onMemberChanged = () => { - this.listMembers(this.props.groupID); + this.listMembers(this.props.groupID, this.state.membersPageInfo.current_page, this.state.membersPerPage); } toggleItemFreezed = (isFreezed) => { @@ -157,7 +192,7 @@ class DepartmentDetail extends React.Component { } render() { - const { members, repos, groups } = this.state; + const { members, membersErrorMsg, repos, groups } = this.state; const groupID = this.props.groupID; const topBtn = 'btn btn-secondary operation-item'; const topbarChildren = ( @@ -259,8 +294,10 @@ class DepartmentDetail extends React.Component {

{gettext('Members')}

- {(members && members.length === 1 && members[0].role === 'Owner') ? + {membersErrorMsg ?

{membersErrorMsg}

: + members.length == 0 ?

{gettext('No members')}

: + @@ -287,6 +324,17 @@ class DepartmentDetail extends React.Component { })}
+ {this.state.membersPageInfo && + + } +
}
diff --git a/frontend/src/pages/sys-admin/groups/group-members.js b/frontend/src/pages/sys-admin/groups/group-members.js index bcdff7bea6..88eecb8d1c 100644 --- a/frontend/src/pages/sys-admin/groups/group-members.js +++ b/frontend/src/pages/sys-admin/groups/group-members.js @@ -6,6 +6,7 @@ import { siteRoot, gettext } from '../../../utils/constants'; import toaster from '../../../components/toast'; import EmptyTip from '../../../components/empty-tip'; import Loading from '../../../components/loading'; +import Paginator from '../../../components/paginator'; import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; import SysAdminGroupAddMemberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog'; import SysAdminGroupRoleEditor from '../../../components/select-editor/sysadmin-group-role-editor'; @@ -19,8 +20,16 @@ class Content extends Component { super(props); } + getPreviousPageList = () => { + this.props.getListByPage(this.props.pageInfo.current_page - 1); + } + + getNextPageList = () => { + this.props.getListByPage(this.props.pageInfo.current_page + 1); + } + render() { - const { loading, errorMsg, items } = this.props; + const { loading, errorMsg, items, pageInfo, curPerPage } = this.props; if (loading) { return ; } else if (errorMsg) { @@ -53,6 +62,16 @@ class Content extends Component { })} + {pageInfo && + + } ); return items.length ? table : emptyTip; @@ -148,16 +167,33 @@ class GroupMembers extends Component { errorMsg: '', groupName: '', memberList: [], + pageInfo: {}, + currentPage: 1, + perPage: 25, isAddMemberDialogOpen: false }; } componentDidMount () { - seafileAPI.sysAdminListGroupMembers(this.props.groupID).then((res) => { + + let urlParams = (new URL(window.location)).searchParams; + const { currentPage, perPage } = this.state; + this.setState({ + currentPage: parseInt(urlParams.get('page') || currentPage), + perPage: parseInt(urlParams.get('per_page') || perPage) + }, () => { + this.getListByPage(this.state.currentPage); + }); + } + + getListByPage = (page) => { + const { perPage } = this.state; + seafileAPI.sysAdminListGroupMembers(this.props.groupID, page, perPage).then((res) => { this.setState({ loading: false, memberList: res.data.members, - groupName: res.data.group_name + groupName: res.data.group_name, + pageInfo: res.data.page_info }); }).catch((error) => { this.setState({ @@ -167,6 +203,14 @@ class GroupMembers extends Component { }); } + resetPerPage = (perPage) => { + this.setState({ + perPage: perPage + }, () => { + this.getListByPage(1); + }); + } + toggleAddMemgerDialog = () => { this.setState({isAddMemberDialogOpen: !this.state.isAddMemberDialogOpen}); } @@ -250,6 +294,10 @@ class GroupMembers extends Component { items={this.state.memberList} removeMember={this.removeMember} updateMemberRole={this.updateMemberRole} + pageInfo={this.state.pageInfo} + curPerPage={this.state.perPage} + getListByPage={this.getListByPage} + resetPerPage={this.resetPerPage} /> diff --git a/frontend/src/pages/sys-admin/invitations/invitations.js b/frontend/src/pages/sys-admin/invitations/invitations.js index 3b10f62d5c..55977f6879 100644 --- a/frontend/src/pages/sys-admin/invitations/invitations.js +++ b/frontend/src/pages/sys-admin/invitations/invitations.js @@ -275,7 +275,7 @@ class Invitations extends Component { deleteItemInBatch = () => { seafileAPI.sysAdminDeleteExpiredInvitations().then(res => { const prevItems = this.state.items; - const items = this.state.items.filter(item => !item.is_expired); + const items = this.state.items.filter(item => !item.is_expired || item.accept_time); if (items.length < prevItems.length) { this.setState({ items: items diff --git a/frontend/src/settings.js b/frontend/src/settings.js index 0215241ee9..11f52afa69 100644 --- a/frontend/src/settings.js +++ b/frontend/src/settings.js @@ -9,6 +9,7 @@ import CommonToolbar from './components/toolbar/common-toolbar'; import SideNav from './components/user-settings/side-nav'; import UserAvatarForm from './components/user-settings/user-avatar-form'; import UserBasicInfoForm from './components/user-settings/user-basic-info-form'; +import WebAPIAuthToken from './components/user-settings/web-api-auth-token'; import WebdavPassword from './components/user-settings/webdav-password'; import LanguageSetting from './components/user-settings/language-setting'; import ListInAddressBook from './components/user-settings/list-in-address-book'; @@ -25,8 +26,9 @@ import './css/user-settings.css'; const { canUpdatePassword, passwordOperationText, - enableAddressBook, + enableGetAuthToken, enableWebdavSecret, + enableAddressBook, twoFactorAuthEnabled, enableWechatWork, enableDingtalk, @@ -40,6 +42,7 @@ class Settings extends React.Component { this.sideNavItems = [ {show: true, href: '#user-basic-info', text: gettext('Profile')}, {show: canUpdatePassword, href: '#update-user-passwd', text: gettext('Password')}, + {show: enableGetAuthToken, href: '#get-auth-token', text: gettext('Web API Auth Token')}, {show: enableWebdavSecret, href: '#update-webdav-passwd', text: gettext('WebDav Password')}, {show: enableAddressBook, href: '#list-in-address-book', text: gettext('Global Address Book')}, {show: true, href: '#lang-setting', text: gettext('Language')}, @@ -129,9 +132,11 @@ class Settings extends React.Component { {passwordOperationText} } + + {enableGetAuthToken && } {enableWebdavSecret && } {enableAddressBook && this.state.userInfo && - } + } {isPro && } {twoFactorAuthEnabled && } diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 1a4484fe6e..cb51b43a3f 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -42,6 +42,7 @@ export const enableUploadFolder = window.app.pageOptions.enableUploadFolder === export const enableResumableFileUpload = window.app.pageOptions.enableResumableFileUpload === 'True'; export const resumableUploadFileBlockSize = window.app.pageOptions.resumableUploadFileBlockSize; export const storages = window.app.pageOptions.storages; // storage backends +export const libraryTemplates = window.app.pageOptions.libraryTemplates; // library templates export const enableRepoSnapshotLabel = window.app.pageOptions.enableRepoSnapshotLabel; export const shareLinkPasswordMinLength = window.app.pageOptions.shareLinkPasswordMinLength; export const shareLinkExpireDaysMin = window.app.pageOptions.shareLinkExpireDaysMin; diff --git a/media/css/print_for_md_file_view.css b/media/css/print_for_md_file_view.css index dc059f0f7f..632232da2a 100644 --- a/media/css/print_for_md_file_view.css +++ b/media/css/print_for_md_file_view.css @@ -8,3 +8,14 @@ .editor { border: none!important; } +.seafile-editor-main { + display: block!important; +} +.seafile-editor-main-panel { + display: block!important; + width: 100%!important; +} +.seafile-editor-resize, +.seafile-editor-side-panel { + display: none; +} diff --git a/seahub/api2/endpoints/admin/address_book/groups.py b/seahub/api2/endpoints/admin/address_book/groups.py index 1cdfd03f85..6179c6dd0e 100644 --- a/seahub/api2/endpoints/admin/address_book/groups.py +++ b/seahub/api2/endpoints/admin/address_book/groups.py @@ -12,8 +12,7 @@ from seaserv import seafile_api, ccnet_api from pysearpc import SearpcError from seahub.avatar.settings import AVATAR_DEFAULT_SIZE -from seahub.avatar.templatetags.avatar_tags import api_avatar_url, \ - get_default_avatar_url +from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email from seahub.utils import is_org_context @@ -28,6 +27,7 @@ from seahub.api2.authentication import TokenAuthentication logger = logging.getLogger(__name__) + def address_book_group_to_dict(group): if isinstance(group, int): group = ccnet_api.get_group(group) @@ -192,8 +192,7 @@ class AdminAddressBookGroup(APIView): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) for m in members: - member_info = self._get_address_book_group_memeber_info(request, - m, avatar_size) + member_info = self._get_address_book_group_memeber_info(request, m, avatar_size) if member_info['role'] == 'Owner': continue ret_members.append(member_info) @@ -264,6 +263,6 @@ class AdminAddressBookGroup(APIView): "owner": group_owner, } admin_operation.send(sender=None, admin_name=request.user.username, - operation=GROUP_DELETE, detail=admin_op_detail) + operation=GROUP_DELETE, detail=admin_op_detail) return Response({'success': True}) diff --git a/seahub/api2/endpoints/admin/dingtalk.py b/seahub/api2/endpoints/admin/dingtalk.py index 400af0a541..54ab6a3bdf 100644 --- a/seahub/api2/endpoints/admin/dingtalk.py +++ b/seahub/api2/endpoints/admin/dingtalk.py @@ -38,6 +38,7 @@ DEPARTMENT_OWNER = 'system admin' logger = logging.getLogger(__name__) + def update_dingtalk_user_info(email, name, contact_email, avatar_url): # make sure the contact_email is unique @@ -57,17 +58,19 @@ def update_dingtalk_user_info(email, name, contact_email, avatar_url): except Exception as e: logger.error(e) - try: - image_name = 'dingtalk_avatar' - image_file = requests.get(avatar_url).content - avatar = Avatar.objects.filter(emailuser=email, primary=True).first() - avatar = avatar or Avatar(emailuser=email, primary=True) - avatar_file = ContentFile(image_file) - avatar_file.name = image_name - avatar.avatar = avatar_file - avatar.save() - except Exception as e: - logger.error(e) + if avatar_url: + try: + image_name = 'dingtalk_avatar' + image_file = requests.get(avatar_url).content + avatar = Avatar.objects.filter(emailuser=email, primary=True).first() + avatar = avatar or Avatar(emailuser=email, primary=True) + avatar_file = ContentFile(image_file) + avatar_file.name = image_name + avatar.avatar = avatar_file + avatar.save() + except Exception as e: + logger.error(e) + class AdminDingtalkDepartments(APIView): @@ -213,8 +216,10 @@ class AdminDingtalkUsersBatch(APIView): }) try: - update_dingtalk_user_info(email, user.get('name'), - user.get('contact_email'), user.get('avatar')) + update_dingtalk_user_info(email, + user.get('name'), + user.get('contact_email'), + user.get('avatar')) except Exception as e: logger.error(e) @@ -418,8 +423,10 @@ class AdminDingtalkDepartmentsImport(APIView): failed.append(failed_msg) try: - update_dingtalk_user_info(email, api_user.get('name'), - api_user.get('contact_email'), api_user.get('avatar')) + update_dingtalk_user_info(email, + api_user.get('name'), + api_user.get('contact_email'), + api_user.get('avatar')) except Exception as e: logger.error(e) diff --git a/seahub/api2/endpoints/admin/group_members.py b/seahub/api2/endpoints/admin/group_members.py index 79135dfe91..8887a9aa57 100644 --- a/seahub/api2/endpoints/admin/group_members.py +++ b/seahub/api2/endpoints/admin/group_members.py @@ -45,18 +45,33 @@ class AdminGroupMembers(APIView): return api_error(status.HTTP_404_NOT_FOUND, 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 try: - members = ccnet_api.get_group_members(group_id) + page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '100')) + except ValueError: + page = 1 + per_page = 100 + + start = (page - 1) * per_page + limit = per_page + 1 + + try: + members = ccnet_api.get_group_members(group_id, start, limit) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + if len(members) > per_page: + members = members[:per_page] + has_next_page = True + else: + has_next_page = False + group_members_info = [] for m in members: member_info = get_group_member_info(request, group_id, m.user_name, avatar_size) @@ -65,9 +80,12 @@ class AdminGroupMembers(APIView): group_members = { 'group_id': group_id, 'group_name': group.group_name, - 'members': group_members_info + 'members': group_members_info, + 'page_info': { + 'has_next_page': has_next_page, + 'current_page': page + } } - return Response(group_members) def post(self, request, group_id): diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py index ff51ae71e4..49ab80037d 100644 --- a/seahub/api2/endpoints/admin/users.py +++ b/seahub/api2/endpoints/admin/users.py @@ -607,7 +607,10 @@ class AdminUsers(APIView): c = {'user': request.user.username, 'email': email, 'password': password} try: send_html_email(_('You are invited to join %s') % get_site_name(), - 'sysadmin/user_add_email.html', c, None, [email]) + 'sysadmin/user_add_email.html', + c, + None, + [email2contact_email(email)]) add_user_tip = _('Successfully added user %(user)s. An email notification has been sent.') % {'user': email} except Exception as e: logger.error(str(e)) @@ -927,7 +930,10 @@ class AdminUser(APIView): if user_obj.is_active and IS_EMAIL_CONFIGURED: try: send_html_email(_(u'Your account on %s is activated') % get_site_name(), - 'sysadmin/user_activation_email.html', {'username': user_obj.email}, None, [user_obj.email]) + 'sysadmin/user_activation_email.html', + {'username': user_obj.email}, + None, + [email2contact_email(user_obj.email)]) update_status_tip = _('Edit succeeded, an email has been sent.') except Exception as e: logger.error(e) diff --git a/seahub/api2/endpoints/auth_token_by_session.py b/seahub/api2/endpoints/auth_token_by_session.py index bd58b32c57..b3e4039b9c 100644 --- a/seahub/api2/endpoints/auth_token_by_session.py +++ b/seahub/api2/endpoints/auth_token_by_session.py @@ -3,9 +3,12 @@ from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework import status from seahub.api2.throttling import UserRateThrottle -from seahub.api2.utils import get_token_v1 +from seahub.api2.utils import get_token_v1, api_error + +from seahub.settings import ENABLE_GET_AUTH_TOKEN_BY_SESSION class AuthTokenBySession(APIView): @@ -18,6 +21,10 @@ class AuthTokenBySession(APIView): def get(self, request): + if not ENABLE_GET_AUTH_TOKEN_BY_SESSION: + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + token = get_token_v1(request.user.username) return Response({'token': token.key}) diff --git a/seahub/api2/endpoints/file_history.py b/seahub/api2/endpoints/file_history.py index 94ac02faa9..64c75a2a1c 100644 --- a/seahub/api2/endpoints/file_history.py +++ b/seahub/api2/endpoints/file_history.py @@ -133,15 +133,15 @@ class FileHistoryView(APIView): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) result = [] + present_time = datetime.utcnow() for commit in file_revisions: - present_time = datetime.utcnow() history_time = datetime.utcfromtimestamp(commit.ctime) - if (keep_days == -1) or ((present_time - history_time).days < keep_days): - info = get_file_history_info(commit, avatar_size) - info['path'] = path - result.append(info) - - next_start_commit = next_start_commit if result else False + if (keep_days != -1) and ((present_time - history_time).days > keep_days): + next_start_commit = False + break + info = get_file_history_info(commit, avatar_size) + info['path'] = path + result.append(info) return Response({ "data": result, diff --git a/seahub/api2/endpoints/group_members.py b/seahub/api2/endpoints/group_members.py index 7be9b80236..8f364001c5 100644 --- a/seahub/api2/endpoints/group_members.py +++ b/seahub/api2/endpoints/group_members.py @@ -45,13 +45,23 @@ class GroupMembers(APIView): except ValueError: avatar_size = AVATAR_DEFAULT_SIZE + try: + page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '100')) + except ValueError: + page = 1 + per_page = 100 + + start = (page - 1) * per_page + limit = per_page + try: # only group member can get info of all group members if not is_group_member(group_id, request.user.username): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - members = ccnet_api.get_group_members(group_id) + members = ccnet_api.get_group_members(group_id, start, limit) except SearpcError as e: logger.error(e) diff --git a/seahub/api2/endpoints/invitations.py b/seahub/api2/endpoints/invitations.py index 8354d1db49..c4eb0ea51e 100644 --- a/seahub/api2/endpoints/invitations.py +++ b/seahub/api2/endpoints/invitations.py @@ -140,7 +140,6 @@ class InvitationsBatchView(APIView): i = Invitation.objects.add(inviter=request.user.username, accepter=accepter) - result['success'].append(i.to_dict()) m = i.send_to(email=accepter) if m.status != STATUS.sent: @@ -148,5 +147,7 @@ class InvitationsBatchView(APIView): 'email': accepter, 'error_msg': _('Failed to send email, email service is not properly configured, please contact administrator.'), }) + else: + result['success'].append(i.to_dict()) return Response(result) diff --git a/seahub/handlers.py b/seahub/handlers.py index 67ed19c8af..1d7c9b994b 100644 --- a/seahub/handlers.py +++ b/seahub/handlers.py @@ -50,14 +50,10 @@ try: library_template = kwargs['library_template'] if LIBRARY_TEMPLATES and library_template: - if isinstance(library_template, str): - library_template = library_template.encode('utf-8') - try: dir_path_list = LIBRARY_TEMPLATES[library_template] for dir_path in dir_path_list: - seafile_api.mkdir_with_parents(repo_id, '/', - dir_path.strip('/'), related_users) + seafile_api.mkdir_with_parents(repo_id, '/', dir_path.strip('/'), creator) except Exception as e: logger.error(e) diff --git a/seahub/invitations/models.py b/seahub/invitations/models.py index 33caf20060..a04e5b380f 100644 --- a/seahub/invitations/models.py +++ b/seahub/invitations/models.py @@ -30,7 +30,7 @@ class InvitationManager(models.Manager): self).filter(inviter=inviter).order_by('-invite_time') def delete_all_expire_invitation(self): - super(InvitationManager, self).filter(expire_time__lte=timezone.now()).delete() + super(InvitationManager, self).filter(expire_time__lte=timezone.now(), accept_time__isnull=True).delete() def get_by_token(self, token): qs = self.filter(token=token) diff --git a/seahub/oauth/views.py b/seahub/oauth/views.py index a74ce48d2b..c6d4b2f5fa 100644 --- a/seahub/oauth/views.py +++ b/seahub/oauth/views.py @@ -186,8 +186,7 @@ def oauth_callback(request): # update user's profile name = user_info['name'] if 'name' in user_info else '' - contact_email = user_info['contact_email'] if \ - 'contact_email' in user_info else '' + contact_email = user_info['contact_email'] if 'contact_email' in user_info else '' profile = Profile.objects.get_profile_by_user(email) if not profile: @@ -205,6 +204,6 @@ def oauth_callback(request): api_token = get_api_token(request) # redirect user to home page - response = HttpResponseRedirect(request.session['oauth_redirect']) + response = HttpResponseRedirect(request.session.get('oauth_redirect', '/')) response.set_cookie('seahub_auth', email + '@' + api_token.key) return response diff --git a/seahub/profile/templates/profile/set_profile_react.html b/seahub/profile/templates/profile/set_profile_react.html index 52c2a504ed..0f576553b6 100644 --- a/seahub/profile/templates/profile/set_profile_react.html +++ b/seahub/profile/templates/profile/set_profile_react.html @@ -18,12 +18,14 @@ window.app.pageOptions = { enableUpdateUserInfo: {% if ENABLE_UPDATE_USER_INFO %} true {% else %} false {% endif %}, nameLabel: "{% trans "Name:" context "true name" %}", enableUserSetContactEmail: {% if ENABLE_USER_SET_CONTACT_EMAIL %} true {% else %} false {% endif %}, - + canUpdatePassword: {% if not is_ldap_user and ENABLE_CHANGE_PASSWORD %} true {% else %} false {% endif %}, {% if not is_ldap_user and ENABLE_CHANGE_PASSWORD %} passwordOperationText: {% if user_unusable_password %}"{% trans "Set Password" %}"{% else %}"{% trans "Update" %}"{% endif %}, {% endif %} + enableGetAuthToken: {% if ENABLE_GET_AUTH_TOKEN_BY_SESSION %} true {% else %} false {% endif %}, + enableWebdavSecret: {% if ENABLE_WEBDAV_SECRET %} true {% else %} false {% endif %}, {% if ENABLE_WEBDAV_SECRET %} webdavPasswd: '{{ webdav_passwd|escapejs }}', diff --git a/seahub/profile/views.py b/seahub/profile/views.py index 802ec72547..df736d5e04 100644 --- a/seahub/profile/views.py +++ b/seahub/profile/views.py @@ -118,6 +118,7 @@ def edit_profile(request): 'is_ldap_user': is_ldap_user(request.user), 'two_factor_auth_enabled': has_two_factor_auth(), 'ENABLE_CHANGE_PASSWORD': settings.ENABLE_CHANGE_PASSWORD, + 'ENABLE_GET_AUTH_TOKEN_BY_SESSION': settings.ENABLE_GET_AUTH_TOKEN_BY_SESSION, 'ENABLE_WEBDAV_SECRET': settings.ENABLE_WEBDAV_SECRET, 'ENABLE_DELETE_ACCOUNT': ENABLE_DELETE_ACCOUNT, 'ENABLE_UPDATE_USER_INFO': ENABLE_UPDATE_USER_INFO, diff --git a/seahub/settings.py b/seahub/settings.py index 3d43f66453..496477ce54 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -394,6 +394,9 @@ FORCE_PASSWORD_CHANGE = True # Enable a user to change password in 'settings' page. ENABLE_CHANGE_PASSWORD = True +# Enable a user to get auth token in 'settings' page. +ENABLE_GET_AUTH_TOKEN_BY_SESSION = False + ENABLE_DELETE_ACCOUNT = True ENABLE_UPDATE_USER_INFO = True diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 284507653b..604aed6a02 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -81,6 +81,14 @@ {% endfor %} return storages; })(), + // library template + libraryTemplates: (function () { + var libraryTemplates = []; + {% for template in library_templates %} + libraryTemplates.push("{{template}}"); + {% endfor %} + return libraryTemplates; + })(), enableRepoSnapshotLabel: {% if enable_repo_snapshot_label %} true {% else %} false {% endif %}, shareLinkPasswordMinLength: {{ share_link_password_min_length }}, sideNavFooterCustomHtml: "{{ side_nav_footer_custom_html|safe|escapejs }}", diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index 6c87dfbfa4..f0e94448c5 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -1186,6 +1186,7 @@ def react_fake_view(request, **kwargs): 'max_upload_file_size': max_upload_file_size, 'seafile_collab_server': SEAFILE_COLLAB_SERVER, 'storages': get_library_storages(request), + 'library_templates': list(LIBRARY_TEMPLATES.keys()), 'enable_repo_snapshot_label': settings.ENABLE_REPO_SNAPSHOT_LABEL, 'resumable_upload_file_block_size': settings.RESUMABLE_UPLOAD_FILE_BLOCK_SIZE, 'max_number_of_files_for_fileupload': settings.MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD, diff --git a/seahub/work_weixin/settings.py b/seahub/work_weixin/settings.py index 239909746f..a780b9ccc4 100644 --- a/seahub/work_weixin/settings.py +++ b/seahub/work_weixin/settings.py @@ -26,6 +26,10 @@ WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL WORK_WEIXIN_GET_USER_PROFILE_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_PROFILE_URL', 'https://qyapi.weixin.qq.com/cgi-bin/user/get') + +MP_WORK_WEIXIN_AUTHORIZATION_URL = getattr(settings, 'MP_WORK_WEIXIN_AUTHORIZATION_URL', + 'https://open.weixin.qq.com/connect/oauth2/authorize') + # # work weixin notifications WORK_WEIXIN_NOTIFICATIONS_URL = getattr(settings, 'WORK_WEIXIN_NOTIFICATIONS_URL', 'https://qyapi.weixin.qq.com/cgi-bin/message/send') diff --git a/seahub/work_weixin/views.py b/seahub/work_weixin/views.py index bb8fc7bc79..807fea28ba 100644 --- a/seahub/work_weixin/views.py +++ b/seahub/work_weixin/views.py @@ -4,7 +4,9 @@ import uuid import logging import requests -import urllib.request, urllib.parse, urllib.error +import urllib.request +import urllib.parse +import urllib.error from django.http import HttpResponseRedirect from django.utils.translation import ugettext as _ @@ -15,7 +17,7 @@ from seahub import auth from seahub.utils import render_error from seahub.base.accounts import User from seahub.work_weixin.settings import WORK_WEIXIN_AUTHORIZATION_URL, WORK_WEIXIN_CORP_ID, \ - WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_PROVIDER, \ + WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_PROVIDER, MP_WORK_WEIXIN_AUTHORIZATION_URL, \ WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, WORK_WEIXIN_UID_PREFIX, \ WORK_WEIXIN_USER_INFO_AUTO_UPDATE, REMEMBER_ME from seahub.work_weixin.utils import work_weixin_oauth_check, get_work_weixin_access_token, \ @@ -44,7 +46,13 @@ def work_weixin_oauth_login(request): 'redirect_uri': get_site_scheme_and_netloc() + reverse('work_weixin_oauth_callback'), 'state': state, } - authorization_url = WORK_WEIXIN_AUTHORIZATION_URL + '?' + urllib.parse.urlencode(data) + + if 'micromessenger' in request.META.get('HTTP_USER_AGENT').lower(): + data['response_type'] = 'code' + data['scope'] = 'snsapi_base' + authorization_url = MP_WORK_WEIXIN_AUTHORIZATION_URL + '?' + urllib.parse.urlencode(data) + '#wechat_redirect' + else: + authorization_url = WORK_WEIXIN_AUTHORIZATION_URL + '?' + urllib.parse.urlencode(data) return HttpResponseRedirect(authorization_url) diff --git a/thirdpart/shibboleth/backends.py b/thirdpart/shibboleth/backends.py index 9154c7376d..47e15f184b 100644 --- a/thirdpart/shibboleth/backends.py +++ b/thirdpart/shibboleth/backends.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.db import connection from seaserv import ccnet_api from seahub.auth.backends import RemoteUserBackend @@ -7,6 +6,7 @@ from seahub.base.accounts import User from registration.models import ( notify_admins_on_activate_request, notify_admins_on_register_complete) + class ShibbolethRemoteUserBackend(RemoteUserBackend): """ This backend is to be used in conjunction with the ``RemoteUserMiddleware`` @@ -49,6 +49,9 @@ class ShibbolethRemoteUserBackend(RemoteUserBackend): if not local_ccnet_users: local_ccnet_users = ccnet_api.search_emailusers('LDAP', username, -1, -1) + if username not in [item.email for item in local_ccnet_users]: + local_ccnet_users = [] + if not local_ccnet_users: if self.create_unknown_user: user = User.objects.create_user(