Support mentioning tickets on different trackers

fixes: ~sircmpwn/todo.sr.ht/157
This commit is contained in:
Ivan Habunek 2019-03-07 08:02:57 +01:00 committed by Drew DeVault
parent cfb6701ce1
commit 2db8c25c02
4 changed files with 130 additions and 20 deletions

View File

@ -2,8 +2,9 @@ import pytest
import re
from srht.database import db
from todosrht.tickets import add_comment, find_mentioned_users
from todosrht.tickets import USER_MENTION_PATTERN
from todosrht.tickets import add_comment
from todosrht.tickets import find_mentioned_users, find_mentioned_tickets
from todosrht.tickets import USER_MENTION_PATTERN, TICKET_MENTION_PATTERN
from todosrht.types import TicketResolution, TicketStatus
from todosrht.types import TicketSubscription, EventType
@ -276,3 +277,82 @@ def test_notifications_and_events(mailbox):
assert t2_mention.comment == comment
assert t2_mention.user == commenter
def test_ticket_mention_pattern():
def match(text):
return re.findall(TICKET_MENTION_PATTERN, text)
assert match("#1, #13, and #372") == [
('', '', '', '1'),
('', '', '', '13'),
('', '', '', '372')
]
assert match("some#1, other#13, and trackers#372") == [
('', '', 'some', '1'),
('', '', 'other', '13'),
('', '', 'trackers', '372')
]
assert match("~foo/some#1, ~bar/other#13, and ~baz/trackers#372") == [
('~foo/', 'foo', 'some', '1'),
('~bar/', 'bar', 'other', '13'),
('~baz/', 'baz', 'trackers', '372')
]
def test_find_mentioned_tickets():
u1 = UserFactory()
tr1 = TrackerFactory(owner=u1)
t11 = TicketFactory(tracker=tr1, scoped_id=1)
t12 = TicketFactory(tracker=tr1, scoped_id=13)
tr2 = TrackerFactory(owner=u1)
t21 = TicketFactory(tracker=tr2, scoped_id=1)
t22 = TicketFactory(tracker=tr2, scoped_id=42)
u3 = UserFactory()
tr3 = TrackerFactory(owner=u3)
t31 = TicketFactory(tracker=tr3, scoped_id=1)
t32 = TicketFactory(tracker=tr3, scoped_id=442)
db.session.commit()
# Texts with no matching ticket mentions
texts = [
"Nothing to see here, move along",
"Do not exist: #500, foo#300, ~bar/foo#123",
f"Also do not exist: {u1}/{tr1.name}#42, {u1}/{tr1.name}#442",
]
for text in texts:
for tr in [tr1, tr2, tr3]:
assert find_mentioned_tickets(tr, text) == set()
# Mentioning ticket by number only matches tickets in the same tracker
text = "winning tickets are: #1, #13 and #42"
assert find_mentioned_tickets(tr1, text) == {t11, t12}
assert find_mentioned_tickets(tr2, text) == {t21, t22}
assert find_mentioned_tickets(tr3, text) == {t31}
# Mentioning ticket by number and tracker name matches tickets in the
# repository with the given name, owned by the same user as the repository
# on which the comment is posted
text = f"winning tickets are: {tr1.name}#1, {tr1.name}#13 and {tr1.name}#42"
assert find_mentioned_tickets(tr1, text) == {t11, t12}
assert find_mentioned_tickets(tr2, text) == {t11, t12}
assert find_mentioned_tickets(tr3, text) == set() # owned by u3
text = f"winning tickets are: {tr2.name}#1, {tr2.name}#13 and {tr2.name}#42"
assert find_mentioned_tickets(tr1, text) == {t21, t22}
assert find_mentioned_tickets(tr2, text) == {t21, t22}
assert find_mentioned_tickets(tr3, text) == set() # owned by u3
text = f"winning tickets are: {tr3.name}#1, {tr3.name}#13 and {tr3.name}#42"
assert find_mentioned_tickets(tr1, text) == set() # owned by u1
assert find_mentioned_tickets(tr2, text) == set() # owned by u1
assert find_mentioned_tickets(tr3, text) == {t31}
# Fully qualified mentions include user, tracker and ticket ID
for tr in [tr1, tr2, tr3]:
for t in [t11, t12, t21, t22, t31, t32]:
assert find_mentioned_tickets(tr, f"mentioning {t.ref()}") == {t}

View File

@ -24,11 +24,12 @@ def cache_comment_markup(func):
@cache_comment_markup
def render_comment(comment):
tracker = comment.ticket.tracker
users = find_mentioned_users(comment.text)
tickets = find_mentioned_tickets(comment.ticket.tracker, comment.text)
tickets = find_mentioned_tickets(tracker, comment.text)
users_map = {str(u): u for u in users}
tickets_map = {str(t.scoped_id): t for t in tickets}
tickets_map = {t.ref(): t for t in tickets}
def urlize_user(match):
username = match.group(0)
@ -39,14 +40,19 @@ def render_comment(comment):
return username
def urlize_ticket(match):
scoped_id = match.group(1)
if scoped_id in tickets_map:
ticket = tickets_map[scoped_id]
url = urls.ticket_url(ticket)
title = escape(f"{ticket.ref()}: {ticket.title}")
return f'<a href="{url}" title="{title}">#{scoped_id}</a>'
text = match.group(0)
ticket_id = match.group('ticket_id')
tracker_name = match.group('tracker_name') or tracker.name
owner = match.group('username') or tracker.owner.username
return match.group(0)
ticket_ref = f"~{owner}/{tracker_name}#{ticket_id}"
if ticket_ref not in tickets_map:
return text
ticket = tickets_map[ticket_ref]
url = urls.ticket_url(ticket)
title = escape(f"{ticket.ref()}: {ticket.title}")
return f'<a href="{url}" title="{title}">{text}</a>'
# Replace ticket and username mentions with linked version
text = comment.text

View File

@ -6,9 +6,9 @@ from srht.database import db
from todosrht.email import notify, format_lines
from todosrht.types import Event, EventType, EventNotification
from todosrht.types import TicketComment, TicketStatus, TicketSubscription
from todosrht.types import TicketSeen, TicketAssignee, User, Ticket
from todosrht.types import TicketSeen, TicketAssignee, User, Ticket, Tracker
from todosrht.urls import ticket_url
from sqlalchemy import func
from sqlalchemy import func, or_, and_
smtp_user = cfg("mail", "smtp-user", default=None)
smtp_from = cfg("mail", "smtp-from", default=None)
@ -29,8 +29,14 @@ USER_MENTION_PATTERN = re.compile(r"""
\b # Word boundary
""", re.VERBOSE)
# Matches ticket mentions, e.g. #17
TICKET_MENTION_PATTERN = re.compile(r"#(\d+)\b")
# Matches ticket mentions, e.g. #17, tracker#17 and ~user/tracker#17
TICKET_MENTION_PATTERN = re.compile(r"""
(?<!\S) # No leading non-whitespace characters)
(~(?P<username>\w+)/)? # Optional username
(?P<tracker_name>\w+)? # Optional tracker name
\#(?P<ticket_id>\d+) # Ticket ID
\b # Word boundary
""", re.VERBOSE)
def find_mentioned_users(text):
usernames = re.findall(USER_MENTION_PATTERN, text)
@ -38,12 +44,27 @@ def find_mentioned_users(text):
return set(users)
def find_mentioned_tickets(tracker, text):
ids = re.findall(TICKET_MENTION_PATTERN, text)
tickets = (Ticket.query
.filter_by(tracker=tracker)
.filter(Ticket.scoped_id.in_(ids))
filters = or_()
for match in re.finditer(TICKET_MENTION_PATTERN, text):
username = match.group('username') or tracker.owner.username
tracker_name = match.group('tracker_name') or tracker.name
ticket_id = int(match.group('ticket_id'))
filters.append(and_(
Ticket.scoped_id == ticket_id,
Tracker.name == tracker_name,
User.username == username,
))
# No tickets mentioned
if len(filters) == 0:
return set()
return set(Ticket.query
.join(Tracker, User)
.filter(filters)
.all())
return set(tickets)
def _create_comment(ticket, user, text):
comment = TicketComment()

View File

@ -69,3 +69,6 @@ class Ticket(Base):
self.tracker.owner.canonical_name,
self.tracker.name,
self.scoped_id)
def __repr__(self):
return f"<Ticket {self.id}>"