1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-18 16:36:15 +00:00

support collaborate notis interval (#4841)

* support collaborate notis interval

* fix unit-test

* fix unit-test

* fix unit-test

* fix unit-test

* fix unit-test

* update text

* fix error msg
This commit is contained in:
Alex Happy
2021-03-25 21:44:58 +08:00
committed by GitHub
parent 855b457e5f
commit eca5a3b933
8 changed files with 186 additions and 74 deletions

View File

@@ -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 (
<div className="setting-item" id="email-notice">
<h3 className="setting-item-heading">{gettext('Email Notification of File Changes')}</h3>
<h3 className="setting-item-heading">{gettext('Email Notification')}</h3>
<h6 className="">{gettext('Notifications of file changes')}</h6>
<p className="mb-1">{gettext('The list of added, deleted and modified files will be sent to your mailbox.')}</p>
<form method="post" action="" id="set-email-notice-interval-form" onSubmit={this.formSubmit}>
{this.intervalOptions.map((item, index) => {
{this.fileUpdatesOptions.map((item, index) => {
return (
<React.Fragment key={index}>
<input type="radio" name="interval" value={item.interval} className="align-middle" id={`interval-option${index + 1}`} checked={currentInterval == item.interval} onChange={this.inputChange} />
<React.Fragment key={`file-updates-${index}`}>
<input type="radio" name="interval" value={item.interval} className="align-middle" id={`file-updates-interval-option${index + 1}`} checked={fileUpdatesEmailInterval == item.interval} onChange={this.inputFileUpdatesEmailIntervalChange} />
<label className="align-middle m-0 ml-2" htmlFor={`interval-option${index + 1}`}>{item.text}</label>
<br />
</React.Fragment>
);
})}
<button type="submit" className="btn btn-outline-primary mt-2">{gettext('Submit')}</button>
</form>
<h6 className="mt-4">{gettext('Notifications of collaboration')}</h6>
<p className="mb-1">{gettext('Whether the notifications of collaboration such as sharing library or joining group should be sent to your mailbox.')}</p>
<form method="post" action="" id="set-email-notice-interval-form" onSubmit={this.formSubmit}>
{this.collaborateOptions.map((item, index) => {
return (
<React.Fragment key={`collaborate-${index}`}>
<input type="radio" name="interval" value={item.interval} className="align-middle" id={`collaborate-interval-option${index + 1}`} checked={collaborateEmailInterval == item.interval} onChange={this.inputCollaborateEmailIntervalChange} />
<label className="align-middle m-0 ml-2" htmlFor={`interval-option${index + 1}`}>{item.text}</label>
<br />
</React.Fragment>
);
})}
</form>
<button type="submit" className="btn btn-outline-primary mt-2" onClick={this.formSubmit}>{gettext('Submit')}</button>
</div>
);
}

View File

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

View File

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

View File

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

View File

@@ -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 %},

View File

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

View File

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

View File

@@ -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):