Support mentioning tickets on different trackers
fixes: ~sircmpwn/todo.sr.ht/157
This commit is contained in:
parent
cfb6701ce1
commit
2db8c25c02
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}>"
|
||||
|
|
Loading…
Reference in New Issue