emails: updated emails design and data structure they provide.

- more consistent UI for emails
- nicer formatting, plaintext emails
- major cleanup and fixes lots of incosistencies
- additionally debug-style now allows to test emails in browser for faster development
This commit is contained in:
Daniel Dourvaris 2019-11-05 19:07:09 +01:00
parent a5dd4ceb99
commit 453a6ee9a2
23 changed files with 1284 additions and 265 deletions

View file

@ -42,6 +42,14 @@ def includeme(config):
name='debug_style_home',
pattern=ADMIN_PREFIX + '/debug_style',
debug_style=True)
config.add_route(
name='debug_style_email',
pattern=ADMIN_PREFIX + '/debug_style/email/{email_id}',
debug_style=True)
config.add_route(
name='debug_style_email_plain_rendered',
pattern=ADMIN_PREFIX + '/debug_style/email-rendered/{email_id}',
debug_style=True)
config.add_route(
name='debug_style_template',
pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}',

View file

@ -20,10 +20,15 @@
import os
import logging
import datetime
from pyramid.view import view_config
from pyramid.renderers import render_to_response
from rhodecode.apps._base import BaseAppView
from rhodecode.lib.celerylib import run_task, tasks
from rhodecode.lib.utils2 import AttributeDict
from rhodecode.model.db import User
from rhodecode.model.notification import EmailNotificationModel
log = logging.getLogger(__name__)
@ -45,6 +50,271 @@ class DebugStyleView(BaseAppView):
'debug_style/index.html', self._get_template_context(c),
request=self.request)
@view_config(
route_name='debug_style_email', request_method='GET',
renderer=None)
@view_config(
route_name='debug_style_email_plain_rendered', request_method='GET',
renderer=None)
def render_email(self):
c = self.load_default_context()
email_id = self.request.matchdict['email_id']
c.active = 'emails'
pr = AttributeDict(
pull_request_id=123,
title='digital_ocean: fix redis, elastic search start on boot, '
'fix fd limits on supervisor, set postgres 11 version',
description='''
Check if we should use full-topic or mini-topic.
- full topic produces some problems with merge states etc
- server-mini-topic needs probably tweeks.
''',
repo_name='foobar',
source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
target_ref_parts=AttributeDict(type='branch', name='master'),
)
target_repo = AttributeDict(repo_name='repo_group/target_repo')
source_repo = AttributeDict(repo_name='repo_group/source_repo')
user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
email_kwargs = {
'test': {},
'message': {
'body': 'message body !'
},
'email_test': {
'user': user,
'date': datetime.datetime.now(),
'rhodecode_version': c.rhodecode_version
},
'password_reset': {
'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
'user': user,
'date': datetime.datetime.now(),
'email': 'test@rhodecode.com',
'first_admin_email': User.get_first_super_admin().email
},
'password_reset_confirmation': {
'new_password': 'new-password-example',
'user': user,
'date': datetime.datetime.now(),
'email': 'test@rhodecode.com',
'first_admin_email': User.get_first_super_admin().email
},
'registration': {
'user': user,
'date': datetime.datetime.now(),
},
'pull_request_comment': {
'user': user,
'status_change': None,
'status_change_type': None,
'pull_request': pr,
'pull_request_commits': [],
'pull_request_target_repo': target_repo,
'pull_request_target_repo_url': 'http://target-repo/url',
'pull_request_source_repo': source_repo,
'pull_request_source_repo_url': 'http://source-repo/url',
'pull_request_url': 'http://localhost/pr1',
'pr_comment_url': 'http://comment-url',
'comment_file': None,
'comment_line': None,
'comment_type': 'note',
'comment_body': 'This is my comment body. *I like !*',
'renderer_type': 'markdown',
'mention': True,
},
'pull_request_comment+status': {
'user': user,
'status_change': 'approved',
'status_change_type': 'approved',
'pull_request': pr,
'pull_request_commits': [],
'pull_request_target_repo': target_repo,
'pull_request_target_repo_url': 'http://target-repo/url',
'pull_request_source_repo': source_repo,
'pull_request_source_repo_url': 'http://source-repo/url',
'pull_request_url': 'http://localhost/pr1',
'pr_comment_url': 'http://comment-url',
'comment_type': 'todo',
'comment_file': None,
'comment_line': None,
'comment_body': '''
I think something like this would be better
```py
def db():
global connection
return connection
```
''',
'renderer_type': 'markdown',
'mention': True,
},
'pull_request_comment+file': {
'user': user,
'status_change': None,
'status_change_type': None,
'pull_request': pr,
'pull_request_commits': [],
'pull_request_target_repo': target_repo,
'pull_request_target_repo_url': 'http://target-repo/url',
'pull_request_source_repo': source_repo,
'pull_request_source_repo_url': 'http://source-repo/url',
'pull_request_url': 'http://localhost/pr1',
'pr_comment_url': 'http://comment-url',
'comment_file': 'rhodecode/model/db.py',
'comment_line': 'o1210',
'comment_type': 'todo',
'comment_body': '''
I like this !
But please check this code::
def main():
print 'ok'
This should work better !
''',
'renderer_type': 'rst',
'mention': True,
},
'cs_comment': {
'user': user,
'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
'status_change': None,
'status_change_type': None,
'commit_target_repo_url': 'http://foo.example.com/#comment1',
'repo_name': 'test-repo',
'comment_type': 'note',
'comment_file': None,
'comment_line': None,
'commit_comment_url': 'http://comment-url',
'comment_body': 'This is my comment body. *I like !*',
'renderer_type': 'markdown',
'mention': True,
},
'cs_comment+status': {
'user': user,
'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
'status_change': 'approved',
'status_change_type': 'approved',
'commit_target_repo_url': 'http://foo.example.com/#comment1',
'repo_name': 'test-repo',
'comment_type': 'note',
'comment_file': None,
'comment_line': None,
'commit_comment_url': 'http://comment-url',
'comment_body': '''
Hello **world**
This is a multiline comment :)
- list
- list2
''',
'renderer_type': 'markdown',
'mention': True,
},
'cs_comment+file': {
'user': user,
'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
'status_change': None,
'status_change_type': None,
'commit_target_repo_url': 'http://foo.example.com/#comment1',
'repo_name': 'test-repo',
'comment_type': 'note',
'comment_file': 'test-file.py',
'comment_line': 'n100',
'commit_comment_url': 'http://comment-url',
'comment_body': 'This is my comment body. *I like !*',
'renderer_type': 'markdown',
'mention': True,
},
'pull_request': {
'user': user,
'pull_request': pr,
'pull_request_commits': [
('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
my-account: moved email closer to profile as it's similar data just moved outside.
'''),
('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
users: description edit fixes
- tests
- added metatags info
'''),
],
'pull_request_target_repo': target_repo,
'pull_request_target_repo_url': 'http://target-repo/url',
'pull_request_source_repo': source_repo,
'pull_request_source_repo_url': 'http://source-repo/url',
'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
}
}
template_type = email_id.split('+')[0]
(c.subject, c.headers, c.email_body,
c.email_body_plaintext) = EmailNotificationModel().render_email(
template_type, **email_kwargs.get(email_id, {}))
test_email = self.request.GET.get('email')
if test_email:
recipients = [test_email]
run_task(tasks.send_email, recipients, c.subject,
c.email_body_plaintext, c.email_body)
if self.request.matched_route.name == 'debug_style_email_plain_rendered':
template = 'debug_style/email_plain_rendered.mako'
else:
template = 'debug_style/email.mako'
return render_to_response(
template, self._get_template_context(c),
request=self.request)
@view_config(
route_name='debug_style_template', request_method='GET',
renderer=None)
@ -53,7 +323,16 @@ class DebugStyleView(BaseAppView):
c = self.load_default_context()
c.active = os.path.splitext(t_path)[0]
c.came_from = ''
c.email_types = {
'cs_comment+file': {},
'cs_comment+status': {},
'pull_request_comment+file': {},
'pull_request_comment+status': {},
}
c.email_types.update(EmailNotificationModel.email_types)
return render_to_response(
'debug_style/' + t_path, self._get_template_context(c),
request=self.request)
request=self.request)

View file

@ -98,7 +98,7 @@ class TestRepoCommitCommentsView(TestController):
assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
author = notification.created_by_user.username_and_name
sbj = '{0} left a {1} on commit `{2}` in the {3} repository'.format(
sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
author, comment_type, h.show_id(commit), backend.repo_name)
assert sbj == notification.subject
@ -159,7 +159,7 @@ class TestRepoCommitCommentsView(TestController):
assert comment.revision == commit_id
author = notification.created_by_user.username_and_name
sbj = '{0} left a {1} on file `{2}` in commit `{3}` in the {4} repository'.format(
sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
author, comment_type, f_path, h.show_id(commit), backend.repo_name)
assert sbj == notification.subject
@ -230,7 +230,7 @@ class TestRepoCommitCommentsView(TestController):
assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
author = notification.created_by_user.username_and_name
sbj = '[status: Approved] {0} left a note on commit `{1}` in the {2} repository'.format(
sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
author, h.show_id(commit), backend.repo_name)
assert sbj == notification.subject

View file

@ -467,7 +467,7 @@ class TestPullrequestsView(object):
.filter(Notification.created_by == pull_request.author.user_id,
Notification.type_ == Notification.TYPE_PULL_REQUEST,
Notification.subject.contains(
"wants you to review pull request #%s" % pull_request_id))
"requested a pull request review. !%s" % pull_request_id))
assert len(notifications.all()) == 1
# Change reviewers and check that a notification was made
@ -549,11 +549,10 @@ class TestPullrequestsView(object):
pull_request_id = pull_request.pull_request_id
repo_name = pull_request.target_repo.scm_instance().name,
response = self.app.post(
route_path('pullrequest_merge',
repo_name=str(repo_name[0]),
pull_request_id=pull_request_id),
params={'csrf_token': csrf_token}).follow()
url = route_path('pullrequest_merge',
repo_name=str(repo_name[0]),
pull_request_id=pull_request_id)
response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
pull_request = PullRequest.get(pull_request_id)
@ -735,12 +734,12 @@ class TestPullrequestsView(object):
backend.pull_heads(source, heads=['change-rebased'])
# update PR
self.app.post(
route_path('pullrequest_update',
repo_name=target.repo_name,
pull_request_id=pull_request_id),
params={'update_commits': 'true', 'csrf_token': csrf_token},
status=200)
url = route_path('pullrequest_update',
repo_name=target.repo_name,
pull_request_id=pull_request_id)
self.app.post(url,
params={'update_commits': 'true', 'csrf_token': csrf_token},
status=200)
# check that we have now both revisions
pull_request = PullRequest.get(pull_request_id)
@ -801,12 +800,12 @@ class TestPullrequestsView(object):
vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
# update PR
self.app.post(
route_path('pullrequest_update',
repo_name=target.repo_name,
pull_request_id=pull_request_id),
params={'update_commits': 'true', 'csrf_token': csrf_token},
status=200)
url = route_path('pullrequest_update',
repo_name=target.repo_name,
pull_request_id=pull_request_id)
self.app.post(url,
params={'update_commits': 'true', 'csrf_token': csrf_token},
status=200)
response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
assert response.status_int == 200
@ -961,12 +960,12 @@ class TestPullrequestsView(object):
else:
vcs.strip(pr_util.commit_ids['new-feature'])
response = self.app.post(
route_path('pullrequest_update',
repo_name=pull_request.target_repo.repo_name,
pull_request_id=pull_request.pull_request_id),
params={'update_commits': 'true',
'csrf_token': csrf_token})
url = route_path('pullrequest_update',
repo_name=pull_request.target_repo.repo_name,
pull_request_id=pull_request.pull_request_id)
response = self.app.post(url,
params={'update_commits': 'true',
'csrf_token': csrf_token})
assert response.status_int == 200
assert response.body == 'true'
@ -1208,14 +1207,11 @@ class TestPullrequestsControllerDelete(object):
def assert_pull_request_status(pull_request, expected_status):
status = ChangesetStatusModel().calculated_review_status(
pull_request=pull_request)
status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
assert status == expected_status
@pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
@pytest.mark.usefixtures("autologin_user")
def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
response = app.get(
route_path(route, repo_name=backend_svn.repo_name), status=404)
app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)

View file

@ -359,7 +359,7 @@ class CommentsModel(BaseModel):
kwargs.update({
'commit': commit_obj,
'commit_message': commit_obj.message,
'commit_target_repo': target_repo_url,
'commit_target_repo_url': target_repo_url,
'commit_comment_url': commit_comment_url,
})
@ -382,6 +382,11 @@ class CommentsModel(BaseModel):
pull_request_id=pull_request_obj.pull_request_id,
_anchor='comment-%s' % comment.comment_id)
pr_url = h.route_url(
'pullrequest_show',
repo_name=pr_target_repo.repo_name,
pull_request_id=pull_request_obj.pull_request_id, )
# set some variables for email notification
pr_target_repo_url = h.route_url(
'repo_summary', repo_name=pr_target_repo.repo_name)
@ -393,10 +398,11 @@ class CommentsModel(BaseModel):
kwargs.update({
'pull_request': pull_request_obj,
'pr_id': pull_request_obj.pull_request_id,
'pr_target_repo': pr_target_repo,
'pr_target_repo_url': pr_target_repo_url,
'pr_source_repo': pr_source_repo,
'pr_source_repo_url': pr_source_repo_url,
'pull_request_url': pr_url,
'pull_request_target_repo': pr_target_repo,
'pull_request_target_repo_url': pr_target_repo_url,
'pull_request_source_repo': pr_source_repo,
'pull_request_source_repo_url': pr_source_repo_url,
'pr_comment_url': pr_comment_url,
'pr_closing': closing_pr,
})

View file

@ -1110,8 +1110,8 @@ class PullRequestModel(BaseModel):
pr_target_repo = pull_request_obj.target_repo
pr_url = h.route_url('pullrequest_show',
repo_name=pr_target_repo.repo_name,
pull_request_id=pull_request_obj.pull_request_id,)
repo_name=pr_target_repo.repo_name,
pull_request_id=pull_request_obj.pull_request_id,)
# set some variables for email notification
pr_target_repo_url = h.route_url(

View file

@ -612,7 +612,8 @@ class UserModel(BaseModel):
'password_reset_url': pwd_reset_url,
'user': user,
'email': user_email,
'date': datetime.datetime.now()
'date': datetime.datetime.now(),
'first_admin_email': User.get_first_super_admin().email
}
(subject, headers, email_body,
@ -670,7 +671,8 @@ class UserModel(BaseModel):
'new_password': new_passwd,
'user': user,
'email': user_email,
'date': datetime.datetime.now()
'date': datetime.datetime.now(),
'first_admin_email': User.get_first_super_admin().email
}
(subject, headers, email_body,

View file

@ -342,6 +342,8 @@ function registerRCRoutes() {
pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
pyroutes.register('debug_style_home', '/_admin/debug_style', []);
pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
pyroutes.register('apiv2', '/_admin/api', []);
pyroutes.register('admin_settings_license', '/_admin/settings/license', []);

View file

@ -0,0 +1,29 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head></head>
<body>
SUBJECT:
<pre>${c.subject}</pre>
HEADERS:
<pre>
${c.headers}
</pre>
PLAINTEXT:
<pre>
${c.email_body_plaintext|n}
</pre>
</body>
</html>
<br/><br/>
HTML:
${c.email_body|n}

View file

@ -0,0 +1,49 @@
## -*- coding: utf-8 -*-
<%inherit file="/base/base.mako"/>
<%def name="title()">
${_('Show notification')} ${c.rhodecode_user.username}
%if c.rhodecode_name:
&middot; ${h.branding(c.rhodecode_name)}
%endif
</%def>
<%def name="breadcrumbs_links()">
${h.link_to(_('My Notifications'), h.route_path('notifications_show_all'))}
&raquo;
${_('Show notification')}
</%def>
<%def name="menu_bar_nav()">
${self.menu_items(active='admin')}
</%def>
<%def name="main()">
<div class="box">
<!-- box / title -->
<div class="title">
Rendered plain text using markup renderer
</div>
<div class="table">
<div >
<div class="notification-header">
GRAVATAR
<div class="desc">
DESC
</div>
</div>
<div class="notification-body">
<div class="notification-subject">
<h3>${_('Subject')}: ${c.subject}</h3>
</div>
${c.email_body|n}
</div>
</div>
</div>
</div>
</%def>

View file

@ -0,0 +1,34 @@
## -*- coding: utf-8 -*-
<%inherit file="/debug_style/index.html"/>
<%def name="breadcrumbs_links()">
${h.link_to(_('Style'), h.route_path('debug_style_home'))}
&raquo;
${c.active}
</%def>
<%def name="real_main()">
<div class="box">
<div class="title">
${self.breadcrumbs()}
</div>
<div class='sidebar-col-wrapper'>
${self.sidebar()}
<div class="main-content">
<h2>Emails</h2>
<ul>
% for elem in sorted(c.email_types.keys()):
<li>
<a href="${request.route_path('debug_style_email', email_id=elem, _query={'user':c.rhodecode_user.username})}">${elem}</a>
|
<a href="${request.route_path('debug_style_email_plain_rendered', email_id=elem, _query={'user':c.rhodecode_user.username})}">plain rendered</a>
</li>
% endfor
</ul>
</div> <!-- .main-content -->
</div>
</div> <!-- .box -->
</%def>

View file

@ -52,6 +52,7 @@
<div class="sidebar">
<ul class="nav nav-pills nav-stacked">
<li class="${'active' if c.active=='index' else ''}"><a href="${h.route_path('debug_style_home')}">${_('Index')}</a></li>
<li class="${'active' if c.active=='emails' else ''}"><a href="${h.route_path('debug_style_template', t_path='emails.html')}">${_('Emails')}</a></li>
<li class="${'active' if c.active=='typography' else ''}"><a href="${h.route_path('debug_style_template', t_path='typography.html')}">${_('Typography')}</a></li>
<li class="${'active' if c.active=='forms' else ''}"><a href="${h.route_path('debug_style_template', t_path='forms.html')}">${_('Forms')}</a></li>
<li class="${'active' if c.active=='buttons' else ''}"><a href="${h.route_path('debug_style_template', t_path='buttons.html')}">${_('Buttons')}</a></li>

View file

@ -2,15 +2,23 @@
## helpers
<%def name="tag_button(text, tag_type=None)">
<%
color_scheme = {
'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
}
%>
<pre style="display:inline;border-radius:2px;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
<%
color_scheme = {
'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
}
css_style = ';'.join([
'display:inline',
'border-radius:2px',
'font-size:12px',
'padding:.2em',
])
%>
<pre style="${css_style}; ${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
</%def>
<%def name="status_text(text, tag_type=None)">
@ -25,6 +33,34 @@
<span style="font-weight:bold;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</span>
</%def>
<%def name="gravatar_img(email, size=16)">
<%
css_style = ';'.join([
'padding: 0',
'margin: -4px 0',
'border-radius: 50%',
'box-sizing: content-box',
'display: inline',
'line-height: 1em',
'min-width: 16px',
'min-height: 16px',
])
%>
<img alt="gravatar" style="${css_style}" src="${h.gravatar_url(email, size)}" height="${size}" width="${size}">
</%def>
<%def name="link_css()">\
<%
css_style = ';'.join([
'color:#427cc9',
'text-decoration:none',
'cursor:pointer'
])
%>\
${css_style}\
</%def>
## Constants
<%
text_regular = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;"
@ -35,8 +71,8 @@ text_monospace = "'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'U
## headers we additionally can set for email
<%def name="headers()" filter="n,trim"></%def>
<%def name="plaintext_footer()">
${_('This is a notification from RhodeCode. %(instance_url)s') % {'instance_url': instance_url}}
<%def name="plaintext_footer()" filter="trim">
${_('This is a notification from RhodeCode.')} ${instance_url}
</%def>
<%def name="body_plaintext()" filter="n,trim">
@ -52,36 +88,122 @@ ${self.plaintext_footer()}
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>${self.subject()}</title>
<style type="text/css">
/* Based on The MailChimp Reset INLINE: Yes. */
#outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; font-family: ${text_regular|n}}
/* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
.ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
/* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
#backgroundTable {margin:0; padding:0; line-height: 100% !important;}
/* Based on The MailChimp Reset INLINE: Yes. */
#outlook a {
padding: 0;
}
/* Force Outlook to provide a "view in browser" menu link. */
body {
width: 100% !important;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
margin: 0;
padding: 0;
font-family: ${text_regular|n}
}
/* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
.ExternalClass {
width: 100%;
}
/* Force Hotmail to display emails at full width */
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
line-height: 100%;
}
/* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
#backgroundTable {
margin: 0;
padding: 0;
line-height: 100% !important;
}
/* End reset */
/* defaults for images*/
img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
a img {border:none;}
.image_fix {display:block;}
img {
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
a img {
border: none;
}
.image_fix {
display: block;
}
body {
line-height: 1.2em;
}
p {
margin: 0 0 20px;
}
h1, h2, h3, h4, h5, h6 {
color: #323232 !important;
}
a {
color: #427cc9;
text-decoration: none;
outline: none;
cursor: pointer;
}
a:focus {
outline: none;
}
a:hover {
color: #305b91;
}
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
color: #427cc9 !important;
text-decoration: none !important;
}
h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
color: #305b91 !important;
}
h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
color: #305b91 !important;
}
table {
font-size: 13px;
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
table td {
padding: .65em 1em .65em 0;
border-collapse: collapse;
vertical-align: top;
text-align: left;
}
input {
display: inline;
border-radius: 2px;
border: 1px solid #dbd9da;
padding: .5em;
}
input:focus {
outline: 1px solid #979797
}
body {line-height:1.2em;}
p {margin: 0 0 20px;}
h1, h2, h3, h4, h5, h6 {color:#323232!important;}
a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
a:focus {outline:none;}
a:hover {color: #305b91;}
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
input:focus {outline: 1px solid #979797}
@media only screen and (-webkit-min-device-pixel-ratio: 2) {
/* Put your iPhone 4g styles in here */
/* Put your iPhone 4g styles in here */
}
/* Android targeting */
@ -96,6 +218,262 @@ ${self.plaintext_footer()}
}
/* end Android targeting */
/** MARKDOWN styling **/
div.markdown-block {
clear: both;
overflow: hidden;
margin: 0;
padding: 3px 5px 3px
}
div.markdown-block h1, div.markdown-block h2, div.markdown-block h3, div.markdown-block h4, div.markdown-block h5, div.markdown-block h6 {
border-bottom: none !important;
padding: 0 !important;
overflow: visible !important
}
div.markdown-block h1, div.markdown-block h2 {
border-bottom: 1px #e6e5e5 solid !important
}
div.markdown-block h1 {
font-size: 32px;
margin: 15px 0 15px 0 !important;
padding-bottom: 5px !important
}
div.markdown-block h2 {
font-size: 24px !important;
margin: 34px 0 10px 0 !important;
padding-top: 15px !important;
padding-bottom: 8px !important
}
div.markdown-block h3 {
font-size: 18px !important;
margin: 30px 0 8px 0 !important;
padding-bottom: 2px !important
}
div.markdown-block h4 {
font-size: 13px !important;
margin: 18px 0 3px 0 !important
}
div.markdown-block h5 {
font-size: 12px !important;
margin: 15px 0 3px 0 !important
}
div.markdown-block h6 {
font-size: 12px;
color: #777777;
margin: 15px 0 3px 0 !important
}
div.markdown-block hr {
border: 0;
color: #e6e5e5;
background-color: #e6e5e5;
height: 3px;
margin-bottom: 13px
}
div.markdown-block ol, div.markdown-block ul, div.markdown-block p, div.markdown-block blockquote, div.markdown-block dl, div.markdown-block li, div.markdown-block table {
margin: 3px 0 13px 0 !important;
color: #424242 !important;
font-size: 13px !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-weight: normal !important;
overflow: visible !important;
line-height: 140% !important
}
div.markdown-block pre {
margin: 3px 0 13px 0 !important;
padding: .5em;
color: #424242 !important;
font-size: 13px !important;
overflow: visible !important;
line-height: 140% !important;
background-color: #F5F5F5
}
div.markdown-block img {
border-style: none;
background-color: #fff;
padding-right: 20px;
max-width: 100%
}
div.markdown-block strong {
font-weight: 600;
margin: 0
}
div.markdown-block ul.checkbox, div.markdown-block ol.checkbox {
padding-left: 20px !important;
margin-top: 0 !important;
margin-bottom: 18px !important
}
div.markdown-block ul, div.markdown-block ol {
padding-left: 30px !important;
margin-top: 0 !important;
margin-bottom: 18px !important
}
div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
list-style: none !important;
margin: 6px !important;
padding: 0 !important
}
div.markdown-block ul li, div.markdown-block ol li {
list-style: disc !important;
margin: 6px !important;
padding: 0 !important
}
div.markdown-block ol li {
list-style: decimal !important
}
div.markdown-block #message {
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px;
border: 1px solid #dbd9da;
display: block;
width: 100%;
height: 60px;
margin: 6px 0
}
div.markdown-block button, div.markdown-block #ws {
font-size: 13px;
padding: 4px 6px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px;
border: 1px solid #dbd9da;
background-color: #eeeeee
}
div.markdown-block code, div.markdown-block pre, div.markdown-block #ws, div.markdown-block #message {
font-family: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
font-size: 11px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px;
background-color: white;
color: #7E7F7F
}
div.markdown-block code {
border: 1px solid #eeeeee;
margin: 0 2px;
padding: 0 5px
}
div.markdown-block pre {
border: 1px solid #dbd9da;
overflow: auto;
padding: .5em;
background-color: #F5F5F5
}
div.markdown-block pre > code {
border: 0;
margin: 0;
padding: 0
}
div.rst-block {
clear: both;
overflow: hidden;
margin: 0;
padding: 3px 5px 3px
}
div.rst-block h2 {
font-weight: normal
}
div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
border-bottom: 0 !important;
margin: 0 !important;
padding: 0 !important;
line-height: 1.5em !important
}
div.rst-block h1:first-child {
padding-top: .25em !important
}
div.rst-block h2, div.rst-block h3 {
margin: 1em 0 !important
}
div.rst-block h1, div.rst-block h2 {
border-bottom: 1px #e6e5e5 solid !important
}
div.rst-block h2 {
margin-top: 1.5em !important;
padding-top: .5em !important
}
div.rst-block p {
color: black !important;
margin: 1em 0 !important;
line-height: 1.5em !important
}
div.rst-block ul {
list-style: disc !important;
margin: 1em 0 1em 2em !important;
clear: both
}
div.rst-block ol {
list-style: decimal;
margin: 1em 0 1em 2em !important
}
div.rst-block pre, div.rst-block code {
font: 12px "Bitstream Vera Sans Mono", "Courier", monospace
}
div.rst-block code {
font-size: 12px !important;
background-color: ghostWhite !important;
color: #444 !important;
padding: 0 .2em !important;
border: 1px solid #dedede !important
}
div.rst-block pre code {
padding: 0 !important;
font-size: 12px !important;
background-color: #eee !important;
border: none !important
}
div.rst-block pre {
margin: 1em 0;
padding: 15px;
border: 1px solid #eeeeee;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px;
overflow: auto;
font-size: 12px;
color: #444;
background-color: #F5F5F5
}
</style>
<!-- Targeting Windows Mobile -->
@ -106,26 +484,30 @@ ${self.plaintext_footer()}
<![endif]-->
<!--[if gte mso 9]>
<style>
/* Target Outlook 2007 and 2010 */
</style>
<style>
/* Target Outlook 2007 and 2010 */
</style>
<![endif]-->
</head>
<body>
<!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
<table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
<tr>
<td valign="top" style="padding:0;">
<td valign="top" style="padding:0;">
<table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
<tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
<a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
${_('RhodeCode')}
% if rhodecode_instance_name:
- ${rhodecode_instance_name}
% endif
</a>
</td></tr>
<tr><td style="padding:15px;" valign="top">${self.body()}</td></tr>
<tr>
<td style="width:100%;padding:10px 15px;background-color:#202020" valign="top">
<a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
${_('RhodeCode')}
% if rhodecode_instance_name:
- ${rhodecode_instance_name}
% endif
</a>
</td>
</tr>
<tr>
<td style="padding:15px;" valign="top">${self.body()}</td>
</tr>
</table>
</td>
</tr>
@ -133,10 +515,11 @@ ${self.plaintext_footer()}
<!-- End of wrapper table -->
<div style="clear: both"></div>
<p>
<a style="margin-top:15px;margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace} " href="${instance_url}">
${self.plaintext_footer()}
</a>
</p>
<div style="margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace}">
${_('This is a notification from RhodeCode.')}
<a style="font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace}" href="${instance_url}">
${instance_url}
</a>
</div>
</body>
</html>

View file

@ -6,24 +6,25 @@
<%def name="subject()" filter="n,trim,whitespace_filter">
<%
data = {
'user': h.person(user),
'user': '@'+h.person(user),
'repo_name': repo_name,
'commit_id': h.show_id(commit),
'status': status_change,
'comment_file': comment_file,
'comment_line': comment_line,
'comment_type': comment_type,
'commit_id': h.show_id(commit),
}
%>
${_('[mention]') if mention else ''} \
% if comment_file:
${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the {repo_name} repository').format(**data) |n}
${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
% else:
% if status_change:
${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the {repo_name} repository').format(**data) |n}
${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
% else:
${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the {repo_name} repository').format(**data) |n}
${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
% endif
% endif
@ -35,31 +36,38 @@ ${_('[mention]') if mention else ''} \
data = {
'user': h.person(user),
'repo_name': repo_name,
'commit_id': h.show_id(commit),
'status': status_change,
'comment_file': comment_file,
'comment_line': comment_line,
'comment_type': comment_type,
'commit_id': h.show_id(commit),
}
%>
${self.subject()}
* ${_('Comment link')}: ${commit_comment_url}
%if status_change:
* ${_('Commit status')}: ${_('Status was changed to')}: *${status_change}*
%endif
* ${_('Commit')}: ${h.show_id(commit)}
* ${_('Commit message')}: ${commit.message}
%if comment_file:
* ${_('File: {comment_file} on line {comment_line}').format(**data)}
%endif
% if comment_type == 'todo':
${_('`TODO` comment')}:
% else:
${_('`Note` comment')}:
% endif
${comment_body |n, trim}
---
%if status_change:
${_('Commit status was changed to')}: *${status_change}*
%endif
${comment_body|n}
${self.plaintext_footer()}
</%def>
@ -67,42 +75,87 @@ ${self.plaintext_footer()}
<%
data = {
'user': h.person(user),
'repo': commit_target_repo,
'repo_name': repo_name,
'commit_id': h.show_id(commit),
'comment_file': comment_file,
'comment_line': comment_line,
'comment_type': comment_type,
'renderer_type': renderer_type or 'plain',
'repo': commit_target_repo_url,
'repo_name': repo_name,
'commit_id': h.show_id(commit),
}
%>
<table style="text-align:left;vertical-align:middle;">
<tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
% if comment_file:
<h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)}</a> ${_('in the {repo} repository').format(**data) |n}</h4>
% else:
<h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n}</a> ${_('in the {repo} repository').format(**data) |n}</h4>
% endif
</td></tr>
<table style="text-align:left;vertical-align:middle;width: 100%">
<tr>
<td style="width:100%;border-bottom:1px solid #dbd9da;">
<tr><td style="padding-right:20px;padding-top:15px;">${_('Commit')}</td><td style="padding-top:15px;"><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${h.show_id(commit)}</a></td></tr>
<tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td></tr>
<h4 style="margin: 0">
<div style="margin-bottom: 4px; color:#7E7F7F">
@${h.person(user.username)}
</div>
${_('left a')}
<a href="${commit_comment_url}" style="${base.link_css()}">
% if comment_file:
${_('{comment_type} on file `{comment_file}` in commit.').format(**data)}
% else:
${_('{comment_type} on commit.').format(**data) |n}
% endif
</a>
<div style="margin-top: 10px"></div>
${_('Commit')} <code>${data['commit_id']}</code> ${_('of repository')}: ${data['repo_name']}
</h4>
</td>
</tr>
</table>
<table style="text-align:left;vertical-align:middle;width: 100%">
## spacing def
<tr>
<td style="width: 130px"></td>
<td></td>
</tr>
% if status_change:
<tr>
<td style="padding-right:20px;">${_('Status')}</td>
<td style="padding-right:20px;">${_('Commit Status')}:</td>
<td>
${_('The commit status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
${_('Status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
</td>
</tr>
% endif
<tr>
<td style="padding-right:20px;">
<td style="padding-right:20px;">${_('Commit')}:</td>
<td>
<a href="${commit_comment_url}" style="${base.link_css()}">${h.show_id(commit)}</a>
</td>
</tr>
<tr>
<td style="padding-right:20px;">${_('Commit message')}:</td>
<td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td>
</tr>
% if comment_file:
<tr>
<td style="padding-right:20px;">${_('File')}:</td>
<td><a href="${commit_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
</tr>
% endif
<tr style="background-image: linear-gradient(to right, black 33%, rgba(255,255,255,0) 0%);background-position: bottom;background-size: 3px 1px;background-repeat: repeat-x;">
<td colspan="2" style="padding-right:20px;">
% if comment_type == 'todo':
${(_('TODO comment on line: {comment_line}') if comment_file else _('TODO comment')).format(**data)}
${_('`TODO` comment')}:
% else:
${(_('Note comment on line: {comment_line}') if comment_file else _('Note comment')).format(**data)}
${_('`Note` comment')}:
% endif
</td>
<td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
</tr>
<td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
</tr>
</table>

View file

@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
<%inherit file="base.mako"/>
<%namespace name="base" file="base.mako"/>
<%def name="subject()" filter="n,trim,whitespace_filter">
RhodeCode test email: ${h.format_date(date)}
@ -7,7 +8,13 @@ RhodeCode test email: ${h.format_date(date)}
## plain text version of the email. Empty by default
<%def name="body_plaintext()" filter="n,trim">
Test Email from RhodeCode version: ${rhodecode_version}, sent by: ${user}
Test Email from RhodeCode version: ${rhodecode_version}
Email sent by: ${h.person(user)}
---
${self.plaintext_footer()}
</%def>
${body_plaintext()}
Test Email from RhodeCode version: ${rhodecode_version}
<br/><br/>
Email sent by: <strong>${h.person(user)}</strong>

View file

@ -9,13 +9,13 @@
<%def name="body_plaintext()" filter="n,trim">
${body}
---
${self.plaintext_footer()}
</%def>
## BODY GOES BELOW
<table style="text-align:left;vertical-align:top;">
<tr><td style="padding-right:20px;padding-top:15px;white-space:pre-wrap">${body}</td></tr>
<tr>
<td style="padding-right:20px;padding-top:15px;white-space:pre-wrap">${body}</td>
</tr>
</table>
<p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;display:block;color:#666666;text-decoration:none;" href="${instance_url}">
${self.plaintext_footer()}
</a></p>

View file

@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
<%inherit file="base.mako"/>
<%namespace name="base" file="base.mako"/>
<%def name="subject()" filter="n,trim,whitespace_filter">
RhodeCode Password reset
@ -7,16 +8,18 @@ RhodeCode Password reset
## plain text version of the email. Empty by default
<%def name="body_plaintext()" filter="n,trim">
Hi ${user.username},
Hello ${user.username},
There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
On ${h.format_date(date)} there was a request to reset your password using the email address `${email}`
*If you didn't do this, please contact your RhodeCode administrator.*
*If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}*
You can continue, and generate new password by clicking following URL:
${password_reset_url}
This link will be active for 10 minutes.
---
${self.plaintext_footer()}
</%def>
@ -24,10 +27,11 @@ ${self.plaintext_footer()}
<p>
Hello ${user.username},
</p><p>
There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
<br/>
<strong>If you did not request a password reset, please contact your RhodeCode administrator.</strong>
On ${h.format_date(date)} there was a request to reset your password using the email address `${email}`
<br/><br/>
<strong>If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.</strong>
</p><p>
<a href="${password_reset_url}">${_('Generate new password here')}.</a>
This link will be active for 10 minutes.
You can continue, and generate new password by clicking following URL:<br/><br/>
<a href="${password_reset_url}" style="${base.link_css()}">${password_reset_url}</a>
<br/><br/>This link will be active for 10 minutes.
</p>

View file

@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
<%inherit file="base.mako"/>
<%namespace name="base" file="base.mako"/>
<%def name="subject()" filter="n,trim,whitespace_filter">
Your new RhodeCode password
@ -7,14 +8,15 @@ Your new RhodeCode password
## plain text version of the email. Empty by default
<%def name="body_plaintext()" filter="n,trim">
Hi ${user.username},
Hello ${user.username},
Below is your new access password for RhodeCode.
Below is your new access password for RhodeCode requested via password reset link.
*If you didn't do this, please contact your RhodeCode administrator.*
*If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.*
password: ${new_password}
new password: ${new_password}
---
${self.plaintext_footer()}
</%def>
@ -22,8 +24,8 @@ ${self.plaintext_footer()}
<p>
Hello ${user.username},
</p><p>
Below is your new access password for RhodeCode.
<br/>
<strong>If you didn't request a new password, please contact your RhodeCode administrator.</strong>
Below is your new access password for RhodeCode requested via password reset link.
<br/><br/>
<strong>If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.</strong>
</p>
<p>password: <pre>${new_password}</pre>
<p>new password: <code>${new_password}</code>

View file

@ -6,27 +6,29 @@
<%def name="subject()" filter="n,trim,whitespace_filter">
<%
data = {
'user': h.person(user),
'pr_title': pull_request.title,
'pr_id': pull_request.pull_request_id,
'user': '@'+h.person(user),
'repo_name': repo_name,
'status': status_change,
'comment_file': comment_file,
'comment_line': comment_line,
'comment_type': comment_type,
'pr_title': pull_request.title,
'pr_id': pull_request.pull_request_id,
}
%>
${(_('[mention]') if mention else '')} \
% if comment_file:
${_('{user} left a {comment_type} on file `{comment_file}` in pull request #{pr_id} "{pr_title}"').format(**data) |n}
${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
% else:
% if status_change:
${_('[status: {status}] {user} left a {comment_type} on pull request #{pr_id} "{pr_title}"').format(**data) |n}
${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
% else:
${_('{user} left a {comment_type} on pull request #{pr_id} "{pr_title}"').format(**data) |n}
${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
% endif
% endif
</%def>
## PLAINTEXT VERSION OF BODY
@ -34,34 +36,51 @@ ${(_('[mention]') if mention else '')} \
<%
data = {
'user': h.person(user),
'pr_title': pull_request.title,
'pr_id': pull_request.pull_request_id,
'repo_name': repo_name,
'status': status_change,
'comment_file': comment_file,
'comment_line': comment_line,
'comment_type': comment_type,
'pr_title': pull_request.title,
'pr_id': pull_request.pull_request_id,
'source_ref_type': pull_request.source_ref_parts.type,
'source_ref_name': pull_request.source_ref_parts.name,
'target_ref_type': pull_request.target_ref_parts.type,
'target_ref_name': pull_request.target_ref_parts.name,
'source_repo': pull_request_source_repo.repo_name,
'target_repo': pull_request_target_repo.repo_name,
'source_repo_url': pull_request_source_repo_url,
'target_repo_url': pull_request_target_repo_url,
}
%>
${self.subject()}
${h.literal(_('Pull request !{pr_id}: `{pr_title}`').format(**data))}
* ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
* ${_('Comment link')}: ${pr_comment_url}
* ${_('Source repository')}: ${pr_source_repo_url}
%if status_change and not closing_pr:
* ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
%elif status_change and closing_pr:
* ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
%if comment_file:
* ${_('File: {comment_file} on line {comment_line}').format(comment_file=comment_file, comment_line=comment_line)}
%endif
%if comment_file:
* ${_('File: {comment_file} on line {comment_line}').format(**data)}
%endif
% if comment_type == 'todo':
${_('`TODO` comment')}:
% else:
${_('`Note` comment')}:
% endif
${comment_body |n, trim}
---
%if status_change and not closing_pr:
${_('{user} submitted pull request #{pr_id} status: *{status}*').format(**data)}
%elif status_change and closing_pr:
${_('{user} submitted pull request #{pr_id} status: *{status} and closed*').format(**data)}
%endif
${comment_body |n}
${self.plaintext_footer()}
</%def>
@ -69,46 +88,104 @@ ${self.plaintext_footer()}
<%
data = {
'user': h.person(user),
'pr_title': pull_request.title,
'pr_id': pull_request.pull_request_id,
'status': status_change,
'comment_file': comment_file,
'comment_line': comment_line,
'comment_type': comment_type,
'renderer_type': renderer_type or 'plain',
'pr_title': pull_request.title,
'pr_id': pull_request.pull_request_id,
'status': status_change,
'source_ref_type': pull_request.source_ref_parts.type,
'source_ref_name': pull_request.source_ref_parts.name,
'target_ref_type': pull_request.target_ref_parts.type,
'target_ref_name': pull_request.target_ref_parts.name,
'source_repo': pull_request_source_repo.repo_name,
'target_repo': pull_request_target_repo.repo_name,
'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
}
%>
<table style="text-align:left;vertical-align:middle;">
<tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
% if comment_file:
<h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on file `{comment_file}` in pull request #{pr_id} "{pr_title}"').format(**data) |n}</a></h4>
% else:
<h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on pull request #{pr_id} "{pr_title}"').format(**data) |n}</a></h4>
% endif
<table style="text-align:left;vertical-align:middle;width: 100%">
<tr>
<td style="width:100%;border-bottom:1px solid #dbd9da;">
</td></tr>
<tr><td style="padding-right:20px;padding-top:15px;">${_('Source')}</td><td style="padding-top:15px;"><a style="color:#427cc9;text-decoration:none;cursor:pointer" href="${pr_source_repo_url}">${pr_source_repo.repo_name}</a></td></tr>
<h4 style="margin: 0">
<div style="margin-bottom: 4px; color:#7E7F7F">
@${h.person(user.username)}
</div>
${_('left a')}
<a href="${pr_comment_url}" style="${base.link_css()}">
% if comment_file:
${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
% else:
${_('{comment_type} on pull request.').format(**data) |n}
% endif
</a>
<div style="margin-top: 10px"></div>
${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
</h4>
</td>
</tr>
</table>
<table style="text-align:left;vertical-align:middle;width: 100%">
## spacing def
<tr>
<td style="width: 130px"></td>
<td></td>
</tr>
% if status_change:
<tr>
<td style="padding-right:20px;">${_('Status')}</td>
<td>
% if closing_pr:
${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
% else:
${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
% endif
</td>
</tr>
% endif
<tr>
<td style="padding-right:20px;">
% if comment_type == 'todo':
${(_('TODO comment on line: {comment_line}') if comment_file else _('TODO comment')).format(**data)}
<td style="padding-right:20px;">${_('Review Status')}:</td>
<td>
% if closing_pr:
${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
% else:
${(_('Note comment on line: {comment_line}') if comment_file else _('Note comment')).format(**data)}
${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
% endif
</td>
<td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td>
</tr>
% endif
<tr>
<td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
<td style="line-height:20px;">
${base.tag_button('{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name))} ${_('of')} ${data['source_repo_url']}
&rarr;
${base.tag_button('{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name))} ${_('of')} ${data['target_repo_url']}
</td>
</tr>
<tr>
<td style="padding-right:20px;">${_('Pull request')}:</td>
<td>
<a href="${pull_request_url}" style="${base.link_css()}">
!${pull_request.pull_request_id}
</a>
</td>
</tr>
% if comment_file:
<tr>
<td style="padding-right:20px;">${_('File')}:</td>
<td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
</tr>
% endif
<tr style="background-image: linear-gradient(to right, black 33%, rgba(255,255,255,0) 0%);background-position: bottom;background-size: 3px 1px;background-repeat: repeat-x;">
<td colspan="2" style="padding-right:20px;">
% if comment_type == 'todo':
${_('`TODO` comment')}:
% else:
${_('`Note` comment')}:
% endif
</td>
</tr>
<td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
</tr>
</table>

View file

@ -2,19 +2,20 @@
<%inherit file="base.mako"/>
<%namespace name="base" file="base.mako"/>
## EMAIL SUBJECT
<%def name="subject()" filter="n,trim,whitespace_filter">
<%
data = {
'user': h.person(user),
'user': '@'+h.person(user),
'pr_id': pull_request.pull_request_id,
'pr_title': pull_request.title,
}
%>
${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s"') % data |n}
${_('{user} requested a pull request review. !{pr_id}: "{pr_title}"').format(**data) |n}
</%def>
## PLAINTEXT VERSION OF BODY
<%def name="body_plaintext()" filter="n,trim">
<%
data = {
@ -25,32 +26,36 @@ data = {
'source_ref_name': pull_request.source_ref_parts.name,
'target_ref_type': pull_request.target_ref_parts.type,
'target_ref_name': pull_request.target_ref_parts.name,
'repo_url': pull_request_source_repo_url
'repo_url': pull_request_source_repo_url,
'source_repo': pull_request_source_repo.repo_name,
'target_repo': pull_request_target_repo.repo_name,
'source_repo_url': pull_request_source_repo_url,
'target_repo_url': pull_request_target_repo_url,
}
%>
${self.subject()}
${h.literal(_('Pull request !{pr_id}: `{pr_title}`').format(**data))}
${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % data)}
* ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
* ${_('Link')}: ${pull_request_url}
* ${_('Pull Request link')}: ${pull_request_url}
* ${_('Title')}: ${pull_request.title}
* ${_('Description')}:
${pull_request.description}
${pull_request.description | trim}
* ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
% for commit_id, message in pull_request_commits:
- ${h.short_id(commit_id)}
${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
- ${h.short_id(commit_id)}
${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
% endfor
---
${self.plaintext_footer()}
</%def>
<%
@ -63,23 +68,77 @@ data = {
'target_ref_type': pull_request.target_ref_parts.type,
'target_ref_name': pull_request.target_ref_parts.name,
'repo_url': pull_request_source_repo_url,
'source_repo': pull_request_source_repo.repo_name,
'target_repo': pull_request_target_repo.repo_name,
'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url)
'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
}
%>
<table style="text-align:left;vertical-align:middle;">
<tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${pull_request_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % data }</a></h4></td></tr>
<tr><td style="padding-right:20px;padding-top:15px;">${_('Title')}</td><td style="padding-top:15px;">${pull_request.title}</td></tr>
<tr><td style="padding-right:20px;">${_('Source')}</td><td>${base.tag_button(pull_request.source_ref_parts.name)} ${h.literal(_('%(source_ref_type)s of %(source_repo_url)s') % data)}</td></tr>
<tr><td style="padding-right:20px;">${_('Target')}</td><td>${base.tag_button(pull_request.target_ref_parts.name)} ${h.literal(_('%(target_ref_type)s of %(target_repo_url)s') % data)}</td></tr>
<tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${pull_request.description}</td></tr>
<tr><td style="padding-right:20px;">${_ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}</td>
<td><ol style="margin:0 0 0 1em;padding:0;text-align:left;">
% for commit_id, message in pull_request_commits:
<li style="margin:0 0 1em;"><pre style="margin:0 0 .5em">${h.short_id(commit_id)}</pre>
${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
</li>
% endfor
</ol></td>
<table style="text-align:left;vertical-align:middle;width: 100%">
<tr>
<td style="width:100%;border-bottom:1px solid #dbd9da;">
<h4 style="margin: 0">
<div style="margin-bottom: 4px; color:#7E7F7F">
@${h.person(user.username)}
</div>
${_('requested a')}
<a href="${pull_request_url}" style="${base.link_css()}">
${_('pull request review.').format(**data) }
</a>
<div style="margin-top: 10px"></div>
${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
</h4>
</td>
</tr>
</table>
<table style="text-align:left;vertical-align:middle;width: 100%">
## spacing def
<tr>
<td style="width: 130px"></td>
<td></td>
</tr>
<tr>
<td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
<td style="line-height:20px;">
${base.tag_button('{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name))} ${_('of')} ${data['source_repo_url']}
&rarr;
${base.tag_button('{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name))} ${_('of')} ${data['target_repo_url']}
</td>
</tr>
<tr>
<td style="padding-right:20px;">${_('Pull request')}:</td>
<td>
<a href="${pull_request_url}" style="${base.link_css()}">
!${pull_request.pull_request_id}
</a>
</td>
</tr>
<tr>
<td style="padding-right:20px;">${_('Description')}:</td>
<td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
</tr>
<tr>
<td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
<td></td>
</tr>
<tr>
<td colspan="2">
<ol style="margin:0 0 0 1em;padding:0;text-align:left;">
% for commit_id, message in pull_request_commits:
<li style="margin:0 0 1em;">
<pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
</li>
% endfor
</ol>
</td>
</tr>
</table>

View file

@ -15,7 +15,8 @@ Email Plaintext Body
</%def>
## BODY GOES BELOW
<b>Email Body</b>
${h.short_id('0' * 40)}
${_('Translation')}
<strong>Email Body</strong>
<br/>
<br/>
`h.short_id()`: ${h.short_id('0' * 40)}<br/>
${_('Translation String')}<br/>

View file

@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
<%inherit file="base.mako"/>
<%namespace name="base" file="base.mako"/>
<%def name="subject()" filter="n,trim,whitespace_filter">
RhodeCode new user registration: ${user.username}
@ -14,14 +15,45 @@ A new user `${user.username}` has registered on ${h.format_date(date)}
- Email: ${user.email}
- Profile link: ${h.route_url('user_profile', username=user.username)}
---
${self.plaintext_footer()}
</%def>
## BODY GOES BELOW
<table style="text-align:left;vertical-align:middle;">
<tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${h.route_url('user_profile', username=user.username)}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('New user %(user)s has registered on %(date)s') % {'user': user.username, 'date': h.format_date(date)}}</a></h4></td></tr>
<tr><td style="padding-right:20px;padding-top:20px;">${_('Username')}</td><td style="line-height:1;padding-top:20px;"><img style="margin-bottom:-5px;text-align:left;border:1px solid #dbd9da" src="${h.gravatar_url(user.email, 16)}" height="16" width="16">&nbsp;${user.username}</td></tr>
<tr><td style="padding-right:20px;">${_('Full Name')}</td><td>${user.first_name} ${user.last_name}</td></tr>
<tr><td style="padding-right:20px;">${_('Email')}</td><td>${user.email}</td></tr>
<tr><td style="padding-right:20px;">${_('Profile')}</td><td><a href="${h.route_url('user_profile', username=user.username)}">${h.route_url('user_profile', username=user.username)}</a></td></tr>
</table>
<table style="text-align:left;vertical-align:middle;width: 100%">
<tr>
<td style="width:100%;border-bottom:1px solid #dbd9da;">
<h4 style="margin: 0">
<a href="${h.route_url('user_profile', username=user.username)}" style="${base.link_css()}">
${_('New user {user} has registered on {date}').format(user=user.username, date=h.format_date(date))}
</a>
</h4>
</td>
</tr>
</table>
<table style="text-align:left;vertical-align:middle;width: 100%">
## spacing def
<tr>
<td style="width: 130px"></td>
<td></td>
</tr>
<tr>
<td style="padding-right:20px;padding-top:20px;">${_('Username')}:</td>
<td style="line-height:1;padding-top:20px;">${user.username}</td>
</tr>
<tr>
<td style="padding-right:20px;">${_('Full Name')}:</td>
<td>${user.first_name} ${user.last_name}</td>
</tr>
<tr>
<td style="padding-right:20px;">${_('Email')}:</td>
<td>${user.email}</td>
</tr>
<tr>
<td style="padding-right:20px;">${_('Profile')}:</td>
<td>
<a href="${h.route_url('user_profile', username=user.username)}">${h.route_url('user_profile', username=user.username)}</a>
</td>
</tr>
</table>

View file

@ -1,4 +1,3 @@
import collections
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2019 RhodeCode GmbH
@ -20,9 +19,11 @@ import collections
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
import collections
from rhodecode.lib.partial_renderer import PyramidPartialRenderer
from rhodecode.lib.utils2 import AttributeDict
from rhodecode.model.db import User
from rhodecode.model.notification import EmailNotificationModel
@ -47,29 +48,26 @@ def test_render_email(app, http_host_only_stub):
assert body_plaintext == 'Email Plaintext Body'
# body
notification_footer = 'This is a notification from RhodeCode. http://%s/' \
% http_host_only_stub
assert notification_footer in body
notification_footer1 = 'This is a notification from RhodeCode.'
notification_footer2 = 'http://{}/'.format(http_host_only_stub)
assert notification_footer1 in body
assert notification_footer2 in body
assert 'Email Body' in body
def test_render_pr_email(app, user_admin):
ref = collections.namedtuple('Ref',
'name, type')(
'fxies123', 'book'
)
ref = collections.namedtuple(
'Ref', 'name, type')('fxies123', 'book')
pr = collections.namedtuple('PullRequest',
'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
source_repo = target_repo = collections.namedtuple('Repo',
'type, repo_name')(
'hg', 'pull_request_1')
source_repo = target_repo = collections.namedtuple(
'Repo', 'type, repo_name')('hg', 'pull_request_1')
kwargs = {
'user': '<marcin@rhodecode.com> Marcin Kuzminski',
'user': User.get_first_super_admin(),
'pull_request': pr,
'pull_request_commits': [],
@ -86,7 +84,7 @@ def test_render_pr_email(app, user_admin):
EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
# subject
assert subject == 'Marcin Kuzminski wants you to review pull request #200: "Example Pull Request"'
assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
@pytest.mark.parametrize('mention', [
@ -98,24 +96,21 @@ def test_render_pr_email(app, user_admin):
EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
])
def test_render_comment_subject_no_newlines(app, mention, email_type):
ref = collections.namedtuple('Ref',
'name, type')(
'fxies123', 'book'
)
ref = collections.namedtuple(
'Ref', 'name, type')('fxies123', 'book')
pr = collections.namedtuple('PullRequest',
'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
source_repo = target_repo = collections.namedtuple('Repo',
'type, repo_name')(
'hg', 'pull_request_1')
source_repo = target_repo = collections.namedtuple(
'Repo', 'type, repo_name')('hg', 'pull_request_1')
kwargs = {
'user': '<marcin@rhodecode.com> Marcin Kuzminski',
'user': User.get_first_super_admin(),
'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
'status_change': 'approved',
'commit_target_repo': AttributeDict(),
'commit_target_repo_url': 'http://foo.example.com/#comment1',
'repo_name': 'test-repo',
'comment_file': 'test-file.py',
'comment_line': 'n100',
@ -126,8 +121,6 @@ def test_render_comment_subject_no_newlines(app, mention, email_type):
'mention': mention,
'pr_comment_url': 'http://comment-url',
'pr_source_repo': AttributeDict(repo_name='foobar'),
'pr_source_repo_url': 'http://soirce-repo/url',
'pull_request': pr,
'pull_request_commits': [],
@ -136,6 +129,8 @@ def test_render_comment_subject_no_newlines(app, mention, email_type):
'pull_request_source_repo': source_repo,
'pull_request_source_repo_url': 'x',
'pull_request_url': 'http://code.rc.com/_pr/123'
}
subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
email_type, **kwargs)