2017-10-27 02:36:54 +02:00
|
|
|
"""Taiga integration for Zulip.
|
2016-04-28 15:46:00 +02:00
|
|
|
|
|
|
|
Tips for notification output:
|
|
|
|
|
|
|
|
*Emojis*: most of the events have specific emojis e.g.
|
|
|
|
- :notebook: - change of subject/name/description
|
|
|
|
- :chart_with_upwards_trend: - change of status
|
|
|
|
etc. If no there's no meaningful emoji for certain event, the defaults are used:
|
|
|
|
- :thought_balloon: - event connected to commenting
|
|
|
|
- :busts_in_silhouette: - event connected to a certain user
|
|
|
|
- :package: - all other events connected to user story
|
|
|
|
- :calendar: - all other events connected to milestones
|
|
|
|
- :clipboard: - all other events connected to tasks
|
|
|
|
- :bulb: - all other events connected to issues
|
|
|
|
|
2017-10-27 02:36:54 +02:00
|
|
|
*Text formatting*: if there has been a change of a property, the new
|
|
|
|
value should always be in bold; otherwise the subject of US/task
|
|
|
|
should be in bold.
|
2016-04-28 15:46:00 +02:00
|
|
|
"""
|
|
|
|
|
2017-03-03 19:01:52 +01:00
|
|
|
from typing import Any, Dict, List, Mapping, Optional, Tuple, Text
|
2016-05-25 15:02:02 +02:00
|
|
|
|
|
|
|
from django.utils.translation import ugettext as _
|
2016-06-06 01:56:35 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2016-05-25 15:02:02 +02:00
|
|
|
|
2017-10-31 04:25:48 +01:00
|
|
|
from zerver.decorator import api_key_only_webhook_view
|
2017-09-30 04:18:16 +02:00
|
|
|
from zerver.lib.actions import check_send_stream_message
|
2016-04-28 15:46:00 +02:00
|
|
|
from zerver.lib.response import json_success, json_error
|
2017-10-31 04:25:48 +01:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
2017-05-02 01:00:50 +02:00
|
|
|
from zerver.models import UserProfile
|
2016-04-28 15:46:00 +02:00
|
|
|
|
|
|
|
import ujson
|
|
|
|
|
2016-05-12 22:49:36 +02:00
|
|
|
@api_key_only_webhook_view('Taiga')
|
2016-04-28 15:46:00 +02:00
|
|
|
@has_request_variables
|
2017-05-02 01:00:50 +02:00
|
|
|
def api_taiga_webhook(request, user_profile, message=REQ(argument_type='body'),
|
2016-05-06 21:19:34 +02:00
|
|
|
stream=REQ(default='taiga'), topic=REQ(default='General')):
|
2017-05-02 01:00:50 +02:00
|
|
|
# type: (HttpRequest, UserProfile, Dict[str, Any], Text, Text) -> HttpResponse
|
2016-04-28 15:46:00 +02:00
|
|
|
parsed_events = parse_message(message)
|
|
|
|
|
2016-07-13 21:52:39 +02:00
|
|
|
content_lines = []
|
2016-04-28 15:46:00 +02:00
|
|
|
for event in parsed_events:
|
2016-07-13 21:52:39 +02:00
|
|
|
content_lines.append(generate_content(event) + '\n')
|
|
|
|
content = "".join(sorted(content_lines))
|
2016-04-28 15:46:00 +02:00
|
|
|
|
2017-09-30 04:18:16 +02:00
|
|
|
check_send_stream_message(user_profile, request.client, stream, topic, content)
|
2016-04-28 15:46:00 +02:00
|
|
|
|
|
|
|
return json_success()
|
|
|
|
|
|
|
|
templates = {
|
2017-10-22 05:30:45 +02:00
|
|
|
'epic': {
|
|
|
|
'create': u':package: %(user)s created epic **%(subject)s**',
|
|
|
|
'set_assigned_to': u':busts_in_silhouette: %(user)s assigned epic **%(subject)s** to %(new)s.',
|
|
|
|
'unset_assigned_to': u':busts_in_silhouette: %(user)s unassigned epic **%(subject)s**',
|
|
|
|
'changed_assigned_to': u':busts_in_silhouette: %(user)s reassigned epic **%(subject)s**'
|
|
|
|
' from %(old)s to %(new)s.',
|
|
|
|
'blocked': u':lock: %(user)s blocked epic **%(subject)s**',
|
|
|
|
'unblocked': u':unlock: %(user)s unblocked epic **%(subject)s**',
|
|
|
|
'changed_status': u':chart_increasing: %(user)s changed status of epic **%(subject)s**'
|
|
|
|
' from %(old)s to %(new)s.',
|
|
|
|
'renamed': u':notebook: %(user)s renamed epic from **%(old)s** to **%(new)s**',
|
|
|
|
'description_diff': u':notebook: %(user)s updated description of epic **%(subject)s**',
|
|
|
|
'commented': u':thought_balloon: %(user)s commented on epic **%(subject)s**',
|
|
|
|
'delete': u':cross_mark: %(user)s deleted epic **%(subject)s**',
|
|
|
|
},
|
2017-10-25 03:31:11 +02:00
|
|
|
'relateduserstory': {
|
2017-10-27 02:36:54 +02:00
|
|
|
'create': (u':package: %(user)s added a related user story '
|
|
|
|
u'**%(userstory_subject)s** to the epic **%(epic_subject)s**'),
|
2017-10-27 02:31:10 +02:00
|
|
|
'delete': (u':cross_mark: %(user)s removed a related user story ' +
|
|
|
|
u'**%(userstory_subject)s** from the epic **%(epic_subject)s**'),
|
2017-10-25 03:31:11 +02:00
|
|
|
},
|
2016-04-28 15:46:00 +02:00
|
|
|
'userstory': {
|
|
|
|
'create': u':package: %(user)s created user story **%(subject)s**.',
|
|
|
|
'set_assigned_to': u':busts_in_silhouette: %(user)s assigned user story **%(subject)s** to %(new)s.',
|
2017-01-10 19:22:26 +01:00
|
|
|
'unset_assigned_to': u':busts_in_silhouette: %(user)s unassigned user story **%(subject)s**.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'changed_assigned_to': u':busts_in_silhouette: %(user)s reassigned user story **%(subject)s**'
|
2016-12-02 08:15:05 +01:00
|
|
|
' from %(old)s to %(new)s.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'points': u':game_die: %(user)s changed estimation of user story **%(subject)s**.',
|
|
|
|
'blocked': u':lock: %(user)s blocked user story **%(subject)s**.',
|
|
|
|
'unblocked': u':unlock: %(user)s unblocked user story **%(subject)s**.',
|
|
|
|
'set_milestone': u':calendar: %(user)s added user story **%(subject)s** to sprint %(new)s.',
|
2017-01-10 19:22:26 +01:00
|
|
|
'unset_milestone': u':calendar: %(user)s removed user story **%(subject)s** from sprint %(old)s.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'changed_milestone': u':calendar: %(user)s changed sprint of user story **%(subject)s** from %(old)s'
|
2016-12-02 08:15:05 +01:00
|
|
|
' to %(new)s.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'changed_status': u':chart_with_upwards_trend: %(user)s changed status of user story **%(subject)s**'
|
2016-12-02 08:15:05 +01:00
|
|
|
' from %(old)s to %(new)s.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'closed': u':checkered_flag: %(user)s closed user story **%(subject)s**.',
|
|
|
|
'reopened': u':package: %(user)s reopened user story **%(subject)s**.',
|
|
|
|
'renamed': u':notebook: %(user)s renamed user story from %(old)s to **%(new)s**.',
|
2017-01-10 19:22:26 +01:00
|
|
|
'description_diff': u':notebook: %(user)s updated description of user story **%(subject)s**.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'commented': u':thought_balloon: %(user)s commented on user story **%(subject)s**.',
|
|
|
|
'delete': u':x: %(user)s deleted user story **%(subject)s**.'
|
|
|
|
},
|
|
|
|
'milestone': {
|
|
|
|
'create': u':calendar: %(user)s created sprint **%(subject)s**.',
|
|
|
|
'renamed': u':notebook: %(user)s renamed sprint from %(old)s to **%(new)s**.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'estimated_start': u':calendar: %(user)s changed estimated start of sprint **%(subject)s**'
|
2016-12-02 08:15:05 +01:00
|
|
|
' from %(old)s to %(new)s.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'estimated_finish': u':calendar: %(user)s changed estimated finish of sprint **%(subject)s**'
|
2016-12-02 08:15:05 +01:00
|
|
|
' from %(old)s to %(new)s.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'delete': u':x: %(user)s deleted sprint **%(subject)s**.'
|
|
|
|
},
|
|
|
|
'task': {
|
|
|
|
'create': u':clipboard: %(user)s created task **%(subject)s**.',
|
|
|
|
'set_assigned_to': u':busts_in_silhouette: %(user)s assigned task **%(subject)s** to %(new)s.',
|
2017-01-10 19:22:26 +01:00
|
|
|
'unset_assigned_to': u':busts_in_silhouette: %(user)s unassigned task **%(subject)s**.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'changed_assigned_to': u':busts_in_silhouette: %(user)s reassigned task **%(subject)s**'
|
2016-12-02 08:15:05 +01:00
|
|
|
' from %(old)s to %(new)s.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'blocked': u':lock: %(user)s blocked task **%(subject)s**.',
|
|
|
|
'unblocked': u':unlock: %(user)s unblocked task **%(subject)s**.',
|
|
|
|
'set_milestone': u':calendar: %(user)s added task **%(subject)s** to sprint %(new)s.',
|
|
|
|
'changed_milestone': u':calendar: %(user)s changed sprint of task **%(subject)s** from %(old)s to %(new)s.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'changed_status': u':chart_with_upwards_trend: %(user)s changed status of task **%(subject)s**'
|
2016-12-02 08:15:05 +01:00
|
|
|
' from %(old)s to %(new)s.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'renamed': u':notebook: %(user)s renamed task %(old)s to **%(new)s**.',
|
2017-01-10 19:22:26 +01:00
|
|
|
'description_diff': u':notebook: %(user)s updated description of task **%(subject)s**.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'commented': u':thought_balloon: %(user)s commented on task **%(subject)s**.',
|
|
|
|
'delete': u':x: %(user)s deleted task **%(subject)s**.',
|
|
|
|
'changed_us': u':clipboard: %(user)s moved task **%(subject)s** from user story %(old)s to %(new)s.'
|
|
|
|
},
|
|
|
|
'issue': {
|
|
|
|
'create': u':bulb: %(user)s created issue **%(subject)s**.',
|
2017-05-07 20:13:35 +02:00
|
|
|
'set_assigned_to': u':busts_in_silhouette: %(user)s assigned issue **%(subject)s** to %(new)s.',
|
2017-01-10 19:22:26 +01:00
|
|
|
'unset_assigned_to': u':busts_in_silhouette: %(user)s unassigned issue **%(subject)s**.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'changed_assigned_to': u':busts_in_silhouette: %(user)s reassigned issue **%(subject)s**'
|
2016-12-02 08:15:05 +01:00
|
|
|
' from %(old)s to %(new)s.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'changed_priority': u':rocket: %(user)s changed priority of issue **%(subject)s** from %(old)s to %(new)s.',
|
|
|
|
'changed_severity': u':warning: %(user)s changed severity of issue **%(subject)s** from %(old)s to %(new)s.',
|
2016-07-17 19:53:11 +02:00
|
|
|
'changed_status': u':chart_with_upwards_trend: %(user)s changed status of issue **%(subject)s**'
|
|
|
|
' from %(old)s to %(new)s.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'changed_type': u':bulb: %(user)s changed type of issue **%(subject)s** from %(old)s to %(new)s.',
|
|
|
|
'renamed': u':notebook: %(user)s renamed issue %(old)s to **%(new)s**.',
|
2017-01-10 19:22:26 +01:00
|
|
|
'description_diff': u':notebook: %(user)s updated description of issue **%(subject)s**.',
|
2016-04-28 15:46:00 +02:00
|
|
|
'commented': u':thought_balloon: %(user)s commented on issue **%(subject)s**.',
|
|
|
|
'delete': u':x: %(user)s deleted issue **%(subject)s**.'
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
return_type = Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]
|
|
|
|
def get_old_and_new_values(change_type: str,
|
|
|
|
message: Mapping[str, Any]) -> return_type:
|
2016-04-28 15:46:00 +02:00
|
|
|
""" Parses the payload and finds previous and current value of change_type."""
|
|
|
|
if change_type in ['subject', 'name', 'estimated_finish', 'estimated_start']:
|
|
|
|
old = message["change"]["diff"][change_type]["from"]
|
|
|
|
new = message["change"]["diff"][change_type]["to"]
|
|
|
|
return old, new
|
|
|
|
|
|
|
|
try:
|
2017-01-10 19:22:26 +01:00
|
|
|
old = message["change"]["diff"][change_type]["from"]
|
2016-04-28 15:46:00 +02:00
|
|
|
except KeyError:
|
|
|
|
old = None
|
|
|
|
|
|
|
|
try:
|
2017-01-10 19:22:26 +01:00
|
|
|
new = message["change"]["diff"][change_type]["to"]
|
2016-04-28 15:46:00 +02:00
|
|
|
except KeyError:
|
|
|
|
new = None
|
|
|
|
|
|
|
|
return old, new
|
|
|
|
|
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def parse_comment(message: Mapping[str, Any]) -> Dict[str, Any]:
|
2016-04-28 15:46:00 +02:00
|
|
|
""" Parses the comment to issue, task or US. """
|
|
|
|
return {
|
|
|
|
'event': 'commented',
|
|
|
|
'type': message["type"],
|
|
|
|
'values': {
|
2017-01-10 19:22:26 +01:00
|
|
|
'user': get_owner_name(message),
|
|
|
|
'subject': get_subject(message)
|
2016-04-28 15:46:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def parse_create_or_delete(message: Mapping[str, Any]) -> Dict[str, Any]:
|
2016-04-28 15:46:00 +02:00
|
|
|
""" Parses create or delete event. """
|
2017-10-25 03:31:11 +02:00
|
|
|
if message["type"] == 'relateduserstory':
|
|
|
|
return {
|
|
|
|
'type': message["type"],
|
|
|
|
'event': message["action"],
|
|
|
|
'values': {
|
|
|
|
'user': get_owner_name(message),
|
|
|
|
'epic_subject': message['data']['epic']['subject'],
|
|
|
|
'userstory_subject': message['data']['user_story']['subject'],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-28 15:46:00 +02:00
|
|
|
return {
|
|
|
|
'type': message["type"],
|
|
|
|
'event': message["action"],
|
2017-01-24 06:34:26 +01:00
|
|
|
'values': {
|
|
|
|
'user': get_owner_name(message),
|
|
|
|
'subject': get_subject(message)
|
|
|
|
}
|
2016-04-28 15:46:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def parse_change_event(change_type: str, message: Mapping[str, Any]) -> Optional[Dict[str, Any]]:
|
2016-04-28 15:46:00 +02:00
|
|
|
""" Parses change event. """
|
2017-01-10 19:22:26 +01:00
|
|
|
evt = {} # type: Dict[str, Any]
|
2016-04-28 15:46:00 +02:00
|
|
|
values = {
|
2017-01-10 19:22:26 +01:00
|
|
|
'user': get_owner_name(message),
|
|
|
|
'subject': get_subject(message)
|
|
|
|
} # type: Dict[str, Any]
|
2016-04-28 15:46:00 +02:00
|
|
|
|
2017-01-10 19:22:26 +01:00
|
|
|
if change_type in ["description_diff", "points"]:
|
2016-04-28 15:46:00 +02:00
|
|
|
event_type = change_type
|
|
|
|
|
|
|
|
elif change_type in ["milestone", "assigned_to"]:
|
|
|
|
old, new = get_old_and_new_values(change_type, message)
|
|
|
|
if not old:
|
|
|
|
event_type = "set_" + change_type
|
|
|
|
values["new"] = new
|
2017-01-10 19:22:26 +01:00
|
|
|
elif not new:
|
|
|
|
event_type = "unset_" + change_type
|
|
|
|
values["old"] = old
|
2016-04-28 15:46:00 +02:00
|
|
|
else:
|
|
|
|
event_type = "changed_" + change_type
|
|
|
|
values.update({'old': old, 'new': new})
|
|
|
|
|
|
|
|
elif change_type == "is_blocked":
|
|
|
|
if message["change"]["diff"]["is_blocked"]["to"]:
|
|
|
|
event_type = "blocked"
|
|
|
|
else:
|
|
|
|
event_type = "unblocked"
|
|
|
|
|
|
|
|
elif change_type == "is_closed":
|
|
|
|
if message["change"]["diff"]["is_closed"]["to"]:
|
|
|
|
event_type = "closed"
|
|
|
|
else:
|
|
|
|
event_type = "reopened"
|
|
|
|
|
|
|
|
elif change_type == "user_story":
|
|
|
|
old, new = get_old_and_new_values(change_type, message)
|
|
|
|
event_type = "changed_us"
|
|
|
|
values.update({'old': old, 'new': new})
|
|
|
|
|
|
|
|
elif change_type in ["subject", 'name']:
|
|
|
|
event_type = 'renamed'
|
|
|
|
old, new = get_old_and_new_values(change_type, message)
|
|
|
|
values.update({'old': old, 'new': new})
|
|
|
|
|
|
|
|
elif change_type in ["estimated_finish", "estimated_start"]:
|
|
|
|
old, new = get_old_and_new_values(change_type, message)
|
|
|
|
if not old == new:
|
|
|
|
event_type = change_type
|
|
|
|
values.update({'old': old, 'new': new})
|
|
|
|
else:
|
|
|
|
# date hasn't changed
|
|
|
|
return None
|
|
|
|
|
|
|
|
elif change_type in ["priority", "severity", "type", "status"]:
|
|
|
|
event_type = 'changed_' + change_type
|
|
|
|
old, new = get_old_and_new_values(change_type, message)
|
|
|
|
values.update({'old': old, 'new': new})
|
|
|
|
|
|
|
|
else:
|
|
|
|
# we are not supporting this type of event
|
|
|
|
return None
|
|
|
|
|
|
|
|
evt.update({"type": message["type"], "event": event_type, "values": values})
|
|
|
|
return evt
|
|
|
|
|
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def parse_message(message: Mapping[str, Any]) -> List[Dict[str, Any]]:
|
2016-04-28 15:46:00 +02:00
|
|
|
""" Parses the payload by delegating to specialized functions. """
|
|
|
|
events = []
|
|
|
|
if message["action"] in ['create', 'delete']:
|
|
|
|
events.append(parse_create_or_delete(message))
|
|
|
|
elif message["action"] == 'change':
|
|
|
|
if message["change"]["diff"]:
|
|
|
|
for value in message["change"]["diff"]:
|
|
|
|
parsed_event = parse_change_event(value, message)
|
2016-11-30 21:45:02 +01:00
|
|
|
if parsed_event:
|
|
|
|
events.append(parsed_event)
|
2016-04-28 15:46:00 +02:00
|
|
|
if message["change"]["comment"]:
|
|
|
|
events.append(parse_comment(message))
|
|
|
|
|
|
|
|
return events
|
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def generate_content(data: Mapping[str, Any]) -> str:
|
2016-04-28 15:46:00 +02:00
|
|
|
""" Gets the template string and formats it with parsed data. """
|
2017-08-24 17:31:04 +02:00
|
|
|
return templates[data['type']][data['event']] % data['values']
|
2017-01-10 19:22:26 +01:00
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def get_owner_name(message: Mapping[str, Any]) -> str:
|
2017-01-10 19:22:26 +01:00
|
|
|
return message["by"]["full_name"]
|
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def get_subject(message: Mapping[str, Any]) -> str:
|
2017-01-10 19:22:26 +01:00
|
|
|
data = message["data"]
|
|
|
|
return data.get("subject", data.get("name"))
|