##// END OF EJS Templates
feat(configs): deprecared old hooks protocol and ssh wrapper....
feat(configs): deprecared old hooks protocol and ssh wrapper. New defaults are now set on v2 keys, so previous installation are automatically set to new keys. Fallback mode is still available.

File last commit:

r5116:cc48fcd2 default
r5496:cab50adf default
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