##// END OF EJS Templates
release: Bump version 5.1.0 to 5.1.1
release: Bump version 5.1.0 to 5.1.1

File last commit:

r5116:cc48fcd2 default
r5481:be158c0f v5.1.1 stable
Show More
notification.py
456 lines | 17.7 KiB | text/x-python | PythonLexer
copyrights: updated for 2023
r5088 # Copyright (C) 2011-2023 RhodeCode GmbH
project: added all source files and assets
r1 #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Model for notifications
"""
import logging
import traceback
emails: added premailer for inline style formatting to make emails render nicer on all email clients.
r4275 import premailer
core: dropped deprecated PartialRenderer that depends on pylons.
r2310 from pyramid.threadlocal import get_current_request
project: added all source files and assets
r1 from sqlalchemy.sql.expression import false, true
import rhodecode
from rhodecode.lib import helpers as h
from rhodecode.model import BaseModel
from rhodecode.model.db import Notification, User, UserNotification
from rhodecode.model.meta import Session
notifications: fixed translation problem in my notifications.
r1387 from rhodecode.translation import TranslationString
project: added all source files and assets
r1
log = logging.getLogger(__name__)
class NotificationModel(BaseModel):
cls = Notification
def __get_notification(self, notification):
if isinstance(notification, Notification):
return notification
python3: fix usage of int/long
r4935 elif isinstance(notification, int):
project: added all source files and assets
r1 return Notification.get(notification)
else:
if notification:
python3: fix usage of int/long
r4935 raise Exception('notification must be int or Instance'
project: added all source files and assets
r1 ' of Notification got %s' % type(notification))
def create(
notifications: skip double rendering just to generate email title/desc
r4560 self, created_by, notification_subject='', notification_body='',
project: added all source files and assets
r1 notification_type=Notification.TYPE_MESSAGE, recipients=None,
mention_recipients=None, with_email=True, email_kwargs=None):
"""
Creates notification of given type
:param created_by: int, str or User instance. User who created this
notification
notifications: skip double rendering just to generate email title/desc
r4560 :param notification_subject: subject of notification itself,
it will be generated automatically from notification_type if not specified
project: added all source files and assets
r1 :param notification_body: body of notification text
notifications: skip double rendering just to generate email title/desc
r4560 it will be generated automatically from notification_type if not specified
project: added all source files and assets
r1 :param notification_type: type of notification, based on that we
pick templates
:param recipients: list of int, str or User objects, when None
is given send to all admins
:param mention_recipients: list of int, str or User objects,
that were mentioned
:param with_email: send email with this notification
:param email_kwargs: dict with arguments to generate email
"""
from rhodecode.lib.celerylib import tasks, run_task
if recipients and not getattr(recipients, '__iter__', False):
raise Exception('recipients must be an iterable object')
notifications: skip double rendering just to generate email title/desc
r4560 if not (notification_subject and notification_body) and not notification_type:
raise ValueError('notification_subject, and notification_body '
'cannot be empty when notification_type is not specified')
project: added all source files and assets
r1 created_by_obj = self._get_user(created_by)
notifications: skip double rendering just to generate email title/desc
r4560
if not created_by_obj:
raise Exception('unknown user %s' % created_by)
project: added all source files and assets
r1 # default MAIN body if not given
email_kwargs = email_kwargs or {'body': notification_body}
mention_recipients = mention_recipients or set()
if recipients is None:
# recipients is None means to all admins
recipients_objs = User.query().filter(User.admin == true()).all()
log.debug('sending notifications %s to admins: %s',
notification_type, recipients_objs)
else:
notifications: use a set initially instead of list later converted to set.
r3273 recipients_objs = set()
project: added all source files and assets
r1 for u in recipients:
obj = self._get_user(u)
if obj:
notifications: use a set initially instead of list later converted to set.
r3273 recipients_objs.add(obj)
project: added all source files and assets
r1 else: # we didn't find this user, log the error and carry on
log.error('cannot notify unknown user %r', u)
if not recipients_objs:
raise Exception('no valid recipients specified')
log.debug('sending notifications %s to %s',
notification_type, recipients_objs)
# add mentioned users into recipients
final_recipients = set(recipients_objs).union(mention_recipients)
api: allow extra recipients for pr/commit comments api methods...
r4049
notifications: skip double rendering just to generate email title/desc
r4560 (subject, email_body, email_body_plaintext) = \
EmailNotificationModel().render_email(notification_type, **email_kwargs)
if not notification_subject:
notification_subject = subject
if not notification_body:
notification_body = email_body_plaintext
project: added all source files and assets
r1 notification = Notification.create(
created_by=created_by_obj, subject=notification_subject,
body=notification_body, recipients=final_recipients,
type_=notification_type
)
if not with_email: # skip sending email, and just create notification
return notification
# don't send email to person who created this comment
notifications: use a set initially instead of list later converted to set.
r3273 rec_objs = set(recipients_objs).difference({created_by_obj})
project: added all source files and assets
r1
# now notify all recipients in question
for recipient in rec_objs.union(mention_recipients):
# inject current recipient
email_kwargs['recipient'] = recipient
email_kwargs['mention'] = recipient in mention_recipients
emails: set References header for threading in mail user agents even with different subjects...
r4447 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
project: added all source files and assets
r1 notification_type, **email_kwargs)
emails: set References header for threading in mail user agents even with different subjects...
r4447 extra_headers = None
if 'thread_ids' in email_kwargs:
extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
log.debug('Creating notification email task for user:`%s`', recipient)
celery: update how reqquest object is passed arround....
r4878 task = run_task(tasks.send_email, recipient.email, subject,
email_body_plaintext, email_body, extra_headers=extra_headers)
project: added all source files and assets
r1 log.debug('Created email task: %s', task)
return notification
def delete(self, user, notification):
# we don't want to remove actual notification just the assignment
try:
notification = self.__get_notification(notification)
user = self._get_user(user)
if notification and user:
obj = UserNotification.query()\
.filter(UserNotification.user == user)\
.filter(UserNotification.notification == notification)\
.one()
Session().delete(obj)
return True
except Exception:
log.error(traceback.format_exc())
raise
def get_for_user(self, user, filter_=None):
"""
Get mentions for given user, filter them if filter dict is given
"""
user = self._get_user(user)
q = UserNotification.query()\
.filter(UserNotification.user == user)\
.join((
Notification, UserNotification.notification_id ==
Notification.notification_id))
notifications: ported views to pyramid
r1920 if filter_ == ['all']:
q = q # no filter
elif filter_ == ['unread']:
q = q.filter(UserNotification.read == false())
elif filter_:
project: added all source files and assets
r1 q = q.filter(Notification.type_.in_(filter_))
cleanup: few fixes
r5116 q = q.order_by(Notification.created_on.desc())
notifications: ported views to pyramid
r1920 return q
project: added all source files and assets
r1
def mark_read(self, user, notification):
try:
notification = self.__get_notification(notification)
user = self._get_user(user)
if notification and user:
obj = UserNotification.query()\
.filter(UserNotification.user == user)\
.filter(UserNotification.notification == notification)\
.one()
obj.read = True
Session().add(obj)
return True
except Exception:
log.error(traceback.format_exc())
raise
def mark_all_read_for_user(self, user, filter_=None):
user = self._get_user(user)
q = UserNotification.query()\
.filter(UserNotification.user == user)\
.filter(UserNotification.read == false())\
.join((
Notification, UserNotification.notification_id ==
Notification.notification_id))
notifications: ported views to pyramid
r1920 if filter_ == ['unread']:
q = q.filter(UserNotification.read == false())
elif filter_:
project: added all source files and assets
r1 q = q.filter(Notification.type_.in_(filter_))
# this is a little inefficient but sqlalchemy doesn't support
# update on joined tables :(
for obj in q.all():
obj.read = True
Session().add(obj)
def get_unread_cnt_for_user(self, user):
user = self._get_user(user)
return UserNotification.query()\
.filter(UserNotification.read == false())\
.filter(UserNotification.user == user).count()
def get_unread_for_user(self, user):
user = self._get_user(user)
return [x.notification for x in UserNotification.query()
.filter(UserNotification.read == false())
.filter(UserNotification.user == user).all()]
def get_user_notification(self, user, notification):
user = self._get_user(user)
notification = self.__get_notification(notification)
return UserNotification.query()\
.filter(UserNotification.notification == notification)\
.filter(UserNotification.user == user).scalar()
notifications: removed usage of pylons translation from models.
r1921 def make_description(self, notification, translate, show_age=True):
project: added all source files and assets
r1 """
Creates a human readable description based on properties
of notification object
"""
notifications: removed usage of pylons translation from models.
r1921 _ = translate
project: added all source files and assets
r1 _map = {
notification.TYPE_CHANGESET_COMMENT: [
_('%(user)s commented on commit %(date_or_age)s'),
_('%(user)s commented on commit at %(date_or_age)s'),
],
notification.TYPE_MESSAGE: [
_('%(user)s sent message %(date_or_age)s'),
_('%(user)s sent message at %(date_or_age)s'),
],
notification.TYPE_MENTION: [
_('%(user)s mentioned you %(date_or_age)s'),
_('%(user)s mentioned you at %(date_or_age)s'),
],
notification.TYPE_REGISTRATION: [
_('%(user)s registered in RhodeCode %(date_or_age)s'),
_('%(user)s registered in RhodeCode at %(date_or_age)s'),
],
notification.TYPE_PULL_REQUEST: [
_('%(user)s opened new pull request %(date_or_age)s'),
_('%(user)s opened new pull request at %(date_or_age)s'),
],
notifications: added update PR case.
r4137 notification.TYPE_PULL_REQUEST_UPDATE: [
_('%(user)s updated pull request %(date_or_age)s'),
_('%(user)s updated pull request at %(date_or_age)s'),
],
project: added all source files and assets
r1 notification.TYPE_PULL_REQUEST_COMMENT: [
_('%(user)s commented on pull request %(date_or_age)s'),
_('%(user)s commented on pull request at %(date_or_age)s'),
],
}
templates = _map[notification.type_]
if show_age:
template = templates[0]
date_or_age = h.age(notification.created_on)
pr-versioning: implemented versioning for pull requests....
r1368 if translate:
date_or_age = translate(date_or_age)
notifications: fixed translation problem in my notifications.
r1387
if isinstance(date_or_age, TranslationString):
date_or_age = date_or_age.interpolate()
project: added all source files and assets
r1 else:
template = templates[1]
date_or_age = h.format_date(notification.created_on)
return template % {
'user': notification.created_by_user.username,
'date_or_age': date_or_age,
}
emails: added logic to allow overwriting the default email titles via rcextensions.
r4448 # Templates for Titles, that could be overwritten by rcextensions
# Title of email for pull-request update
EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
# Title of email for request for pull request review
EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
# Title of email for general comment on pull request
EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
# Title of email for general comment which includes status change on pull request
EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
# Title of email for inline comment on a file in pull request
EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
# Title of email for general comment on commit
EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
# Title of email for general comment which includes status change on commit
EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
# Title of email for inline comment on a file in commit
EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
premailer: replace logger instead of altering handlers....
r4857 import cssutils
# hijack css utils logger and replace with ours
log = logging.getLogger('rhodecode.cssutils.premailer')
models: major update for python3,...
r5070 log.setLevel(logging.INFO)
premailer: replace logger instead of altering handlers....
r4857 cssutils.log.setLog(log)
emails: added logic to allow overwriting the default email titles via rcextensions.
r4448
project: added all source files and assets
r1 class EmailNotificationModel(BaseModel):
TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
pull-requests: added update pull-requests email+notifications...
r4120 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
project: added all source files and assets
r1 TYPE_MAIN = Notification.TYPE_MESSAGE
TYPE_PASSWORD_RESET = 'password_reset'
TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
TYPE_EMAIL_TEST = 'email_test'
exception-tracker: enable send email on exception
r4276 TYPE_EMAIL_EXCEPTION = 'exception'
automation: enabled automated check for new versions.
r4634 TYPE_UPDATE_AVAILABLE = 'update_available'
project: added all source files and assets
r1 TYPE_TEST = 'test'
email_types = {
partial-renderer: use package resource format for templates....
r2313 TYPE_MAIN:
'rhodecode:templates/email_templates/main.mako',
TYPE_TEST:
'rhodecode:templates/email_templates/test.mako',
exception-tracker: enable send email on exception
r4276 TYPE_EMAIL_EXCEPTION:
'rhodecode:templates/email_templates/exception_tracker.mako',
automation: enabled automated check for new versions.
r4634 TYPE_UPDATE_AVAILABLE:
'rhodecode:templates/email_templates/update_available.mako',
partial-renderer: use package resource format for templates....
r2313 TYPE_EMAIL_TEST:
'rhodecode:templates/email_templates/email_test.mako',
TYPE_REGISTRATION:
'rhodecode:templates/email_templates/user_registration.mako',
TYPE_PASSWORD_RESET:
'rhodecode:templates/email_templates/password_reset.mako',
TYPE_PASSWORD_RESET_CONFIRMATION:
'rhodecode:templates/email_templates/password_reset_confirmation.mako',
TYPE_COMMIT_COMMENT:
'rhodecode:templates/email_templates/commit_comment.mako',
TYPE_PULL_REQUEST:
'rhodecode:templates/email_templates/pull_request_review.mako',
TYPE_PULL_REQUEST_COMMENT:
'rhodecode:templates/email_templates/pull_request_comment.mako',
pull-requests: added update pull-requests email+notifications...
r4120 TYPE_PULL_REQUEST_UPDATE:
'rhodecode:templates/email_templates/pull_request_update.mako',
project: added all source files and assets
r1 }
models: major update for python3,...
r5070 premailer_instance = premailer.Premailer(
#cssutils_logging_handler=log.handlers[0],
#cssutils_logging_level=logging.INFO
)
emails: added premailer for inline style formatting to make emails render nicer on all email clients.
r4275
project: added all source files and assets
r1 def __init__(self):
"""
Example usage::
emails: set References header for threading in mail user agents even with different subjects...
r4447 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
project: added all source files and assets
r1 EmailNotificationModel.TYPE_TEST, **email_kwargs)
"""
modernize: python3 updates
r5096 super().__init__()
notifications: skip fetching of settings in notifications....
r1497 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
project: added all source files and assets
r1
def _update_kwargs_for_render(self, kwargs):
"""
Inject params required for Mako rendering
:param kwargs:
"""
emails: fixed newlines in email templates that can break email sending code.
r1728
notifications: skip fetching of settings in notifications....
r1497 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
pull-requests: added update pull-requests email+notifications...
r4120 kwargs['rhodecode_version'] = rhodecode.__version__
home: moved home and repo group views into pyramid....
r1774 instance_url = h.route_url('home')
project: added all source files and assets
r1 _kwargs = {
home: moved home and repo group views into pyramid....
r1774 'instance_url': instance_url,
dan
notifications: properly inject the custom email headers into templates.
r4461 'whitespace_filter': self.whitespace_filter,
'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
project: added all source files and assets
r1 }
_kwargs.update(kwargs)
return _kwargs
emails: fixed newlines in email templates that can break email sending code.
r1728 def whitespace_filter(self, text):
return text.replace('\n', '').replace('\t', '')
core: dropped deprecated PartialRenderer that depends on pylons.
r2310 def get_renderer(self, type_, request):
project: added all source files and assets
r1 template_name = self.email_types[type_]
core: dropped deprecated PartialRenderer that depends on pylons.
r2310 return request.get_partial_renderer(template_name)
project: added all source files and assets
r1
def render_email(self, type_, **kwargs):
"""
renders template for email, and returns a tuple of
docs: fixed docstring
r500 (subject, email_headers, email_html_body, email_plaintext_body)
project: added all source files and assets
r1 """
translations: moved methods from new request subscriber to actual methods of custom class....
r4842 request = get_current_request()
project: added all source files and assets
r1 # translator and helpers inject
_kwargs = self._update_kwargs_for_render(kwargs)
core: dropped deprecated PartialRenderer that depends on pylons.
r2310 email_template = self.get_renderer(type_, request=request)
project: added all source files and assets
r1 subject = email_template.render('subject', **_kwargs)
try:
body_plaintext = email_template.render('body_plaintext', **_kwargs)
except AttributeError:
# it's not defined in template, ok we can skip it
body_plaintext = ''
# render WHOLE template
body = email_template.render(None, **_kwargs)
emails: added premailer for inline style formatting to make emails render nicer on all email clients.
r4275 try:
# Inline CSS styles and conversion
body = self.premailer_instance.transform(body)
except Exception:
log.exception('Failed to parse body with premailer')
pass
emails: set References header for threading in mail user agents even with different subjects...
r4447 return subject, body, body_plaintext