diff --git a/frontend/package.json b/frontend/package.json index da2fca8962..9894f636c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,7 @@ "react-responsive": "^6.1.2", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "0.2.163", + "seafile-js": "0.2.164", "socket.io-client": "^2.2.0", "unified": "^7.0.0", "url-parse": "^1.4.3", diff --git a/frontend/src/components/common/account.js b/frontend/src/components/common/account.js index 9c660233df..4df5a81088 100644 --- a/frontend/src/components/common/account.js +++ b/frontend/src/components/common/account.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import { Utils } from '../../utils/utils'; import { seafileAPI } from '../../utils/seafile-api'; -import { siteRoot, gettext, appAvatarURL } from '../../utils/constants'; +import { siteRoot, gettext, appAvatarURL, logoutUrl } from '../../utils/constants'; import toaster from '../toast'; const propTypes = { @@ -164,7 +164,7 @@ class Account extends Component { {gettext('Settings')} {this.renderMenu()} - {gettext('Log out')} + {gettext('Log out')} diff --git a/frontend/src/components/common/logout.js b/frontend/src/components/common/logout.js index 863243f6fc..bcceb9b63f 100644 --- a/frontend/src/components/common/logout.js +++ b/frontend/src/components/common/logout.js @@ -1,10 +1,10 @@ import React from 'react'; -import { siteRoot, gettext } from '../../utils/constants'; +import { gettext, logoutUrl } from '../../utils/constants'; export default function Logout() { return ( - + ); -} \ No newline at end of file +} diff --git a/frontend/src/components/dialog/about-dialog.js b/frontend/src/components/dialog/about-dialog.js index da68b584d2..e003523b4e 100644 --- a/frontend/src/components/dialog/about-dialog.js +++ b/frontend/src/components/dialog/about-dialog.js @@ -43,7 +43,7 @@ class AboutDialog extends React.Component {

logo

-

{gettext('Server Version: ')}{seafileVersion}
© 2020 {gettext('Seafile')}

+

{gettext('Server Version: ')}{seafileVersion}
© {(new Date()).getFullYear()} {gettext('Seafile')}

{this.renderExternalAboutLinks()}

{gettext('About Us')}

diff --git a/frontend/src/components/dialog/manage-members-dialog.js b/frontend/src/components/dialog/manage-members-dialog.js index 6608a83868..3885013b3d 100644 --- a/frontend/src/components/dialog/manage-members-dialog.js +++ b/frontend/src/components/dialog/manage-members-dialog.js @@ -1,12 +1,14 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { gettext } from '../../utils/constants'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Table } from 'reactstrap'; -import { seafileAPI } from '../../utils/seafile-api.js'; -import RoleEditor from '../select-editor/role-editor'; -import UserSelect from '../user-select.js'; import { Utils } from '../../utils/utils'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import RoleEditor from '../select-editor/role-editor'; +import UserSelect from '../user-select'; import toaster from '../toast'; +import Loading from '../loading'; + import '../../css/manage-members-dialog.css'; const propTypes = { @@ -21,13 +23,45 @@ class ManageMembersDialog extends React.Component { constructor(props) { super(props); this.state = { + isLoading: true, // first loading + isLoadingMore: false, groupMembers: [], + page: 1, + perPage: 100, + hasNextPage: false, selectedOption: null, errMessage: [], - isItemFreezed: false, + isItemFreezed: false }; } + componentDidMount() { + this.listGroupMembers(this.state.page); + } + + listGroupMembers = (page) => { + const { groupID } = this.props; + const { perPage, groupMembers } = this.state; + seafileAPI.listGroupMembers(groupID, page, perPage).then((res) => { + const members = res.data; + this.setState({ + isLoading: false, + isLoadingMore: false, + page: page, + hasNextPage: members.length < perPage ? false : true, + groupMembers: groupMembers.concat(members) + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + this.setState({ + isLoading: false, + isLoadingMore: false, + hasNextPage: false + }); + }); + } + onSelectChange = (option) => { this.setState({ selectedOption: option, @@ -41,8 +75,9 @@ class ManageMembersDialog extends React.Component { emails.push(this.state.selectedOption[i].email); } seafileAPI.addGroupMembers(this.props.groupID, emails).then((res) => { - this.onGroupMembersChange(); + const newMembers = res.data.success; this.setState({ + groupMembers: [].concat(newMembers, this.state.groupMembers), selectedOption: null, }); this.refs.userSelect.clearSelect(); @@ -57,21 +92,6 @@ class ManageMembersDialog extends React.Component { }); } - listGroupMembers = () => { - seafileAPI.listGroupMembers(this.props.groupID).then((res) => { - this.setState({ - groupMembers: res.data - }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - onGroupMembersChange = () => { - this.listGroupMembers(); - } - toggleItemFreezed = (isFreezed) => { this.setState({ isItemFreezed: isFreezed @@ -82,11 +102,43 @@ class ManageMembersDialog extends React.Component { this.props.toggleManageMembersDialog(); } - componentDidMount() { - this.listGroupMembers(); + handleScroll = (event) => { + // isLoadingMore: to avoid repeated request + const { page, hasNextPage, isLoadingMore } = this.state; + if (hasNextPage && !isLoadingMore) { + const clientHeight = event.target.clientHeight; + const scrollHeight = event.target.scrollHeight; + const scrollTop = event.target.scrollTop; + const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight); + if (isBottom) { // scroll to the bottom + this.setState({isLoadingMore: true}, () => { + this.listGroupMembers(page + 1); + }); + } + } + } + + changeMember = (targetMember) => { + this.setState({ + groupMembers: this.state.groupMembers.map((item) => { + if (item.email == targetMember.email) { + item = targetMember; + } + return item; + }) + }); + } + + deleteMember = (targetMember) => { + const groupMembers = this.state.groupMembers; + groupMembers.splice(groupMembers.indexOf(targetMember), 1); + this.setState({ + groupMembers: groupMembers + }); } render() { + const { isLoading, hasNextPage } = this.state; return ( {gettext('Manage group members')} @@ -113,36 +165,41 @@ class ManageMembersDialog extends React.Component { ); }) } -
- - - - - - - - - - - { - this.state.groupMembers.length > 0 && - this.state.groupMembers.map((item, index = 0) => { +
+ {isLoading ? : ( + +
{gettext('Name')}{gettext('Role')}
+ + + + + + + + + + { + this.state.groupMembers.length > 0 && + this.state.groupMembers.map((item, index) => { return ( - - ); }) - } - -
{gettext('Name')}{gettext('Role')}
+ } + + + {hasNextPage && } + + )}
@@ -157,7 +214,8 @@ ManageMembersDialog.propTypes = propTypes; const MemberPropTypes = { item: PropTypes.object.isRequired, - onGroupMembersChange: PropTypes.func.isRequired, + changeMember: PropTypes.func.isRequired, + deleteMember: PropTypes.func.isRequired, groupID: PropTypes.string.isRequired, isOwner: PropTypes.bool.isRequired, }; @@ -175,16 +233,18 @@ class Member extends React.PureComponent { onChangeUserRole = (role) => { let isAdmin = role === 'Admin' ? 'True' : 'False'; seafileAPI.setGroupAdmin(this.props.groupID, this.props.item.email, isAdmin).then((res) => { - this.props.onGroupMembersChange(); - }); - this.setState({ - highlight: false, + this.props.changeMember(res.data); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); }); } - deleteMember = (name) => { - seafileAPI.deleteGroupMember(this.props.groupID, name).then((res) => { - this.props.onGroupMembersChange(); + deleteMember = () => { + const { item } = this.props; + seafileAPI.deleteGroupMember(this.props.groupID, item.email).then((res) => { + this.props.deleteMember(item); + toaster.success(gettext('Successfully deleted {name}.').replace('{name}', item.name)); }).catch(error => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); @@ -243,8 +303,7 @@ class Member extends React.PureComponent { {(deleteAuthority && !this.props.isItemFreezed) && + onClick={this.deleteMember}> } diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index eab1d7eaf5..614fcd6c46 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -171,8 +171,10 @@ class DirentListItem extends React.Component { onItemClick = (e) => { e.preventDefault(); - const dirent = this.props.dirent; + if (this.state.isRenameing) { + return; + } if (Utils.imageCheck(dirent.name)) { this.props.showImagePopup(dirent); } else { @@ -664,19 +666,19 @@ class DirentListItem extends React.Component { ); const mobileItem = ( - +
{dirent.encoded_thumbnail_src ? - : + : } {dirent.is_locked && {gettext('locked')}}
- + {this.state.isRenameing ? : - {dirent.name} + {dirent.name} }
{dirent.size && {dirent.size}} diff --git a/frontend/src/components/search/search-result-item.js b/frontend/src/components/search/search-result-item.js index 431a85b17a..a78a91eb3a 100644 --- a/frontend/src/components/search/search-result-item.js +++ b/frontend/src/components/search/search-result-item.js @@ -19,6 +19,11 @@ class SearchResultItem extends React.Component { let className = item.link_content ? 'item-img' : 'lib-item-img'; let folderIconUrl = item.link_content ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(true); let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name, 192); + + if (item.thumbnail_url !== '') { + fileIconUrl = item.thumbnail_url; + } + return (
  • diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js index 4d450a42ae..ea0ed6c7e6 100644 --- a/frontend/src/components/search/search.js +++ b/frontend/src/components/search/search.js @@ -178,6 +178,7 @@ class Search extends Component { items[i]['is_dir'] = data[i].is_dir; items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1); items[i]['content'] = data[i].content_highlight; + items[i]['thumbnail_url'] = data[i].thumbnail_url; } return items; } diff --git a/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js index ef58c04371..a3f8f02cbd 100644 --- a/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js +++ b/frontend/src/components/shared-repo-list-view/shared-repo-list-item.js @@ -2,7 +2,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; -import { Link } from '@reach/router'; +import { Link, navigate } from '@reach/router'; import { Utils } from '../../utils/utils'; import { gettext, siteRoot, isPro, username, folderPermEnabled, isSystemStaff, enableResetEncryptedRepoPassword, isEmailConfigured } from '../../utils/constants'; import ModalPortal from '../../components/modal-portal'; @@ -479,14 +479,21 @@ class SharedRepoListItem extends React.Component { ); } + visitRepo = () => { + if (!this.state.isRenaming) { + navigate(this.repoURL); + } + } + renderMobileUI = () => { let { iconUrl, iconTitle, libPath } = this.getRepoComputeParams(); let { repo } = this.props; + this.repoURL = libPath; return ( - {iconTitle}/ - + {iconTitle}/ + {this.state.isRenaming ? : {repo.repo_name} diff --git a/frontend/src/models/sysadmin-admin-user.js b/frontend/src/models/sysadmin-admin-user.js index 5724d0463b..a7a9442d32 100644 --- a/frontend/src/models/sysadmin-admin-user.js +++ b/frontend/src/models/sysadmin-admin-user.js @@ -6,6 +6,7 @@ class SysAdminAdminUser { this.contact_email = object.contact_email; this.login_id = object.login_id; this.last_login = object.last_login; + this.last_access_time = object.last_access_time; this.create_time = object.create_time; this.is_active = object.is_active; this.is_staff = object.is_staff; diff --git a/frontend/src/models/sysadmin-user.js b/frontend/src/models/sysadmin-user.js index d09f189af4..b59532bc8d 100644 --- a/frontend/src/models/sysadmin-user.js +++ b/frontend/src/models/sysadmin-user.js @@ -5,6 +5,7 @@ class SysAdminUser { this.contact_email = object.contact_email; this.login_id = object.login_id; this.last_login = object.last_login; + this.last_access_time = object.last_access_time; this.create_time = object.create_time; this.is_active = object.is_active; this.is_staff = object.is_staff; diff --git a/frontend/src/pages/my-libs/mylib-repo-list-item.js b/frontend/src/pages/my-libs/mylib-repo-list-item.js index c3f2ff8719..9b2b19355e 100644 --- a/frontend/src/pages/my-libs/mylib-repo-list-item.js +++ b/frontend/src/pages/my-libs/mylib-repo-list-item.js @@ -2,7 +2,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import MediaQuery from 'react-responsive'; import moment from 'moment'; -import { Link } from '@reach/router'; +import { Link, navigate } from '@reach/router'; import { Utils } from '../../utils/utils'; import { seafileAPI } from '../../utils/seafile-api'; import { gettext, siteRoot, storages } from '../../utils/constants'; @@ -121,6 +121,12 @@ class MylibRepoListItem extends React.Component { } } + visitRepo = () => { + if (!this.state.isRenaming && this.props.repo.repo_name) { + navigate(this.repoURL); + } + } + onRepoClick = () => { this.props.onRepoClick(this.props.repo); } @@ -317,12 +323,12 @@ class MylibRepoListItem extends React.Component { let repo = this.props.repo; let iconUrl = Utils.getLibIconUrl(repo); let iconTitle = Utils.getLibIconTitle(repo); - let repoURL = `${siteRoot}library/${repo.repo_id}/${Utils.encodePath(repo.repo_name)}/`; + let repoURL = this.repoURL = `${siteRoot}library/${repo.repo_id}/${Utils.encodePath(repo.repo_name)}/`; return ( - {iconTitle} - + {iconTitle} + {this.state.isRenaming && ( diff --git a/frontend/src/pages/shared-libs/shared-libs.js b/frontend/src/pages/shared-libs/shared-libs.js index 782e2d6e56..63cc2ad1c6 100644 --- a/frontend/src/pages/shared-libs/shared-libs.js +++ b/frontend/src/pages/shared-libs/shared-libs.js @@ -3,7 +3,7 @@ import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap'; import PropTypes from 'prop-types'; import moment from 'moment'; import cookie from 'react-cookies'; -import { Link } from '@reach/router'; +import { Link, navigate } from '@reach/router'; import { gettext, siteRoot, isPro } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; @@ -194,6 +194,10 @@ class Item extends Component { } } + visitRepo = () => { + navigate(this.repoURL); + } + render() { if (this.state.unshared) { return null; @@ -207,7 +211,7 @@ class Item extends Component { let iconVisibility = this.state.showOpIcon ? '' : ' invisible'; let shareIconClassName = 'op-icon sf2-icon-share repo-share-btn' + iconVisibility; let leaveShareIconClassName = 'op-icon sf2-icon-x3' + iconVisibility; - let shareRepoUrl =`${siteRoot}library/${data.repo_id}/${Utils.encodePath(data.repo_name)}/`; + let shareRepoUrl = this.repoURL = `${siteRoot}library/${data.repo_id}/${Utils.encodePath(data.repo_name)}/`; const desktopItem = ( @@ -248,8 +252,8 @@ class Item extends Component { const mobileItem = ( - {data.icon_title} - + {data.icon_title} + {data.repo_name}
    {data.owner_name} {data.size} diff --git a/frontend/src/pages/starred/starred.js b/frontend/src/pages/starred/starred.js index efab48e1b6..3cbe54bf24 100644 --- a/frontend/src/pages/starred/starred.js +++ b/frontend/src/pages/starred/starred.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap'; -import { Link } from '@reach/router'; +import { Link, navigate } from '@reach/router'; import moment from 'moment'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; @@ -170,6 +170,15 @@ class Item extends Component { }); } + visitItem = () => { + const data = this.props.data; + if (data.is_dir) { + navigate(data.dirent_view_url); + } else { + window.open(data.dirent_view_url); + } + } + render() { if (this.state.unstarred) { @@ -206,14 +215,14 @@ class Item extends Component { const mobileItem = ( - + { data.thumbnail_url ? : {gettext('icon')} } - + { data.is_dir ? {data.obj_name} : {data.obj_name} diff --git a/frontend/src/pages/sys-admin/users/users-content.js b/frontend/src/pages/sys-admin/users/users-content.js index 796f238af4..9dbd85bfe8 100644 --- a/frontend/src/pages/sys-admin/users/users-content.js +++ b/frontend/src/pages/sys-admin/users/users-content.js @@ -83,7 +83,7 @@ class Content extends Component { const colSpaceText = {spaceEl}{` / ${gettext('Quota')}`}; const colNameText = `${gettext('Name')} / ${gettext('Contact Email')}`; - const colCreatedText = `${gettext('Created At')} / ${gettext('Last Login')}`; + const colCreatedText = `${gettext('Created At')} / ${gettext('Last Login')} / ${gettext('Last Access')}`; if (isPro) { columns.push( {width: '20%', text: colNameText}, @@ -422,6 +422,8 @@ class Item extends Component { {`${item.create_time ? moment(item.create_time).format('YYYY-MM-DD HH:mm') : '--'} /`}
    {`${item.last_login ? moment(item.last_login).fromNow() : '--'}`} +
    + {`${item.last_access_time ? moment(item.last_access_time).fromNow() : '--'}`} {(item.email != username && isOpIconShown) && diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index bd10bb2a49..cc0c08999f 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -3,6 +3,7 @@ export const gettext = window.gettext; export const siteRoot = window.app.config.siteRoot; export const loginUrl = window.app.config.loginUrl; +export const logoutUrl = window.app.config.logoutUrl; export const avatarInfo = window.app.config.avatarInfo; export const logoPath = window.app.config.logoPath; export const mediaUrl = window.app.config.mediaUrl; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 92acc676f5..01646cbf3a 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -484,7 +484,7 @@ export const Utils = { return list; }, - getFileOperationList: function(currentRepoInfo, dirent, isContextmenu) { + getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { let list = []; const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK, COMMENT, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT } = TextTranslation; @@ -516,7 +516,7 @@ export const Utils = { if (isPro) { if (dirent.is_locked) { - if (dirent.locked_by_me || dirent.lock_owner == 'OnlineOffice') { + if (dirent.locked_by_me || dirent.lock_owner == 'OnlineOffice' || isRepoOwner || currentRepoInfo.is_admin) { list.push(UNLOCK); } } else { @@ -551,7 +551,7 @@ export const Utils = { getDirentOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { return dirent.type == 'dir' ? Utils.getFolderOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu) : - Utils.getFileOperationList(currentRepoInfo, dirent, isContextmenu); + Utils.getFileOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu); }, sharePerms: function(permission) { diff --git a/requirements.txt b/requirements.txt index c3acf608ba..a66fd304b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ Django==2.2.14 future captcha django-statici18n -django-post_office==3.3.0 django-webpack_loader gunicorn mysqlclient diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py index 4440d32121..6e2844ab5b 100644 --- a/seahub/api2/endpoints/admin/users.py +++ b/seahub/api2/endpoints/admin/users.py @@ -19,6 +19,7 @@ from seaserv import seafile_api, ccnet_api from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error, to_python_boolean +from seahub.api2.models import TokenV2 import seahub.settings as settings from seahub.settings import SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, INIT_PASSWD, \ @@ -33,9 +34,12 @@ from seahub.profile.settings import CONTACT_CACHE_TIMEOUT, CONTACT_CACHE_PREFIX, from seahub.utils import is_valid_username2, is_org_context, \ is_pro_version, normalize_cache_key, is_valid_email, \ IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \ - gen_shared_link, gen_shared_upload_link + gen_shared_link, gen_shared_upload_link, \ + get_file_audit_events, get_file_update_events + from seahub.utils.file_size import get_file_size_unit -from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr +from seahub.utils.timeutils import timestamp_to_isoformat_timestr, \ + datetime_to_isoformat_timestr, utc_to_local from seahub.utils.user_permissions import get_user_role from seahub.utils.repo import normalize_repo_status_code from seahub.constants import DEFAULT_ADMIN @@ -57,6 +61,43 @@ logger = logging.getLogger(__name__) json_content_type = 'application/json; charset=utf-8' +def get_user_last_access_time(email, last_login_time): + + device_last_access = '' + audit_last_access = '' + update_last_access = '' + + devices = TokenV2.objects.filter(user=email).order_by('-last_accessed') + if devices: + device_last_access = devices[0].last_accessed + + audit_events = get_file_audit_events(email, 0, None, 0, 1) or [] + if audit_events: + audit_last_access = audit_events[0].timestamp + + update_events = get_file_update_events(email, 0, None, 0, 1) or [] + if update_events: + update_last_access = update_events[0].timestamp + + last_access_time_list = [] + if last_login_time: + last_access_time_list.append(last_login_time) + + if device_last_access: + last_access_time_list.append(device_last_access) + + if audit_last_access: + last_access_time_list.append(utc_to_local(audit_last_access)) + + if update_last_access: + last_access_time_list.append(utc_to_local(update_last_access)) + + if not last_access_time_list: + return '' + else: + return datetime_to_isoformat_timestr(sorted(last_access_time_list)[-1]) + + def get_user_upload_link_info(uls): data = {} @@ -204,6 +245,7 @@ def update_user_info(request, user, password, is_active, is_staff, role, logger.error(e) seafile_api.set_user_quota(email, -1) + def get_user_info(email): user = User.objects.get(email=email) @@ -297,8 +339,15 @@ class AdminAdminUsers(APIView): user_info['quota_total'] = -1 user_info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) + last_login_obj = UserLastLogin.objects.get_by_username(user.email) - user_info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) if last_login_obj else '' + if last_login_obj: + user_info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) + user_info['last_access_time'] = get_user_last_access_time(user.email, + last_login_obj.last_login) + else: + user_info['last_login'] = '' + user_info['last_access_time'] = get_user_last_access_time(user.email, '') try: admin_role = AdminRole.objects.get_admin_role(user.email) @@ -312,6 +361,7 @@ class AdminAdminUsers(APIView): } return Response(result) + class AdminUsers(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) @@ -363,7 +413,13 @@ class AdminUsers(APIView): info['quota_total'] = seafile_api.get_user_quota(user.email) last_login_obj = UserLastLogin.objects.get_by_username(user.email) - info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) if last_login_obj else '' + if last_login_obj: + info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) + info['last_access_time'] = get_user_last_access_time(user.email, + last_login_obj.last_login) + else: + info['last_login'] = '' + info['last_access_time'] = get_user_last_access_time(user.email, '') info['role'] = get_user_role(user) @@ -423,8 +479,10 @@ class AdminUsers(APIView): return api_error(status.HTTP_400_BAD_REQUEST, error_msg) try: - data = self.get_info_of_users_order_by_quota_usage(source, direction, - page, per_page) + data = self.get_info_of_users_order_by_quota_usage(source, + direction, + page, + per_page) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' @@ -448,8 +506,10 @@ class AdminUsers(APIView): return api_error(status.HTTP_400_BAD_REQUEST, error_msg) try: - data = self.get_info_of_users_order_by_quota_usage(source, direction, - page, per_page) + data = self.get_info_of_users_order_by_quota_usage(source, + direction, + page, + per_page) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' @@ -489,10 +549,19 @@ class AdminUsers(APIView): info['quota_usage'] = -1 info['quota_total'] = -1 - info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) - last_login_obj = UserLastLogin.objects.get_by_username(user.email) - info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) if last_login_obj else '' info['role'] = get_user_role(user) + + info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) + + last_login_obj = UserLastLogin.objects.get_by_username(user.email) + if last_login_obj: + info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) + info['last_access_time'] = get_user_last_access_time(user.email, + last_login_obj.last_login) + else: + info['last_login'] = '' + info['last_access_time'] = get_user_last_access_time(user.email, '') + if getattr(settings, 'MULTI_INSTITUTION', False): info['institution'] = profile.institution if profile else '' @@ -569,8 +638,7 @@ class AdminUsers(APIView): if is_org_context(request): org_id = request.user.org.org_id - org_quota_mb = seafile_api.get_org_quota(org_id) / \ - get_file_size_unit('MB') + org_quota_mb = seafile_api.get_org_quota(org_id) / get_file_size_unit('MB') if quota_total_mb > org_quota_mb: error_msg = 'Failed to set quota: maximum quota is %d MB' % org_quota_mb @@ -611,6 +679,7 @@ class AdminUsers(APIView): 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)) @@ -669,8 +738,16 @@ class AdminLDAPUsers(APIView): info['quota_total'] = seafile_api.get_user_quota(user.email) info['quota_usage'] = seafile_api.get_user_self_usage(user.email) info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) + last_login_obj = UserLastLogin.objects.get_by_username(user.email) - info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) if last_login_obj else '' + if last_login_obj: + info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) + info['last_access_time'] = get_user_last_access_time(user.email, + last_login_obj.last_login) + else: + info['last_login'] = '' + info['last_access_time'] = get_user_last_access_time(user.email, '') + data.append(info) result = {'ldap_user_list': data, 'has_next_page': has_next_page} @@ -859,8 +936,16 @@ class AdminSearchUser(APIView): info['quota_total'] = seafile_api.get_user_quota(user.email) info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) + last_login_obj = UserLastLogin.objects.get_by_username(user.email) - info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) if last_login_obj else '' + if last_login_obj: + info['last_login'] = datetime_to_isoformat_timestr(last_login_obj.last_login) + info['last_access_time'] = get_user_last_access_time(user.email, + last_login_obj.last_login) + else: + info['last_login'] = '' + info['last_access_time'] = get_user_last_access_time(user.email, '') + info['role'] = get_user_role(user) if getattr(settings, 'MULTI_INSTITUTION', False): @@ -984,8 +1069,7 @@ class AdminUser(APIView): if is_org_context(request): org_id = request.user.org.org_id - org_quota_mb = seafile_api.get_org_quota(org_id) / \ - get_file_size_unit('MB') + org_quota_mb = seafile_api.get_org_quota(org_id) / get_file_size_unit('MB') if quota_total_mb > org_quota_mb: error_msg = 'Failed to set quota: maximum quota is %d MB' % org_quota_mb @@ -1115,7 +1199,7 @@ class AdminUserResetPassword(APIView): contact_email = Profile.objects.get_contact_email_by_user(email) try: send_html_email(_(u'Password has been reset on %s') % get_site_name(), - 'sysadmin/user_reset_email.html', c, None, [contact_email]) + 'sysadmin/user_reset_email.html', c, None, [contact_email]) reset_tip = _('Successfully reset password to %(passwd)s, an email has been sent to %(user)s.') % \ {'passwd': new_password, 'user': contact_email} except Exception as e: @@ -1299,7 +1383,7 @@ class AdminUserBeSharedRepos(APIView): if email not in nickname_dict: if '@seafile_group' in email: group_id = get_group_id_by_repo_owner(email) - group_name= group_id_to_name(group_id) + group_name = group_id_to_name(group_id) nickname_dict[email] = group_name else: nickname_dict[email] = email2nickname(email) diff --git a/seahub/api2/endpoints/admin/users_batch.py b/seahub/api2/endpoints/admin/users_batch.py index 48c111d0de..75a99f7d0f 100644 --- a/seahub/api2/endpoints/admin/users_batch.py +++ b/seahub/api2/endpoints/admin/users_batch.py @@ -15,7 +15,6 @@ from django.utils.translation import ugettext as _ from seaserv import seafile_api -import seahub.settings as settings from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error @@ -218,7 +217,7 @@ class AdminUsersBatch(APIView): "email": email, } admin_operation.send(sender=None, admin_name=request.user.username, - operation=USER_DELETE, detail=admin_op_detail) + operation=USER_DELETE, detail=admin_op_detail) if operation == 'set-institution': institution = request.POST.get('institution', None) @@ -228,7 +227,7 @@ class AdminUsersBatch(APIView): if institution != '': try: - obj_insti = Institution.objects.get(name=institution) + Institution.objects.get(name=institution) except Institution.DoesNotExist: error_msg = 'Institution %s does not exist' % institution return api_error(status.HTTP_400_BAD_REQUEST, error_msg) @@ -372,14 +371,14 @@ class AdminImportUsers(APIView): except Exception as e: logger.error(e) - send_html_email_with_dj_template( - email, dj_template='sysadmin/user_batch_add_email.html', - subject=_('You are invited to join %s') % get_site_name(), - context={ - 'user': email2nickname(request.user.username), - 'email': email, - 'password': password, - }) + send_html_email_with_dj_template(email, + subject=_('You are invited to join %s') % get_site_name(), + dj_template='sysadmin/user_batch_add_email.html', + context={ + 'user': email2nickname(request.user.username), + 'email': email, + 'password': password + }) user = User.objects.get(email=email) @@ -407,4 +406,3 @@ class AdminImportUsers(APIView): operation=USER_ADD, detail=admin_op_detail) return Response(result) - diff --git a/seahub/api2/endpoints/file.py b/seahub/api2/endpoints/file.py index 2068d87bd9..b223b8c204 100644 --- a/seahub/api2/endpoints/file.py +++ b/seahub/api2/endpoints/file.py @@ -24,7 +24,7 @@ from seahub.views import check_folder_permission from seahub.utils.file_op import check_file_lock, if_locked_by_online_office from seahub.views.file import can_preview_file, can_edit_file from seahub.constants import PERMISSION_READ_WRITE -from seahub.utils.repo import parse_repo_perm +from seahub.utils.repo import parse_repo_perm, is_repo_admin, is_repo_owner from seahub.utils.file_types import MARKDOWN, TEXT from seahub.settings import MAX_UPLOAD_FILE_NAME_LEN, \ @@ -57,7 +57,7 @@ class FileView(APIView): file_name = file_obj.obj_name file_size = file_obj.size can_preview, error_msg = can_preview_file(file_name, file_size, repo) - can_edit, error_msg = can_edit_file(file_name, file_size, repo) + can_edit, error_msg = can_edit_file(file_name, file_size, repo) else: can_preview = False can_edit = False @@ -295,11 +295,9 @@ class FileView(APIView): return api_error(status.HTTP_403_FORBIDDEN, error_msg) # rename file - new_file_name = check_filename_with_rename(repo_id, parent_dir, - new_file_name) + new_file_name = check_filename_with_rename(repo_id, parent_dir, new_file_name) try: - seafile_api.rename_file(repo_id, parent_dir, oldname, - new_file_name, username) + seafile_api.rename_file(repo_id, parent_dir, oldname, new_file_name, username) except SearpcError as e: logger.error(e) error_msg = 'Internal Server Error' @@ -394,8 +392,8 @@ class FileView(APIView): new_file_name = check_filename_with_rename(dst_repo_id, dst_dir, filename) try: seafile_api.move_file(src_repo_id, src_dir, filename, - dst_repo_id, dst_dir, new_file_name, replace=False, - username=username, need_progress=0, synchronous=1) + dst_repo_id, dst_dir, new_file_name, replace=False, + username=username, need_progress=0, synchronous=1) except SearpcError as e: logger.error(e) error_msg = 'Internal Server Error' @@ -460,7 +458,7 @@ class FileView(APIView): new_file_name = check_filename_with_rename(dst_repo_id, dst_dir, filename) try: seafile_api.copy_file(src_repo_id, src_dir, filename, dst_repo_id, - dst_dir, new_file_name, username, 0, synchronous=1) + dst_dir, new_file_name, username, 0, synchronous=1) except SearpcError as e: logger.error(e) error_msg = 'Internal Server Error' @@ -586,7 +584,9 @@ class FileView(APIView): error_msg = _("File is not locked.") return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - if locked_by_me or locked_by_online_office: + if locked_by_me or locked_by_online_office or \ + is_repo_owner(request, repo_id, username) or \ + is_repo_admin(username, repo_id): # unlock file try: seafile_api.unlock_file(repo_id, path) diff --git a/seahub/api2/endpoints/invitation.py b/seahub/api2/endpoints/invitation.py index 6dd043be4a..5ace67cd82 100644 --- a/seahub/api2/endpoints/invitation.py +++ b/seahub/api2/endpoints/invitation.py @@ -15,13 +15,13 @@ from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error from seahub.invitations.models import Invitation from seahub.base.accounts import User -from post_office.models import STATUS -from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY +from seahub.utils.mail import send_html_email_with_dj_template from seahub.utils import get_site_name logger = logging.getLogger(__name__) json_content_type = 'application/json; charset=utf-8' + def invitation_owner_check(func): """Check whether user is the invitation inviter. """ @@ -34,6 +34,7 @@ def invitation_owner_check(func): return _decorated + class InvitationView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated, CanInviteGuest) @@ -108,13 +109,12 @@ class InvitationRevokeView(APIView): 'site_name': site_name, } - m = send_html_email_with_dj_template( - email, dj_template='invitations/invitation_revoke_email.html', - subject=subject, - context=context, - priority=MAIL_PRIORITY.now - ) - if m.status != STATUS.sent: + send_success = send_html_email_with_dj_template(email, + subject=subject, + dj_template='invitations/invitation_revoke_email.html', + context=context) + + if not send_success: logger.warning('send revoke access email to %s failed') return Response({'success': True}) diff --git a/seahub/api2/endpoints/invitations.py b/seahub/api2/endpoints/invitations.py index c4eb0ea51e..4a4afc4279 100644 --- a/seahub/api2/endpoints/invitations.py +++ b/seahub/api2/endpoints/invitations.py @@ -6,7 +6,6 @@ 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 post_office.models import STATUS from seahub.api2.authentication import TokenAuthentication from seahub.api2.permissions import CanInviteGuest @@ -70,8 +69,9 @@ class InvitationsView(APIView): i = Invitation.objects.add(inviter=request.user.username, accepter=accepter) - m = i.send_to(email=accepter) - if m.status == STATUS.sent: + send_success = i.send_to(email=accepter) + + if send_success: return Response(i.to_dict(), status=201) else: return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -119,11 +119,13 @@ class InvitationsBatchView(APIView): continue if Invitation.objects.filter(inviter=request.user.username, - accepter=accepter).count() > 0: + accepter=accepter).count() > 0: + result['failed'].append({ 'email': accepter, 'error_msg': _('%s is already invited.') % accepter }) + continue try: @@ -139,13 +141,15 @@ class InvitationsBatchView(APIView): pass i = Invitation.objects.add(inviter=request.user.username, - accepter=accepter) + accepter=accepter) - m = i.send_to(email=accepter) - if m.status != STATUS.sent: + send_success = i.send_to(email=accepter) + + if not send_success: result['failed'].append({ 'email': accepter, - 'error_msg': _('Failed to send email, email service is not properly configured, please contact administrator.'), + 'error_msg': _('Failed to send email, email service is not properly configured, \ + please contact administrator.'), }) else: result['success'].append(i.to_dict()) diff --git a/seahub/api2/endpoints/repo_share_invitation.py b/seahub/api2/endpoints/repo_share_invitation.py index 178f2d4f23..e6910b1189 100644 --- a/seahub/api2/endpoints/repo_share_invitation.py +++ b/seahub/api2/endpoints/repo_share_invitation.py @@ -6,7 +6,6 @@ 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 django.utils.translation import ugettext as _ from seaserv import seafile_api @@ -14,8 +13,7 @@ from seahub.api2.authentication import TokenAuthentication from seahub.api2.permissions import CanInviteGuest from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error -from seahub.invitations.models import Invitation, RepoShareInvitation -from post_office.models import STATUS +from seahub.invitations.models import RepoShareInvitation from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE from seahub.share.utils import is_repo_admin from seahub.utils import is_org_context @@ -82,7 +80,7 @@ class RepoShareInvitationView(APIView): logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - + return Response({'success': True}) def delete(self, request, repo_id, format=None): @@ -124,11 +122,11 @@ class RepoShareInvitationView(APIView): if not shared_obj: error_msg = 'repo share invitation not found.' return api_error(status.HTTP_404_NOT_FOUND, error_msg) - + shared_obj.delete() except Exception as e: logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - + return Response({'success': True}) diff --git a/seahub/api2/endpoints/repo_share_invitations.py b/seahub/api2/endpoints/repo_share_invitations.py index 90134bbbba..cacf8baf8d 100644 --- a/seahub/api2/endpoints/repo_share_invitations.py +++ b/seahub/api2/endpoints/repo_share_invitations.py @@ -8,7 +8,6 @@ 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 post_office.models import STATUS from seaserv import seafile_api @@ -20,7 +19,6 @@ from seahub.base.accounts import User from seahub.utils import is_valid_email from seahub.invitations.models import Invitation, RepoShareInvitation from seahub.invitations.utils import block_accepter -from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE, GUEST_USER from seahub.share.utils import is_repo_admin from seahub.utils import is_org_context @@ -29,6 +27,7 @@ from seahub.base.templatetags.seahub_tags import email2nickname json_content_type = 'application/json; charset=utf-8' logger = logging.getLogger(__name__) + class RepoShareInvitationsView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated, CanInviteGuest) @@ -47,7 +46,7 @@ class RepoShareInvitationsView(APIView): if not repo: error_msg = 'Library %s not found.' % repo_id return api_error(status.HTTP_404_NOT_FOUND, error_msg) - + if seafile_api.get_dir_id_by_path(repo.id, path) is None: return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) @@ -124,7 +123,7 @@ class RepoShareInvitationsBatchView(APIView): if username != repo_owner and not is_repo_admin(username, repo_id): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - + # main result = {} result['failed'] = [] @@ -173,7 +172,7 @@ class RepoShareInvitationsBatchView(APIView): continue except User.DoesNotExist: pass - + if invitation_queryset.filter(accepter=accepter).exists(): invitation = invitation_queryset.filter(accepter=accepter)[0] else: @@ -181,12 +180,12 @@ class RepoShareInvitationsBatchView(APIView): inviter=request.user.username, accepter=accepter) if shared_queryset.filter(invitation=invitation).exists(): - result['failed'].append({ - 'email': accepter, - 'error_msg': _('This item has been shared to %s.') % accepter - }) - continue - + result['failed'].append({ + 'email': accepter, + 'error_msg': _('This item has been shared to %s.') % accepter + }) + continue + try: RepoShareInvitation.objects.add( invitation=invitation, repo_id=repo_id, path=path, permission=permission) @@ -203,11 +202,13 @@ class RepoShareInvitationsBatchView(APIView): result['success'].append(data) - m = invitation.send_to(email=accepter) - if m.status != STATUS.sent: + send_sucess = invitation.send_to(email=accepter) + + if not send_sucess: result['failed'].append({ 'email': accepter, - 'error_msg': _('Failed to send email, email service is not properly configured, please contact administrator.'), + 'error_msg': _('Failed to send email, email service is not properly configured, \ + please contact administrator.'), }) return Response(result) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 77d047d85c..7455867d34 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -72,6 +72,7 @@ from seahub.utils import gen_file_get_url, gen_token, gen_file_upload_url, \ gen_shared_upload_link, convert_cmmt_desc_link, is_valid_dirent_name, \ normalize_file_path, get_no_duplicate_obj_name, normalize_dir_path +from seahub.utils.file_types import IMAGE from seahub.utils.file_revisions import get_file_revisions_after_renamed from seahub.utils.devices import do_unlink_device from seahub.utils.repo import get_repo_owner, get_library_storages, \ @@ -424,6 +425,7 @@ class Search(APIView): throttle_classes = (UserRateThrottle, ) def get(self, request, format=None): + if not HAS_FILE_SEARCH: error_msg = 'Search not supported.' return api_error(status.HTTP_404_NOT_FOUND, error_msg) @@ -601,6 +603,16 @@ class Search(APIView): else: e['repo_type'] = '' + e['thumbnail_url'] = '' + filetype, fileext = get_file_type_and_ext(e.get('name', '')) + + if filetype == IMAGE: + thumbnail_url = reverse('api2-thumbnail', + args=[e.get('repo_id', '')], + request=request) + params = '?p={}&size={}'.format(quote(e.get('fullpath', '').encode('utf-8')), 72) + e['thumbnail_url'] = thumbnail_url + params + has_more = True if total > current_page * per_page else False return Response({"total":total, "results":results, "has_more":has_more}) @@ -5054,7 +5066,7 @@ class OfficeGenerateView(APIView): return HttpResponse(json.dumps(ret_dict), status=200, content_type=json_content_type) class ThumbnailView(APIView): - authentication_classes = (TokenAuthentication,) + authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated,) throttle_classes = (UserRateThrottle, ) diff --git a/seahub/base/accounts.py b/seahub/base/accounts.py index b94dcf9012..3c2c464e39 100644 --- a/seahub/base/accounts.py +++ b/seahub/base/accounts.py @@ -10,7 +10,6 @@ from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django.contrib.sites.shortcuts import get_current_site -import seaserv from seaserv import ccnet_threaded_rpc, unset_repo_passwd, \ seafile_api, ccnet_api from constance import config @@ -24,7 +23,7 @@ from seahub.role_permissions.utils import get_enabled_role_permissions_by_role, get_enabled_admin_role_permissions_by_role from seahub.utils import is_user_password_strong, get_site_name, \ clear_token, get_system_admins, is_pro_version, IS_EMAIL_CONFIGURED -from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY +from seahub.utils.mail import send_html_email_with_dj_template from seahub.utils.licenseparse import user_number_over_limit from seahub.share.models import ExtraSharePermission @@ -41,7 +40,8 @@ logger = logging.getLogger(__name__) ANONYMOUS_EMAIL = 'Anonymous' -UNUSABLE_PASSWORD = '!' # This will never be a valid hash +UNUSABLE_PASSWORD = '!' # This will never be a valid hash + class UserManager(object): @@ -122,6 +122,7 @@ class UserManager(object): return user + class UserPermissions(object): def __init__(self, user): self.user = user @@ -222,6 +223,7 @@ class UserPermissions(object): return self._get_perm_by_roles('can_publish_repo') + class AdminPermissions(object): def __init__(self, user): self.user = user @@ -322,8 +324,8 @@ class User(object): return True def save(self): - emailuser = ccnet_threaded_rpc.get_emailuser(self.username) - if emailuser: + emailuser = ccnet_api.get_emailuser(self.username) + if emailuser and emailuser.source.lower() in ("db", "ldapimport"): if not hasattr(self, 'password'): self.set_unusable_password() @@ -385,12 +387,10 @@ class User(object): if orgs: for org in orgs: org_id = org.org_id - shared_in_repos = seafile_api.get_org_share_in_repo_list(org_id, - username, -1, -1) + shared_in_repos = seafile_api.get_org_share_in_repo_list(org_id, username, -1, -1) for r in shared_in_repos: - seafile_api.org_remove_share(org_id, - r.repo_id, r.user, username) + seafile_api.org_remove_share(org_id, r.repo_id, r.user, username) else: shared_in_repos = seafile_api.get_share_in_repo_list(username, -1, -1) for r in shared_in_repos: @@ -478,15 +478,12 @@ class User(object): user_language = Profile.objects.get_user_language(u.email) translation.activate(user_language) - send_html_email_with_dj_template( - u.email, dj_template='sysadmin/user_freeze_email.html', - subject=_('Account %(account)s froze on %(site)s.') % { - "account": self.email, - "site": get_site_name(), - }, - context={'user': self.email}, - priority=MAIL_PRIORITY.now - ) + send_html_email_with_dj_template(u.email, + subject=_('Account %(account)s froze on %(site)s.') % { + "account": self.email, + "site": get_site_name()}, + dj_template='sysadmin/user_freeze_email.html', + context={'user': self.email}) # restore current language translation.activate(cur_language) @@ -535,10 +532,11 @@ class User(object): for r in passwd_setted_repos: unset_repo_passwd(r.id, self.email) + class AuthBackend(object): def get_user_with_import(self, username): - emailuser = seaserv.get_emailuser_with_import(username) + emailuser = ccnet_api.get_emailuser_with_import(username) if not emailuser: raise User.DoesNotExist('User matching query does not exits.') @@ -580,7 +578,8 @@ class AuthBackend(object): if user.check_password(password): return user -########## Register related + +# Register related class RegistrationBackend(object): """ A registration backend which follows a simple workflow: @@ -653,10 +652,10 @@ class RegistrationBackend(object): # since user will be activated after registration, # so we will not use email sending, just create acitvated user new_user = RegistrationProfile.objects.create_active_user(username, email, - password, site, - send_email=False) + password, site, + send_email=False) # login the user - new_user.backend=settings.AUTHENTICATION_BACKENDS[0] + new_user.backend = settings.AUTHENTICATION_BACKENDS[0] login(request, new_user) else: @@ -702,7 +701,7 @@ class RegistrationBackend(object): user=activated, request=request) # login the user - activated.backend=settings.AUTHENTICATION_BACKENDS[0] + activated.backend = settings.AUTHENTICATION_BACKENDS[0] login(request, activated) return activated @@ -753,17 +752,17 @@ class RegistrationForm(forms.Form): Validates that the requested email is not already in use, and requires the password to be entered twice to catch typos. """ - attrs_dict = { 'class': 'input' } + attrs_dict = {'class': 'input'} + + email = forms.CharField(widget=forms.TextInput(attrs=dict(attrs_dict, maxlength=75)), + label=_("Email address")) - email = forms.CharField(widget=forms.TextInput(attrs=dict(attrs_dict, - maxlength=75)), - label=_("Email address")) userid = forms.RegexField(regex=r'^\w+$', max_length=40, required=False, widget=forms.TextInput(), label=_("Username"), - error_messages={ 'invalid': _("This value must be of length 40") }) + error_messages={'invalid': _("This value must be of length 40")}) password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), label=_("Password")) @@ -825,12 +824,13 @@ class RegistrationForm(forms.Form): raise forms.ValidationError(_("The two password fields didn't match.")) return self.cleaned_data + class DetailedRegistrationForm(RegistrationForm): - attrs_dict = { 'class': 'input' } + attrs_dict = {'class': 'input'} try: from seahub.settings import REGISTRATION_DETAILS_MAP - except: + except ImportError: REGISTRATION_DETAILS_MAP = None if REGISTRATION_DETAILS_MAP: diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index c79d589bb5..6b7ca9b252 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -133,6 +133,7 @@ def base(request): 'constance_enabled': dj_settings.CONSTANCE_ENABLED, 'FILE_SERVER_ROOT': file_server_root, 'LOGIN_URL': dj_settings.LOGIN_URL, + 'LOGOUT_URL': dj_settings.LOGOUT_URL, 'enable_thumbnail': ENABLE_THUMBNAIL, 'thumbnail_size_for_original': THUMBNAIL_SIZE_FOR_ORIGINAL, 'enable_guest_invitation': ENABLE_GUEST_INVITATION, diff --git a/seahub/invitations/models.py b/seahub/invitations/models.py index a04e5b380f..2db61cfb99 100644 --- a/seahub/invitations/models.py +++ b/seahub/invitations/models.py @@ -2,7 +2,6 @@ from datetime import timedelta from django.db import models -from django.template.loader import render_to_string from django.utils import timezone from django.utils.translation import ugettext as _ @@ -10,11 +9,12 @@ from seahub.base.fields import LowerCaseCharField from seahub.invitations.settings import INVITATIONS_TOKEN_AGE from seahub.utils import gen_token, get_site_name from seahub.utils.timeutils import datetime_to_isoformat_timestr -from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY +from seahub.utils.mail import send_html_email_with_dj_template from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE GUEST = 'Guest' + class InvitationManager(models.Manager): def add(self, inviter, accepter, invite_type=GUEST): token = gen_token(max_length=32) @@ -26,8 +26,7 @@ class InvitationManager(models.Manager): return i def get_by_inviter(self, inviter): - return super(InvitationManager, - self).filter(inviter=inviter).order_by('-invite_time') + return super(InvitationManager, self).filter(inviter=inviter).order_by('-invite_time') def delete_all_expire_invitation(self): super(InvitationManager, self).filter(expire_time__lte=timezone.now(), accept_time__isnull=True).delete() @@ -93,16 +92,13 @@ class Invitation(models.Model): context = self.to_dict() context['site_name'] = get_site_name() - # subject = render_to_string('invitations/invitation_email_subject.txt', - # context).rstrip() subject = _('%(user)s invited you to join %(site_name)s.') % { 'user': self.inviter, 'site_name': get_site_name()} - return send_html_email_with_dj_template( - email, dj_template='invitations/invitation_email.html', - context=context, - subject=subject, - priority=MAIL_PRIORITY.now - ) + + return send_html_email_with_dj_template(email, + subject=subject, + dj_template='invitations/invitation_email.html', + context=context) class RepoShareInvitationManager(models.Manager): @@ -115,7 +111,7 @@ class RepoShareInvitationManager(models.Manager): ) obj.save() return obj - + def list_by_repo_id_and_path(self, repo_id, path): return self.select_related('invitation').filter( invitation__expire_time__gte=timezone.now(), @@ -132,11 +128,13 @@ class RepoShareInvitationManager(models.Manager): return qs[0] else: return None - + def list_by_invitation(self, invitation): return self.select_related('invitation').filter(invitation=invitation) + class RepoShareInvitation(models.Model): + PERMISSION_CHOICES = ( (PERMISSION_READ, 'read only'), (PERMISSION_READ_WRITE, 'read and write') diff --git a/seahub/notifications/management/commands/notify_admins_on_virus.py b/seahub/notifications/management/commands/notify_admins_on_virus.py index c32867edf3..545a71c3bf 100644 --- a/seahub/notifications/management/commands/notify_admins_on_virus.py +++ b/seahub/notifications/management/commands/notify_admins_on_virus.py @@ -12,13 +12,15 @@ import seaserv from seaserv import seafile_api from seahub.profile.models import Profile -from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY +from seahub.utils.mail import send_html_email_with_dj_template from seahub.utils import get_site_name # Get an instance of a logger logger = logging.getLogger(__name__) + class Command(BaseCommand): + help = 'Send Email notifications to admins if there are virus files detected .' label = "notifications_notify_admins_on_virus" @@ -52,11 +54,9 @@ class Command(BaseCommand): user_language = self.get_user_language(u.email) translation.activate(user_language) - send_html_email_with_dj_template( - u.email, dj_template='notifications/notify_virus.html', - subject=_('Virus detected on %s') % get_site_name(), - priority=MAIL_PRIORITY.now - ) + send_html_email_with_dj_template(u.email, + subject=_('Virus detected on %s') % get_site_name(), + dj_template='notifications/notify_virus.html') # restore current language translation.activate(cur_language) @@ -68,11 +68,9 @@ class Command(BaseCommand): return for mail in notify_list: - send_html_email_with_dj_template( - mail, dj_template='notifications/notify_virus.html', - subject=_('Virus detected on %s') % get_site_name(), - priority=MAIL_PRIORITY.now - ) + send_html_email_with_dj_template(mail, + subject=_('Virus detected on %s') % get_site_name(), + dj_template='notifications/notify_virus.html') def email_repo_owner(self, repo_file): repo_id, file_path = repo_file.split(':', 1) @@ -88,16 +86,12 @@ class Command(BaseCommand): translation.activate(user_language) contact_email = Profile.objects.get_contact_email_by_user(owner) - send_html_email_with_dj_template( - contact_email, dj_template='notifications/notify_virus.html', - context={'owner': owner, - 'file_url': reverse('view_lib_file', - args=[repo_id, file_path]), - 'file_name': os.path.basename(file_path), - }, - subject=_('Virus detected on %s') % get_site_name(), - priority=MAIL_PRIORITY.now - ) + send_html_email_with_dj_template(contact_email, + subject=_('Virus detected on %s') % get_site_name(), + dj_template='notifications/notify_virus.html', + context={'owner': owner, + 'file_url': reverse('view_lib_file', args=[repo_id, file_path]), + 'file_name': os.path.basename(file_path)}) # restore current language translation.activate(cur_language) diff --git a/seahub/onlyoffice/settings.py b/seahub/onlyoffice/settings.py index 645b4adfe6..23d401be47 100644 --- a/seahub/onlyoffice/settings.py +++ b/seahub/onlyoffice/settings.py @@ -11,3 +11,5 @@ ONLYOFFICE_JWT_SECRET = getattr(settings, 'ONLYOFFICE_JWT_SECRET', '') # if True, file will be saved when user click save btn on file editing page ONLYOFFICE_FORCE_SAVE = getattr(settings, 'ONLYOFFICE_FORCE_SAVE', False) + +ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN = getattr(settings, 'ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN', False) diff --git a/seahub/settings.py b/seahub/settings.py index 1db61efdbb..f259595aef 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -218,7 +218,6 @@ INSTALLED_APPS = [ 'statici18n', 'constance', 'constance.backends.database', - 'post_office', 'termsandconditions', 'webpack_loader', diff --git a/seahub/share/views.py b/seahub/share/views.py index 87e8ea4214..61ec42fd14 100644 --- a/seahub/share/views.py +++ b/seahub/share/views.py @@ -3,34 +3,30 @@ import os import logging import json -from dateutil.relativedelta import relativedelta -from constance import config from django.core.cache import cache from django.http import HttpResponse, HttpResponseRedirect, Http404, \ HttpResponseBadRequest from django.utils.translation import ugettext as _, activate from django.contrib import messages -from django.utils import timezone from django.utils.html import escape + import seaserv from seaserv import seafile_api -from seaserv import ccnet_threaded_rpc from pysearpc import SearpcError from seahub.share.forms import FileLinkShareForm, \ UploadLinkShareForm -from seahub.share.models import FileShare, UploadLinkShare, OrgFileShare +from seahub.share.models import FileShare, UploadLinkShare from seahub.share.signals import share_repo_to_user_successful from seahub.auth.decorators import login_required, login_required_ajax from seahub.base.decorators import require_POST from seahub.contacts.signals import mail_sended from seahub.views import is_registered_user, check_folder_permission -from seahub.utils import string2list, gen_shared_link, \ - gen_shared_upload_link, IS_EMAIL_CONFIGURED, check_filename_with_rename, \ +from seahub.utils import string2list, IS_EMAIL_CONFIGURED, check_filename_with_rename, \ is_valid_username, is_valid_email, send_html_email, is_org_context, \ gen_token, normalize_cache_key, get_site_name -from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY +from seahub.utils.mail import send_html_email_with_dj_template from seahub.settings import SITE_ROOT, REPLACE_FROM_EMAIL, \ ADD_REPLY_TO_HEADER, SHARE_LINK_EMAIL_LANGUAGE, \ SHARE_LINK_AUDIT_CODE_TIMEOUT @@ -39,21 +35,25 @@ from seahub.profile.models import Profile # Get an instance of a logger logger = logging.getLogger(__name__) -########## rpc wrapper + +# rpc wrapper def is_org_repo_owner(username, repo_id): owner = seaserv.seafserv_threaded_rpc.get_org_repo_owner(repo_id) return True if owner == username else False + def org_share_repo(org_id, repo_id, from_user, to_user, permission): return seaserv.seafserv_threaded_rpc.org_add_share(org_id, repo_id, from_user, to_user, permission) + def org_remove_share(org_id, repo_id, from_user, to_user): return seaserv.seafserv_threaded_rpc.org_remove_share(org_id, repo_id, from_user, to_user) -########## functions + +# functions def share_to_group(request, repo, group, permission): """Share repo to group with given permission. """ @@ -83,6 +83,7 @@ def share_to_group(request, repo, group, permission): logger.error(e) return False + def share_to_user(request, repo, to_user, permission): """Share repo to a user with given permission. """ @@ -109,8 +110,8 @@ def share_to_user(request, repo, to_user, permission): else: seafile_api.share_repo(repo_id, from_user, to_user, permission) except SearpcError as e: - return False - logger.error(e) + logger.error(e) + return False else: # send a signal when sharing repo successful share_repo_to_user_successful.send(sender=None, @@ -119,7 +120,8 @@ def share_to_user(request, repo, to_user, permission): path='/', org_id=org_id) return True -########## share link + +# share link @login_required_ajax def send_shared_link(request): """ @@ -131,7 +133,8 @@ def send_shared_link(request): content_type = 'application/json; charset=utf-8' if not IS_EMAIL_CONFIGURED: - data = json.dumps({'error':_('Sending shared link failed. Email service is not properly configured, please contact administrator.')}) + data = json.dumps({'error': _('Sending shared link failed. \ + Email service is not properly configured, please contact administrator.')}) return HttpResponse(data, status=500, content_type=content_type) form = FileLinkShareForm(request.POST) @@ -207,6 +210,7 @@ def send_shared_link(request): return HttpResponseBadRequest(json.dumps(form.errors), content_type=content_type) + @login_required def save_shared_link(request): """Save public share link to one's library. @@ -246,6 +250,7 @@ def save_shared_link(request): messages.success(request, _('Successfully saved.')) return HttpResponseRedirect(next_page) + @login_required_ajax def send_shared_upload_link(request): """ @@ -257,7 +262,8 @@ def send_shared_upload_link(request): content_type = 'application/json; charset=utf-8' if not IS_EMAIL_CONFIGURED: - data = json.dumps({'error':_('Sending shared upload link failed. Email service is not properly configured, please contact administrator.')}) + data = json.dumps({'error': _('Sending shared upload link failed. \ + Email service is not properly configured, please contact administrator.')}) return HttpResponse(data, status=500, content_type=content_type) form = UploadLinkShareForm(request.POST) @@ -317,6 +323,7 @@ def send_shared_upload_link(request): return HttpResponseBadRequest(json.dumps(form.errors), content_type=content_type) + @login_required_ajax @require_POST def ajax_private_share_dir(request): @@ -359,8 +366,7 @@ def ajax_private_share_dir(request): sub_repo_id = seaserv.seafserv_threaded_rpc.create_org_virtual_repo( org_id, repo_id, path, name, name, username) else: - sub_repo_id = seafile_api.create_virtual_repo(repo_id, path, - name, name, username) + sub_repo_id = seafile_api.create_virtual_repo(repo_id, path, name, name, username) sub_repo = seafile_api.get_repo(sub_repo_id) except SearpcError as e: result['error'] = e.msg @@ -423,6 +429,7 @@ def ajax_private_share_dir(request): data = json.dumps({"error": _("Please check the email(s) you entered")}) return HttpResponse(data, status=400, content_type=content_type) + def ajax_get_link_audit_code(request): """ Generate a token, and record that token with email in cache, expires in @@ -455,19 +462,18 @@ def ajax_get_link_audit_code(request): # send code to user via email subject = _("Verification code for visiting share links") - c = { - 'code': code, - } - try: - send_html_email_with_dj_template( - email, dj_template='share/audit_code_email.html', - context=c, subject=subject, priority=MAIL_PRIORITY.now - ) - return HttpResponse(json.dumps({'success': True}), status=200, - content_type=content_type) - except Exception as e: + c = {'code': code} + + send_success = send_html_email_with_dj_template(email, + subject=subject, + dj_template='share/audit_code_email.html', + context=c) + + if not send_success: logger.error('Failed to send audit code via email to %s') - logger.error(e) return HttpResponse(json.dumps({ "error": _("Failed to send a verification code, please try again later.") }), status=500, content_type=content_type) + + return HttpResponse(json.dumps({'success': True}), status=200, + content_type=content_type) diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 08590d10a2..23871dcdfa 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -40,6 +40,7 @@ siteName: '{{ site_name|escapejs }}', siteRoot: '{{ SITE_ROOT }}', loginUrl: '{{ LOGIN_URL }}', + logoutUrl: '{{ LOGOUT_URL }}', isPro: '{{ is_pro }}', isDocs: '{{ is_docs }}', lang: '{{ LANGUAGE_CODE }}', diff --git a/seahub/templates/react_app.html b/seahub/templates/react_app.html index 6ed66e672b..8f5f6168af 100644 --- a/seahub/templates/react_app.html +++ b/seahub/templates/react_app.html @@ -1,5 +1,6 @@ {% extends "base_for_react.html" %} {% load render_bundle from webpack_loader %} +{% load seahub_tags %} {% block extra_style %} {% render_bundle 'app' 'css' %} @@ -16,6 +17,15 @@ uploadLinkExpireDaysMin: {{ upload_link_expire_days_min }}, uploadLinkExpireDaysMax: {{ upload_link_expire_days_max }} }); + {% if onlyoffice_desktop_editors_portal_login %} + let params = { + "displayName": "{{request.user.username|email2nickname|escapejs}}", + "email": "{{request.user.username|escapejs}}", + "domain": "{{service_url}}", + "provider": "{{site_name}}" + } + window.AscDesktopEditor.execCommand('portal:login', JSON.stringify(params)); + {% endif %} {% render_bundle 'app' 'js' %} {% endblock %} diff --git a/seahub/utils/mail.py b/seahub/utils/mail.py index a70ad68048..245880e730 100644 --- a/seahub/utils/mail.py +++ b/seahub/utils/mail.py @@ -1,27 +1,23 @@ # Copyright (c) 2012-2016 Seafile Ltd. import os -from django.template import Context, loader -from post_office import mail -from post_office.models import PRIORITY -from constance import config +import logging +from django.template import loader +from django.core.mail import EmailMessage from seahub.utils import get_site_scheme_and_netloc, get_site_name from seahub.settings import MEDIA_URL, LOGO_PATH, \ MEDIA_ROOT, CUSTOM_LOGO_PATH -MAIL_PRIORITY = PRIORITY # 'low medium high now' +logger = logging.getLogger(__name__) -def send_html_email_with_dj_template(recipients, subject, dj_template, - context={}, sender=None, template=None, - message='', headers=None, - priority=None, backend=''): + +def send_html_email_with_dj_template(recipients, subject, dj_template, context={}): """ Arguments: - `recipients`: - `subject`: - - `sender`: - - `template`: + - `dj_template`: - `context`: """ @@ -42,7 +38,12 @@ def send_html_email_with_dj_template(recipients, subject, dj_template, t = loader.get_template(dj_template) html_message = t.render(context) - return mail.send(recipients, sender=sender, template=template, context=context, - subject=subject, message=message, - html_message=html_message, headers=headers, priority=priority, - backend=backend) + mail = EmailMessage(subject=subject, body=html_message, to=[recipients]) + mail.content_subtype = "html" + + try: + mail.send() + return True + except Exception as e: + logger.error(e) + return False diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index dab3b2804a..06d9e30f84 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -60,7 +60,7 @@ from seahub.settings import AVATAR_FILE_STORAGE, \ DTABLE_WEB_SERVER from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP -from seahub.onlyoffice.settings import ENABLE_ONLYOFFICE +from seahub.onlyoffice.settings import ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN from seahub.ocm.settings import ENABLE_OCM, OCM_REMOTE_SERVERS from seahub.constants import HASH_URLS, PERMISSION_READ @@ -1180,6 +1180,7 @@ def react_fake_view(request, **kwargs): max_upload_file_size = -1 return render(request, "react_app.html", { + "onlyoffice_desktop_editors_portal_login": ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN, "guide_enabled": guide_enabled, 'trash_repos_expire_days': expire_days if expire_days > 0 else 30, 'dtable_web_server': DTABLE_WEB_SERVER, diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index cf58430e2e..9c3876f384 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -57,7 +57,6 @@ from seahub.utils.ldap import get_ldap_info from seahub.utils.licenseparse import parse_license, user_number_over_limit from seahub.utils.rpc import mute_seafile_api from seahub.utils.sysinfo import get_platform_name -from seahub.utils.mail import send_html_email_with_dj_template from seahub.utils.ms_excel import write_xls from seahub.utils.user_permissions import get_basic_user_roles, \ get_user_role, get_basic_admin_roles