diff --git a/data/model/legacy.py b/data/model/legacy.py
index 31801f1da..3a1e55526 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -579,6 +579,13 @@ def get_user(username):
return None
+def get_namespace_user(username):
+ try:
+ return User.get(User.username == username)
+ except User.DoesNotExist:
+ return None
+
+
def get_user_or_org(username):
try:
return User.get(User.username == username, User.robot == False)
diff --git a/endpoints/notificationevent.py b/endpoints/notificationevent.py
index 45a42287f..617beb177 100644
--- a/endpoints/notificationevent.py
+++ b/endpoints/notificationevent.py
@@ -1,7 +1,9 @@
import logging
from notificationhelper import build_event_data
+from util.jinjautil import get_template_env
+template_env = get_template_env("events")
logger = logging.getLogger(__name__)
class InvalidNotificationEventException(Exception):
@@ -14,7 +16,7 @@ class NotificationEvent(object):
def get_level(self, event_data, notification_data):
"""
Returns a 'level' representing the severity of the event.
- Valid values are: 'info', 'warning', 'error', 'primary'
+ Valid values are: 'info', 'warning', 'error', 'primary', 'success'
"""
raise NotImplementedError
@@ -28,7 +30,10 @@ class NotificationEvent(object):
"""
Returns a human readable HTML message for the given notification data.
"""
- raise NotImplementedError
+ return template_env.get_template(self.event_name() + '.html').render({
+ 'event_data': event_data,
+ 'notification_data': notification_data
+ })
def get_sample_data(self, repository=None):
"""
@@ -59,28 +64,11 @@ class RepoPushEvent(NotificationEvent):
return 'repo_push'
def get_level(self, event_data, notification_data):
- return 'info'
+ return 'primary'
def get_summary(self, event_data, notification_data):
return 'Repository %s updated' % (event_data['repository'])
- def get_message(self, event_data, notification_data):
- if not event_data.get('updated_tags', {}).keys():
- html = """
- Repository %s has been updated via a push.
- """ % (event_data['homepage'],
- event_data['repository'])
- else:
- html = """
- Repository %s has been updated via a push.
-
- Tags Updated: %s
- """ % (event_data['homepage'],
- event_data['repository'],
- ', '.join(event_data['updated_tags'].keys()))
-
- return html
-
def get_sample_data(self, repository):
return build_event_data(repository, {
'updated_tags': {'latest': 'someimageid', 'foo': 'anotherimage'},
@@ -108,26 +96,7 @@ class BuildQueueEvent(NotificationEvent):
}, subpage='/build?current=%s' % build_uuid)
def get_summary(self, event_data, notification_data):
- return 'Build queued for repository %s' % (event_data['repository'])
-
- def get_message(self, event_data, notification_data):
- is_manual = event_data['is_manual']
- if is_manual:
- html = """
- A new build has been manually queued to start on repository %s.
-
- Build ID: %s
- """ % (event_data['homepage'], event_data['repository'], event_data['build_id'])
- else:
- html = """
- A new build has been queued via a %s trigger to start on repository %s.
-
- Build ID: %s
- """ % (event_data['homepage'], event_data['trigger_kind'],
- event_data['repository'], event_data['build_id'])
-
- return html
-
+ return 'Build queued for repository %s' % (event_data['repository'])
class BuildStartEvent(NotificationEvent):
@@ -151,15 +120,6 @@ class BuildStartEvent(NotificationEvent):
def get_summary(self, event_data, notification_data):
return 'Build started for repository %s' % (event_data['repository'])
- def get_message(self, event_data, notification_data):
- html = """
- A new build has started on repository %s.
-
- Build ID: %s
- """ % (event_data['homepage'], event_data['repository'], event_data['build_id'])
-
- return html
-
class BuildSuccessEvent(NotificationEvent):
@classmethod
@@ -167,7 +127,7 @@ class BuildSuccessEvent(NotificationEvent):
return 'build_success'
def get_level(self, event_data, notification_data):
- return 'primary'
+ return 'success'
def get_sample_data(self, repository):
build_uuid = 'fake-build-id'
@@ -182,15 +142,6 @@ class BuildSuccessEvent(NotificationEvent):
def get_summary(self, event_data, notification_data):
return 'Build succeeded for repository %s' % (event_data['repository'])
- def get_message(self, event_data, notification_data):
- html = """
- A build has finished on repository %s.
-
- Build ID: %s
- """ % (event_data['homepage'], event_data['repository'], event_data['build_id'])
-
- return html
-
class BuildFailureEvent(NotificationEvent):
@classmethod
@@ -214,13 +165,3 @@ class BuildFailureEvent(NotificationEvent):
def get_summary(self, event_data, notification_data):
return 'Build failure for repository %s' % (event_data['repository'])
- def get_message(self, event_data, notification_data):
- html = """
- A build has failed on repository %s.
-
- Reason: %s
- Build ID: %s
- """ % (event_data['homepage'], event_data['repository'],
- event_data['error_message'], event_data['build_id'])
-
- return html
diff --git a/endpoints/notificationhelper.py b/endpoints/notificationhelper.py
index 6f80f83d0..89ec22c0c 100644
--- a/endpoints/notificationhelper.py
+++ b/endpoints/notificationhelper.py
@@ -1,5 +1,6 @@
from app import app, notification_queue
from data import model
+from auth.auth_context import get_authenticated_user, get_validated_oauth_token
import json
@@ -27,21 +28,37 @@ def build_event_data(repo, extra_data={}, subpage=None):
event_data.update(extra_data)
return event_data
-def build_notification_data(notification, event_data):
+def build_notification_data(notification, event_data, performer_data=None):
+ if not performer_data:
+ performer_data = {}
+
+ oauth_token = get_validated_oauth_token()
+ if oauth_token:
+ performer_data['oauth_token_id'] = oauth_token.id
+ performer_data['oauth_token_application_id'] = oauth_token.application.client_id
+ performer_data['oauth_token_application'] = oauth_token.application.name
+
+ performer_user = get_authenticated_user()
+ if performer_user:
+ performer_data['entity_id'] = performer_user.id
+ performer_data['entity_name'] = performer_user.username
+
return {
'notification_uuid': notification.uuid,
'repository_namespace': notification.repository.namespace_user.username,
'repository_name': notification.repository.name,
- 'event_data': event_data
+ 'event_data': event_data,
+ 'performer_data': performer_data
}
-def spawn_notification(repo, event_name, extra_data={}, subpage=None, pathargs=[]):
+def spawn_notification(repo, event_name, extra_data={}, subpage=None, pathargs=[],
+ performer_data=None):
event_data = build_event_data(repo, extra_data=extra_data, subpage=subpage)
notifications = model.list_repo_notifications(repo.namespace_user.username, repo.name,
event_name=event_name)
- for notification in notifications:
- notification_data = build_notification_data(notification, event_data)
+ for notification in list(notifications):
+ notification_data = build_notification_data(notification, event_data, performer_data)
path = [repo.namespace_user.username, repo.name, event_name] + pathargs
notification_queue.put(path, json.dumps(notification_data))
diff --git a/endpoints/notificationmethod.py b/endpoints/notificationmethod.py
index 78104ea2e..0d43498f2 100644
--- a/endpoints/notificationmethod.py
+++ b/endpoints/notificationmethod.py
@@ -279,6 +279,7 @@ class HipchatMethod(NotificationMethod):
'info': 'gray',
'warning': 'yellow',
'error': 'red',
+ 'success': 'green',
'primary': 'purple'
}.get(level, 'gray')
@@ -303,6 +304,56 @@ class HipchatMethod(NotificationMethod):
raise NotificationMethodPerformException(ex.message)
+from HTMLParser import HTMLParser
+
+class SlackAdjuster(HTMLParser):
+ def __init__(self):
+ self.reset()
+ self.result = []
+
+ def handle_data(self, d):
+ self.result.append(d)
+
+ def get_attr(self, attrs, name):
+ for attr in attrs:
+ if attr[0] == name:
+ return attr[1]
+
+ return ''
+
+ def handle_starttag(self, tag, attrs):
+ if tag == 'a':
+ self.result.append('<%s|' % (self.get_attr(attrs, 'href'), ))
+
+ if tag == 'i':
+ self.result.append('_')
+
+ if tag == 'b' or tag == 'strong':
+ self.result.append('*')
+
+ if tag == 'img':
+ self.result.append(self.get_attr(attrs, 'alt'))
+ self.result.append(' ')
+
+ def handle_endtag(self, tag):
+ if tag == 'a':
+ self.result.append('>')
+
+ if tag == 'b' or tag == 'strong':
+ self.result.append('*')
+
+ if tag == 'i':
+ self.result.append('_')
+
+ def get_data(self):
+ return ''.join(self.result)
+
+def adjust_tags(html):
+ s = SlackAdjuster()
+ s.feed(html)
+ return s.get_data()
+
+
class SlackMethod(NotificationMethod):
""" Method for sending notifications to Slack via the API:
https://api.slack.com/docs/attachments
@@ -318,12 +369,11 @@ class SlackMethod(NotificationMethod):
if not config_data.get('subdomain', '').isalnum():
raise CannotValidateNotificationMethodException('Missing Slack Subdomain Name')
- def formatForSlack(self, message):
+ def format_for_slack(self, message):
message = message.replace('\n', '')
message = re.sub(r'\s+', ' ', message)
message = message.replace('
', '\n')
- message = re.sub(r'(.+)', '<\\1|\\2>', message)
- return message
+ return adjust_tags(message)
def perform(self, notification, event_handler, notification_data):
config_data = json.loads(notification.config_json)
@@ -346,6 +396,7 @@ class SlackMethod(NotificationMethod):
'info': '#ffffff',
'warning': 'warning',
'error': 'danger',
+ 'success': 'good',
'primary': 'good'
}.get(level, '#ffffff')
@@ -359,8 +410,9 @@ class SlackMethod(NotificationMethod):
'attachments': [
{
'fallback': summary,
- 'text': self.formatForSlack(message),
- 'color': color
+ 'text': self.format_for_slack(message),
+ 'color': color,
+ 'mrkdwn_in': ["text"]
}
]
}
diff --git a/events/build_failure.html b/events/build_failure.html
new file mode 100644
index 000000000..cb93ebff4
--- /dev/null
+++ b/events/build_failure.html
@@ -0,0 +1,2 @@
+Build failed for repository
+{{ event_data.repository | repository_reference }} ({{ event_data.build_id }}): {{ event_data.error_message }}
\ No newline at end of file
diff --git a/events/build_queued.html b/events/build_queued.html
new file mode 100644
index 000000000..a4ecf8e41
--- /dev/null
+++ b/events/build_queued.html
@@ -0,0 +1,9 @@
+{% if event_data.is_manual and notification_data.performer_data.entity_name %}
+{{ notification_data.performer_data.entity_name | user_reference }} queued a
+build
+{% elif event_data.trigger_kind %}
+Build queued via a {{ event_data.trigger_kind }} trigger
+{% else %}
+Build queued
+{% endif %}
+ for repository {{ event_data.repository | repository_reference }} ({{ event_data.build_id }})
diff --git a/events/build_start.html b/events/build_start.html
new file mode 100644
index 000000000..81a0c7fd2
--- /dev/null
+++ b/events/build_start.html
@@ -0,0 +1,2 @@
+Build started for repository
+{{ event_data.repository | repository_reference }} ({{ event_data.build_id }})
diff --git a/events/build_success.html b/events/build_success.html
new file mode 100644
index 000000000..aee961326
--- /dev/null
+++ b/events/build_success.html
@@ -0,0 +1,2 @@
+Build completed for repository
+{{ event_data.repository | repository_reference }} ({{ event_data.build_id }})
\ No newline at end of file
diff --git a/events/repo_push.html b/events/repo_push.html
new file mode 100644
index 000000000..0c531d909
--- /dev/null
+++ b/events/repo_push.html
@@ -0,0 +1,12 @@
+{% if notification_data.performer_data.entity_name %}
+{{ notification_data.performer_data.entity_name | user_reference }} pushed
+{% else %}
+Push of
+{% endif %}
+
+{% if event_data.updated_tags %}
+ {{ 'tags' | icon_image }}
+ {% for tag in event_data.updated_tags %}{%if loop.index > 1 %}, {% endif %}{{ (event_data.repository, tag) | repository_tag_reference }}{% endfor %} in
+{% endif %}
+
+ repository {{ event_data.repository | repository_reference }}
\ No newline at end of file
diff --git a/static/img/icons/tags.png b/static/img/icons/tags.png
new file mode 100644
index 000000000..a9e34afaf
Binary files /dev/null and b/static/img/icons/tags.png differ
diff --git a/static/img/icons/wrench.png b/static/img/icons/wrench.png
new file mode 100644
index 000000000..6819d2448
Binary files /dev/null and b/static/img/icons/wrench.png differ
diff --git a/util/jinjautil.py b/util/jinjautil.py
new file mode 100644
index 000000000..2d10414f8
--- /dev/null
+++ b/util/jinjautil.py
@@ -0,0 +1,91 @@
+from app import get_app_url
+from data import model
+from util.gravatar import compute_hash
+from util.names import parse_robot_username
+from jinja2 import Template, Environment, FileSystemLoader, contextfilter
+
+def icon_path(icon_name):
+ return '%s/static/img/icons/%s.png' % (get_app_url(), icon_name)
+
+def icon_image(icon_name):
+ return '' % (icon_path(icon_name), icon_name)
+
+def user_reference(username):
+ user = model.get_namespace_user(username)
+ if not user:
+ return username
+
+ is_robot = False
+ if user.robot:
+ parts = parse_robot_username(username)
+ user = model.get_namespace_user(parts[0])
+
+ return """ %s""" % (icon_path('wrench'), username)
+
+ alt = 'Organization' if user.organization else 'User'
+ return """
+
+
+ %s
+ """ % (compute_hash(user.email), alt, username)
+
+
+def repository_tag_reference(repository_path_and_tag):
+ (repository_path, tag) = repository_path_and_tag
+ (namespace, repository) = repository_path.split('/')
+ owner = model.get_namespace_user(namespace)
+ if not owner:
+ return tag
+
+ return """%s""" % (get_app_url(), namespace, repository,
+ tag, tag)
+
+def repository_reference(pair):
+ if isinstance(pair, tuple):
+ (namespace, repository) = pair
+ else:
+ pair = pair.split('/')
+ namespace = pair[0]
+ repository = pair[1]
+
+ owner = model.get_namespace_user(namespace)
+ if not owner:
+ return "%s/%s" % (namespace, repository)
+
+ return """
+
+
+ %s/%s
+
+ """ % (compute_hash(owner.email), get_app_url(), namespace, repository, namespace, repository)
+
+
+def admin_reference(username):
+ user = model.get_user_or_org(username)
+ if not user:
+ return 'account settings'
+
+ if user.organization:
+ return """
+ organization's admin setting
+ """ % (get_app_url(), username)
+ else:
+ return """
+ account settings
+ """ % (get_app_url())
+
+
+def get_template_env(searchpath):
+ template_loader = FileSystemLoader(searchpath=searchpath)
+ template_env = Environment(loader=template_loader)
+ add_filters(template_env)
+ return template_env
+
+
+def add_filters(template_env):
+ template_env.filters['icon_image'] = icon_image
+ template_env.filters['user_reference'] = user_reference
+ template_env.filters['admin_reference'] = admin_reference
+ template_env.filters['repository_reference'] = repository_reference
+ template_env.filters['repository_tag_reference'] = repository_tag_reference
diff --git a/util/useremails.py b/util/useremails.py
index 52e064ea1..507af1724 100644
--- a/util/useremails.py
+++ b/util/useremails.py
@@ -1,58 +1,11 @@
from flask.ext.mail import Message
from app import mail, app, get_app_url
-from jinja2 import Template, Environment, FileSystemLoader, contextfilter
from data import model
from util.gravatar import compute_hash
+from util.jinjautil import get_template_env
-def user_reference(username):
- user = model.get_user_or_org(username)
- if not user:
- return username
-
- return """
-
-
- %s
- """ % (compute_hash(user.email), username)
-
-
-def repository_reference(pair):
- (namespace, repository) = pair
-
- owner = model.get_user(namespace)
- if not owner:
- return "%s/%s" % (namespace, repository)
-
- return """
-
-
- %s/%s
-
- """ % (compute_hash(owner.email), get_app_url(), namespace, repository, namespace, repository)
-
-
-def admin_reference(username):
- user = model.get_user(username)
- if not user:
- return 'account settings'
-
- if user.organization:
- return """
- organization's admin setting
- """ % (get_app_url(), username)
- else:
- return """
- account settings
- """ % (get_app_url())
-
-
-template_loader = FileSystemLoader(searchpath="emails")
-template_env = Environment(loader=template_loader)
-template_env.filters['user_reference'] = user_reference
-template_env.filters['admin_reference'] = admin_reference
-template_env.filters['repository_reference'] = repository_reference
-
+template_env = get_template_env("emails")
def send_email(recipient, subject, template_file, parameters):
app_title = app.config['REGISTRY_TITLE_SHORT']