1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-04-27 19:05:16 +00:00

Send notification to cloud user (#6415)

* send subscription expire notification

* optimize code

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
This commit is contained in:
awu0403 2025-04-21 13:30:35 +08:00 committed by GitHub
parent 0b8aa00f4d
commit 8cc5815107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 411 additions and 9 deletions

View File

@ -8,6 +8,7 @@ import { Utils, isMobile } from './utils/utils';
import SystemNotification from './components/system-notification'; import SystemNotification from './components/system-notification';
import EventBus from './components/common/event-bus'; import EventBus from './components/common/event-bus';
import Header from './components/header'; import Header from './components/header';
import SystemUserNotification from './components/system-user-notification';
import SidePanel from './components/side-panel'; import SidePanel from './components/side-panel';
import ResizeBar from './components/resize-bar'; import ResizeBar from './components/resize-bar';
import { import {
@ -278,6 +279,7 @@ class App extends Component {
return ( return (
<React.Fragment> <React.Fragment>
<SystemNotification /> <SystemNotification />
<SystemUserNotification />
<Header <Header
isSidePanelClosed={isSidePanelClosed} isSidePanelClosed={isSidePanelClosed}
onCloseSidePanel={this.onCloseSidePanel} onCloseSidePanel={this.onCloseSidePanel}

View File

@ -0,0 +1,38 @@
import React from 'react';
import { gettext } from '../utils/constants';
import { notificationAPI } from '../utils/notification-api';
import '../css/system-notification.css';
import PropTypes from 'prop-types';
class SystemUserNotificationItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isClosed: false
};
}
close = () => {
this.setState({ isClosed: true });
notificationAPI.setSysUserNotificationToSeen(this.props.notificationID);
};
render() {
if (this.state.isClosed) {
return null;
}
return (
<div id="info-bar" className="d-flex justify-content-between">
<span className="mr-3" aria-hidden="true"></span>
<p id="info-bar-info" className="m-0" dangerouslySetInnerHTML={{ __html: this.props.msg }}></p>
<button className="close sf2-icon-x1" title={gettext('Close')} aria-label={gettext('Close')} onClick={this.close}></button>
</div>
);
}
}
SystemUserNotificationItem.propTypes = {
msg: PropTypes.string.isRequired,
notificationID: PropTypes.number.isRequired,
};
export default SystemUserNotificationItem;

View File

@ -0,0 +1,43 @@
import React from 'react';
import '../css/system-notification.css';
import SystemUserNotificationItem from './system-user-notification-item';
import { notificationAPI } from '../utils/notification-api';
class SystemUserNotification extends React.Component {
constructor(props) {
super(props);
this.state = {
userNoteMsgs: []
};
}
componentDidMount() {
notificationAPI.listSysUserUnseenNotifications().then((res) => {
this.setState({
userNoteMsgs: res.data.notifications
});
});
}
render() {
let { userNoteMsgs } = this.state;
if (!userNoteMsgs) {
return null;
}
const userNoteMsgItem = userNoteMsgs.map((item, index) => {
return (
<SystemUserNotificationItem
key={index}
notificationItem={item}
msg={item.msg_format}
notificationID={item.id}
/>
);
});
return userNoteMsgItem;
}
}
export default SystemUserNotification;

View File

@ -0,0 +1,65 @@
import axios from 'axios';
import cookie from 'react-cookies';
import { siteRoot } from './constants';
class NotificationAPI {
init({ server, username, password, token }) {
this.server = server;
this.username = username;
this.password = password;
this.token = token;
if (this.token && this.server) {
this.req = axios.create({
baseURL: this.server,
headers: { 'Authorization': 'Token ' + this.token },
});
}
return this;
}
initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') {
var server = siteRoot.substring(0, siteRoot.length - 1);
this.server = server;
} else {
this.server = siteRoot;
}
this.req = axios.create({
headers: {
'X-CSRFToken': xcsrfHeaders,
}
});
return this;
}
_sendPostRequest(url, form) {
if (form.getHeaders) {
return this.req.post(url, form, {
headers: form.getHeaders()
});
} else {
return this.req.post(url, form);
}
}
listSysUserUnseenNotifications() {
const url = this.server + '/api/v2.1/sys-user-notifications/unseen/';
return this.req.get(url);
}
setSysUserNotificationToSeen(notificationID) {
const url = this.server + 'api/v2.1/sys-user-notifications/' + notificationID + '/seen/';
return this.req.put(url);
}
}
let notificationAPI = new NotificationAPI();
let xcsrfHeaders = cookie.load('sfcsrftoken');
notificationAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
export { notificationAPI };

View File

@ -22,10 +22,12 @@ from seahub.base.accounts import User
from seahub.base.models import OrgLastActivityTime from seahub.base.models import OrgLastActivityTime
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error, to_python_boolean
from seahub.api2.permissions import IsProVersion from seahub.api2.permissions import IsProVersion
from seahub.role_permissions.utils import get_available_roles from seahub.role_permissions.utils import get_available_roles
from seahub.organizations.models import OrgSAMLConfig from seahub.organizations.models import OrgSAMLConfig
from seahub.utils.ccnet_db import CcnetDB
try: try:
from seahub.settings import ORG_MEMBER_QUOTA_ENABLED from seahub.settings import ORG_MEMBER_QUOTA_ENABLED
@ -516,6 +518,7 @@ class AdminOrganizationsBaseInfo(APIView):
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
org_ids = request.GET.getlist('org_ids',[]) org_ids = request.GET.getlist('org_ids',[])
include_org_staffs = to_python_boolean(request.GET.get('include_org_staffs', 'false'))
orgs = [] orgs = []
for org_id in org_ids: for org_id in org_ids:
try: try:
@ -525,5 +528,13 @@ class AdminOrganizationsBaseInfo(APIView):
except Exception: except Exception:
continue continue
base_info = {'org_id': org.org_id, 'org_name': org.org_name} base_info = {'org_id': org.org_id, 'org_name': org.org_name}
staffs = []
if include_org_staffs:
try:
ccnet_db = CcnetDB()
staffs = ccnet_db.get_org_staffs(int(org_id))
except Exception:
pass
base_info['org_staffs'] = staffs
orgs.append(base_info) orgs.append(base_info)
return Response({'organization_list': orgs}) return Response({'organization_list': orgs})

View File

@ -12,8 +12,9 @@ from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
from seahub.notifications.models import Notification from seahub.notifications.models import Notification, SysUserNotification
from seahub.notifications.settings import NOTIFICATION_CACHE_TIMEOUT from seahub.notifications.settings import NOTIFICATION_CACHE_TIMEOUT
from seahub.base.accounts import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -168,3 +169,98 @@ class AdminSysNotificationView(APIView):
return Response({'success': True}) return Response({'success': True})
class AdminSysUserNotificationsView(APIView):
"""
admin notifications of designated user
"""
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsAdminUser,)
def get(self, request):
try:
page = int(request.GET.get('page', 1))
per_page = int(request.GET.get('per_page', 25))
except Exception as e:
error_msg = 'per_page or page invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
start, end = (page - 1) * per_page, page * per_page
try:
notifications = SysUserNotification.objects.all().order_by('-id')
notifications_count = notifications.count()
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({
'notifications': [n.to_dict() for n in notifications[start: end]],
"total_count": notifications_count
})
def post(self,request):
msg = request.data.get('msg', '')
username = request.data.get('username', '')
# arguments check
if not msg:
error_msg = 'msg invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if not username:
error_msg = 'user invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
try:
User.objects.get(email=username)
except User.DoesNotExist:
error_msg = 'User %s not found.' % username
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
notification = SysUserNotification.objects.create_sys_user_notificatioin(msg, username)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'notification': notification.to_dict()})
class AdminSysUserNotificationView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsAdminUser,)
def delete(self, request, nid):
"""
delete a system-to-user notification
Permission checking:
1.login and is admin user.
"""
try:
nid = int(nid)
except ValueError:
error_msg = 'nid invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if nid <= 0:
error_msg = 'nid invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
notification = SysUserNotification.objects.filter(id=nid).first()
if not notification:
error_msg = 'notification %s not found.' % nid
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
notification.delete()
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'success': True})

View File

@ -10,7 +10,7 @@ from rest_framework import status
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.notifications.models import UserNotification from seahub.notifications.models import UserNotification, SysUserNotification
from seahub.notifications.utils import update_notice_detail, update_sdoc_notice_detail from seahub.notifications.utils import update_notice_detail, update_sdoc_notice_detail
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
@ -370,3 +370,61 @@ class AllNotificationsView(APIView):
return Response({'success': True}) return Response({'success': True})
class SysUserNotificationUnseenView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request):
"""
get the unseen sys-user-notifications by login user
"""
username = request.user.username
notifications = SysUserNotification.objects.unseen_notes(username)
return Response({
'notifications': [n.to_dict() for n in notifications],
})
class SysUserNotificationSeenView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def put(self, request, nid):
"""
mark a sys-user-notification seen by login user
Permission checking:
1. login user.
"""
# arguments check
username = request.user.username
try:
nid = int(nid)
except ValueError:
error_msg = 'nid invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if nid <= 0:
error_msg = 'nid invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resouce check
notification = SysUserNotification.objects.filter(id=nid).first()
if not notification:
error_msg = 'notification %s not found.' % nid
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
if notification.to_user != username:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# set notification to seen
try:
notification.update_notification_to_seen()
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'notification': notification.to_dict()})

View File

@ -9,7 +9,7 @@ from django.urls import reverse
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.forms import ModelForm, Textarea from django.forms import ModelForm, Textarea
from django.utils.html import escape from django.utils.html import escape, urlize
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.core.cache import cache from django.core.cache import cache
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -17,8 +17,9 @@ from django.template.loader import render_to_string
from seaserv import seafile_api, ccnet_api from seaserv import seafile_api, ccnet_api
from seahub.base.fields import LowerCaseCharField from seahub.base.fields import LowerCaseCharField
from seahub.base.templatetags.seahub_tags import email2nickname from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
from seahub.invitations.models import Invitation from seahub.invitations.models import Invitation
from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.utils import normalize_cache_key, get_site_scheme_and_netloc from seahub.utils import normalize_cache_key, get_site_scheme_and_netloc
from seahub.constants import HASH_URLS from seahub.constants import HASH_URLS
from seahub.file_participants.utils import list_file_participants from seahub.file_participants.utils import list_file_participants
@ -42,6 +43,9 @@ class NotificationManager(models.Manager):
########## system notification ########## system notification
class Notification(models.Model): class Notification(models.Model):
"""
global system notification
"""
message = models.CharField(max_length=512) message = models.CharField(max_length=512)
primary = models.BooleanField(default=False, db_index=True) primary = models.BooleanField(default=False, db_index=True)
objects = NotificationManager() objects = NotificationManager()
@ -61,6 +65,62 @@ class NotificationForm(ModelForm):
'message': Textarea(), 'message': Textarea(),
} }
class SysUserNotificationManager(models.Manager):
def create_sys_user_notificatioin(self, msg, user):
notification = self.create(
message = msg,
to_user = user,
)
return notification
def unseen_notes(self, user):
notes = self.filter(to_user=user, seen=0)
return notes
class SysUserNotification(models.Model):
"""
system notification to designated user
"""
message = models.TextField(null=False, blank=False)
to_user = models.CharField(max_length=255, db_index=True)
seen = models.BooleanField(default=False, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
objects = SysUserNotificationManager()
class Meta:
ordering = ["-created_at"]
def update_notification_to_seen(self):
self.seen = True
self.save()
@property
def format_msg(self):
return urlize(self.message, autoescape=True)
def to_dict(self):
email = self.to_user
orgs = ccnet_api.get_orgs_by_user(email)
org_name = ''
try:
if orgs:
org_name = orgs[0].org_name
except Exception as e:
logger.error(e)
return {
'id': self.id,
'msg': self.message,
'username': self.to_user,
'name': email2nickname(self.to_user),
'contact_email': email2contact_email(self.to_user),
'seen': self.seen,
'org_name': org_name,
'created_at': datetime_to_isoformat_timestr(self.created_at),
'msg_format': self.format_msg
}
########## user notification ########## user notification
MSG_TYPE_GROUP_JOIN_REQUEST = 'group_join_request' MSG_TYPE_GROUP_JOIN_REQUEST = 'group_join_request'
MSG_TYPE_ADD_USER_TO_GROUP = 'add_user_to_group' MSG_TYPE_ADD_USER_TO_GROUP = 'add_user_to_group'

View File

@ -92,7 +92,8 @@ from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchV
from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView
from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView
from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView, AllNotificationsView from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView, \
SysUserNotificationSeenView, AllNotificationsView, SysUserNotificationUnseenView
from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView
from seahub.api2.endpoints.user_avatar import UserAvatarView from seahub.api2.endpoints.user_avatar import UserAvatarView
from seahub.api2.endpoints.wikis import WikisView, WikiView from seahub.api2.endpoints.wikis import WikisView, WikiView
@ -192,7 +193,8 @@ from seahub.api2.endpoints.admin.group_owned_libraries import AdminGroupOwnedLib
from seahub.api2.endpoints.admin.user_activities import UserActivitiesView from seahub.api2.endpoints.admin.user_activities import UserActivitiesView
from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords
from seahub.api2.endpoints.admin.notifications import AdminNotificationsView from seahub.api2.endpoints.admin.notifications import AdminNotificationsView
from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView, \
AdminSysUserNotificationView, AdminSysUserNotificationsView
from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAccessLogs, AdminLogsFileUpdateLogs, \ from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAccessLogs, AdminLogsFileUpdateLogs, \
AdminLogsSharePermissionLogs, AdminLogsFileTransferLogs, AdminLogGroupMemberAuditLogs AdminLogsSharePermissionLogs, AdminLogsFileTransferLogs, AdminLogGroupMemberAuditLogs
from seahub.api2.endpoints.admin.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition from seahub.api2.endpoints.admin.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition
@ -530,6 +532,8 @@ urlpatterns = [
re_path(r'^api/v2.1/sdoc-notification/$', SdocNotificationView.as_view(), name='api-v2.1-notification'), re_path(r'^api/v2.1/sdoc-notification/$', SdocNotificationView.as_view(), name='api-v2.1-notification'),
re_path(r'^api/v2.1/all-notifications/$', AllNotificationsView.as_view(), name='api-v2.1-all-notification'), re_path(r'^api/v2.1/all-notifications/$', AllNotificationsView.as_view(), name='api-v2.1-all-notification'),
re_path(r'^api/v2.1/sys-user-notifications/(?P<nid>\d+)/seen/$', SysUserNotificationSeenView.as_view(), name='api-v2.1-notification-seen'),
re_path(r'^api/v2.1/sys-user-notifications/unseen/$', SysUserNotificationUnseenView.as_view(), name='api-v2.1-notification-unseen'),
## user::invitations ## user::invitations
re_path(r'^api/v2.1/invitations/$', InvitationsView.as_view()), re_path(r'^api/v2.1/invitations/$', InvitationsView.as_view()),
re_path(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()), re_path(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()),
@ -796,7 +800,8 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'), re_path(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'),
re_path(r'^api/v2.1/admin/sys-notifications/$', AdminSysNotificationsView.as_view(), name='api-2.1-admin-sys-notifications'), re_path(r'^api/v2.1/admin/sys-notifications/$', AdminSysNotificationsView.as_view(), name='api-2.1-admin-sys-notifications'),
re_path(r'^api/v2.1/admin/sys-notifications/(?P<nid>\d+)/$', AdminSysNotificationView.as_view(),name='api-2.1-admin-sys-notification'), re_path(r'^api/v2.1/admin/sys-notifications/(?P<nid>\d+)/$', AdminSysNotificationView.as_view(),name='api-2.1-admin-sys-notification'),
re_path(r'^api/v2.1/admin/sys-user-notifications/$', AdminSysUserNotificationsView.as_view(), name='api-2.1-admin-sys-user-notifications'),
re_path(r'^api/v2.1/admin/sys-user-notifications/(?P<nid>\d+)/$', AdminSysUserNotificationView.as_view(), name='api-2.1-admin-sys-user-notification'),
## admin::terms and conditions ## admin::terms and conditions
re_path(r'^api/v2.1/admin/terms-and-conditions/$', AdminTermsAndConditions.as_view(), name='api-v2.1-admin-terms-and-conditions'), re_path(r'^api/v2.1/admin/terms-and-conditions/$', AdminTermsAndConditions.as_view(), name='api-v2.1-admin-terms-and-conditions'),
re_path(r'^api/v2.1/admin/terms-and-conditions/(?P<term_id>\d+)/$', AdminTermAndCondition.as_view(), name='api-v2.1-admin-term-and-condition'), re_path(r'^api/v2.1/admin/terms-and-conditions/(?P<term_id>\d+)/$', AdminTermAndCondition.as_view(), name='api-v2.1-admin-term-and-condition'),

View File

@ -244,3 +244,15 @@ class CcnetDB:
'is_manual_set': is_manual_set 'is_manual_set': is_manual_set
} }
return CcnetUserRole(**params) return CcnetUserRole(**params)
def get_org_staffs(self, org_id):
sql = f"""
SELECT email
FROM `{self.db_name}`.`OrgUser`
WHERE org_id={org_id} AND is_staff=1
"""
with connection.cursor() as cursor:
cursor.execute(sql)
staffs = cursor.fetchall()
return [s[0] for s in staffs]

View File

@ -1641,3 +1641,15 @@ CREATE TABLE `group_member_audit` (
KEY `idx_group_member_audit_user` (`user`), KEY `idx_group_member_audit_user` (`user`),
KEY `idx_group_member_audit_group_id` (`group_id`) KEY `idx_group_member_audit_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `notifications_sysusernotification` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`message` longtext NOT NULL,
`to_user` varchar(255) NOT NULL,
`seen` tinyint(1) NOT NULL,
`created_at` datetime(6) NOT NULL,
PRIMARY KEY (`id`),
KEY `notifications_sysusernotification_to_user_e0c9101e` (`to_user`),
KEY `notifications_sysusernotification_seen_9d851bf7` (`seen`),
KEY `notifications_sysusernotification_created_at_56ffd2a0` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;