1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-27 11:01:14 +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-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",

View File

@ -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 {
</div>
<a href={siteRoot + 'profile/'} className="item">{gettext('Settings')}</a>
{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>

View File

@ -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 (
<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>
</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>
<div className="about-content">
<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><a href={href} target="_blank">{gettext('About Us')}</a></p>
</div>

View File

@ -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 (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Manage group members')}</ModalHeader>
@ -113,36 +165,41 @@ class ManageMembersDialog extends React.Component {
);
})
}
<div className="manage-members">
<Table size="sm" className="manage-members-table">
<thead>
<tr>
<th width="15%"></th>
<th width="45%">{gettext('Name')}</th>
<th width="30%">{gettext('Role')}</th>
<th width="10%"></th>
</tr>
</thead>
<tbody>
{
this.state.groupMembers.length > 0 &&
this.state.groupMembers.map((item, index = 0) => {
<div className="manage-members" onScroll={this.handleScroll}>
{isLoading ? <Loading /> : (
<Fragment>
<Table size="sm" className="manage-members-table">
<thead>
<tr>
<th width="15%"></th>
<th width="45%">{gettext('Name')}</th>
<th width="30%">{gettext('Role')}</th>
<th width="10%"></th>
</tr>
</thead>
<tbody>
{
this.state.groupMembers.length > 0 &&
this.state.groupMembers.map((item, index) => {
return (
<React.Fragment key={index}>
<Member
key={index}
item={item}
onGroupMembersChange={this.onGroupMembersChange}
changeMember={this.changeMember}
deleteMember={this.deleteMember}
groupID={this.props.groupID}
isOwner={this.props.isOwner}
isItemFreezed={this.state.isItemFreezed}
toggleItemFreezed={this.toggleItemFreezed}
/>
</React.Fragment>
);
})
}
</tbody>
</Table>
}
</tbody>
</Table>
{hasNextPage && <Loading />}
</Fragment>
)}
</div>
</ModalBody>
<ModalFooter>
@ -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) &&
<i
className="fa fa-times delete-group-member-icon"
name={item.email}
onClick={this.deleteMember.bind(this, item.email)}>
onClick={this.deleteMember}>
</i>
}
</td>

View File

@ -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 = (
<tr>
<td>
<td onClick={this.onItemClick}>
<div className="dir-icon">
{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="" />
}
{dirent.is_locked && <img className="locked" src={mediaUrl + 'img/file-locked-32.png'} alt={gettext('locked')} title={lockedInfo}/>}
</div>
</td>
<td>
<td onClick={this.onItemClick}>
{this.state.isRenameing ?
<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 />
{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 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 (
<li className="search-result-item" onClick={this.onClickHandler}>
<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]['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;
}

View File

@ -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 (
<Fragment>
<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>
<td onClick={this.visitRepo}><img src={iconUrl} title={iconTitle} width="24" alt={iconTitle}/></td>
<td onClick={this.visitRepo}>
{this.state.isRenaming ?
<Rename name={repo.repo_name} onRenameConfirm={this.onRenameConfirm} onRenameCancel={this.onRenameCancel} /> :
<Link to={libPath}>{repo.repo_name}</Link>

View File

@ -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;

View File

@ -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;

View File

@ -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 (
<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>
<td onClick={this.visitRepo}><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td onClick={this.visitRepo}>
{this.state.isRenaming && (
<Rename
name={repo.repo_name}

View File

@ -29,6 +29,11 @@ class ResultsItem extends React.Component {
let linkContent = decodeURI(item.fullpath).substring(1);
let folderIconUrl = linkContent ? 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 (
<li className="search-result-item">
<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 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 = (
<Fragment>
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
@ -248,8 +252,8 @@ class Item extends Component {
const mobileItem = (
<Fragment>
<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>
<td onClick={this.visitRepo}><img src={data.icon_url} title={data.icon_title} alt={data.icon_title} width="24" /></td>
<td onClick={this.visitRepo}>
<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">{data.size}</span>

View File

@ -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 = (
<tr>
<td className="text-center">
<td className="text-center" onClick={this.visitItem}>
{
data.thumbnail_url ?
<img className="thumbnail" src={data.thumbnail_url} alt="" /> :
<img src={data.item_icon_url} alt={gettext('icon')} width="24" />
}
</td>
<td>
<td onClick={this.visitItem}>
{ data.is_dir ?
<Link to={data.dirent_view_url}>{data.obj_name}</Link> :
<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 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') : '--'} /`}
<br />
{`${item.last_login ? moment(item.last_login).fromNow() : '--'}`}
<br />
{`${item.last_access_time ? moment(item.last_access_time).fromNow() : '--'}`}
</td>
<td>
{(item.email != username && isOpIconShown) &&

View File

@ -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;

View File

@ -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) {

View File

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

View File

@ -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)

View File

@ -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)

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.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)

View File

@ -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})

View File

@ -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())

View File

@ -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})

View File

@ -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)

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, \
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, )

View File

@ -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:

View File

@ -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,

View File

@ -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')

View File

@ -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)

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
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',
'constance',
'constance.backends.database',
'post_office',
'termsandconditions',
'webpack_loader',

View File

@ -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)

View File

@ -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 }}',

View File

@ -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 %}
</script>
{% render_bundle 'app' 'js' %}
{% endblock %}

View File

@ -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

View File

@ -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,

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.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