diff --git a/frontend/src/components/user-settings/email-notice.js b/frontend/src/components/user-settings/email-notice.js index 408e4e14be..136cce2567 100644 --- a/frontend/src/components/user-settings/email-notice.js +++ b/frontend/src/components/user-settings/email-notice.js @@ -5,7 +5,8 @@ import { Utils } from '../../utils/utils'; import toaster from '../toast'; const { - initialEmailNotificationInterval + fileUpdatesEmailInterval, + collaborateEmailInterval } = window.app.pageOptions; class EmailNotice extends React.Component { @@ -14,7 +15,7 @@ class EmailNotice extends React.Component { super(props); // interval: in seconds - this.intervalOptions = [ + this.fileUpdatesOptions = [ {interval: 0, text: gettext('Don\'t send emails')}, {interval: 3600, text: gettext('Per hour')}, {interval: 14400, text: gettext('Per 4 hours')}, @@ -22,22 +23,37 @@ class EmailNotice extends React.Component { {interval: 604800, text: gettext('Per week')} ]; + this.collaborateOptions = [ + {interval: 0, text: gettext('Don\'t send emails')}, + {interval: 3600, text: gettext('Per hour') + ' (' + gettext('If notifications have not be read within one hour, they will be sent to your mailbox') + ')'} + ]; + this.state = { - currentInterval: initialEmailNotificationInterval + fileUpdatesEmailInterval: fileUpdatesEmailInterval, + collaborateEmailInterval: collaborateEmailInterval }; } - inputChange = (e) => { + inputFileUpdatesEmailIntervalChange = (e) => { if (e.target.checked) { this.setState({ - currentInterval: e.target.value + fileUpdatesEmailInterval: parseInt(e.target.value) + }); + } + } + + inputCollaborateEmailIntervalChange = (e) => { + if (e.target.checked) { + this.setState({ + collaborateEmailInterval: parseInt(e.target.value) }); } } formSubmit = (e) => { e.preventDefault(); - seafileAPI.updateEmailNotificationInterval(this.state.currentInterval).then((res) => { + let { fileUpdatesEmailInterval, collaborateEmailInterval } = this.state; + seafileAPI.updateEmailNotificationInterval(fileUpdatesEmailInterval, collaborateEmailInterval).then((res) => { toaster.success(gettext('Success')); }).catch((error) => { let errorMsg = Utils.getErrorMsg(error); @@ -46,23 +62,38 @@ class EmailNotice extends React.Component { } render() { - const { currentInterval } = this.state; + const { fileUpdatesEmailInterval, collaborateEmailInterval } = this.state; return (
-

{gettext('Email Notification of File Changes')}

+

{gettext('Email Notification')}

+
{gettext('Notifications of file changes')}

{gettext('The list of added, deleted and modified files will be sent to your mailbox.')}

- {this.intervalOptions.map((item, index) => { + {this.fileUpdatesOptions.map((item, index) => { return ( - - + +
); })} - + +
{gettext('Notifications of collaboration')}
+

{gettext('Whether the notifications of collaboration such as sharing library or joining group should be sent to your mailbox.')}

+
+ {this.collaborateOptions.map((item, index) => { + return ( + + + +
+
+ ); + })} +
+
); } diff --git a/seahub/api2/views.py b/seahub/api2/views.py index e47af3b271..7af19ccae1 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -336,8 +336,10 @@ class AccountInfo(APIView): except InstitutionAdmin.DoesNotExist: info['is_inst_admin'] = False - interval = UserOptions.objects.get_file_updates_email_interval(email) - info['email_notification_interval'] = 0 if interval is None else interval + file_updates_email_interval = UserOptions.objects.get_file_updates_email_interval(email) + info['file_updates_email_interval'] = 0 if file_updates_email_interval is None else file_updates_email_interval + collaborate_email_interval = UserOptions.objects.get_collaborate_email_interval(email) + info['collaborate_email_interval'] = 0 if collaborate_email_interval is None else collaborate_email_interval return info def get(self, request, format=None): @@ -358,13 +360,20 @@ class AccountInfo(APIView): return api_error(status.HTTP_400_BAD_REQUEST, _("Name should not include '/'.")) - email_interval = request.data.get("email_notification_interval", None) - if email_interval is not None: + file_updates_email_interval = request.data.get("file_updates_email_interval", None) + if file_updates_email_interval is not None: try: - email_interval = int(email_interval) + file_updates_email_interval = int(file_updates_email_interval) except ValueError: return api_error(status.HTTP_400_BAD_REQUEST, - 'email_interval invalid') + 'file_updates_email_interval invalid') + collaborate_email_interval = request.data.get("collaborate_email_interval", None) + if collaborate_email_interval is not None: + try: + collaborate_email_interval = int(collaborate_email_interval) + except ValueError: + return api_error(status.HTTP_400_BAD_REQUEST, + 'collaborate_email_interval invalid') # update user info @@ -375,12 +384,19 @@ class AccountInfo(APIView): profile.nickname = name profile.save() - if email_interval is not None: - if email_interval <= 0: + if file_updates_email_interval is not None: + if file_updates_email_interval <= 0: UserOptions.objects.unset_file_updates_email_interval(username) else: UserOptions.objects.set_file_updates_email_interval( - username, email_interval) + username, file_updates_email_interval) + + if collaborate_email_interval is not None: + if collaborate_email_interval <= 0: + UserOptions.objects.unset_collaborate_email_interval(username) + else: + UserOptions.objects.set_collaborate_email_interval( + username, collaborate_email_interval) return Response(self._get_account_info(request)) diff --git a/seahub/notifications/management/commands/send_notices.py b/seahub/notifications/management/commands/send_notices.py index 1138a1ce9c..a4e37217ac 100644 --- a/seahub/notifications/management/commands/send_notices.py +++ b/seahub/notifications/management/commands/send_notices.py @@ -23,6 +23,7 @@ from seahub.invitations.models import Invitation from seahub.profile.models import Profile from seahub.constants import HASH_URLS from seahub.utils import get_site_name +from seahub.options.models import UserOptions, KEY_COLLABORATE_EMAIL_INTERVAL, KEY_COLLABORATE_LAST_EMAILED_TIME # Get an instance of a logger logger = logging.getLogger(__name__) @@ -208,64 +209,73 @@ class Command(BaseCommand): return Profile.objects.get_user_language(username) def do_action(self): - now = datetime.datetime.now() + emails = [] + user_file_updates_email_intervals = [] + for ele in UserOptions.objects.filter( + option_key=KEY_COLLABORATE_EMAIL_INTERVAL): + try: + user_file_updates_email_intervals.append( + (ele.email, int(ele.option_val)) + ) + emails.append(ele.email) + except Exception as e: + logger.error(e) + self.stderr.write('[%s]: %s' % (str(datetime.datetime.now()), e)) + continue - try: - cmd_last_check = CommandsLastCheck.objects.get(command_type=self.label) - logger.debug('Last check time is %s' % cmd_last_check.last_check) + user_last_emailed_time_dict = {} + for ele in UserOptions.objects.filter(option_key=KEY_COLLABORATE_LAST_EMAILED_TIME).filter(email__in=emails): + try: + user_last_emailed_time_dict[ele.email] = datetime.datetime.strptime( + ele.option_val, "%Y-%m-%d %H:%M:%S") + except Exception as e: + logger.error(e) + self.stderr.write('[%s]: %s' % (str(datetime.datetime.now()), e)) + continue - unseen_notices = UserNotification.objects.get_all_notifications( - seen=False, time_since=cmd_last_check.last_check) - - logger.debug('Update last check time to %s' % now) - cmd_last_check.last_check = now - cmd_last_check.save() - except CommandsLastCheck.DoesNotExist: - logger.debug('No last check time found, get all unread notices.') - unseen_notices = UserNotification.objects.get_all_notifications( - seen=False) - - logger.debug('Create new last check time: %s' % now) - CommandsLastCheck(command_type=self.label, last_check=now).save() - - email_ctx = {} - for notice in unseen_notices: - if notice.to_user in email_ctx: - email_ctx[notice.to_user] += 1 + # save current language + cur_language = translation.get_language() + for (to_user, interval_val) in user_file_updates_email_intervals: + # get last_emailed_time if any, defaults to today 00:00:00.0 + last_emailed_time = user_last_emailed_time_dict.get(to_user, None) + now = datetime.datetime.now().replace(microsecond=0) + if not last_emailed_time: + last_emailed_time = datetime.datetime.now().replace(hour=0).replace( + minute=0).replace(second=0).replace(microsecond=0) else: - email_ctx[notice.to_user] = 1 + if (now - last_emailed_time).total_seconds() < interval_val: + continue - for to_user, count in list(email_ctx.items()): - # save current language - cur_language = translation.get_language() + # get notices + user_notices_qs = UserNotification.objects.get_all_notifications(seen=False, time_since=last_emailed_time) + user_notices, count = list(user_notices_qs), user_notices_qs.count() + if not count: + continue # get and active user language user_language = self.get_user_language(to_user) translation.activate(user_language) - logger.debug('Set language code to %s for user: %s' % (user_language, to_user)) - self.stdout.write('[%s] Set language code to %s' % ( - str(datetime.datetime.now()), user_language)) + logger.debug('Set language code to %s for user: %s' % ( + user_language, to_user)) + self.stdout.write('[%s] Set language code to %s for user: %s' % ( + str(datetime.datetime.now()), user_language, to_user)) + # format mail content and send notices = [] - for notice in unseen_notices: - logger.info('Processing unseen notice: [%s]' % (notice)) - + for notice in user_notices: d = json.loads(notice.detail) - - repo_id = d.get('repo_id', None) - group_id = d.get('group_id', None) + repo_id = d.get('repo_id') + group_id = d.get('group_id') try: if repo_id and not seafile_api.get_repo(repo_id): notice.delete() continue - if group_id and not ccnet_api.get_group(group_id): notice.delete() continue except Exception as e: logger.error(e) continue - if notice.to_user != to_user: continue @@ -297,7 +307,6 @@ class Command(BaseCommand): if not notices: continue - user_name = email2nickname(to_user) contact_email = Profile.objects.get_contact_email_by_user(to_user) to_user = contact_email # use contact email if any @@ -312,7 +321,9 @@ class Command(BaseCommand): send_html_email(_('New notice on %s') % get_site_name(), 'notifications/notice_email.html', c, None, [to_user]) - + # set new last_emailed_time + UserOptions.objects.set_collaborate_last_emailed_time( + to_user, now) logger.info('Successfully sent email to %s' % to_user) self.stdout.write('[%s] Successfully sent email to %s' % (str(datetime.datetime.now()), to_user)) except Exception as e: diff --git a/seahub/options/models.py b/seahub/options/models.py index 2d65feeb82..320bfb96ad 100644 --- a/seahub/options/models.py +++ b/seahub/options/models.py @@ -36,6 +36,8 @@ KEY_DEFAULT_REPO = "default_repo" KEY_WEBDAV_SECRET = "webdav_secret" KEY_FILE_UPDATES_EMAIL_INTERVAL = "file_updates_email_interval" KEY_FILE_UPDATES_LAST_EMAILED_TIME = "file_updates_last_emailed_time" +KEY_COLLABORATE_EMAIL_INTERVAL = 'collaborate_email_interval' +KEY_COLLABORATE_LAST_EMAILED_TIME = 'collaborate_last_emailed_time' class CryptoOptionNotSetError(Exception): @@ -306,6 +308,42 @@ class UserOptionsManager(models.Manager): def unset_file_updates_last_emailed_time(self, username): return self.unset_user_option(username, KEY_FILE_UPDATES_LAST_EMAILED_TIME) + def set_collaborate_email_interval(self, username, seconds): + return self.set_user_option(username, KEY_COLLABORATE_EMAIL_INTERVAL, + str(seconds)) + + def get_collaborate_email_interval(self, username): + val = self.get_user_option(username, KEY_COLLABORATE_EMAIL_INTERVAL) + if not val: + return None + try: + return int(val) + except ValueError: + logger.error('Failed to convert string %s to int' % val) + return None + + def unset_collaborate_email_interval(self, username): + return self.unset_user_option(username, KEY_COLLABORATE_EMAIL_INTERVAL) + + def set_collaborate_last_emailed_time(self, username, time_dt): + return self.set_user_option( + username, KEY_COLLABORATE_LAST_EMAILED_TIME, + time_dt.strftime("%Y-%m-%d %H:%M:%S")) + + def get_collaborate_last_emailed_time(self, username): + val = self.get_user_option(username, KEY_COLLABORATE_LAST_EMAILED_TIME) + if not val: + return None + + try: + return datetime.strptime(val, "%Y-%m-%d %H:%M:%S") + except Exception: + logger.error('Failed to convert string %s to datetime obj' % val) + return None + + def unset_collaborate_last_emailed_time(self, username): + return self.unset_user_option(username, KEY_COLLABORATE_LAST_EMAILED_TIME) + class UserOptions(models.Model): email = LowerCaseCharField(max_length=255, db_index=True) diff --git a/seahub/profile/templates/profile/set_profile_react.html b/seahub/profile/templates/profile/set_profile_react.html index 0f576553b6..5a054dfc79 100644 --- a/seahub/profile/templates/profile/set_profile_react.html +++ b/seahub/profile/templates/profile/set_profile_react.html @@ -49,7 +49,8 @@ window.app.pageOptions = { })(), {% if is_pro %} - initialEmailNotificationInterval: {{ email_notification_interval }}, + fileUpdatesEmailInterval: {{ file_updates_email_interval }}, + collaborateEmailInterval: {{ collaborate_email_interval }}, {% endif %} twoFactorAuthEnabled: {% if two_factor_auth_enabled %} true {% else %} false {% endif %}, diff --git a/seahub/profile/views.py b/seahub/profile/views.py index df736d5e04..333b7206cf 100644 --- a/seahub/profile/views.py +++ b/seahub/profile/views.py @@ -84,8 +84,10 @@ def edit_profile(request): else: webdav_passwd = '' - email_inverval = UserOptions.objects.get_file_updates_email_interval(username) - email_inverval = email_inverval if email_inverval is not None else 0 + file_updates_email_interval = UserOptions.objects.get_file_updates_email_interval(username) + file_updates_email_interval = file_updates_email_interval if file_updates_email_interval is not None else 0 + collaborate_email_interval = UserOptions.objects.get_collaborate_email_interval(username) + collaborate_email_interval = collaborate_email_interval if collaborate_email_interval is not None else 0 if work_weixin_oauth_check(): enable_wechat_work = True @@ -123,7 +125,8 @@ def edit_profile(request): 'ENABLE_DELETE_ACCOUNT': ENABLE_DELETE_ACCOUNT, 'ENABLE_UPDATE_USER_INFO': ENABLE_UPDATE_USER_INFO, 'webdav_passwd': webdav_passwd, - 'email_notification_interval': email_inverval, + 'file_updates_email_interval': file_updates_email_interval, + 'collaborate_email_interval': collaborate_email_interval, 'social_next_page': reverse('edit_profile'), 'enable_wechat_work': enable_wechat_work, 'social_connected': social_connected, diff --git a/tests/api/views/test_account_info.py b/tests/api/views/test_account_info.py index 67e7f8f70e..07954ca667 100644 --- a/tests/api/views/test_account_info.py +++ b/tests/api/views/test_account_info.py @@ -26,22 +26,23 @@ class AccountInfoTest(BaseTestCase): def test_update(self, ): self.login_as(self.user) - resp = self._do_put('name=foo&email_notification_interval=3000') + resp = self._do_put('name=foo&file_updates_email_interval=3000&collaborate_email_interval=3000') self.assertEqual(200, resp.status_code) json_resp = json.loads(resp.content) - assert json_resp['email_notification_interval'] == 3000 + assert json_resp['file_updates_email_interval'] == 3000 + assert json_resp['collaborate_email_interval'] == 3000 assert json_resp['name'] == 'foo' def test_update_email_nofification_interval(self, ): self.login_as(self.user) - resp = self._do_put('email_notification_interval=3000') + resp = self._do_put('file_updates_email_interval=3000') self.assertEqual(200, resp.status_code) json_resp = json.loads(resp.content) - assert json_resp['email_notification_interval'] == 3000 + assert json_resp['file_updates_email_interval'] == 3000 - resp = self._do_put('email_notification_interval=0') + resp = self._do_put('file_updates_email_interval=0') self.assertEqual(200, resp.status_code) json_resp = json.loads(resp.content) - assert json_resp['email_notification_interval'] == 0 + assert json_resp['file_updates_email_interval'] == 0 diff --git a/tests/seahub/notifications/management/commands/test_send_notices.py b/tests/seahub/notifications/management/commands/test_send_notices.py index 1b56a6108f..5317394727 100644 --- a/tests/seahub/notifications/management/commands/test_send_notices.py +++ b/tests/seahub/notifications/management/commands/test_send_notices.py @@ -12,6 +12,7 @@ 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 @@ -21,6 +22,16 @@ except ImportError: 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( @@ -95,13 +106,13 @@ class CommandTest(BaseTestCase): self.assertEqual(len(mail.outbox), 0) detail = file_comment_msg_to_json(self.repo.id, '/foo', - self.user.username, 'test comment') - UserNotification.objects.add_file_comment_msg('a@a.com', detail) + '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] == 'a@a.com' - assert 'new comment from user %s' % self.user.username in mail.outbox[0].body + 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):