1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-13 12:45:46 +00:00

Merge branch '7.1' into master

This commit is contained in:
lian 2021-02-05 11:55:34 +08:00
commit c29a571826
39 changed files with 488 additions and 277 deletions

View File

@ -34,7 +34,7 @@
"react-responsive": "^6.1.2", "react-responsive": "^6.1.2",
"react-select": "^2.4.1", "react-select": "^2.4.1",
"reactstrap": "^6.4.0", "reactstrap": "^6.4.0",
"seafile-js": "0.2.163", "seafile-js": "0.2.164",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"unified": "^7.0.0", "unified": "^7.0.0",
"url-parse": "^1.4.3", "url-parse": "^1.4.3",

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api'; 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'; import toaster from '../toast';
const propTypes = { const propTypes = {
@ -164,7 +164,7 @@ class Account extends Component {
</div> </div>
<a href={siteRoot + 'profile/'} className="item">{gettext('Settings')}</a> <a href={siteRoot + 'profile/'} className="item">{gettext('Settings')}</a>
{this.renderMenu()} {this.renderMenu()}
<a href={siteRoot + 'accounts/logout/'} className="item">{gettext('Log out')}</a> <a href={logoutUrl} className="item">{gettext('Log out')}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { siteRoot, gettext } from '../../utils/constants'; import { gettext, logoutUrl } from '../../utils/constants';
export default function Logout() { export default function Logout() {
return ( return (
<a className="logout-icon" href={`${siteRoot}accounts/logout/`} title={gettext('Log out')}> <a className="logout-icon" href={logoutUrl} title={gettext('Log out')}>
<i className="sf3-font sf3-font-logout" style={{fontSize: '24px'}}></i> <i className="sf3-font sf3-font-logout" style={{fontSize: '24px'}}></i>
</a> </a>
); );

View File

@ -43,7 +43,7 @@ class AboutDialog extends React.Component {
<button type="button" className="close" onClick={this.toggle}><span aria-hidden="true">×</span></button> <button type="button" className="close" onClick={this.toggle}><span aria-hidden="true">×</span></button>
<div className="about-content"> <div className="about-content">
<p><img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" /></p> <p><img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" /></p>
<p>{gettext('Server Version: ')}{seafileVersion}<br />© 2020 {gettext('Seafile')}</p> <p>{gettext('Server Version: ')}{seafileVersion}<br />© {(new Date()).getFullYear()} {gettext('Seafile')}</p>
<p>{this.renderExternalAboutLinks()}</p> <p>{this.renderExternalAboutLinks()}</p>
<p><a href={href} target="_blank">{gettext('About Us')}</a></p> <p><a href={href} target="_blank">{gettext('About Us')}</a></p>
</div> </div>

View File

@ -1,12 +1,14 @@
import React from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Table } from 'reactstrap'; 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 { 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 toaster from '../toast';
import Loading from '../loading';
import '../../css/manage-members-dialog.css'; import '../../css/manage-members-dialog.css';
const propTypes = { const propTypes = {
@ -21,13 +23,45 @@ class ManageMembersDialog extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isLoading: true, // first loading
isLoadingMore: false,
groupMembers: [], groupMembers: [],
page: 1,
perPage: 100,
hasNextPage: false,
selectedOption: null, selectedOption: null,
errMessage: [], 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) => { onSelectChange = (option) => {
this.setState({ this.setState({
selectedOption: option, selectedOption: option,
@ -41,8 +75,9 @@ class ManageMembersDialog extends React.Component {
emails.push(this.state.selectedOption[i].email); emails.push(this.state.selectedOption[i].email);
} }
seafileAPI.addGroupMembers(this.props.groupID, emails).then((res) => { seafileAPI.addGroupMembers(this.props.groupID, emails).then((res) => {
this.onGroupMembersChange(); const newMembers = res.data.success;
this.setState({ this.setState({
groupMembers: [].concat(newMembers, this.state.groupMembers),
selectedOption: null, selectedOption: null,
}); });
this.refs.userSelect.clearSelect(); 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) => { toggleItemFreezed = (isFreezed) => {
this.setState({ this.setState({
isItemFreezed: isFreezed isItemFreezed: isFreezed
@ -82,11 +102,43 @@ class ManageMembersDialog extends React.Component {
this.props.toggleManageMembersDialog(); this.props.toggleManageMembersDialog();
} }
componentDidMount() { handleScroll = (event) => {
this.listGroupMembers(); // 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() { render() {
const { isLoading, hasNextPage } = this.state;
return ( return (
<Modal isOpen={true} toggle={this.toggle}> <Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Manage group members')}</ModalHeader> <ModalHeader toggle={this.toggle}>{gettext('Manage group members')}</ModalHeader>
@ -113,7 +165,9 @@ class ManageMembersDialog extends React.Component {
); );
}) })
} }
<div className="manage-members"> <div className="manage-members" onScroll={this.handleScroll}>
{isLoading ? <Loading /> : (
<Fragment>
<Table size="sm" className="manage-members-table"> <Table size="sm" className="manage-members-table">
<thead> <thead>
<tr> <tr>
@ -126,23 +180,26 @@ class ManageMembersDialog extends React.Component {
<tbody> <tbody>
{ {
this.state.groupMembers.length > 0 && this.state.groupMembers.length > 0 &&
this.state.groupMembers.map((item, index = 0) => { this.state.groupMembers.map((item, index) => {
return ( return (
<React.Fragment key={index}>
<Member <Member
key={index}
item={item} item={item}
onGroupMembersChange={this.onGroupMembersChange} changeMember={this.changeMember}
deleteMember={this.deleteMember}
groupID={this.props.groupID} groupID={this.props.groupID}
isOwner={this.props.isOwner} isOwner={this.props.isOwner}
isItemFreezed={this.state.isItemFreezed} isItemFreezed={this.state.isItemFreezed}
toggleItemFreezed={this.toggleItemFreezed} toggleItemFreezed={this.toggleItemFreezed}
/> />
</React.Fragment>
); );
}) })
} }
</tbody> </tbody>
</Table> </Table>
{hasNextPage && <Loading />}
</Fragment>
)}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
@ -157,7 +214,8 @@ ManageMembersDialog.propTypes = propTypes;
const MemberPropTypes = { const MemberPropTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
onGroupMembersChange: PropTypes.func.isRequired, changeMember: PropTypes.func.isRequired,
deleteMember: PropTypes.func.isRequired,
groupID: PropTypes.string.isRequired, groupID: PropTypes.string.isRequired,
isOwner: PropTypes.bool.isRequired, isOwner: PropTypes.bool.isRequired,
}; };
@ -175,16 +233,18 @@ class Member extends React.PureComponent {
onChangeUserRole = (role) => { onChangeUserRole = (role) => {
let isAdmin = role === 'Admin' ? 'True' : 'False'; let isAdmin = role === 'Admin' ? 'True' : 'False';
seafileAPI.setGroupAdmin(this.props.groupID, this.props.item.email, isAdmin).then((res) => { seafileAPI.setGroupAdmin(this.props.groupID, this.props.item.email, isAdmin).then((res) => {
this.props.onGroupMembersChange(); this.props.changeMember(res.data);
}); }).catch(error => {
this.setState({ let errMessage = Utils.getErrorMsg(error);
highlight: false, toaster.danger(errMessage);
}); });
} }
deleteMember = (name) => { deleteMember = () => {
seafileAPI.deleteGroupMember(this.props.groupID, name).then((res) => { const { item } = this.props;
this.props.onGroupMembersChange(); 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 => { }).catch(error => {
let errMessage = Utils.getErrorMsg(error); let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage); toaster.danger(errMessage);
@ -243,8 +303,7 @@ class Member extends React.PureComponent {
{(deleteAuthority && !this.props.isItemFreezed) && {(deleteAuthority && !this.props.isItemFreezed) &&
<i <i
className="fa fa-times delete-group-member-icon" className="fa fa-times delete-group-member-icon"
name={item.email} onClick={this.deleteMember}>
onClick={this.deleteMember.bind(this, item.email)}>
</i> </i>
} }
</td> </td>

View File

@ -171,8 +171,10 @@ class DirentListItem extends React.Component {
onItemClick = (e) => { onItemClick = (e) => {
e.preventDefault(); e.preventDefault();
const dirent = this.props.dirent; const dirent = this.props.dirent;
if (this.state.isRenameing) {
return;
}
if (Utils.imageCheck(dirent.name)) { if (Utils.imageCheck(dirent.name)) {
this.props.showImagePopup(dirent); this.props.showImagePopup(dirent);
} else { } else {
@ -664,19 +666,19 @@ class DirentListItem extends React.Component {
); );
const mobileItem = ( const mobileItem = (
<tr> <tr>
<td> <td onClick={this.onItemClick}>
<div className="dir-icon"> <div className="dir-icon">
{dirent.encoded_thumbnail_src ? {dirent.encoded_thumbnail_src ?
<img src={`${siteRoot}${dirent.encoded_thumbnail_src}`} className="thumbnail cursor-pointer" onClick={this.onItemClick} alt="" /> : <img src={`${siteRoot}${dirent.encoded_thumbnail_src}`} className="thumbnail cursor-pointer" alt="" /> :
<img src={iconUrl} width="24" alt="" /> <img src={iconUrl} width="24" alt="" />
} }
{dirent.is_locked && <img className="locked" src={mediaUrl + 'img/file-locked-32.png'} alt={gettext('locked')} title={lockedInfo}/>} {dirent.is_locked && <img className="locked" src={mediaUrl + 'img/file-locked-32.png'} alt={gettext('locked')} title={lockedInfo}/>}
</div> </div>
</td> </td>
<td> <td onClick={this.onItemClick}>
{this.state.isRenameing ? {this.state.isRenameing ?
<Rename hasSuffix={dirent.type !== 'dir'} name={dirent.name} onRenameConfirm={this.onRenameConfirm} onRenameCancel={this.onRenameCancel} /> : <Rename hasSuffix={dirent.type !== 'dir'} name={dirent.name} onRenameConfirm={this.onRenameConfirm} onRenameCancel={this.onRenameCancel} /> :
<a href={dirent.type === 'dir' ? dirHref : fileHref} onClick={this.onItemClick}>{dirent.name}</a> <a href={dirent.type === 'dir' ? dirHref : fileHref}>{dirent.name}</a>
} }
<br /> <br />
{dirent.size && <span className="item-meta-info">{dirent.size}</span>} {dirent.size && <span className="item-meta-info">{dirent.size}</span>}

View File

@ -19,6 +19,11 @@ class SearchResultItem extends React.Component {
let className = item.link_content ? 'item-img' : 'lib-item-img'; let className = item.link_content ? 'item-img' : 'lib-item-img';
let folderIconUrl = item.link_content ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(true); let folderIconUrl = item.link_content ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(true);
let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name, 192); let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name, 192);
if (item.thumbnail_url !== '') {
fileIconUrl = item.thumbnail_url;
}
return ( return (
<li className="search-result-item" onClick={this.onClickHandler}> <li className="search-result-item" onClick={this.onClickHandler}>
<img className={className} src={fileIconUrl} alt="" /> <img className={className} src={fileIconUrl} alt="" />

View File

@ -178,6 +178,7 @@ class Search extends Component {
items[i]['is_dir'] = data[i].is_dir; items[i]['is_dir'] = data[i].is_dir;
items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1); items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1);
items[i]['content'] = data[i].content_highlight; items[i]['content'] = data[i].content_highlight;
items[i]['thumbnail_url'] = data[i].thumbnail_url;
} }
return items; return items;
} }

View File

@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
import { Link } from '@reach/router'; import { Link, navigate } from '@reach/router';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import { gettext, siteRoot, isPro, username, folderPermEnabled, isSystemStaff, enableResetEncryptedRepoPassword, isEmailConfigured } from '../../utils/constants'; import { gettext, siteRoot, isPro, username, folderPermEnabled, isSystemStaff, enableResetEncryptedRepoPassword, isEmailConfigured } from '../../utils/constants';
import ModalPortal from '../../components/modal-portal'; import ModalPortal from '../../components/modal-portal';
@ -479,14 +479,21 @@ class SharedRepoListItem extends React.Component {
); );
} }
visitRepo = () => {
if (!this.state.isRenaming) {
navigate(this.repoURL);
}
}
renderMobileUI = () => { renderMobileUI = () => {
let { iconUrl, iconTitle, libPath } = this.getRepoComputeParams(); let { iconUrl, iconTitle, libPath } = this.getRepoComputeParams();
let { repo } = this.props; let { repo } = this.props;
this.repoURL = libPath;
return ( return (
<Fragment> <Fragment>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}> <tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}>
<td><img src={iconUrl} title={iconTitle} width="24" alt={iconTitle}/></td> <td onClick={this.visitRepo}><img src={iconUrl} title={iconTitle} width="24" alt={iconTitle}/></td>
<td> <td onClick={this.visitRepo}>
{this.state.isRenaming ? {this.state.isRenaming ?
<Rename name={repo.repo_name} onRenameConfirm={this.onRenameConfirm} onRenameCancel={this.onRenameCancel} /> : <Rename name={repo.repo_name} onRenameConfirm={this.onRenameConfirm} onRenameCancel={this.onRenameCancel} /> :
<Link to={libPath}>{repo.repo_name}</Link> <Link to={libPath}>{repo.repo_name}</Link>

View File

@ -6,6 +6,7 @@ class SysAdminAdminUser {
this.contact_email = object.contact_email; this.contact_email = object.contact_email;
this.login_id = object.login_id; this.login_id = object.login_id;
this.last_login = object.last_login; this.last_login = object.last_login;
this.last_access_time = object.last_access_time;
this.create_time = object.create_time; this.create_time = object.create_time;
this.is_active = object.is_active; this.is_active = object.is_active;
this.is_staff = object.is_staff; this.is_staff = object.is_staff;

View File

@ -5,6 +5,7 @@ class SysAdminUser {
this.contact_email = object.contact_email; this.contact_email = object.contact_email;
this.login_id = object.login_id; this.login_id = object.login_id;
this.last_login = object.last_login; this.last_login = object.last_login;
this.last_access_time = object.last_access_time;
this.create_time = object.create_time; this.create_time = object.create_time;
this.is_active = object.is_active; this.is_active = object.is_active;
this.is_staff = object.is_staff; this.is_staff = object.is_staff;

View File

@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive'; import MediaQuery from 'react-responsive';
import moment from 'moment'; import moment from 'moment';
import { Link } from '@reach/router'; import { Link, navigate } from '@reach/router';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { gettext, siteRoot, storages } from '../../utils/constants'; 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 = () => { onRepoClick = () => {
this.props.onRepoClick(this.props.repo); this.props.onRepoClick(this.props.repo);
} }
@ -317,12 +323,12 @@ class MylibRepoListItem extends React.Component {
let repo = this.props.repo; let repo = this.props.repo;
let iconUrl = Utils.getLibIconUrl(repo); let iconUrl = Utils.getLibIconUrl(repo);
let iconTitle = Utils.getLibIconTitle(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 ( return (
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onClick={this.onRepoClick}> <tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onClick={this.onRepoClick}>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td> <td onClick={this.visitRepo}><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td> <td onClick={this.visitRepo}>
{this.state.isRenaming && ( {this.state.isRenaming && (
<Rename <Rename
name={repo.repo_name} name={repo.repo_name}

View File

@ -29,6 +29,11 @@ class ResultsItem extends React.Component {
let linkContent = decodeURI(item.fullpath).substring(1); let linkContent = decodeURI(item.fullpath).substring(1);
let folderIconUrl = linkContent ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(true); let folderIconUrl = linkContent ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl(true);
let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name, 192); let fileIconUrl = item.is_dir ? folderIconUrl : Utils.getFileIconUrl(item.name, 192);
if (item.thumbnail_url !== '') {
fileIconUrl = item.thumbnail_url;
}
return ( return (
<li className="search-result-item"> <li className="search-result-item">
<img className={linkContent ? 'item-img' : 'lib-item-img'} src={fileIconUrl} alt=""/> <img className={linkContent ? 'item-img' : 'lib-item-img'} src={fileIconUrl} alt=""/>

View File

@ -3,7 +3,7 @@ import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import cookie from 'react-cookies'; import cookie from 'react-cookies';
import { Link } from '@reach/router'; import { Link, navigate } from '@reach/router';
import { gettext, siteRoot, isPro } from '../../utils/constants'; import { gettext, siteRoot, isPro } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
@ -194,6 +194,10 @@ class Item extends Component {
} }
} }
visitRepo = () => {
navigate(this.repoURL);
}
render() { render() {
if (this.state.unshared) { if (this.state.unshared) {
return null; return null;
@ -207,7 +211,7 @@ class Item extends Component {
let iconVisibility = this.state.showOpIcon ? '' : ' invisible'; let iconVisibility = this.state.showOpIcon ? '' : ' invisible';
let shareIconClassName = 'op-icon sf2-icon-share repo-share-btn' + iconVisibility; let shareIconClassName = 'op-icon sf2-icon-share repo-share-btn' + iconVisibility;
let leaveShareIconClassName = 'op-icon sf2-icon-x3' + 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 = ( const desktopItem = (
<Fragment> <Fragment>
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}> <tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
@ -248,8 +252,8 @@ class Item extends Component {
const mobileItem = ( const mobileItem = (
<Fragment> <Fragment>
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}> <tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
<td><img src={data.icon_url} title={data.icon_title} alt={data.icon_title} width="24" /></td> <td onClick={this.visitRepo}><img src={data.icon_url} title={data.icon_title} alt={data.icon_title} width="24" /></td>
<td> <td onClick={this.visitRepo}>
<Link to={shareRepoUrl}>{data.repo_name}</Link><br /> <Link to={shareRepoUrl}>{data.repo_name}</Link><br />
<span className="item-meta-info" title={data.owner_contact_email}>{data.owner_name}</span> <span className="item-meta-info" title={data.owner_contact_email}>{data.owner_name}</span>
<span className="item-meta-info">{data.size}</span> <span className="item-meta-info">{data.size}</span>

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap'; import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap';
import { Link } from '@reach/router'; import { Link, navigate } from '@reach/router';
import moment from 'moment'; import moment from 'moment';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils'; 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() { render() {
if (this.state.unstarred) { if (this.state.unstarred) {
@ -206,14 +215,14 @@ class Item extends Component {
const mobileItem = ( const mobileItem = (
<tr> <tr>
<td className="text-center"> <td className="text-center" onClick={this.visitItem}>
{ {
data.thumbnail_url ? data.thumbnail_url ?
<img className="thumbnail" src={data.thumbnail_url} alt="" /> : <img className="thumbnail" src={data.thumbnail_url} alt="" /> :
<img src={data.item_icon_url} alt={gettext('icon')} width="24" /> <img src={data.item_icon_url} alt={gettext('icon')} width="24" />
} }
</td> </td>
<td> <td onClick={this.visitItem}>
{ data.is_dir ? { data.is_dir ?
<Link to={data.dirent_view_url}>{data.obj_name}</Link> : <Link to={data.dirent_view_url}>{data.obj_name}</Link> :
<a className="normal" href={data.dirent_view_url} target="_blank">{data.obj_name}</a> <a className="normal" href={data.dirent_view_url} target="_blank">{data.obj_name}</a>

View File

@ -83,7 +83,7 @@ class Content extends Component {
const colSpaceText = <Fragment>{spaceEl}{` / ${gettext('Quota')}`}</Fragment>; const colSpaceText = <Fragment>{spaceEl}{` / ${gettext('Quota')}`}</Fragment>;
const colNameText = `${gettext('Name')} / ${gettext('Contact Email')}`; 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) { if (isPro) {
columns.push( columns.push(
{width: '20%', text: colNameText}, {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.create_time ? moment(item.create_time).format('YYYY-MM-DD HH:mm') : '--'} /`}
<br /> <br />
{`${item.last_login ? moment(item.last_login).fromNow() : '--'}`} {`${item.last_login ? moment(item.last_login).fromNow() : '--'}`}
<br />
{`${item.last_access_time ? moment(item.last_access_time).fromNow() : '--'}`}
</td> </td>
<td> <td>
{(item.email != username && isOpIconShown) && {(item.email != username && isOpIconShown) &&

View File

@ -3,6 +3,7 @@ export const gettext = window.gettext;
export const siteRoot = window.app.config.siteRoot; export const siteRoot = window.app.config.siteRoot;
export const loginUrl = window.app.config.loginUrl; export const loginUrl = window.app.config.loginUrl;
export const logoutUrl = window.app.config.logoutUrl;
export const avatarInfo = window.app.config.avatarInfo; export const avatarInfo = window.app.config.avatarInfo;
export const logoPath = window.app.config.logoPath; export const logoPath = window.app.config.logoPath;
export const mediaUrl = window.app.config.mediaUrl; export const mediaUrl = window.app.config.mediaUrl;

View File

@ -484,7 +484,7 @@ export const Utils = {
return list; return list;
}, },
getFileOperationList: function(currentRepoInfo, dirent, isContextmenu) { getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) {
let list = []; let list = [];
const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK, const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK,
COMMENT, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT } = TextTranslation; COMMENT, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT } = TextTranslation;
@ -516,7 +516,7 @@ export const Utils = {
if (isPro) { if (isPro) {
if (dirent.is_locked) { 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); list.push(UNLOCK);
} }
} else { } else {
@ -551,7 +551,7 @@ export const Utils = {
getDirentOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { getDirentOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) {
return dirent.type == 'dir' ? return dirent.type == 'dir' ?
Utils.getFolderOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu) : Utils.getFolderOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu) :
Utils.getFileOperationList(currentRepoInfo, dirent, isContextmenu); Utils.getFileOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu);
}, },
sharePerms: function(permission) { sharePerms: function(permission) {

View File

@ -2,7 +2,6 @@ Django==2.2.14
future future
captcha captcha
django-statici18n django-statici18n
django-post_office==3.3.0
django-webpack_loader django-webpack_loader
gunicorn gunicorn
mysqlclient mysqlclient

View File

@ -19,6 +19,7 @@ from seaserv import seafile_api, ccnet_api
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error, to_python_boolean from seahub.api2.utils import api_error, to_python_boolean
from seahub.api2.models import TokenV2
import seahub.settings as settings import seahub.settings as settings
from seahub.settings import SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, INIT_PASSWD, \ 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, \ from seahub.utils import is_valid_username2, is_org_context, \
is_pro_version, normalize_cache_key, is_valid_email, \ is_pro_version, normalize_cache_key, is_valid_email, \
IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \ 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.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.user_permissions import get_user_role
from seahub.utils.repo import normalize_repo_status_code from seahub.utils.repo import normalize_repo_status_code
from seahub.constants import DEFAULT_ADMIN from seahub.constants import DEFAULT_ADMIN
@ -57,6 +61,43 @@ logger = logging.getLogger(__name__)
json_content_type = 'application/json; charset=utf-8' 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): def get_user_upload_link_info(uls):
data = {} data = {}
@ -204,6 +245,7 @@ def update_user_info(request, user, password, is_active, is_staff, role,
logger.error(e) logger.error(e)
seafile_api.set_user_quota(email, -1) seafile_api.set_user_quota(email, -1)
def get_user_info(email): def get_user_info(email):
user = User.objects.get(email=email) user = User.objects.get(email=email)
@ -297,8 +339,15 @@ class AdminAdminUsers(APIView):
user_info['quota_total'] = -1 user_info['quota_total'] = -1
user_info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) user_info['create_time'] = timestamp_to_isoformat_timestr(user.ctime)
last_login_obj = UserLastLogin.objects.get_by_username(user.email) 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: try:
admin_role = AdminRole.objects.get_admin_role(user.email) admin_role = AdminRole.objects.get_admin_role(user.email)
@ -312,6 +361,7 @@ class AdminAdminUsers(APIView):
} }
return Response(result) return Response(result)
class AdminUsers(APIView): class AdminUsers(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
@ -363,7 +413,13 @@ class AdminUsers(APIView):
info['quota_total'] = seafile_api.get_user_quota(user.email) info['quota_total'] = seafile_api.get_user_quota(user.email)
last_login_obj = UserLastLogin.objects.get_by_username(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) info['role'] = get_user_role(user)
@ -423,8 +479,10 @@ class AdminUsers(APIView):
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
try: try:
data = self.get_info_of_users_order_by_quota_usage(source, direction, data = self.get_info_of_users_order_by_quota_usage(source,
page, per_page) direction,
page,
per_page)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
@ -448,8 +506,10 @@ class AdminUsers(APIView):
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
try: try:
data = self.get_info_of_users_order_by_quota_usage(source, direction, data = self.get_info_of_users_order_by_quota_usage(source,
page, per_page) direction,
page,
per_page)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
@ -489,10 +549,19 @@ class AdminUsers(APIView):
info['quota_usage'] = -1 info['quota_usage'] = -1
info['quota_total'] = -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['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): if getattr(settings, 'MULTI_INSTITUTION', False):
info['institution'] = profile.institution if profile else '' info['institution'] = profile.institution if profile else ''
@ -569,8 +638,7 @@ class AdminUsers(APIView):
if is_org_context(request): if is_org_context(request):
org_id = request.user.org.org_id org_id = request.user.org.org_id
org_quota_mb = seafile_api.get_org_quota(org_id) / \ org_quota_mb = seafile_api.get_org_quota(org_id) / get_file_size_unit('MB')
get_file_size_unit('MB')
if quota_total_mb > org_quota_mb: if quota_total_mb > org_quota_mb:
error_msg = 'Failed to set quota: maximum quota is %d 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, c,
None, None,
[email2contact_email(email)]) [email2contact_email(email)])
add_user_tip = _('Successfully added user %(user)s. An email notification has been sent.') % {'user': email} add_user_tip = _('Successfully added user %(user)s. An email notification has been sent.') % {'user': email}
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
@ -669,8 +738,16 @@ class AdminLDAPUsers(APIView):
info['quota_total'] = seafile_api.get_user_quota(user.email) info['quota_total'] = seafile_api.get_user_quota(user.email)
info['quota_usage'] = seafile_api.get_user_self_usage(user.email) info['quota_usage'] = seafile_api.get_user_self_usage(user.email)
info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) info['create_time'] = timestamp_to_isoformat_timestr(user.ctime)
last_login_obj = UserLastLogin.objects.get_by_username(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, '')
data.append(info) data.append(info)
result = {'ldap_user_list': data, 'has_next_page': has_next_page} 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['quota_total'] = seafile_api.get_user_quota(user.email)
info['create_time'] = timestamp_to_isoformat_timestr(user.ctime) info['create_time'] = timestamp_to_isoformat_timestr(user.ctime)
last_login_obj = UserLastLogin.objects.get_by_username(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) info['role'] = get_user_role(user)
if getattr(settings, 'MULTI_INSTITUTION', False): if getattr(settings, 'MULTI_INSTITUTION', False):
@ -984,8 +1069,7 @@ class AdminUser(APIView):
if is_org_context(request): if is_org_context(request):
org_id = request.user.org.org_id org_id = request.user.org.org_id
org_quota_mb = seafile_api.get_org_quota(org_id) / \ org_quota_mb = seafile_api.get_org_quota(org_id) / get_file_size_unit('MB')
get_file_size_unit('MB')
if quota_total_mb > org_quota_mb: if quota_total_mb > org_quota_mb:
error_msg = 'Failed to set quota: maximum quota is %d MB' % org_quota_mb error_msg = 'Failed to set quota: maximum quota is %d MB' % org_quota_mb

View File

@ -15,7 +15,6 @@ from django.utils.translation import ugettext as _
from seaserv import seafile_api from seaserv import seafile_api
import seahub.settings as settings
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
@ -228,7 +227,7 @@ class AdminUsersBatch(APIView):
if institution != '': if institution != '':
try: try:
obj_insti = Institution.objects.get(name=institution) Institution.objects.get(name=institution)
except Institution.DoesNotExist: except Institution.DoesNotExist:
error_msg = 'Institution %s does not exist' % institution error_msg = 'Institution %s does not exist' % institution
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
@ -372,13 +371,13 @@ class AdminImportUsers(APIView):
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
send_html_email_with_dj_template( send_html_email_with_dj_template(email,
email, dj_template='sysadmin/user_batch_add_email.html',
subject=_('You are invited to join %s') % get_site_name(), subject=_('You are invited to join %s') % get_site_name(),
dj_template='sysadmin/user_batch_add_email.html',
context={ context={
'user': email2nickname(request.user.username), 'user': email2nickname(request.user.username),
'email': email, 'email': email,
'password': password, 'password': password
}) })
user = User.objects.get(email=email) user = User.objects.get(email=email)
@ -407,4 +406,3 @@ class AdminImportUsers(APIView):
operation=USER_ADD, detail=admin_op_detail) operation=USER_ADD, detail=admin_op_detail)
return Response(result) return Response(result)

View File

@ -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.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.views.file import can_preview_file, can_edit_file
from seahub.constants import PERMISSION_READ_WRITE 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.utils.file_types import MARKDOWN, TEXT
from seahub.settings import MAX_UPLOAD_FILE_NAME_LEN, \ from seahub.settings import MAX_UPLOAD_FILE_NAME_LEN, \
@ -295,11 +295,9 @@ class FileView(APIView):
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# rename file # rename file
new_file_name = check_filename_with_rename(repo_id, parent_dir, new_file_name = check_filename_with_rename(repo_id, parent_dir, new_file_name)
new_file_name)
try: try:
seafile_api.rename_file(repo_id, parent_dir, oldname, seafile_api.rename_file(repo_id, parent_dir, oldname, new_file_name, username)
new_file_name, username)
except SearpcError as e: except SearpcError as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
@ -586,7 +584,9 @@ class FileView(APIView):
error_msg = _("File is not locked.") error_msg = _("File is not locked.")
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) 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 # unlock file
try: try:
seafile_api.unlock_file(repo_id, path) seafile_api.unlock_file(repo_id, path)

View File

@ -15,13 +15,13 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
from seahub.invitations.models import Invitation from seahub.invitations.models import Invitation
from seahub.base.accounts import User from seahub.base.accounts import User
from post_office.models import STATUS from seahub.utils.mail import send_html_email_with_dj_template
from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY
from seahub.utils import get_site_name from seahub.utils import get_site_name
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
json_content_type = 'application/json; charset=utf-8' json_content_type = 'application/json; charset=utf-8'
def invitation_owner_check(func): def invitation_owner_check(func):
"""Check whether user is the invitation inviter. """Check whether user is the invitation inviter.
""" """
@ -34,6 +34,7 @@ def invitation_owner_check(func):
return _decorated return _decorated
class InvitationView(APIView): class InvitationView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, CanInviteGuest) permission_classes = (IsAuthenticated, CanInviteGuest)
@ -108,13 +109,12 @@ class InvitationRevokeView(APIView):
'site_name': site_name, 'site_name': site_name,
} }
m = send_html_email_with_dj_template( send_success = send_html_email_with_dj_template(email,
email, dj_template='invitations/invitation_revoke_email.html',
subject=subject, subject=subject,
context=context, dj_template='invitations/invitation_revoke_email.html',
priority=MAIL_PRIORITY.now context=context)
)
if m.status != STATUS.sent: if not send_success:
logger.warning('send revoke access email to %s failed') logger.warning('send revoke access email to %s failed')
return Response({'success': True}) return Response({'success': True})

View File

@ -6,7 +6,6 @@ from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from post_office.models import STATUS
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.permissions import CanInviteGuest from seahub.api2.permissions import CanInviteGuest
@ -70,8 +69,9 @@ class InvitationsView(APIView):
i = Invitation.objects.add(inviter=request.user.username, i = Invitation.objects.add(inviter=request.user.username,
accepter=accepter) accepter=accepter)
m = i.send_to(email=accepter) send_success = i.send_to(email=accepter)
if m.status == STATUS.sent:
if send_success:
return Response(i.to_dict(), status=201) return Response(i.to_dict(), status=201)
else: else:
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -120,10 +120,12 @@ class InvitationsBatchView(APIView):
if Invitation.objects.filter(inviter=request.user.username, if Invitation.objects.filter(inviter=request.user.username,
accepter=accepter).count() > 0: accepter=accepter).count() > 0:
result['failed'].append({ result['failed'].append({
'email': accepter, 'email': accepter,
'error_msg': _('%s is already invited.') % accepter 'error_msg': _('%s is already invited.') % accepter
}) })
continue continue
try: try:
@ -141,11 +143,13 @@ class InvitationsBatchView(APIView):
i = Invitation.objects.add(inviter=request.user.username, i = Invitation.objects.add(inviter=request.user.username,
accepter=accepter) accepter=accepter)
m = i.send_to(email=accepter) send_success = i.send_to(email=accepter)
if m.status != STATUS.sent:
if not send_success:
result['failed'].append({ result['failed'].append({
'email': accepter, '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: else:
result['success'].append(i.to_dict()) result['success'].append(i.to_dict())

View File

@ -6,7 +6,6 @@ from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.utils.translation import ugettext as _
from seaserv import seafile_api from seaserv import seafile_api
@ -14,8 +13,7 @@ from seahub.api2.authentication import TokenAuthentication
from seahub.api2.permissions import CanInviteGuest from seahub.api2.permissions import CanInviteGuest
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
from seahub.invitations.models import Invitation, RepoShareInvitation from seahub.invitations.models import RepoShareInvitation
from post_office.models import STATUS
from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE
from seahub.share.utils import is_repo_admin from seahub.share.utils import is_repo_admin
from seahub.utils import is_org_context from seahub.utils import is_org_context

View File

@ -8,7 +8,6 @@ from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from post_office.models import STATUS
from seaserv import seafile_api from seaserv import seafile_api
@ -20,7 +19,6 @@ from seahub.base.accounts import User
from seahub.utils import is_valid_email from seahub.utils import is_valid_email
from seahub.invitations.models import Invitation, RepoShareInvitation from seahub.invitations.models import Invitation, RepoShareInvitation
from seahub.invitations.utils import block_accepter 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.constants import PERMISSION_READ, PERMISSION_READ_WRITE, GUEST_USER
from seahub.share.utils import is_repo_admin from seahub.share.utils import is_repo_admin
from seahub.utils import is_org_context 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' json_content_type = 'application/json; charset=utf-8'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RepoShareInvitationsView(APIView): class RepoShareInvitationsView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, CanInviteGuest) permission_classes = (IsAuthenticated, CanInviteGuest)
@ -203,11 +202,13 @@ class RepoShareInvitationsBatchView(APIView):
result['success'].append(data) result['success'].append(data)
m = invitation.send_to(email=accepter) send_sucess = invitation.send_to(email=accepter)
if m.status != STATUS.sent:
if not send_sucess:
result['failed'].append({ result['failed'].append({
'email': accepter, '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) return Response(result)

View File

@ -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, \ gen_shared_upload_link, convert_cmmt_desc_link, is_valid_dirent_name, \
normalize_file_path, get_no_duplicate_obj_name, normalize_dir_path 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.file_revisions import get_file_revisions_after_renamed
from seahub.utils.devices import do_unlink_device from seahub.utils.devices import do_unlink_device
from seahub.utils.repo import get_repo_owner, get_library_storages, \ from seahub.utils.repo import get_repo_owner, get_library_storages, \
@ -424,6 +425,7 @@ class Search(APIView):
throttle_classes = (UserRateThrottle, ) throttle_classes = (UserRateThrottle, )
def get(self, request, format=None): def get(self, request, format=None):
if not HAS_FILE_SEARCH: if not HAS_FILE_SEARCH:
error_msg = 'Search not supported.' error_msg = 'Search not supported.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg) return api_error(status.HTTP_404_NOT_FOUND, error_msg)
@ -601,6 +603,16 @@ class Search(APIView):
else: else:
e['repo_type'] = '' 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 has_more = True if total > current_page * per_page else False
return Response({"total":total, "results":results, "has_more":has_more}) 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) return HttpResponse(json.dumps(ret_dict), status=200, content_type=json_content_type)
class ThumbnailView(APIView): class ThumbnailView(APIView):
authentication_classes = (TokenAuthentication,) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle, ) throttle_classes = (UserRateThrottle, )

View File

@ -10,7 +10,6 @@ from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
import seaserv
from seaserv import ccnet_threaded_rpc, unset_repo_passwd, \ from seaserv import ccnet_threaded_rpc, unset_repo_passwd, \
seafile_api, ccnet_api seafile_api, ccnet_api
from constance import config 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 get_enabled_admin_role_permissions_by_role
from seahub.utils import is_user_password_strong, get_site_name, \ from seahub.utils import is_user_password_strong, get_site_name, \
clear_token, get_system_admins, is_pro_version, IS_EMAIL_CONFIGURED 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.utils.licenseparse import user_number_over_limit
from seahub.share.models import ExtraSharePermission from seahub.share.models import ExtraSharePermission
@ -43,6 +42,7 @@ ANONYMOUS_EMAIL = 'Anonymous'
UNUSABLE_PASSWORD = '!' # This will never be a valid hash UNUSABLE_PASSWORD = '!' # This will never be a valid hash
class UserManager(object): class UserManager(object):
def create_user(self, email, password=None, is_staff=False, is_active=False): def create_user(self, email, password=None, is_staff=False, is_active=False):
@ -122,6 +122,7 @@ class UserManager(object):
return user return user
class UserPermissions(object): class UserPermissions(object):
def __init__(self, user): def __init__(self, user):
self.user = user self.user = user
@ -222,6 +223,7 @@ class UserPermissions(object):
return self._get_perm_by_roles('can_publish_repo') return self._get_perm_by_roles('can_publish_repo')
class AdminPermissions(object): class AdminPermissions(object):
def __init__(self, user): def __init__(self, user):
self.user = user self.user = user
@ -322,8 +324,8 @@ class User(object):
return True return True
def save(self): def save(self):
emailuser = ccnet_threaded_rpc.get_emailuser(self.username) emailuser = ccnet_api.get_emailuser(self.username)
if emailuser: if emailuser and emailuser.source.lower() in ("db", "ldapimport"):
if not hasattr(self, 'password'): if not hasattr(self, 'password'):
self.set_unusable_password() self.set_unusable_password()
@ -385,12 +387,10 @@ class User(object):
if orgs: if orgs:
for org in orgs: for org in orgs:
org_id = org.org_id org_id = org.org_id
shared_in_repos = seafile_api.get_org_share_in_repo_list(org_id, shared_in_repos = seafile_api.get_org_share_in_repo_list(org_id, username, -1, -1)
username, -1, -1)
for r in shared_in_repos: for r in shared_in_repos:
seafile_api.org_remove_share(org_id, seafile_api.org_remove_share(org_id, r.repo_id, r.user, username)
r.repo_id, r.user, username)
else: else:
shared_in_repos = seafile_api.get_share_in_repo_list(username, -1, -1) shared_in_repos = seafile_api.get_share_in_repo_list(username, -1, -1)
for r in shared_in_repos: for r in shared_in_repos:
@ -478,15 +478,12 @@ class User(object):
user_language = Profile.objects.get_user_language(u.email) user_language = Profile.objects.get_user_language(u.email)
translation.activate(user_language) translation.activate(user_language)
send_html_email_with_dj_template( send_html_email_with_dj_template(u.email,
u.email, dj_template='sysadmin/user_freeze_email.html',
subject=_('Account %(account)s froze on %(site)s.') % { subject=_('Account %(account)s froze on %(site)s.') % {
"account": self.email, "account": self.email,
"site": get_site_name(), "site": get_site_name()},
}, dj_template='sysadmin/user_freeze_email.html',
context={'user': self.email}, context={'user': self.email})
priority=MAIL_PRIORITY.now
)
# restore current language # restore current language
translation.activate(cur_language) translation.activate(cur_language)
@ -535,10 +532,11 @@ class User(object):
for r in passwd_setted_repos: for r in passwd_setted_repos:
unset_repo_passwd(r.id, self.email) unset_repo_passwd(r.id, self.email)
class AuthBackend(object): class AuthBackend(object):
def get_user_with_import(self, username): 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: if not emailuser:
raise User.DoesNotExist('User matching query does not exits.') raise User.DoesNotExist('User matching query does not exits.')
@ -580,7 +578,8 @@ class AuthBackend(object):
if user.check_password(password): if user.check_password(password):
return user return user
########## Register related
# Register related
class RegistrationBackend(object): class RegistrationBackend(object):
""" """
A registration backend which follows a simple workflow: A registration backend which follows a simple workflow:
@ -755,9 +754,9 @@ class RegistrationForm(forms.Form):
""" """
attrs_dict = {'class': 'input'} attrs_dict = {'class': 'input'}
email = forms.CharField(widget=forms.TextInput(attrs=dict(attrs_dict, email = forms.CharField(widget=forms.TextInput(attrs=dict(attrs_dict, maxlength=75)),
maxlength=75)),
label=_("Email address")) label=_("Email address"))
userid = forms.RegexField(regex=r'^\w+$', userid = forms.RegexField(regex=r'^\w+$',
max_length=40, max_length=40,
required=False, required=False,
@ -825,12 +824,13 @@ class RegistrationForm(forms.Form):
raise forms.ValidationError(_("The two password fields didn't match.")) raise forms.ValidationError(_("The two password fields didn't match."))
return self.cleaned_data return self.cleaned_data
class DetailedRegistrationForm(RegistrationForm): class DetailedRegistrationForm(RegistrationForm):
attrs_dict = {'class': 'input'} attrs_dict = {'class': 'input'}
try: try:
from seahub.settings import REGISTRATION_DETAILS_MAP from seahub.settings import REGISTRATION_DETAILS_MAP
except: except ImportError:
REGISTRATION_DETAILS_MAP = None REGISTRATION_DETAILS_MAP = None
if REGISTRATION_DETAILS_MAP: if REGISTRATION_DETAILS_MAP:

View File

@ -133,6 +133,7 @@ def base(request):
'constance_enabled': dj_settings.CONSTANCE_ENABLED, 'constance_enabled': dj_settings.CONSTANCE_ENABLED,
'FILE_SERVER_ROOT': file_server_root, 'FILE_SERVER_ROOT': file_server_root,
'LOGIN_URL': dj_settings.LOGIN_URL, 'LOGIN_URL': dj_settings.LOGIN_URL,
'LOGOUT_URL': dj_settings.LOGOUT_URL,
'enable_thumbnail': ENABLE_THUMBNAIL, 'enable_thumbnail': ENABLE_THUMBNAIL,
'thumbnail_size_for_original': THUMBNAIL_SIZE_FOR_ORIGINAL, 'thumbnail_size_for_original': THUMBNAIL_SIZE_FOR_ORIGINAL,
'enable_guest_invitation': ENABLE_GUEST_INVITATION, 'enable_guest_invitation': ENABLE_GUEST_INVITATION,

View File

@ -2,7 +2,6 @@
from datetime import timedelta from datetime import timedelta
from django.db import models from django.db import models
from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _ 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.invitations.settings import INVITATIONS_TOKEN_AGE
from seahub.utils import gen_token, get_site_name from seahub.utils import gen_token, get_site_name
from seahub.utils.timeutils import datetime_to_isoformat_timestr 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 from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE
GUEST = 'Guest' GUEST = 'Guest'
class InvitationManager(models.Manager): class InvitationManager(models.Manager):
def add(self, inviter, accepter, invite_type=GUEST): def add(self, inviter, accepter, invite_type=GUEST):
token = gen_token(max_length=32) token = gen_token(max_length=32)
@ -26,8 +26,7 @@ class InvitationManager(models.Manager):
return i return i
def get_by_inviter(self, inviter): def get_by_inviter(self, inviter):
return super(InvitationManager, return super(InvitationManager, self).filter(inviter=inviter).order_by('-invite_time')
self).filter(inviter=inviter).order_by('-invite_time')
def delete_all_expire_invitation(self): def delete_all_expire_invitation(self):
super(InvitationManager, self).filter(expire_time__lte=timezone.now(), accept_time__isnull=True).delete() 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 = self.to_dict()
context['site_name'] = get_site_name() 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.') % { subject = _('%(user)s invited you to join %(site_name)s.') % {
'user': self.inviter, 'site_name': get_site_name()} 'user': self.inviter, 'site_name': get_site_name()}
return send_html_email_with_dj_template(
email, dj_template='invitations/invitation_email.html', return send_html_email_with_dj_template(email,
context=context,
subject=subject, subject=subject,
priority=MAIL_PRIORITY.now dj_template='invitations/invitation_email.html',
) context=context)
class RepoShareInvitationManager(models.Manager): class RepoShareInvitationManager(models.Manager):
@ -136,7 +132,9 @@ class RepoShareInvitationManager(models.Manager):
def list_by_invitation(self, invitation): def list_by_invitation(self, invitation):
return self.select_related('invitation').filter(invitation=invitation) return self.select_related('invitation').filter(invitation=invitation)
class RepoShareInvitation(models.Model): class RepoShareInvitation(models.Model):
PERMISSION_CHOICES = ( PERMISSION_CHOICES = (
(PERMISSION_READ, 'read only'), (PERMISSION_READ, 'read only'),
(PERMISSION_READ_WRITE, 'read and write') (PERMISSION_READ_WRITE, 'read and write')

View File

@ -12,13 +12,15 @@ import seaserv
from seaserv import seafile_api from seaserv import seafile_api
from seahub.profile.models import Profile 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 from seahub.utils import get_site_name
# Get an instance of a logger # Get an instance of a logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
help = 'Send Email notifications to admins if there are virus files detected .' help = 'Send Email notifications to admins if there are virus files detected .'
label = "notifications_notify_admins_on_virus" label = "notifications_notify_admins_on_virus"
@ -52,11 +54,9 @@ class Command(BaseCommand):
user_language = self.get_user_language(u.email) user_language = self.get_user_language(u.email)
translation.activate(user_language) translation.activate(user_language)
send_html_email_with_dj_template( send_html_email_with_dj_template(u.email,
u.email, dj_template='notifications/notify_virus.html',
subject=_('Virus detected on %s') % get_site_name(), subject=_('Virus detected on %s') % get_site_name(),
priority=MAIL_PRIORITY.now dj_template='notifications/notify_virus.html')
)
# restore current language # restore current language
translation.activate(cur_language) translation.activate(cur_language)
@ -68,11 +68,9 @@ class Command(BaseCommand):
return return
for mail in notify_list: for mail in notify_list:
send_html_email_with_dj_template( send_html_email_with_dj_template(mail,
mail, dj_template='notifications/notify_virus.html',
subject=_('Virus detected on %s') % get_site_name(), subject=_('Virus detected on %s') % get_site_name(),
priority=MAIL_PRIORITY.now dj_template='notifications/notify_virus.html')
)
def email_repo_owner(self, repo_file): def email_repo_owner(self, repo_file):
repo_id, file_path = repo_file.split(':', 1) repo_id, file_path = repo_file.split(':', 1)
@ -88,16 +86,12 @@ class Command(BaseCommand):
translation.activate(user_language) translation.activate(user_language)
contact_email = Profile.objects.get_contact_email_by_user(owner) contact_email = Profile.objects.get_contact_email_by_user(owner)
send_html_email_with_dj_template( send_html_email_with_dj_template(contact_email,
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(), subject=_('Virus detected on %s') % get_site_name(),
priority=MAIL_PRIORITY.now 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 # restore current language
translation.activate(cur_language) translation.activate(cur_language)

View File

@ -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 # 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_FORCE_SAVE = getattr(settings, 'ONLYOFFICE_FORCE_SAVE', False)
ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN = getattr(settings, 'ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN', False)

View File

@ -218,7 +218,6 @@ INSTALLED_APPS = [
'statici18n', 'statici18n',
'constance', 'constance',
'constance.backends.database', 'constance.backends.database',
'post_office',
'termsandconditions', 'termsandconditions',
'webpack_loader', 'webpack_loader',

View File

@ -3,34 +3,30 @@
import os import os
import logging import logging
import json import json
from dateutil.relativedelta import relativedelta
from constance import config
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpResponse, HttpResponseRedirect, Http404, \ from django.http import HttpResponse, HttpResponseRedirect, Http404, \
HttpResponseBadRequest HttpResponseBadRequest
from django.utils.translation import ugettext as _, activate from django.utils.translation import ugettext as _, activate
from django.contrib import messages from django.contrib import messages
from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
import seaserv import seaserv
from seaserv import seafile_api from seaserv import seafile_api
from seaserv import ccnet_threaded_rpc
from pysearpc import SearpcError from pysearpc import SearpcError
from seahub.share.forms import FileLinkShareForm, \ from seahub.share.forms import FileLinkShareForm, \
UploadLinkShareForm 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.share.signals import share_repo_to_user_successful
from seahub.auth.decorators import login_required, login_required_ajax from seahub.auth.decorators import login_required, login_required_ajax
from seahub.base.decorators import require_POST from seahub.base.decorators import require_POST
from seahub.contacts.signals import mail_sended from seahub.contacts.signals import mail_sended
from seahub.views import is_registered_user, check_folder_permission from seahub.views import is_registered_user, check_folder_permission
from seahub.utils import string2list, gen_shared_link, \ from seahub.utils import string2list, IS_EMAIL_CONFIGURED, check_filename_with_rename, \
gen_shared_upload_link, IS_EMAIL_CONFIGURED, check_filename_with_rename, \
is_valid_username, is_valid_email, send_html_email, is_org_context, \ is_valid_username, is_valid_email, send_html_email, is_org_context, \
gen_token, normalize_cache_key, get_site_name 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, \ from seahub.settings import SITE_ROOT, REPLACE_FROM_EMAIL, \
ADD_REPLY_TO_HEADER, SHARE_LINK_EMAIL_LANGUAGE, \ ADD_REPLY_TO_HEADER, SHARE_LINK_EMAIL_LANGUAGE, \
SHARE_LINK_AUDIT_CODE_TIMEOUT SHARE_LINK_AUDIT_CODE_TIMEOUT
@ -39,21 +35,25 @@ from seahub.profile.models import Profile
# Get an instance of a logger # Get an instance of a logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
########## rpc wrapper
# rpc wrapper
def is_org_repo_owner(username, repo_id): def is_org_repo_owner(username, repo_id):
owner = seaserv.seafserv_threaded_rpc.get_org_repo_owner(repo_id) owner = seaserv.seafserv_threaded_rpc.get_org_repo_owner(repo_id)
return True if owner == username else False return True if owner == username else False
def org_share_repo(org_id, repo_id, from_user, to_user, permission): 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, return seaserv.seafserv_threaded_rpc.org_add_share(org_id, repo_id,
from_user, to_user, from_user, to_user,
permission) permission)
def org_remove_share(org_id, repo_id, from_user, to_user): def org_remove_share(org_id, repo_id, from_user, to_user):
return seaserv.seafserv_threaded_rpc.org_remove_share(org_id, repo_id, return seaserv.seafserv_threaded_rpc.org_remove_share(org_id, repo_id,
from_user, to_user) from_user, to_user)
########## functions
# functions
def share_to_group(request, repo, group, permission): def share_to_group(request, repo, group, permission):
"""Share repo to group with given permission. """Share repo to group with given permission.
""" """
@ -83,6 +83,7 @@ def share_to_group(request, repo, group, permission):
logger.error(e) logger.error(e)
return False return False
def share_to_user(request, repo, to_user, permission): def share_to_user(request, repo, to_user, permission):
"""Share repo to a user with given permission. """Share repo to a user with given permission.
""" """
@ -109,8 +110,8 @@ def share_to_user(request, repo, to_user, permission):
else: else:
seafile_api.share_repo(repo_id, from_user, to_user, permission) seafile_api.share_repo(repo_id, from_user, to_user, permission)
except SearpcError as e: except SearpcError as e:
return False
logger.error(e) logger.error(e)
return False
else: else:
# send a signal when sharing repo successful # send a signal when sharing repo successful
share_repo_to_user_successful.send(sender=None, 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) path='/', org_id=org_id)
return True return True
########## share link
# share link
@login_required_ajax @login_required_ajax
def send_shared_link(request): def send_shared_link(request):
""" """
@ -131,7 +133,8 @@ def send_shared_link(request):
content_type = 'application/json; charset=utf-8' content_type = 'application/json; charset=utf-8'
if not IS_EMAIL_CONFIGURED: 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) return HttpResponse(data, status=500, content_type=content_type)
form = FileLinkShareForm(request.POST) form = FileLinkShareForm(request.POST)
@ -207,6 +210,7 @@ def send_shared_link(request):
return HttpResponseBadRequest(json.dumps(form.errors), return HttpResponseBadRequest(json.dumps(form.errors),
content_type=content_type) content_type=content_type)
@login_required @login_required
def save_shared_link(request): def save_shared_link(request):
"""Save public share link to one's library. """Save public share link to one's library.
@ -246,6 +250,7 @@ def save_shared_link(request):
messages.success(request, _('Successfully saved.')) messages.success(request, _('Successfully saved.'))
return HttpResponseRedirect(next_page) return HttpResponseRedirect(next_page)
@login_required_ajax @login_required_ajax
def send_shared_upload_link(request): def send_shared_upload_link(request):
""" """
@ -257,7 +262,8 @@ def send_shared_upload_link(request):
content_type = 'application/json; charset=utf-8' content_type = 'application/json; charset=utf-8'
if not IS_EMAIL_CONFIGURED: 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) return HttpResponse(data, status=500, content_type=content_type)
form = UploadLinkShareForm(request.POST) form = UploadLinkShareForm(request.POST)
@ -317,6 +323,7 @@ def send_shared_upload_link(request):
return HttpResponseBadRequest(json.dumps(form.errors), return HttpResponseBadRequest(json.dumps(form.errors),
content_type=content_type) content_type=content_type)
@login_required_ajax @login_required_ajax
@require_POST @require_POST
def ajax_private_share_dir(request): 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( sub_repo_id = seaserv.seafserv_threaded_rpc.create_org_virtual_repo(
org_id, repo_id, path, name, name, username) org_id, repo_id, path, name, name, username)
else: else:
sub_repo_id = seafile_api.create_virtual_repo(repo_id, path, sub_repo_id = seafile_api.create_virtual_repo(repo_id, path, name, name, username)
name, name, username)
sub_repo = seafile_api.get_repo(sub_repo_id) sub_repo = seafile_api.get_repo(sub_repo_id)
except SearpcError as e: except SearpcError as e:
result['error'] = e.msg 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")}) data = json.dumps({"error": _("Please check the email(s) you entered")})
return HttpResponse(data, status=400, content_type=content_type) return HttpResponse(data, status=400, content_type=content_type)
def ajax_get_link_audit_code(request): def ajax_get_link_audit_code(request):
""" """
Generate a token, and record that token with email in cache, expires in 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 # send code to user via email
subject = _("Verification code for visiting share links") subject = _("Verification code for visiting share links")
c = { c = {'code': code}
'code': code,
} send_success = send_html_email_with_dj_template(email,
try: subject=subject,
send_html_email_with_dj_template( dj_template='share/audit_code_email.html',
email, dj_template='share/audit_code_email.html', context=c)
context=c, subject=subject, priority=MAIL_PRIORITY.now
) if not send_success:
return HttpResponse(json.dumps({'success': True}), status=200,
content_type=content_type)
except Exception as e:
logger.error('Failed to send audit code via email to %s') logger.error('Failed to send audit code via email to %s')
logger.error(e)
return HttpResponse(json.dumps({ return HttpResponse(json.dumps({
"error": _("Failed to send a verification code, please try again later.") "error": _("Failed to send a verification code, please try again later.")
}), status=500, content_type=content_type) }), status=500, content_type=content_type)
return HttpResponse(json.dumps({'success': True}), status=200,
content_type=content_type)

View File

@ -40,6 +40,7 @@
siteName: '{{ site_name|escapejs }}', siteName: '{{ site_name|escapejs }}',
siteRoot: '{{ SITE_ROOT }}', siteRoot: '{{ SITE_ROOT }}',
loginUrl: '{{ LOGIN_URL }}', loginUrl: '{{ LOGIN_URL }}',
logoutUrl: '{{ LOGOUT_URL }}',
isPro: '{{ is_pro }}', isPro: '{{ is_pro }}',
isDocs: '{{ is_docs }}', isDocs: '{{ is_docs }}',
lang: '{{ LANGUAGE_CODE }}', lang: '{{ LANGUAGE_CODE }}',

View File

@ -1,5 +1,6 @@
{% extends "base_for_react.html" %} {% extends "base_for_react.html" %}
{% load render_bundle from webpack_loader %} {% load render_bundle from webpack_loader %}
{% load seahub_tags %}
{% block extra_style %} {% block extra_style %}
{% render_bundle 'app' 'css' %} {% render_bundle 'app' 'css' %}
@ -16,6 +17,15 @@
uploadLinkExpireDaysMin: {{ upload_link_expire_days_min }}, uploadLinkExpireDaysMin: {{ upload_link_expire_days_min }},
uploadLinkExpireDaysMax: {{ upload_link_expire_days_max }} 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 %}
</script> </script>
{% render_bundle 'app' 'js' %} {% render_bundle 'app' 'js' %}
{% endblock %} {% endblock %}

View File

@ -1,27 +1,23 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import os import os
from django.template import Context, loader import logging
from post_office import mail from django.template import loader
from post_office.models import PRIORITY from django.core.mail import EmailMessage
from constance import config
from seahub.utils import get_site_scheme_and_netloc, get_site_name from seahub.utils import get_site_scheme_and_netloc, get_site_name
from seahub.settings import MEDIA_URL, LOGO_PATH, \ from seahub.settings import MEDIA_URL, LOGO_PATH, \
MEDIA_ROOT, CUSTOM_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, def send_html_email_with_dj_template(recipients, subject, dj_template, context={}):
message='', headers=None,
priority=None, backend=''):
""" """
Arguments: Arguments:
- `recipients`: - `recipients`:
- `subject`: - `subject`:
- `sender`: - `dj_template`:
- `template`:
- `context`: - `context`:
""" """
@ -42,7 +38,12 @@ def send_html_email_with_dj_template(recipients, subject, dj_template,
t = loader.get_template(dj_template) t = loader.get_template(dj_template)
html_message = t.render(context) html_message = t.render(context)
return mail.send(recipients, sender=sender, template=template, context=context, mail = EmailMessage(subject=subject, body=html_message, to=[recipients])
subject=subject, message=message, mail.content_subtype = "html"
html_message=html_message, headers=headers, priority=priority,
backend=backend) try:
mail.send()
return True
except Exception as e:
logger.error(e)
return False

View File

@ -60,7 +60,7 @@ from seahub.settings import AVATAR_FILE_STORAGE, \
DTABLE_WEB_SERVER DTABLE_WEB_SERVER
from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP 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.ocm.settings import ENABLE_OCM, OCM_REMOTE_SERVERS
from seahub.constants import HASH_URLS, PERMISSION_READ from seahub.constants import HASH_URLS, PERMISSION_READ
@ -1180,6 +1180,7 @@ def react_fake_view(request, **kwargs):
max_upload_file_size = -1 max_upload_file_size = -1
return render(request, "react_app.html", { return render(request, "react_app.html", {
"onlyoffice_desktop_editors_portal_login": ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN,
"guide_enabled": guide_enabled, "guide_enabled": guide_enabled,
'trash_repos_expire_days': expire_days if expire_days > 0 else 30, 'trash_repos_expire_days': expire_days if expire_days > 0 else 30,
'dtable_web_server': DTABLE_WEB_SERVER, 'dtable_web_server': DTABLE_WEB_SERVER,

View File

@ -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.licenseparse import parse_license, user_number_over_limit
from seahub.utils.rpc import mute_seafile_api from seahub.utils.rpc import mute_seafile_api
from seahub.utils.sysinfo import get_platform_name 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.ms_excel import write_xls
from seahub.utils.user_permissions import get_basic_user_roles, \ from seahub.utils.user_permissions import get_basic_user_roles, \
get_user_role, get_basic_admin_roles get_user_role, get_basic_admin_roles