diff --git a/frontend/src/app.js b/frontend/src/app.js index ebc30d5ca0..eeccc71ba5 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -8,6 +8,7 @@ import { Utils, isMobile } from './utils/utils'; import SystemNotification from './components/system-notification'; import EventBus from './components/common/event-bus'; import Header from './components/header'; +import SystemUserNotification from './components/system-user-notification'; import SidePanel from './components/side-panel'; import ResizeBar from './components/resize-bar'; import { @@ -278,6 +279,7 @@ class App extends Component { return ( +
{ + this.setState({ isClosed: true }); + notificationAPI.setSysUserNotificationToSeen(this.props.notificationID); + }; + + render() { + if (this.state.isClosed) { + return null; + } + return ( +
+ +

+ +
+ ); + } +} + +SystemUserNotificationItem.propTypes = { + msg: PropTypes.string.isRequired, + notificationID: PropTypes.number.isRequired, +}; +export default SystemUserNotificationItem; diff --git a/frontend/src/components/system-user-notification.js b/frontend/src/components/system-user-notification.js new file mode 100644 index 0000000000..45cf5e5278 --- /dev/null +++ b/frontend/src/components/system-user-notification.js @@ -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 ( + + ); + }); + return userNoteMsgItem; + } +} + +export default SystemUserNotification; diff --git a/frontend/src/utils/notification-api.js b/frontend/src/utils/notification-api.js new file mode 100644 index 0000000000..caf0ce9781 --- /dev/null +++ b/frontend/src/utils/notification-api.js @@ -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 }; diff --git a/seahub/api2/endpoints/admin/organizations.py b/seahub/api2/endpoints/admin/organizations.py index f254682c2f..7e380e4f57 100644 --- a/seahub/api2/endpoints/admin/organizations.py +++ b/seahub/api2/endpoints/admin/organizations.py @@ -22,10 +22,12 @@ from seahub.base.accounts import User from seahub.base.models import OrgLastActivityTime from seahub.api2.authentication import TokenAuthentication 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.role_permissions.utils import get_available_roles from seahub.organizations.models import OrgSAMLConfig +from seahub.utils.ccnet_db import CcnetDB + try: from seahub.settings import ORG_MEMBER_QUOTA_ENABLED @@ -515,7 +517,8 @@ class AdminOrganizationsBaseInfo(APIView): error_msg = 'Feature is not enabled.' 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 = [] for org_id in org_ids: try: @@ -525,5 +528,13 @@ class AdminOrganizationsBaseInfo(APIView): except Exception: continue 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) return Response({'organization_list': orgs}) diff --git a/seahub/api2/endpoints/admin/sys_notifications.py b/seahub/api2/endpoints/admin/sys_notifications.py index 18bc2b4b4e..6fb48d0f22 100644 --- a/seahub/api2/endpoints/admin/sys_notifications.py +++ b/seahub/api2/endpoints/admin/sys_notifications.py @@ -12,8 +12,9 @@ from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle 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.base.accounts import User logger = logging.getLogger(__name__) @@ -168,3 +169,98 @@ class AdminSysNotificationView(APIView): 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}) diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index dfc779862d..ec639c429c 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -10,7 +10,7 @@ from rest_framework import status from seahub.api2.authentication import TokenAuthentication 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.api2.utils import api_error @@ -370,3 +370,61 @@ class AllNotificationsView(APIView): 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()}) diff --git a/seahub/notifications/models.py b/seahub/notifications/models.py index 0cad4c4f6c..fe4bce80d4 100644 --- a/seahub/notifications/models.py +++ b/seahub/notifications/models.py @@ -9,7 +9,7 @@ from django.urls import reverse from django.db import models from django.conf import settings 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.core.cache import cache 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 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.utils.timeutils import datetime_to_isoformat_timestr from seahub.utils import normalize_cache_key, get_site_scheme_and_netloc from seahub.constants import HASH_URLS from seahub.file_participants.utils import list_file_participants @@ -42,6 +43,9 @@ class NotificationManager(models.Manager): ########## system notification class Notification(models.Model): + """ + global system notification + """ message = models.CharField(max_length=512) primary = models.BooleanField(default=False, db_index=True) objects = NotificationManager() @@ -61,6 +65,62 @@ class NotificationForm(ModelForm): '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 MSG_TYPE_GROUP_JOIN_REQUEST = 'group_join_request' MSG_TYPE_ADD_USER_TO_GROUP = 'add_user_to_group' diff --git a/seahub/urls.py b/seahub/urls.py index b0f1a2a0eb..d83d38bc7b 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -92,7 +92,8 @@ from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchV from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView 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.user_avatar import UserAvatarView 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.file_scan_records import AdminFileScanRecords 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, \ AdminLogsSharePermissionLogs, AdminLogsFileTransferLogs, AdminLogGroupMemberAuditLogs 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/all-notifications/$', AllNotificationsView.as_view(), name='api-v2.1-all-notification'), + re_path(r'^api/v2.1/sys-user-notifications/(?P\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 re_path(r'^api/v2.1/invitations/$', InvitationsView.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/sys-notifications/$', AdminSysNotificationsView.as_view(), name='api-2.1-admin-sys-notifications'), re_path(r'^api/v2.1/admin/sys-notifications/(?P\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\d+)/$', AdminSysUserNotificationView.as_view(), name='api-2.1-admin-sys-user-notification'), ## 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\d+)/$', AdminTermAndCondition.as_view(), name='api-v2.1-admin-term-and-condition'), diff --git a/seahub/utils/ccnet_db.py b/seahub/utils/ccnet_db.py index d477607edd..e8ba199b46 100644 --- a/seahub/utils/ccnet_db.py +++ b/seahub/utils/ccnet_db.py @@ -244,3 +244,15 @@ class CcnetDB: 'is_manual_set': is_manual_set } 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] diff --git a/sql/mysql.sql b/sql/mysql.sql index 22f6f4dad9..8b96770c45 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1641,3 +1641,15 @@ CREATE TABLE `group_member_audit` ( KEY `idx_group_member_audit_user` (`user`), KEY `idx_group_member_audit_group_id` (`group_id`) ) 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;