diff --git a/frontend/config/webpack.entry.js b/frontend/config/webpack.entry.js
index 6cc83e264f..0c2e1e8fb7 100644
--- a/frontend/config/webpack.entry.js
+++ b/frontend/config/webpack.entry.js
@@ -4,7 +4,6 @@ const entryFiles = {
markdownEditor: "/index.js",
TCAccept: "/tc-accept.js",
TCView: "/tc-view.js",
- userNotifications: "/user-notifications.js",
wiki: "/wiki.js",
fileHistory: "/file-history.js",
fileHistoryOld: "/file-history-old.js",
diff --git a/frontend/src/components/common/notice-item.js b/frontend/src/components/common/notice-item.js
index ec2f2ddbd0..9f7f125402 100644
--- a/frontend/src/components/common/notice-item.js
+++ b/frontend/src/components/common/notice-item.js
@@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
-
import { gettext, siteRoot } from '../../utils/constants';
import { Utils } from '../../utils/utils';
diff --git a/frontend/src/components/common/notification-popover/index.css b/frontend/src/components/common/notification-popover/index.css
new file mode 100644
index 0000000000..31f3fceb26
--- /dev/null
+++ b/frontend/src/components/common/notification-popover/index.css
@@ -0,0 +1,186 @@
+.notification-wrapper .popover {
+ max-width: 300px;
+}
+
+.notification-container {
+ position: absolute;
+ background: #fff;
+ width: 320px;
+ right: -10px;
+ top: -1px;
+ border-radius: 3px;
+ box-shadow: 0 0 5px #ccc;
+}
+
+.notification-container .notification-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 50px;
+ border-bottom: 1px solid #ededed;
+ font-size: 16px;
+ font-weight: 600;
+ position: relative;
+}
+
+.notification-container .notification-header .notification-close-icon {
+ position: absolute;
+ right: 14px;
+ height: 24px;
+ width: 24px;
+ text-align: center;
+ cursor: pointer;
+ color: #000;
+ opacity: 0.5;
+ font-weight: 700;
+}
+
+.notification-container .notification-header .notification-close-icon:hover {
+ opacity: 0.75;
+}
+
+.notification-container .notification-body {
+ padding: 0;
+}
+
+.notification-container .notification-body .show-weixin-qrcode {
+ cursor: pointer;
+ border-bottom: 1px solid #ededed;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ padding-left: 10px;
+}
+
+.show-weixin-qrcode .weixin-icon {
+ color: #999;
+ font-size: 20px;
+ margin-left: 20px;
+}
+
+.notification-container .notification-body .mark-notifications {
+ color: #b4b4b4;
+ cursor: pointer;
+ border-bottom: 1px solid #ededed;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding-right: 1rem;
+}
+
+.notification-container .notification-body .mark-notifications:hover {
+ text-decoration: underline;
+}
+
+.notification-body .notification-list-container {
+ max-height: 260px;
+ overflow: auto;
+}
+
+.notification-list-container .notification-item {
+ padding: 14px 16px 14px 10px;
+ border-bottom: 1px solid #ededed;
+ position: relative;
+ cursor: pointer;
+}
+
+.notification-list-container .notification-item:last-child {
+ border-bottom: none;
+}
+
+.notification-list-container .notification-item:hover {
+ background: #f5f5f5;
+}
+
+.notification-list-container .notification-item .notification-item-header {
+ display: flex;
+ align-items: center
+}
+
+.notification-list-container .notification-item .notification-point {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: red;
+ margin-right: 12px;
+ position: absolute;
+}
+
+.notification-list-container .notification-item .notification-header-info {
+ display: flex;
+ justify-content: space-between;
+ flex: 1;
+ margin-left: 20px;
+ width: calc(100% - 20px);
+}
+
+.notification-user-detail {
+ display: flex;
+ width: 65%;
+}
+
+.notification-user-detail img {
+ margin-top: 3px;
+}
+
+.notification-user-name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ font-weight: 500;
+}
+
+.notification-item .notification-header-info .notification-time {
+ color: #b4b4b4;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ font-size: 13px;
+}
+
+.notification-list-container .notification-item .notification-content-wrapper {
+ font-size: 13px;
+}
+
+.notification-item .notification-content-quotes {
+ width: 8px;
+}
+
+.notification-list-container .notification-item .notification-comment-content {
+ max-width: calc(100% - 16px);
+}
+
+.notification-list-container .notification-item .notification-comment-content p {
+ display: inline-block;
+ letter-spacing: 1px;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-bottom: 0;
+}
+
+.notification-list-container .notification-item .notification-comment-content p img {
+ max-width: 70%;
+ height: auto;
+ max-height: 60px;
+}
+
+.notification-body .notification-footer {
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f9f9f9;
+ cursor: pointer;
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px;
+ border-top: 1px solid #ededed;
+}
+
+.notification-body .notification-footer:hover {
+ text-decoration: underline;
+}
diff --git a/frontend/src/components/common/notification-popover/index.js b/frontend/src/components/common/notification-popover/index.js
new file mode 100644
index 0000000000..fef6e62775
--- /dev/null
+++ b/frontend/src/components/common/notification-popover/index.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Popover } from 'reactstrap';
+import './index.css';
+
+export default class NotificationPopover extends React.Component {
+
+ static propTypes = {
+ headerText: PropTypes.string.isRequired,
+ bodyText: PropTypes.string.isRequired,
+ footerText: PropTypes.string.isRequired,
+ onNotificationListToggle: PropTypes.func,
+ onNotificationDialogToggle: PropTypes.func,
+ listNotifications: PropTypes.func,
+ onMarkAllNotifications: PropTypes.func,
+ children: PropTypes.any,
+ };
+
+ static defaultProps = {
+ headerText: '',
+ bodyText: '',
+ footerText: '',
+ };
+
+ componentDidMount() {
+ document.addEventListener('mousedown', this.handleOutsideClick);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleOutsideClick);
+ }
+
+ handleOutsideClick = (e) => {
+ if (!this.notificationContainerRef.contains(e.target)) {
+ document.removeEventListener('mousedown', this.handleOutsideClick);
+ if (e.target.className === 'tool notification' || e.target.parentNode.className === 'tool notification') {
+ return;
+ }
+ this.props.onNotificationListToggle();
+ }
+ }
+
+ onNotificationDialogToggle = () => {
+ this.props.onNotificationDialogToggle();
+ this.props.onNotificationListToggle();
+ }
+
+ onHandleScroll = () => {
+ if (this.notificationListRef.offsetHeight + this.notificationListRef.scrollTop + 1 >= this.notificationsWrapperRef.offsetHeight) {
+ this.props.listNotifications && this.props.listNotifications();
+ }
+ }
+
+ render() {
+ const { headerText, bodyText, footerText } = this.props;
+ return (
+
+ this.notificationContainerRef = ref}>
+
+ {headerText}
+
+
+
+
{bodyText}
+
this.notificationListRef = ref}>
+
this.notificationsWrapperRef = ref}>
+ {this.props.children}
+
+
+
{footerText}
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/components/common/notification.css b/frontend/src/components/common/notification.css
new file mode 100644
index 0000000000..0677217a68
--- /dev/null
+++ b/frontend/src/components/common/notification.css
@@ -0,0 +1,126 @@
+#notifications {
+ position: relative;
+ width: 32px;
+}
+
+#notice-icon {
+ position: relative;
+ display: block;
+}
+
+@media (max-width: 390px) {
+ #notifications {
+ margin-left: 8px;
+ }
+}
+
+#notifications .title {
+ line-height: 1.5;
+ font-size: 1rem;
+ color: #322;
+ font-weight: normal;
+}
+
+#notifications .sf2-icon-bell {
+ font-size: 24px;
+ line-height: 1;
+ color: #999;
+ vertical-align: middle;
+}
+
+#notifications .num {
+ position: absolute;
+ top: -3px;
+ left: 12px;
+ padding: 0 2px;
+ min-width: 16px;
+ height: 16px;
+ color: #fff;
+ font-size: 9px;
+ line-height: 16px;
+ text-align: center;
+ background: #fc6440;
+ border-radius: 100%;
+}
+
+#notice-popover {
+ top: 38px;
+ right: -12px;
+}
+
+#notice-popover .outer-caret {
+ right: 18px;
+}
+
+#notice-popover a {
+ font-weight: normal;
+}
+
+#notice-popover li {
+ padding: 9px 0 3px;
+ border-bottom: 1px solid #dfdfe1;
+}
+
+#notice-popover li.unread {
+ padding-right: 10px;
+ padding-left: 10px;
+ border-left: 2px solid #feac74;
+}
+
+#notice-popover li.read {
+ padding-right: 10px;
+ padding-left: 10px;
+ border-left: 2px solid transparent;
+}
+
+#notice-popover li:hover {
+ background: #f5f5f7;
+}
+
+#notice-popover li.read:hover {
+ background: #f5f5f7;
+ border-left: 2px solid #dfdfe1;
+}
+
+#notice-popover .avatar {
+ border-radius: 1000px;
+ float: left;
+}
+
+#notice-popover .brief {
+ margin-left: 40px;
+ margin-bottom: 1rem;
+ font-size: 0.8125rem;
+ line-height: 1.5rem;
+}
+
+#notice-popover .time {
+ margin: 0;
+ color: #999;
+ text-align: right;
+ font-size: 0.8125rem;
+ line-height: 1.5rem;
+ clear: both;
+}
+
+#notice-popover .view-all {
+ display: block;
+ padding: 7px 0;
+ text-align: center;
+ color: #a4a4a4;
+}
+
+#notice-popover .sf-popover-close {
+ position: absolute;
+ right: 10px;
+ top: 17px;
+}
+
+#notice-popover .sf-popover-hd {
+ border-bottom: 1px solid #dfdfe1;
+ margin: 0 10px;
+}
+
+#notice-popover .sf-popover-con {
+ max-height: 25rem;
+}
diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js
index 829cc4184c..d8259e6a5f 100644
--- a/frontend/src/components/common/notification.js
+++ b/frontend/src/components/common/notification.js
@@ -1,7 +1,11 @@
import React from 'react';
+import NotificationPopover from './notification-popover';
import { seafileAPI } from '../../utils/seafile-api';
-import { gettext, siteRoot } from '../../utils/constants';
+import { gettext } from '../../utils/constants';
import NoticeItem from './notice-item';
+import UserNotificationsDialog from '../../user-notifications';
+import { Utils } from '../../utils/utils';
+import './notification.css';
class Notification extends React.Component {
constructor(props) {
@@ -10,6 +14,7 @@ class Notification extends React.Component {
showNotice: false,
unseenCount: 0,
noticeList: [],
+ isShowNotificationDialog: this.getInitDialogState(),
};
}
@@ -58,29 +63,60 @@ class Notification extends React.Component {
}
- render() {
+ getInitDialogState = () => {
+ const searchParams = Utils.getUrlSearches();
+ return searchParams.notifications === 'all';
+ }
+ onNotificationDialogToggle = () => {
+ let newSearch = this.state.isShowNotificationDialog ? null : 'all';
+ Utils.updateSearchParameter('notifications', newSearch);
+ this.setState({isShowNotificationDialog: !this.state.isShowNotificationDialog});
+ }
+
+ onNotificationListToggle = () => {
+ this.setState({showNotice: false});
+ }
+
+ onMarkAllNotifications = () => {
+ seafileAPI.updateNotifications().then(() => {
+ this.setState({
+ unseenCount: 0,
+ });
+ }).catch((error) => {
+ this.setState({
+ errorMsg: Utils.getErrorMsg(error, true)
+ });
+ });
+ }
+
+ render() {
+ const { unseenCount } = this.state;
return (
);
}
diff --git a/frontend/src/css/user-notifications.css b/frontend/src/css/user-notifications.css
index 540240785f..4553ecb378 100644
--- a/frontend/src/css/user-notifications.css
+++ b/frontend/src/css/user-notifications.css
@@ -1,17 +1,142 @@
-body {
- overflow: hidden;
+.notification-list-dialog {
+ width: 720px;
+ max-width: 720px;
+ height: calc(100% - 56px);
}
-#wrapper {
+
+.notification-list-dialog .notification-list-content {
height: 100%;
}
-.top-header {
- background: #f4f4f7;
- border-bottom: 1px solid #e8e8e8;
- padding: .5rem 1rem;
- flex-shrink: 0;
+
+.notification-header-close {
+ display: flex;
}
-.op-bar {
- padding: 9px 10px;
- background: #f2f2f2;
- border-radius: 2px;
+
+.notification-header-close .notification-dropdown-toggle {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 24px;
+ width: 24px
+}
+
+.notification-header-close .item-dropdown-icon,
+.notification-header-close .notification-close-icon {
+ height: 24px;
+ width: 24px;
+ cursor: pointer;
+ color: #000;
+ opacity: 0.5;
+}
+
+.notification-header-close .notification-close-icon:hover,
+.notification-header-close .item-dropdown-icon:hover {
+ color: #000;
+ opacity: 0.75;
+}
+
+/* The icon "..." do not need to be bold */
+.notification-header-close .item-dropdown-icon {
+ font-weight: 400;
+}
+
+/* The icon 'x' needs to be bold */
+.notification-header-close .notification-close-icon {
+ font-weight: 700;
+}
+
+.notification-header-close .dropdown-menu {
+ min-width: 8rem;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.notification-header-close .dtable-dropdown-menu.large.dropdown-menu .dropdown-item {
+ padding: 0.25rem 1.5rem;
+ min-height: unset;
+}
+
+.notification-list-content .notification-modal-body {
+ height: 100%;
+ overflow: hidden;
+ padding: 0;
+}
+
+.notification-modal-body .notification-dialog-body {
+ overflow: auto;
+ padding: 2rem 1rem;
+ height: 100%;
+}
+
+.notification-dialog-body table {
+ width: 100%;
+ table-layout: fixed;
+ overflow-y: auto;
+}
+
+.notification-modal-body .notification-dialog-body .paginator {
+ height: 38px;
+}
+
+.notification-dialog-body table thead tr {
+ height: 2.1875rem;
+}
+
+.notification-dialog-body table th {
+ padding: 0.3125rem 0.1875rem;
+ border-bottom: 1px solid #eee;
+ text-align: left;
+ font-weight: normal;
+ line-height: 1.6;
+ color: #9c9c9c;
+}
+
+.notification-dialog-body table tbody tr:hover {
+ background: #f5f5f5;
+ cursor: pointer;
+}
+
+.notification-dialog-body table td {
+ padding: 0.5rem 0.1875rem;
+ border-bottom: 1px solid #eee;
+ color: #333;
+ font-size: 14px;
+ word-break: break-all;
+}
+
+.wechat-dialog-body {
+ display: flex;
+ justify-content: center;
+ padding: 3rem;
+ flex-direction: column;
+ align-items: center;
+}
+
+.wechat-dialog-message {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 1rem;
+ color: #666;
+ font-size: 14px;
+}
+
+.notification-dialog-body .empty-tip {
+ margin: 5.5em 1em;
+ border-radius: 3px;
+ padding: 30px;
+ background-color: #fff;
+ text-align: center;
+}
+
+.notification-dialog-body .empty-tip .no-items-img-tip {
+ width: 100px;
+ height: 100px;
+}
+
+@media (min-width: 768px) {
+ .notification-dialog-body .empty-tip {
+ padding: 30px 80px;
+ }
}
diff --git a/frontend/src/user-notifications.js b/frontend/src/user-notifications.js
index d9fd19c15a..c05da5a9cf 100644
--- a/frontend/src/user-notifications.js
+++ b/frontend/src/user-notifications.js
@@ -1,20 +1,19 @@
import React from 'react';
-import ReactDom from 'react-dom';
-import { navigate } from '@gatsbyjs/reach-router';
+import PropTypes from 'prop-types';
+import { Modal, ModalHeader, ModalBody, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { Utils } from './utils/utils';
-import { gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
+import { gettext } from './utils/constants';
import { seafileAPI } from './utils/seafile-api';
import Loading from './components/loading';
-import Paginator from './components/paginator';
-import CommonToolbar from './components/toolbar/common-toolbar';
import NoticeItem from './components/common/notice-item';
import './css/toolbar.css';
import './css/search.css';
-
import './css/user-notifications.css';
-class UserNotifications extends React.Component {
+const PER_PAGE = 20;
+
+class UserNotificationsDialog extends React.Component {
constructor(props) {
super(props);
@@ -22,19 +21,16 @@ class UserNotifications extends React.Component {
isLoading: true,
errorMsg: '',
currentPage: 1,
- perPage: 25,
hasNextPage: false,
- items: []
+ items: [],
+ isItemMenuShow: false,
};
}
componentDidMount() {
let urlParams = (new URL(window.location)).searchParams;
- const {
- currentPage, perPage
- } = this.state;
+ const { currentPage } = this.state;
this.setState({
- perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => {
this.getItems(this.state.currentPage);
@@ -42,13 +38,13 @@ class UserNotifications extends React.Component {
}
getItems = (page) => {
- const { perPage } = this.state;
- seafileAPI.listNotifications(page, perPage).then((res) => {
+ this.setState({ isLoading: true })
+ seafileAPI.listNotifications(page, PER_PAGE).then((res) => {
this.setState({
isLoading: false,
- items: res.data.notification_list,
+ items: [...this.state.items, ...res.data.notification_list],
currentPage: page,
- hasNextPage: Utils.hasNextPage(page, perPage, res.data.count)
+ hasNextPage: Utils.hasNextPage(page, PER_PAGE, res.data.count)
});
}).catch((error) => {
this.setState({
@@ -56,26 +52,7 @@ class UserNotifications extends React.Component {
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
- }
-
- resetPerPage = (perPage) => {
- this.setState({
- perPage: perPage
- }, () => {
- this.getItems(1);
- });
- }
-
- onSearchedClick = (selectedItem) => {
- if (selectedItem.is_dir === true) {
- let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
- navigate(url, {repalce: true});
- } else {
- let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
- let newWindow = window.open('about:blank');
- newWindow.location.href = url;
- }
- }
+ };
markAllRead = () => {
seafileAPI.updateNotifications().then((res) => {
@@ -91,7 +68,7 @@ class UserNotifications extends React.Component {
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
- }
+ };
clearAll = () => {
seafileAPI.deleteNotifications().then((res) => {
@@ -104,89 +81,61 @@ class UserNotifications extends React.Component {
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
+ };
+
+ toggle = () => {
+ this.props.onNotificationDialogToggle();
+ };
+
+ toggleDropDownMenu = () => {
+ this.setState({isItemMenuShow: !this.state.isItemMenuShow});
+ };
+
+ onHandleScroll = () => {
+ if (!this.state.hasNextPage || this.state.isLoading ||!this.tableRef) {
+ return;
+ }
+ if (this.notificationTableRef.offsetHeight + this.notificationTableRef.scrollTop + 1 >= this.tableRef.offsetHeight) {
+ this.getItems(this.state.currentPage + 1);
+ }
}
- render() {
+ renderHeaderRowBtn = () => {
return (
-
-
-
-
-
-
-
-
{gettext('Notifications')}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {gettext('Mark all read')}
+ {gettext('Clear')}
+
+
+
+
);
}
-}
-
-class Content extends React.Component {
-
- constructor(props) {
- super(props);
- }
-
- getPreviousPage = () => {
- this.props.getListByPage(this.props.currentPage - 1);
- }
-
- getNextPage = () => {
- this.props.getListByPage(this.props.currentPage + 1);
- }
render() {
- const {
- isLoading, errorMsg, items,
- curPerPage, currentPage, hasNextPage
- } = this.props;
-
- if (isLoading) {
- return ;
- }
-
+ const { isLoading, errorMsg, items } = this.state;
+ let content;
if (errorMsg) {
- return {errorMsg}
;
+ content = {errorMsg}
;
}
-
- const isDesktop = Utils.isDesktop();
- const theadData = isDesktop ? [
- {width: '7%', text: ''},
- {width: '73%', text: gettext('Message')},
- {width: '20%', text: gettext('Time')}
- ] : [
- {width: '15%', text: ''},
- {width: '52%', text: gettext('Message')},
- {width: '33%', text: gettext('Time')}
- ];
-
- return (
-
-
+ else {
+ const isDesktop = Utils.isDesktop();
+ const theadData = isDesktop ? [
+ {width: '7%', text: ''},
+ {width: '73%', text: gettext('Message')},
+ {width: '20%', text: gettext('Time')}
+ ] : [
+ {width: '15%', text: ''},
+ {width: '52%', text: gettext('Message')},
+ {width: '33%', text: gettext('Time')}
+ ];
+ content = (
+ this.tableRef = ref}>
{theadData.map((item, index) => {
@@ -200,19 +149,28 @@ class Content extends React.Component {
})}
- {items.length > 0 &&
-
- }
-
+ );
+ if (isLoading) {
+ content = <>{content}>;
+ }
+ }
+
+ return (
+
+ {gettext('Notifications')}
+
+ this.notificationTableRef = ref} onScroll={this.onHandleScroll}>
+ {content}
+
+
+
);
}
}
-ReactDom.render(, document.getElementById('wrapper'));
+UserNotificationsDialog.propTypes = {
+ onNotificationDialogToggle: PropTypes.func.isRequired,
+};
+
+export default UserNotificationsDialog;
diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js
index a3484b7544..1ca7552611 100644
--- a/frontend/src/utils/utils.js
+++ b/frontend/src/utils/utils.js
@@ -1571,4 +1571,34 @@ export const Utils = {
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
},
+ getUrlSearches() {
+ const search = location.search;
+ let searchParams = {};
+ if (search.length === 0) {
+ return searchParams;
+ }
+ let allSearches = search.split('?')[1];
+ let allSearchesArr = allSearches.split('&');
+ allSearchesArr.forEach(item => {
+ let itemArr = item.split('=');
+ searchParams[itemArr[0]] = decodeURI(itemArr[1]);
+ });
+ return searchParams;
+ },
+
+ // If value is null, delete the search parameter; else, add or update the search parameter.
+ updateSearchParameter(key, value) {
+ const { origin, pathname } = location;
+ const searchParams = this.getUrlSearches();
+ searchParams[key] = value;
+ let newSearch = '?';
+ for (let key in searchParams) {
+ let value = searchParams[key];
+ if (value) {
+ newSearch = newSearch === '?' ? `?${key}=${value}` : `${newSearch}&${key}=${value}`;
+ }
+ }
+ history.replaceState(null, '', origin + pathname + newSearch);
+ },
+
};
diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css
index 677ce185ad..54f1172c3b 100644
--- a/media/css/seahub_react.css
+++ b/media/css/seahub_react.css
@@ -728,20 +728,6 @@ a, a:hover { color: #ec8000; }
height: 0;
}
-#notifications {
- position:relative;
- width: 32px;
-}
-#notice-icon {
- position: relative;
- display: block;
-}
-@media (max-width: 390px) {
- #notifications {
- margin-left:8px;
- }
-}
-
/* about dialog */
.about-content {
min-width: 280px;
@@ -749,95 +735,6 @@ a, a:hover { color: #ec8000; }
text-align: center;
}
-/* notifications */
-#notifications .title {
- line-height: 1.5;
- font-size: 1rem;
- color: #322;
- font-weight: normal;
-}
-#notifications .sf2-icon-bell {
- font-size:24px;
- line-height:1;
- color:#999;
- vertical-align: middle;
-}
-#notifications .num {
- position: absolute;
- top: -3px;
- left: 12px;
- padding: 0 2px;
- min-width: 16px;
- height: 16px;
- color: #fff;
- font-size: 9px;
- line-height: 16px;
- text-align: center;
- background: #fc6440;
- border-radius: 100%;
-}
-#notice-popover {
- top:38px;
- right:-12px;
-}
-#notice-popover .outer-caret {
- right:18px;
-}
-#notice-popover a {
- font-weight:normal;
-}
-#notice-popover li {
- padding:9px 0 3px;
- border-bottom:1px solid #dfdfe1;
-}
-#notice-popover li.unread {
- background:#f5f5f7;
- padding-right:10px;
- padding-left:8px;
- border-left:2px solid #feac74;
- margin:0 -10px;
-}
-#notice-popover .avatar {
- border-radius:1000px;
- float:left;
-}
-#notice-popover .brief {
- margin-left:40px;
- margin-bottom: 1rem;
- font-size: 0.8125rem;
- line-height: 1.5rem;
-}
-#notice-popover .time {
- margin:0;
- color:#999;
- text-align:right;
- font-size: 0.8125rem;
- line-height: 1.5rem;
- clear:both;
-}
-#notice-popover .view-all {
- display:block;
- padding:7px 0;
- text-align:center;
- color:#a4a4a4;
-}
-
-#notice-popover .sf-popover-close {
- position: absolute;
- right: 10px;
- top: 17px;
-}
-
-#notice-popover .sf-popover-hd {
- border-bottom: 1px solid #dfdfe1;
- margin: 0 10px;
-}
-
-#notice-popover .sf-popover-con {
- max-height: 25rem;
-}
-
-
/**** sf-popover ****/ /* e.g. top notice popup, group members popup */
.sf-popover-container {
position:relative;
diff --git a/media/css/sf_font3/iconfont.css b/media/css/sf_font3/iconfont.css
index 1cdc7a761c..f7822ebf8f 100644
--- a/media/css/sf_font3/iconfont.css
+++ b/media/css/sf_font3/iconfont.css
@@ -1,10 +1,8 @@
-@font-face {font-family: "sf3-font";
- src: url('iconfont.eot?t=1615973526249'); /* IE9 */
- src: url('iconfont.eot?t=1615973526249#iefix') format('embedded-opentype'), /* IE6-IE8 */
- url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'),
- url('iconfont.woff?t=1615973526249') format('woff'),
- url('iconfont.ttf?t=1615973526249') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
- url('iconfont.svg?t=1615973526249#sf3-font') format('svg'); /* iOS 4.1- */
+@font-face {
+ font-family: "sf3-font"; /* Project id 1230969 */
+ src: url('iconfont.woff2?t=1694053452699') format('woff2'),
+ url('iconfont.woff?t=1694053452699') format('woff'),
+ url('iconfont.ttf?t=1694053452699') format('truetype');
}
.sf3-font {
@@ -15,6 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
+.sf3-font-x-01:before {
+ content: "\e7d8";
+}
+
+.sf3-font-more-level:before {
+ content: "\e7d7";
+}
+
.sf3-font-desktop:before {
content: "\e720";
}
diff --git a/media/css/sf_font3/iconfont.js b/media/css/sf_font3/iconfont.js
index aaa29ca861..61fece4fcd 100644
--- a/media/css/sf_font3/iconfont.js
+++ b/media/css/sf_font3/iconfont.js
@@ -1 +1 @@
-!function(c){var t,h,l,e,o,v,s='',i=(i=document.getElementsByTagName("script"))[i.length-1].getAttribute("data-injectcss");if(i&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}function m(){o||(o=!0,l())}t=function(){var c,t,h,l;(l=document.createElement("div")).innerHTML=s,s=null,(h=l.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",c=h,(t=document.body).firstChild?(l=c,(h=t.firstChild).parentNode.insertBefore(l,h)):t.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(l=t,e=c.document,o=!1,(v=function(){try{e.documentElement.doScroll("left")}catch(c){return void setTimeout(v,50)}m()})(),e.onreadystatechange=function(){"complete"==e.readyState&&(e.onreadystatechange=null,m())})}(window);
\ No newline at end of file
+window._iconfont_svg_string_1230969='',function(l){var c=(c=document.getElementsByTagName("script"))[c.length-1],t=c.getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,o,e,s,v,i=function(c,t){t.parentNode.insertBefore(c,t)};if(t&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}h=function(){var c,t=document.createElement("div");t.innerHTML=l._iconfont_svg_string_1230969,(t=t.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",t=t,(c=document.body).firstChild?i(t,c.firstChild):c.appendChild(t))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),h()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(e=h,s=l.document,v=!1,m(),s.onreadystatechange=function(){"complete"==s.readyState&&(s.onreadystatechange=null,n())})}function n(){v||(v=!0,e())}function m(){try{s.documentElement.doScroll("left")}catch(c){return void setTimeout(m,50)}n()}}(window);
\ No newline at end of file
diff --git a/media/css/sf_font3/iconfont.ttf b/media/css/sf_font3/iconfont.ttf
index b5b17c91e1..501c0ddb99 100644
Binary files a/media/css/sf_font3/iconfont.ttf and b/media/css/sf_font3/iconfont.ttf differ
diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff
index 8a04acf07f..250ee11b92 100644
Binary files a/media/css/sf_font3/iconfont.woff and b/media/css/sf_font3/iconfont.woff differ
diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2
index e04d6f49f1..0db0ca952d 100644
Binary files a/media/css/sf_font3/iconfont.woff2 and b/media/css/sf_font3/iconfont.woff2 differ
diff --git a/seahub/notifications/templates/notifications/notice_email.html b/seahub/notifications/templates/notifications/notice_email.html
index 1f4dff9e62..cddb3a32d1 100644
--- a/seahub/notifications/templates/notifications/notice_email.html
+++ b/seahub/notifications/templates/notifications/notice_email.html
@@ -137,6 +137,6 @@ You've got {{num}} new notices on {{ site_name }}:
{% trans "Go check out at the following page:" %}
- {{ url_base }}{% url 'user_notification_list' %}
+ {{ url_base }}?notifications=all
{% endblock %}
diff --git a/seahub/urls.py b/seahub/urls.py
index ac843fc1d7..e6a0dffd79 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -684,7 +684,6 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/invitations/(?P[a-f0-9]{32})/$', AdminInvitation.as_view(), name='api-v2.1-admin-invitation'),
path('avatar/', include('seahub.avatar.urls')),
- path('notice/', include('seahub.notifications.urls')),
path('contacts/', include('seahub.contacts.urls')),
path('group/', include('seahub.group.urls')),
path('options/', include('seahub.options.urls')),
diff --git a/tests/seahub/notifications/__init__.py b/tests/seahub/notifications/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/seahub/notifications/management/__init__.py b/tests/seahub/notifications/management/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/seahub/notifications/management/commands/__init__.py b/tests/seahub/notifications/management/commands/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/seahub/notifications/management/commands/test_notify_admins_on_virus.py b/tests/seahub/notifications/management/commands/test_notify_admins_on_virus.py
deleted file mode 100644
index 756aa4829a..0000000000
--- a/tests/seahub/notifications/management/commands/test_notify_admins_on_virus.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from django.core import mail
-from django.core.management import call_command
-from django.urls import path
-from django.test import override_settings
-
-import seahub
-from seahub import urls
-from seahub.test_utils import BaseTestCase
-from seahub.views.sysadmin import sysadmin_react_fake_view
-
-urlpatterns = seahub.urls.urlpatterns + [
- path('sys/virus-scan-records/', sysadmin_react_fake_view, name='sys_virus_scan_records'),
-]
-
-@override_settings(ROOT_URLCONF=__name__)
-class CommandTest(BaseTestCase):
-
- def test_can_send(self):
- self.assertEqual(len(mail.outbox), 0)
-
- call_command('notify_admins_on_virus', "%s:%s" % (self.repo.id, self.file))
- assert len(mail.outbox) != 0
-
- def test_can_send_to_repo_owner(self):
- self.assertEqual(len(mail.outbox), 0)
-
- call_command('notify_admins_on_virus', "%s:%s" % (self.repo.id, self.file))
- assert len(mail.outbox) != 0
- assert self.user.username in [e.to[0] for e in mail.outbox]
-
- @override_settings(VIRUS_SCAN_NOTIFY_LIST=['a@a.com', 'b@b.com'])
- def test_can_send_to_nofity_list(self):
- self.assertEqual(len(mail.outbox), 0)
-
- call_command('notify_admins_on_virus', "%s:%s" % (self.repo.id, self.file))
- assert len(mail.outbox) != 0
- assert 'a@a.com' in [e.to[0] for e in mail.outbox]
- assert 'b@b.com' in [e.to[0] for e in mail.outbox]
diff --git a/tests/seahub/notifications/management/commands/test_send_file_updates.py b/tests/seahub/notifications/management/commands/test_send_file_updates.py
deleted file mode 100644
index 3eb27d13ea..0000000000
--- a/tests/seahub/notifications/management/commands/test_send_file_updates.py
+++ /dev/null
@@ -1,194 +0,0 @@
-# encoding: utf-8
-import time
-import datetime
-from mock import patch
-
-from django.core import mail
-from django.core.management import call_command
-from django.utils import timezone
-from django.test import override_settings
-
-from seahub.test_utils import BaseTestCase
-from seahub.options.models import UserOptions
-
-
-class Record(object):
- def __init__(self, **entries):
- self.__dict__.update(entries)
-
-
-class CommandTest(BaseTestCase):
-
- def _repo_evs(self, ):
- l = [
- {'username': self.user.username, 'commit_id': None, 'obj_type': 'repo', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'timestamp': datetime.datetime(2018, 11, 5, 6, 46, 2), 'op_type': 'create', 'path': '/', 'id': 254, 'op_user': 'foo@foo.com', 'repo_name': 'tests\\'},
- {'username': self.user.username, 'commit_id': None, 'obj_type': 'repo', 'repo_id': 'f8dc0bc8-eae0-4063-9beb-790071168794', 'timestamp': datetime.datetime(2018, 11, 6, 9, 52, 6), 'op_type': 'delete', 'path': '/', 'id': 289, 'op_user': 'foo@foo.com', 'repo_name': '123'},
- {'username': self.user.username, 'commit_id': '93fb5d8f07e03e5c947599cd7c948965426aafec', 'obj_type': 'repo', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'timestamp': datetime.datetime(2018, 11, 7, 2, 35, 34), 'old_repo_name': 'tests\\', 'op_type': 'rename', 'path': '/', 'id': 306, 'op_user': 'foo@foo.com', 'repo_name': 'tests\\123'},
- {'username': self.user.username, 'commit_id': None, 'obj_type': 'repo', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'timestamp': datetime.datetime(2018, 11, 7, 3, 13, 2), 'days': 0, 'op_type': 'clean-up-trash', 'path': '/', 'id': 308, 'op_user': 'foo@foo.com', 'repo_name': 'tests\\123'},
- {'username': self.user.username, 'commit_id': None, 'obj_type': 'repo', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'timestamp': datetime.datetime(2018, 11, 7, 3, 12, 43), 'days': 3, 'op_type': 'clean-up-trash', 'path': '/', 'id': 307, 'op_user': 'foo@foo.com', 'repo_name': 'tests\\123'},
- ]
-
- return [Record(**x) for x in l]
-
- def _dir_evs(self, ):
- l = [
- {'username': self.user.username, 'commit_id': '8ff6473e9ef5229a632e1481a1b28d52673220ec', 'obj_type': 'dir', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '0000000000000000000000000000000000000000', 'timestamp': datetime.datetime(2018, 11, 6, 9, 10, 45), 'op_type': 'create', 'path': '/xx', 'id': 260, 'op_user': 'foo@foo.com', 'repo_name': 'tests\\'},
-
- {'username': self.user.username, 'commit_id': 'bb3ef321899d2f75ecf56098cb89e6b13c48cff9', 'obj_type': 'dir', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '0000000000000000000000000000000000000000', 'timestamp': datetime.datetime(2018, 11, 6, 9, 27, 3), 'op_type': 'delete', 'path': '/aa', 'id': 268, 'op_user': 'foo@foo.com', 'repo_name': 'tests\\'},
-
- {'username': self.user.username, 'commit_id': '016435e95ace96902ea1bfa1e7688f45804d5aa4', 'obj_type': 'dir', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '95421aa563cf474dce02b7fadc532c17c11cd97a', 'timestamp': datetime.datetime(2018, 11, 6, 9, 38, 32), 'old_path': '/11', 'op_type': 'move', 'path': '/new/11', 'repo_name': 'tests\\', 'id': 283, 'op_user': 'foo@foo.com', 'size': -1},
-
- {'username': self.user.username, 'commit_id': '712504f1cfd94b0813763a106eb4140a5dba156a', 'obj_type': 'dir', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '4d83a9b62084fef33ec99787425f91df356ae307', 'timestamp': datetime.datetime(2018, 11, 6, 9, 39, 10), 'old_path': '/new', 'op_type': 'rename', 'path': '/new2', 'repo_name': 'tests\\', 'id': 284, 'op_user': 'foo@foo.com', 'size': -1},
-
- {'username': self.user.username, 'commit_id': '2f7021e0804187b8b09ec82142e0f8b53771cc69', 'obj_type': 'dir', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '0000000000000000000000000000000000000000', 'timestamp': datetime.datetime(2018, 11, 6, 9, 27, 6), 'op_type': 'recover', 'path': '/aa', 'id': 269, 'op_user': 'foo@foo.com', 'repo_name': 'tests\\'},
- ]
-
- return [Record(**x) for x in l]
-
- def _file_evs(self, ):
- l = [
- {'username': self.user.username, 'commit_id': '658d8487b7e8916ee25703fbdf978b98ab76e3d4', 'obj_type': 'file', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '0000000000000000000000000000000000000000', 'timestamp': datetime.datetime(2018, 11, 6, 9, 38, 23), 'op_type': 'create', 'path': '/11/new/aa/new/yy/xx/bb/1.txt', 'repo_name': 'tests\\', 'id': 282, 'op_user': 'foo@foo.com', 'size': 0},
-
- {'username': self.user.username, 'commit_id': '04df2a831ba485bb6f216f62c1b47883c3e3433c', 'obj_type': 'file', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': 'd16369af225687671348897a0ad918261866af5d', 'timestamp': datetime.datetime(2018, 11, 6, 9, 0, 14), 'op_type': 'delete', 'path': '/aa1.txt', 'repo_name': 'tests\\', 'id': 257, 'op_user': 'foo@foo.com', 'size': 2},
-
- {'username': self.user.username, 'commit_id': '612f605faa112e4e8928dc08e91c669cea92ef59', 'obj_type': 'file', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': 'd16369af225687671348897a0ad918261866af5d', 'timestamp': datetime.datetime(2018, 11, 6, 9, 0, 22), 'op_type': 'recover', 'path': '/aa1.txt', 'repo_name': 'tests\\', 'id': 258, 'op_user': 'foo@foo.com', 'size': 2},
-
- {'username': self.user.username, 'commit_id': '106e6e12138bf0e12fbd558da73ff24502807f3e', 'obj_type': 'file', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '28054f8015aada8b5232943d072526541f5227f9', 'timestamp': datetime.datetime(2018, 11, 6, 9, 0, 30), 'op_type': 'edit', 'path': '/aa1.txt', 'repo_name': 'tests\\', 'id': 259, 'op_user': 'foo@foo.com', 'size': 4},
-
- {'username': self.user.username, 'commit_id': '1c9a12a2d8cca79f261eb7c65c118a3ea4f7b850', 'obj_type': 'file', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '28054f8015aada8b5232943d072526541f5227f9', 'timestamp': datetime.datetime(2018, 11, 6, 9, 36, 45), 'old_path': '/11/new/aa/new/yy/xx/aa4.txt', 'op_type': 'move', 'path': '/aa4.txt', 'repo_name': 'tests\\', 'id': 279, 'op_user': 'foo@foo.com', 'size': 4},
-
- {'username': self.user.username, 'commit_id': '19cab0f3c53ee00cffe6eaa65f256ccc35a77a72', 'obj_type': 'file', 'repo_id': '7d6b3f36-3ce1-45f1-8c82-b9532e3162c7', 'obj_id': '28054f8015aada8b5232943d072526541f5227f9', 'timestamp': datetime.datetime(2018, 11, 6, 9, 36, 59), 'old_path': '/aa4.txt', 'op_type': 'rename', 'path': '/aa5.txt', 'repo_name': 'tests\\', 'id': 280, 'op_user': 'foo@foo.com', 'size': 4},
- ]
-
- return [Record(**x) for x in l]
-
- @patch('seahub.notifications.management.commands.send_file_updates.get_user_activities_by_timestamp')
- def test_dir_evs(self, mock_get_user_activities_by_timestamp):
- mock_get_user_activities_by_timestamp.return_value = self._dir_evs()
-
- UserOptions.objects.set_file_updates_email_interval(
- self.user.email, 30)
- self.assertEqual(len(mail.outbox), 0)
-
- call_command('send_file_updates')
- mock_get_user_activities_by_timestamp.assert_called_once()
-
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
- for op in ['Created', 'Deleted', 'Moved', 'Restored', 'Renamed', ]:
- assert op in mail.outbox[0].body
-
- @patch('seahub.notifications.management.commands.send_file_updates.get_user_activities_by_timestamp')
- def test_file_evs(self, mock_get_user_activities_by_timestamp):
- mock_get_user_activities_by_timestamp.return_value = self._file_evs()
-
- UserOptions.objects.set_file_updates_email_interval(
- self.user.email, 30)
-
- self.assertEqual(len(mail.outbox), 0)
-
- call_command('send_file_updates')
- mock_get_user_activities_by_timestamp.assert_called_once()
-
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
-
- for op in ['Created', 'Deleted', 'Restored', 'Updated', 'Moved',
- 'Renamed', ]:
- assert op in mail.outbox[0].body
-
- @patch('seahub.notifications.management.commands.send_file_updates.get_user_activities_by_timestamp')
- def test_repo_evs(self, mock_get_user_activities_by_timestamp):
- mock_get_user_activities_by_timestamp.return_value = self._repo_evs()
-
- UserOptions.objects.set_file_updates_email_interval(
- self.user.email, 30)
-
- self.assertEqual(len(mail.outbox), 0)
-
- call_command('send_file_updates')
- mock_get_user_activities_by_timestamp.assert_called_once()
-
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
-
- for op in ['Created', 'Deleted', 'Renamed', 'Removed']:
- assert op in mail.outbox[0].body
-
- @patch('seahub.notifications.management.commands.send_file_updates.get_user_activities_by_timestamp')
- def test_seafevents_api(self, mock_get_user_activities_by_timestamp):
- mock_get_user_activities_by_timestamp.return_value = self._repo_evs()
-
- username = self.user.username
- UserOptions.objects.set_file_updates_email_interval(username, 30)
- assert UserOptions.objects.get_file_updates_last_emailed_time(username) is None
-
- today = datetime.datetime.utcnow().replace(hour=0).replace(
- minute=0).replace(second=0).replace(microsecond=0)
-
- before_dt = datetime.datetime.utcnow().replace(microsecond=0)
- call_command('send_file_updates')
- after_dt = datetime.datetime.utcnow().replace(microsecond=0)
-
- mock_get_user_activities_by_timestamp.assert_called_once()
- args = mock_get_user_activities_by_timestamp.call_args[0]
- assert args[0] == username
- assert args[1] == today
-
- last_emailed_dt = UserOptions.objects.get_file_updates_last_emailed_time(username)
- assert before_dt <= last_emailed_dt
- assert last_emailed_dt <= after_dt
- assert last_emailed_dt == args[2]
-
- @patch('seahub.notifications.management.commands.send_file_updates.get_user_activities_by_timestamp')
- def test_email_interval(self, mock_get_user_activities_by_timestamp):
- mock_get_user_activities_by_timestamp.return_value = self._repo_evs()
-
- username = self.user.username
- assert UserOptions.objects.get_file_updates_last_emailed_time(username) is None
-
- # assume this command will be finished in 5 seconds
- UserOptions.objects.set_file_updates_email_interval(username, 5)
- assert mock_get_user_activities_by_timestamp.called is False
- call_command('send_file_updates')
- assert mock_get_user_activities_by_timestamp.called is True
-
- # still within 5 seconds ...
- mock_get_user_activities_by_timestamp.reset_mock()
- assert mock_get_user_activities_by_timestamp.called is False
- call_command('send_file_updates')
- assert mock_get_user_activities_by_timestamp.called is False
-
- time.sleep(5) # 5 seconds passed
-
- mock_get_user_activities_by_timestamp.reset_mock()
- assert mock_get_user_activities_by_timestamp.called is False
- call_command('send_file_updates')
- assert mock_get_user_activities_by_timestamp.called is True
-
- @override_settings(TIME_ZONE='Asia/Shanghai')
- @patch('seahub.notifications.management.commands.send_file_updates.get_user_activities_by_timestamp')
- def test_timezone_in_email_body(self, mock_get_user_activities_by_timestamp):
- assert timezone.get_default_timezone_name() == 'Asia/Shanghai'
- mock_get_user_activities_by_timestamp.return_value = self._repo_evs()
-
- UserOptions.objects.set_file_updates_email_interval(
- self.user.email, 30)
-
- self.assertEqual(len(mail.outbox), 0)
- call_command('send_file_updates')
- self.assertEqual(len(mail.outbox), 1)
- assert '2018-11-05 14:46:02' in mail.outbox[0].body
-
- @patch('seahub.notifications.management.commands.send_file_updates.get_user_activities_by_timestamp')
- def test_invalid_option_vals(self, mock_get_user_activities_by_timestamp):
- mock_get_user_activities_by_timestamp.return_value = self._repo_evs()
-
- UserOptions.objects.set_file_updates_email_interval(
- self.user.email, 'a')
-
- try:
- call_command('send_file_updates')
- assert True
- except Exception:
- assert False
diff --git a/tests/seahub/notifications/management/commands/test_send_notices.py b/tests/seahub/notifications/management/commands/test_send_notices.py
deleted file mode 100644
index 5317394727..0000000000
--- a/tests/seahub/notifications/management/commands/test_send_notices.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# encoding: utf-8
-from django.core import mail
-from django.core.management import call_command
-
-from seahub.invitations.models import Invitation
-from seahub.notifications.models import (
- UserNotification, repo_share_msg_to_json, file_comment_msg_to_json,
- guest_invitation_accepted_msg_to_json, repo_share_to_group_msg_to_json,
- file_uploaded_msg_to_json, group_join_request_to_json,
- add_user_to_group_to_json, group_msg_to_json)
-from seahub.profile.models import Profile
-from seahub.test_utils import BaseTestCase
-from seahub.notifications.management.commands.send_notices import Command
-from seahub.share.utils import share_dir_to_user, share_dir_to_group
-from seahub.options.models import UserOptions
-
-try:
- from seahub.settings import LOCAL_PRO_DEV_ENV
-except ImportError:
- LOCAL_PRO_DEV_ENV = False
-
-
-class CommandTest(BaseTestCase):
-
- def setUp(self):
- super(CommandTest, self).setUp()
- UserOptions.objects.set_file_updates_email_interval(self.user.username, 3600)
- UserOptions.objects.set_collaborate_email_interval(self.user.username, 3600)
-
- def tearDown(self):
- UserOptions.objects.unset_file_updates_last_emailed_time(self.user.username)
- UserOptions.objects.unset_collaborate_last_emailed_time(self.user.username)
- super(CommandTest, self).tearDown()
-
- def test_can_send_repo_share_msg(self):
- self.assertEqual(len(mail.outbox), 0)
- UserNotification.objects.add_repo_share_msg(
- self.user.username, repo_share_msg_to_json('bar@bar.com', self.repo.id, '/', None))
-
- call_command('send_notices')
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
- assert 'bar has shared a library named' in mail.outbox[0].body
-
- def test_can_send_folder_share_msg(self):
- self.assertEqual(len(mail.outbox), 0)
- share_dir_to_user(self.repo, self.folder, 'bar@bar.com', 'bar@bar.com', self.user.username, 'rw', org_id=None)
- UserNotification.objects.add_repo_share_msg(
- self.user.username, repo_share_msg_to_json('bar@bar.com', self.repo.id, self.folder, None))
-
- call_command('send_notices')
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
- assert 'bar has shared a folder named' in mail.outbox[0].body
-
- def test_can_send_repo_share_to_group_msg(self):
- self.assertEqual(len(mail.outbox), 0)
- UserNotification.objects.add_repo_share_to_group_msg(
- self.user.username,
- repo_share_to_group_msg_to_json('bar@bar.com', self.repo.id, self.group.id, '/', None))
-
- call_command('send_notices')
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
- assert 'bar has shared a library named' in mail.outbox[0].body
- assert 'group/%d' % self.group.id in mail.outbox[0].body
-
- def test_can_send_folder_share_to_group_msg(self):
- folder_path = self.folder
- share_dir_to_group(self.repo, folder_path, self.user.username,
- self.user.username, self.group.id, 'rw', None)
- UserNotification.objects.add_repo_share_to_group_msg(
- self.user.username,
- repo_share_to_group_msg_to_json('bar@bar.com', self.repo.id,
- self.group.id, folder_path, None))
- call_command('send_notices')
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
- assert 'bar has shared a folder named' in mail.outbox[0].body
- assert 'group/%d' % self.group.id in mail.outbox[0].body
-
- # def test_can_send_with_Chinese_lang(self):
- # self.assertEqual(len(mail.outbox), 0)
- # UserNotification.objects.add_repo_share_msg(
- # self.user.username, repo_share_msg_to_json('bar@bar.com', self.repo.id, '/', None))
- # Profile.objects.add_or_update(self.user.username, 'nickname', lang_code='zh-cn')
-
- # call_command('send_notices')
- # self.assertEqual(len(mail.outbox), 1)
- # assert mail.outbox[0].to[0] == self.user.username
- # assert u'bar 共享了资料库' in mail.outbox[0].body
-
- def test_can_send_to_contact_email(self):
- self.assertEqual(len(mail.outbox), 0)
- UserNotification.objects.add_repo_share_msg(
- self.user.username, repo_share_msg_to_json('bar@bar.com', self.repo.id, '/', None))
- p = Profile.objects.add_or_update(self.user.username, 'nickname')
- p.contact_email = 'contact@foo.com'
- p.save()
-
- call_command('send_notices')
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == 'contact@foo.com'
-
- def test_send_file_comment_notice(self):
- self.assertEqual(len(mail.outbox), 0)
-
- detail = file_comment_msg_to_json(self.repo.id, '/foo',
- 'bar@bar.com', 'test comment')
- UserNotification.objects.add_file_comment_msg(self.user.username, detail)
-
- call_command('send_notices')
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
- assert 'new comment from user %s' % 'bar@bar.com' in mail.outbox[0].body
- assert '/foo' in mail.outbox[0].body
-
- def test_send_guest_invitation_notice(self):
- self.assertEqual(len(mail.outbox), 0)
-
- inv = Invitation.objects.add(self.user.username, 'test@test.com')
- inv.accept()
-
- detail = guest_invitation_accepted_msg_to_json(inv.pk)
- UserNotification.objects.add_guest_invitation_accepted_msg(
- inv.inviter, detail)
-
- call_command('send_notices')
- self.assertEqual(len(mail.outbox), 1)
- assert mail.outbox[0].to[0] == self.user.username
- assert 'Guest test@test.com' in mail.outbox[0].body
-
- def test_format_repo_share_msg(self):
- if not LOCAL_PRO_DEV_ENV:
- return
-
- detail = repo_share_msg_to_json('share@share.com', self.repo.id, '', -1)
- notice = UserNotification.objects.add_repo_share_msg('to@to.com', detail)
- resp = Command().format_repo_share_msg(notice)
-
- assert resp.repo_url == '/library/%(repo_id)s/%(repo_name)s/%(path)s' % {
- 'repo_id': self.repo.id, 'repo_name': self.repo.name, 'path': ''}
-
- def test_format_repo_share_to_group_msg(self):
- if not LOCAL_PRO_DEV_ENV:
- return
-
- detail = repo_share_to_group_msg_to_json('repo@share.com', self.repo.id, self.group.id, '', -1)
- notice = UserNotification.objects.add_repo_share_to_group_msg('group@share.com', detail)
- resp = Command().format_repo_share_to_group_msg(notice)
-
- assert resp.repo_url == '/library/%(repo_id)s/%(repo_name)s/%(path)s' % {
- 'repo_id': self.repo.id, 'repo_name': self.repo.name, 'path': ''}
- assert resp.group_url == '/group/%(group_id)s/' % {'group_id': self.group.id}
-
- def test_format_file_uploaded_msg(self):
- upload_to = '/'
- detail = file_uploaded_msg_to_json('upload_msg', self.repo.id, upload_to)
- notice = UserNotification.objects.add_file_uploaded_msg('file@upload.com', detail)
- resp = Command().format_file_uploaded_msg(notice)
-
- assert resp.folder_link == '/library/%(repo_id)s/%(repo_name)s/%(path)s' % {
- 'repo_id': self.repo.id, 'repo_name': self.repo.name, 'path': upload_to.strip('/')}
-
- def test_format_group_join_request(self):
- detail = group_join_request_to_json('group_join', self.group.id, 'join_request_msg')
- notice = UserNotification.objects.add_group_join_request_notice('group_join',
- detail=detail)
- resp = Command().format_group_join_request(notice)
-
- assert resp.grpjoin_group_url == '/#group/%(group_id)s/members/' % {'group_id': self.group.id}
-
- def test_format_add_user_to_group(self):
- detail = add_user_to_group_to_json(self.user.username, self.group.id)
- notice = UserNotification.objects.set_add_user_to_group_notice(self.user.username,
- detail=detail)
- resp = Command().format_add_user_to_group(notice)
-
- assert resp.group_url == '/group/%(group_id)s/' % {'group_id': self.group.id}
diff --git a/tests/seahub/notifications/test_models.py b/tests/seahub/notifications/test_models.py
deleted file mode 100644
index 4c450b0476..0000000000
--- a/tests/seahub/notifications/test_models.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from seahub.notifications.models import (
- UserNotification, repo_share_msg_to_json, file_comment_msg_to_json,
- repo_share_to_group_msg_to_json, file_uploaded_msg_to_json,
- group_join_request_to_json, add_user_to_group_to_json, group_msg_to_json)
-from seahub.share.utils import share_dir_to_user, share_dir_to_group
-from seahub.test_utils import BaseTestCase
-
-
-class UserNotificationTest(BaseTestCase):
- def setUp(self):
- self.clear_cache()
-
- def test_format_file_comment_msg(self):
- detail = file_comment_msg_to_json(self.repo.id, self.file,
- self.user.username, 'test comment')
- notice = UserNotification.objects.add_file_comment_msg('a@a.com', detail)
-
- msg = notice.format_file_comment_msg()
- assert msg is not None
- assert 'new comment from user' in msg
-
- def test_format_file_uploaded_msg(self):
- upload_to = '/'
- detail = file_uploaded_msg_to_json('upload_msg', self.repo.id, upload_to)
- notice = UserNotification.objects.add_file_uploaded_msg('file@upload.com', detail)
-
- msg = notice.format_file_uploaded_msg()
- assert '/library/%(repo_id)s/%(repo_name)s/%(path)s' % {'repo_id': self.repo.id,
- 'repo_name': self.repo.name,
- 'path': upload_to.strip('/')} in msg
-
- def test_format_group_join_request(self):
- detail = group_join_request_to_json('group_join', self.group.id, 'join_request_msg')
- notice = UserNotification.objects.add_group_join_request_notice('group_join',
- detail=detail)
- msg = notice.format_group_join_request()
- assert '/#group/%(group_id)s/members/' % {'group_id': self.group.id} in msg
-
- def test_format_add_user_to_group(self):
- detail = add_user_to_group_to_json(self.user.username, self.group.id)
- notice = UserNotification.objects.set_add_user_to_group_notice(self.user.username,
- detail=detail)
- msg = notice.format_add_user_to_group()
- assert '/group/%(group_id)s/' % {'group_id': self.group.id} in msg
-
- def test_format_repo_share_msg(self):
- notice = UserNotification.objects.add_repo_share_msg(
- self.user.username,
- repo_share_msg_to_json('bar@bar.com', self.repo.id, '/', None))
-
- msg = notice.format_repo_share_msg()
- assert msg is not None
- assert 'bar has shared a library named' in msg
- assert '/library/%(repo_id)s/%(repo_name)s/%(path)s' % {
- 'repo_id': self.repo.id,
- 'repo_name': self.repo.name,
- 'path': ''} in msg
-
- def test_format_repo_share_msg_with_folder(self):
- folder_path = self.folder
- share_dir_to_user(self.repo, folder_path, self.user.username,
- self.user.username, 'bar@bar.com', 'rw', None)
- notice = UserNotification.objects.add_repo_share_msg(
- self.user.username,
- repo_share_msg_to_json('bar@bar.com', self.repo.id, folder_path, None))
- msg = notice.format_repo_share_msg()
-
- assert msg is not None
- assert 'bar has shared a folder named' in msg
-
- def test_format_repo_share_to_group_msg(self):
- notice = UserNotification.objects.add_repo_share_to_group_msg(
- self.user.username,
- repo_share_to_group_msg_to_json('bar@bar.com', self.repo.id, self.group.id, '/', None))
-
- msg = notice.format_repo_share_to_group_msg()
- assert msg is not None
- assert 'bar has shared a library named' in msg
- assert '/group/%(group_id)s/' % {'group_id': self.group.id} in msg
-
- def test_format_repo_share_to_group_msg_with_folder(self):
- folder_path = self.folder
- share_dir_to_group(self.repo, folder_path, self.user.username,
- self.user.username, self.group.id, 'rw', None)
- notice = UserNotification.objects.add_repo_share_to_group_msg(
- self.user.username,
- repo_share_to_group_msg_to_json('bar@bar.com', self.repo.id, self.group.id, folder_path, None))
- msg = notice.format_repo_share_to_group_msg()
-
- assert msg is not None
- assert 'bar has shared a folder named' in msg