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