From b46a60268ffa648e1dcc48127d75ca17dd5f8b86 Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Fri, 8 Sep 2023 13:54:05 +0800 Subject: [PATCH] change notification popover (#5629) * change notification popover delete useless test * Update iconfont and user notification ui * change notification dialog detail * format css * update url of all notifications when send notice email --------- Co-authored-by: lian --- frontend/config/webpack.entry.js | 1 - frontend/src/components/common/notice-item.js | 1 - .../common/notification-popover/index.css | 186 ++++++++++++++++ .../common/notification-popover/index.js | 83 +++++++ .../src/components/common/notification.css | 126 +++++++++++ .../src/components/common/notification.js | 66 ++++-- frontend/src/css/user-notifications.css | 149 ++++++++++++- frontend/src/user-notifications.js | 206 +++++++----------- frontend/src/utils/utils.js | 30 +++ media/css/seahub_react.css | 103 --------- media/css/sf_font3/iconfont.css | 20 +- media/css/sf_font3/iconfont.js | 2 +- media/css/sf_font3/iconfont.ttf | Bin 5684 -> 6056 bytes media/css/sf_font3/iconfont.woff | Bin 3668 -> 3928 bytes media/css/sf_font3/iconfont.woff2 | Bin 2972 -> 3268 bytes .../templates/notifications/notice_email.html | 2 +- seahub/urls.py | 1 - tests/seahub/notifications/__init__.py | 0 .../notifications/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/test_notify_admins_on_virus.py | 38 ---- .../commands/test_send_file_updates.py | 194 ----------------- .../management/commands/test_send_notices.py | 179 --------------- tests/seahub/notifications/test_models.py | 91 -------- 24 files changed, 710 insertions(+), 768 deletions(-) create mode 100644 frontend/src/components/common/notification-popover/index.css create mode 100644 frontend/src/components/common/notification-popover/index.js create mode 100644 frontend/src/components/common/notification.css delete mode 100644 tests/seahub/notifications/__init__.py delete mode 100644 tests/seahub/notifications/management/__init__.py delete mode 100644 tests/seahub/notifications/management/commands/__init__.py delete mode 100644 tests/seahub/notifications/management/commands/test_notify_admins_on_virus.py delete mode 100644 tests/seahub/notifications/management/commands/test_send_file_updates.py delete mode 100644 tests/seahub/notifications/management/commands/test_send_notices.py delete mode 100644 tests/seahub/notifications/test_models.py 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 (
- - {this.state.unseenCount} + + {unseenCount} -
-
-
-

{gettext('Notifications')}

- -
-
-
-
+ + } + {this.state.isShowNotificationDialog && + + }
); } 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 ( - -
-
- - logo - - -
-
-
-
-
-

{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 b5b17c91e103d878a4d5ae2644e70ec3573a84d6..501c0ddb9935ad61c447ad799344c3a619657dc5 100644 GIT binary patch literal 6056 zcmd@&4R0IgdGB+d{Ji7oct?t|q~nKN$Hgb**O5|#|^yZ1MC8Nc847$Go>*u!_4D7<6P!W74p+vC;Weiv{!LtPa#75 zDfs4|;hg`2;<2Z`EGLt@}I=Z zIRroH_*Z-mLg+8}{@tC-+Q~S{;@@aP&dgeRyHy(uJ@E6#_3NL%cAI?--Y6@24k23_tl?_&%nuPu zDYoD*Y7Oxm`4-MQNB}WXovGhjKU&Y%%k?w$SL@%Yf46>RSzgwcjpYxhf7D^!`nR0* zA2-+Er|bXA1$c(B?vo(&V>Um8v=EgjD1}TU;z#Rx(EHtJ1re>lqm(SF86^j5lmG8m z!}tHAsRotWr7=*8q>eE`2kMj5?`;g!EU6!D4Ae2H=NkjHP3q;wK)sXtnZ`g1kUIJW z9W>JF-);=*Gxb*+1MNfV-)RiA6sdo=F=#B+f7}>oJyO5Y7&Mxe@jNy{!^8szgU z_!a7)6+M+eW)xB#5R@R+4_&uTvgyW3JZ7>U!y2^==VqTZtg&n~njNzYyE?Kpx+9U; z5#2fh)y^h`VRfzpQ@fpQyXn!Wf$OiZKVfquO1kj$l&&d3As*F3>6A`^n3>7$fUJeU zkdn!!`KS_Qmm;Pau`jazbKzJlY-i~GoL{j2isD)Ss>yuF=FG?{(nZYJU^F@yLmJVYKPkFS%Zc`|50p%;eYT6{>=(nBa; zJH+XLD`syfFOyZ*o~B6=-7t4{L~&ae-K7OZ0d=0qcF`xN&ke6Cnx@#V2XsBa z9@UlZgLri-`pUr`y711YE6@7cTK)dkwy@v-gLSlnimog68}$D6rz`NYm6g?}DP_AG zDJ90Mx3~9pbilDSBG|9!_M6BV`NO`KUecBK4zB15MIT;Sxl5Eoo5GS5-V|cKtOxAZ z=}9n2rMd|pTv=H;FG^u4thi*o70<#GdfV4@{SWM4vQdm7KWW39m?AlHm^?rVWS%_Y z=sIJfsfBikcEyL%A=4TK)NcqKKrlm86;vgFnopa_s20@IQIj1?Yv>kQG;3w8cvnmm zL?IZ`LwXA5*-Ww@#D*S+MnSankZ2~w2A`N2%32{xuUSnJGs|v|g;yU7#hK6S!HXAP z_IP+jlvJiga<}}hqy-~d(7tr>FC@YKtW|+x?&FJzlwAduWjWyj)bE}cHPCfUPgA-{vDEd^t9AsTCwT&LP z`uD~}SMS9@Yd`FnP{$JWHc4x_xVdZB#Mjr(Lf{L>DdTWB_UuH!v)AKKBEP{{%wfK(O( z&Wz){9UAkUfOCfA?>q8TmuYrAb>zOkalrB1Tz5~c$_ z_+)7=H_ohRlKo3c3aX^pQ#ZF@(9s%8OCi}th-IRTLk$s8PIL&pDJaS*V%(uXm>g2s z0Wf1S2vlecO7xn~_UstmWpvE%cA4hcXU>|YYxjJIv1@q8o+Y>6?>?${eXsdCeXn`k z$}x&ig+z3A+Z`j@Ctdrj?8r#g+UJ_wK61yl*=R!WdvKE`{fjKueL=r_$%82u^A_gK z?=hdCKGTGBVS1v%q@yTqRu6$56hS}~i|K*t6D3d6Op(kaF^^H5rTU!T338<~9#n(rTnKV^FhO}tKEeTMk zqL4IEy-9NnMqvy@>NYsf(nt$!P2vLmSQuoTpyQX~Md3D1;`mGc;rs*Ewr$n}`Jb)< zv!OwC@>yfM?lrxF==RI1=Js&B+nwx>XydzELj4>AUL}02Z?iSJc^ikm&49okJGHCF z(i5`63n$jO;yRG6-g1s}p#5aueUa}!eT?<9JO?0(s@v!CajfJPdEoeA1D{uLNiM!c zlXX#z545oy2i*gkWYryz70qc&0As1f*06S9e*g-mASJFuhoTCTgQ{J{FI3rF6F``{ z3hqIFsX>ieN5w5UwtB4!wJM^qJ81Ibk)uLF4%eadP*h7JZ&0wx40U7n)8cH+uGLnn zcxS8C+G-7QwJH$1R;}T0j>+LF;*lqn@e;mSOkiClJen?eV+L*2b3XNS6KF9aiaePJ*pQHsVYdc923&p`AXJL7y7#QQt z^sjCxk<7x?tCMfqt5>1tYqIjD{mff$y`{*^4-|RzsI2Hrt=qD?S95rWPp;x+tCugg zNavyFs=adgywt*2&q*yU(sLj*$-3#5rp}kxD{LM+fh5-c!zg1+f?x-vMj_J=ij^^Y z!3htV{nAm)hSbr~&uL?Hqc2ji(wH(PQVgR`j3|tyF-)acfCd2X7Q8b)H#Ro+IK3IN zw{~D6XC^l(@J)|w2re(z0v_vl);fOrxRpJ=T@+<$@}MM(;^d?Q@0TS}+|QY+%AUv- zAD_$R<{mHRR{wa5S5BI_i33N}h~(SiZ{Lzk9kOe~_n$sKJbe80{lgG&1$bFLa6pzt zg}-YOZ<2K2fFvPs0NDjm1&-fcV*j0ulTLhF+D7)$Oo7=6or3l>nK5j;u;s&`#?B`; z0E#X&RnagP8Y|m?v<|ps!u(g*m~yxHLkankBKkb7YRzX1sweoiYrl?%U}q?i2-%lg zqub#(!|{07ehTYuGzfnkibg~Bc*tOkT}B&oXwmCRgf;~}aJ{G

?DM*jsjDXE{dh zCSN2a^4sJq82e7&NIS_!#rVJG7%fgoTch7N-?%@%fqvuq#{EkdWJQrL&?vbL1Z@o@ z(!i$M5ZO1DUT{zsc<;gieb#{{{OI8^Vm! zO-8Vbzkn542&J*9>7c0(8y(u~Sb|229tJ4ZUk5GH08i3PNc$gbBOFRv6KP^}Y8l2o zt=7p*4D%*kaas_OrmYnA9%F$0Wb6+ zVp8*W_wCffE|%fMKz|3{7IeFP_P6hRdLSN2cn5qvL4VN4xEZ$nNam~e1U2T+ogOnN zDjc^LTUL%0M1~hlqu1B28r{XwExn_sld4M+eSRhBWBFJlo_^wvZO^$qsv?o3McR)h6sAM}T&2 zl)smlFf~yw{My~WNE&#crx8bfA1;GoGIqZYtyygBZczfY-!%u+CR94 z+EI zuV{Hv!TU5Z$b;k@-e<`aCbM~bQKt6_Swtv}jpo|?9;BWjXGjrumjB1IYfjtWX(;3y-fIfV8$S|HmoNe|qRe~rH1dCs|Jua8!z|H0nw3IYQxaKM8ABDlZ}9`J$>{FpIh2!H}D zph6Hd!adqIxYaXPDi`{Sg|mgCI9*sgT`4Vy510FopvbtC|jj~<}WXlXBP{7j})d( I9KY6=Q*aKUykIwMRNJ&#s~6 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 F{u_!P-4_4= diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff index 8a04acf07feb74698adddd4ab554fa62c3343907..250ee11b928c75130dcecb75ebc91a2ec9a48fa9 100644 GIT binary patch literal 3928 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 PdQ~=;?#|p)LIC~`WOpxz literal 3668 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 C(5;gI diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2 index e04d6f49f1a0fad5370dccd138488a43a8fffa59..0db0ca952d4187eee91e9e706547e80ae484ef1b 100644 GIT binary patch literal 3268 zcmV;#3_J68Pew8T0RR9101U(c3jhEB02inL01R~i0RR9100000000000000000000 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 Cdpg_z literal 2972 zcmV;N3uE+mPew8T0RR9101KP|3jhEB02VX=01Hh30RR9100000000000000000000 z0000SLIzd3Z3lsD8( zmaefVgTf+IbRrb`hUahg{{Lh8VOBQ)wRSbIozxoKPWXddgK*|>=MM5V)%a4%$gtqF z?Q5Uvnj;`EZsLXPT%G{9=6!-b$P=XCqG@0aID=_|54Zws2@pV#=D+QOZDvxLB;gdP zOw=D=g)$R?FMLCgZeFo7vF&OswSW>(;)Pv=VdW#_-Sa_U!xtoORb=ZrRh0o`l{yxt zEM#qkt1!00=vs(Zo$L!?;Z!*CcnHYMF#$DCAKzk?;Y?6%8oU;(iAYvrA(GI@vTclP zNF?F2y9j)GUtgZ=mVo0U__~*qM=NeF@VcO*fYCB~sQQz@+fJZh2f!d_Yr-GUfbMua zv(+`tc7WP9ZqEkOrR3ON?$iR$6pjPG=!m!q27Pv0U8vj zN~w^`BvP@6XOtt56!3zK$Av90fs{8IU;-;`A{0Oku?j#1u?0X0AplT7>;RBM3;@U= z1_2}xLjY2UDF89VEPx2&1^^GS0B;zkD4?ED0N^0Bpx!HaXSP`ZI3a8%gdYhW^5#jW z5K>8?FbTqrB#d(ymGZeYu?>9DG)q>enN>wC^RhTi(sbP(*x@iC`$=FCH(L|XEf#f9 z36aQ(l^lGPyv1@}5YBx}c7y{?`#R#f0#0VN0wPAGp5KV0c9f(#Bf)sr#`i_O;=9Q# za9U;^N#&rwEbcvUVhbRM0D>0*hRq_w;8#r`Bg+=DZ3;lkhE;X}Z zpOZ9?_~XjTl+)}DL0(u?{x7TlmgHoM)^1cr z;%%~O2{IGqc;0N$nvI-d+mb>lf70xTAN)_%^?%P||DUT1PnG#6SBwmBS{7q7zYsEq z&V3uB4@s{y);IYigOE=$nnVW6r*`qN;$n;$yiDEo*t7r|MNi4^A0%;*Nwy{2wlw}0 zO-b+qK#BH{&+S-`0UG?bMrY9fv+rmcPo(#UWjWaS!nR$Tu&g4LCU~v>1gn%brp3A& zjrk3!-W5z=->@LCe;Z`}N@fen#Ll^le)MsG_>iAK&VR8KG&3K87#0Bxh(9t%5G~9B z1kmHV@t+aKM7Hjo++e$Kvr?33*E-f0%Nfj*9oTB%mk3}L72qw10JkCl-APkloU9r4 z_Jb9zreD8UH_R*#e4E}^)Hu5$_;c>#^vhh@sWXI9%I&s{X~P}t--^HgZ?8_}imY|f z)uQTUrptr_45Z%Y6GJ@lI7>bpd>}3wkNi_oZ0au>p|xu%RZB~rxsl+ z`;}`&0Hnv|<(rqx2HdVMg`ajpJcQP`%{XfzgIeUj2m5dNLHVNc2h0E1p2h$xmVIZ7 zt4X(?-3Ehe0F%O|FagJ_Z=38x2R3AS5T7rQ)fUu72TXA3b1eGvisICBq2E5UI>o*d zsuRV|Mdpn~&Wn{4CWWK8MRzea)NUhA2iB*rC}K~`&w51C=hM|H2Tv)V!4nlOnh3qQ zcNAE+Jd^p4Wo_1!>HBphaGo*WWmS~U!b_u6H55ThrLv__DUz<&<{AAd zIXY7FAd(vAY5vTMF;Dw5QieT4_#PE)jiB$rYsO1&182lfflOU5ME)b@ea}ul$ ziS+s{YK_uV2*Zo^YKUpr{=iRaEFnJ&S`8&;ltvMW@s(IC5ZK@VCvav#C=ptnpA}0G z%3j2&4P{qR$_GXsT~@z5@-$xn)*@Vp!+wFwPifgRGB0uUL%6HNg)Jhxa4w(9*VoPK zA}+MN_dg@fM7s$RHn;t*T5vpDBu!+7Oq#iK+H(f7#B{MyGIdmo(7JC3dG!j>x*;Oa z(%-^pw|=3B&M7{nndv0I=F9YFiSsA5ho`3P!WL|q(gJWIVl0?K>s(u{fE}N5cP~;8 zfPf>m#wb*us~4;iW6oO*^bik>#{>5xRL=zh9v;}yqZ0d34sc9G_X_sY;}SpSrNI

U zx9vCX0paLwSSW;dha)xA9^6~PZv#0N$%`B)>&}If1yCsLQv{(f7vQ`}0%bxZnfLBK z+b_`X{yRQ7g6+#@^C`Ty_t<_b{R}CPz|Eb1etI=t z$?C}S6X-{=)utU`j*S+Amnt+RS0$PaHOaN)lVj1oB&uyl=*X(dFwO2r(j%M4N=Fnw zt0;d0x7sV%)zuRF&#Q23SVCaC9oqU>2R}q-vazB ztEojo+thY?U*bi0RKjgItFXo3w-869Q>#;>Ys0zDjwv3(- z+*B&q96h-oiwj3p@B*`ra!dvr4x5A?CkS<+?#qYulPW9OFE`$~ z_)l_!&l&HUc@TvCR(ueGgezjwgW{>LMLY-L&-Sk2QQhA?sMIsdqSO8Sk>@}rWVcC06v!cn5j8Jrqgoqs%s+opU{b1rn#gmP`kU#vpPkKS(t$ z>6`IA*dTR&$n2yetmTt)GN>w)k+1;6S z8K~jbV(XqbBc_=z{|R`libf^v{v`Z@8a&|jB}-;3p#RL9TJ6pSqin|;+D_7uswqJ= ziA54rq5w%r_N5BIV40{EdC?hj)*rtv&z3Tz-t{Ty1mC>NiHeDrNMcDOy;FTODWsH2 zYH6gEPI{zAkV?k5@<+;`h0~O40<|&bQ^+}xNCl%}&-597;Xu80fcZsDI6A~|IYd35uQ(eLy SZ0XG?Qb?etU~ {% 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