Generalize users into participants
This commit is contained in:
parent
f4f961c00f
commit
b4c92a39db
|
@ -2,7 +2,7 @@ import factory
|
|||
from datetime import datetime, timedelta
|
||||
from factory.fuzzy import FuzzyText
|
||||
from srht.database import db
|
||||
from todosrht.types import Tracker, User, Ticket
|
||||
from todosrht.types import Tracker, User, Ticket, Participant, ParticipantType
|
||||
|
||||
future_datetime = datetime.now() + timedelta(days=10)
|
||||
|
||||
|
@ -20,6 +20,15 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
|
|||
sqlalchemy_session = db.session
|
||||
|
||||
|
||||
class ParticipantFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
participant_type = ParticipantType.user
|
||||
user = factory.SubFactory(UserFactory)
|
||||
|
||||
class Meta:
|
||||
model = Participant
|
||||
sqlalchemy_session = db.session
|
||||
|
||||
|
||||
class TrackerFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
owner = factory.SubFactory(UserFactory)
|
||||
name = factory.Sequence(lambda n: f"tracker{n}")
|
||||
|
@ -32,7 +41,7 @@ class TrackerFactory(factory.alchemy.SQLAlchemyModelFactory):
|
|||
class TicketFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
tracker = factory.SubFactory(TrackerFactory)
|
||||
scoped_id = factory.Sequence(lambda n: n)
|
||||
submitter = factory.SubFactory(UserFactory)
|
||||
submitter = factory.SubFactory(ParticipantFactory)
|
||||
title = "A wild ticket appeared"
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -9,20 +9,21 @@ from todosrht.types import TicketResolution, TicketStatus
|
|||
from todosrht.types import TicketSubscription, EventType
|
||||
|
||||
from .factories import UserFactory, TrackerFactory, TicketFactory
|
||||
from .factories import ParticipantFactory
|
||||
|
||||
def test_ticket_comment(mailbox):
|
||||
user = UserFactory()
|
||||
submitter = ParticipantFactory()
|
||||
tracker = TrackerFactory()
|
||||
ticket = TicketFactory(tracker=tracker)
|
||||
|
||||
subscribed_to_ticket = UserFactory()
|
||||
subscribed_to_tracker = UserFactory()
|
||||
subscribed_to_both = UserFactory()
|
||||
subscribed_to_ticket = ParticipantFactory()
|
||||
subscribed_to_tracker = ParticipantFactory()
|
||||
subscribed_to_both = ParticipantFactory()
|
||||
|
||||
sub1 = TicketSubscription(user=subscribed_to_ticket, ticket=ticket)
|
||||
sub2 = TicketSubscription(user=subscribed_to_tracker, tracker=tracker)
|
||||
sub3 = TicketSubscription(user=subscribed_to_both, ticket=ticket)
|
||||
sub4 = TicketSubscription(user=subscribed_to_both, tracker=tracker)
|
||||
sub1 = TicketSubscription(participant=subscribed_to_ticket, ticket=ticket)
|
||||
sub2 = TicketSubscription(participant=subscribed_to_tracker, tracker=tracker)
|
||||
sub3 = TicketSubscription(participant=subscribed_to_both, ticket=ticket)
|
||||
sub4 = TicketSubscription(participant=subscribed_to_both, tracker=tracker)
|
||||
|
||||
db.session.add(sub1)
|
||||
db.session.add(sub2)
|
||||
|
@ -35,22 +36,22 @@ def test_ticket_comment(mailbox):
|
|||
emails = mailbox[-3:]
|
||||
|
||||
assert {e.to for e in emails} == {
|
||||
subscribed_to_ticket.email,
|
||||
subscribed_to_tracker.email,
|
||||
subscribed_to_both.email,
|
||||
subscribed_to_ticket.user.email,
|
||||
subscribed_to_tracker.user.email,
|
||||
subscribed_to_both.user.email,
|
||||
}
|
||||
|
||||
for e in emails:
|
||||
assert e.headers['From'].startswith(user.canonical_name)
|
||||
assert e.headers['From'].startswith(submitter.name)
|
||||
if starts_with:
|
||||
assert e.body.startswith(starts_with)
|
||||
|
||||
def assert_event_notifications_created(event):
|
||||
assert {en.user.email for en in event.notifications} == {
|
||||
subscribed_to_ticket.email,
|
||||
subscribed_to_tracker.email,
|
||||
subscribed_to_both.email,
|
||||
event.user.email,
|
||||
subscribed_to_ticket.user.email,
|
||||
subscribed_to_tracker.user.email,
|
||||
subscribed_to_both.user.email,
|
||||
event.participant.user.email,
|
||||
}
|
||||
|
||||
assert len(mailbox) == 0
|
||||
|
@ -58,12 +59,13 @@ def test_ticket_comment(mailbox):
|
|||
assert ticket.resolution == TicketResolution.unresolved
|
||||
|
||||
# Comment without status change
|
||||
event = add_comment(user, ticket, text="how do you do, i")
|
||||
event = add_comment(submitter, ticket, text="how do you do, i")
|
||||
|
||||
# Submitter gets automatically subscribed
|
||||
assert TicketSubscription.query.filter_by(ticket=ticket, user=user).first()
|
||||
assert TicketSubscription.query.filter_by(
|
||||
ticket=ticket, participant=submitter).first()
|
||||
|
||||
assert event.comment.submitter == user
|
||||
assert event.comment.submitter == submitter
|
||||
assert event.comment.ticket == ticket
|
||||
assert event.comment.text == "how do you do, i"
|
||||
|
||||
|
@ -80,10 +82,10 @@ def test_ticket_comment(mailbox):
|
|||
assert_event_notifications_created(event)
|
||||
|
||||
# Comment and resolve issue
|
||||
event = add_comment(user, ticket, text="see you've met my",
|
||||
event = add_comment(submitter, ticket, text="see you've met my",
|
||||
resolve=True, resolution=TicketResolution.fixed)
|
||||
|
||||
assert event.comment.submitter == user
|
||||
assert event.comment.submitter == submitter
|
||||
assert event.comment.ticket == ticket
|
||||
assert event.comment.text == "see you've met my"
|
||||
|
||||
|
@ -104,9 +106,10 @@ def test_ticket_comment(mailbox):
|
|||
assert_event_notifications_created(event)
|
||||
|
||||
# Comment and reopen issue
|
||||
event = add_comment(user, ticket, text="faithful handyman", reopen=True)
|
||||
event = add_comment(submitter, ticket,
|
||||
text="faithful handyman", reopen=True)
|
||||
|
||||
assert event.comment.submitter == user
|
||||
assert event.comment.submitter == submitter
|
||||
assert event.comment.ticket == ticket
|
||||
assert event.comment.text == "faithful handyman"
|
||||
|
||||
|
@ -122,7 +125,7 @@ def test_ticket_comment(mailbox):
|
|||
assert_event_notifications_created(event)
|
||||
|
||||
# Resolve without commenting
|
||||
event = add_comment(user, ticket,
|
||||
event = add_comment(submitter, ticket,
|
||||
resolve=True, resolution=TicketResolution.wont_fix)
|
||||
|
||||
assert ticket.status == TicketStatus.resolved
|
||||
|
@ -138,7 +141,7 @@ def test_ticket_comment(mailbox):
|
|||
assert_event_notifications_created(event)
|
||||
|
||||
# Reopen without commenting
|
||||
event = add_comment(user, ticket, reopen=True)
|
||||
event = add_comment(submitter, ticket, reopen=True)
|
||||
|
||||
assert ticket.status == TicketStatus.reported
|
||||
assert ticket.resolution == TicketResolution.wont_fix
|
||||
|
@ -153,13 +156,13 @@ def test_ticket_comment(mailbox):
|
|||
|
||||
|
||||
def test_failed_comments():
|
||||
user = UserFactory()
|
||||
participant = ParticipantFactory()
|
||||
tracker = TrackerFactory()
|
||||
ticket = TicketFactory(tracker=tracker)
|
||||
db.session.flush()
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
add_comment(user, ticket)
|
||||
add_comment(participant, ticket)
|
||||
|
||||
|
||||
def test_user_mention_pattern():
|
||||
|
@ -189,25 +192,25 @@ def test_find_mentioned_users():
|
|||
|
||||
assert find_mentioned_users(comment) == set()
|
||||
|
||||
u1 = UserFactory(username="mention1")
|
||||
p1 = ParticipantFactory(user=UserFactory(username="mention1"))
|
||||
db.session.commit()
|
||||
assert find_mentioned_users(comment) == {u1}
|
||||
assert find_mentioned_users(comment) == {p1}
|
||||
|
||||
u2 = UserFactory(username="mention2")
|
||||
p2 = ParticipantFactory(user=UserFactory(username="mention2"))
|
||||
db.session.commit()
|
||||
assert find_mentioned_users(comment) == {u1, u2}
|
||||
assert find_mentioned_users(comment) == {p1, p2}
|
||||
|
||||
u3 = UserFactory(username="mention3")
|
||||
p3 = ParticipantFactory(user=UserFactory(username="mention3"))
|
||||
db.session.commit()
|
||||
assert find_mentioned_users(comment) == {u1, u2, u3}
|
||||
assert find_mentioned_users(comment) == {p1, p2, p3}
|
||||
|
||||
|
||||
def test_notifications_and_events(mailbox):
|
||||
u1 = UserFactory()
|
||||
u2 = UserFactory()
|
||||
u3 = UserFactory() # not mentioned
|
||||
p1 = ParticipantFactory()
|
||||
p2 = ParticipantFactory()
|
||||
p3 = ParticipantFactory() # not mentioned
|
||||
|
||||
commenter = UserFactory()
|
||||
commenter = ParticipantFactory()
|
||||
ticket = TicketFactory()
|
||||
|
||||
t1 = TicketFactory(tracker=ticket.tracker)
|
||||
|
@ -217,16 +220,16 @@ def test_notifications_and_events(mailbox):
|
|||
db.session.flush()
|
||||
|
||||
text = (
|
||||
f"mentioning users {u1.canonical_name}, ~doesnotexist, "
|
||||
f"and {u2.canonical_name} "
|
||||
f"mentioning users {p1.identifier}, ~doesnotexist, "
|
||||
f"and {p2.identifier} "
|
||||
f"also mentioning tickets #{t1.scoped_id}, and #{t2.scoped_id} and #999999"
|
||||
)
|
||||
event = add_comment(commenter, ticket, text)
|
||||
|
||||
assert len(mailbox) == 2
|
||||
|
||||
email1 = next(e for e in mailbox if e.to == u1.email)
|
||||
email2 = next(e for e in mailbox if e.to == u2.email)
|
||||
email1 = next(e for e in mailbox if e.to == p1.user.email)
|
||||
email2 = next(e for e in mailbox if e.to == p2.user.email)
|
||||
|
||||
expected_title = f"{ticket.ref()}: {ticket.title}"
|
||||
expected_body = f"You were mentioned in {ticket.ref()} by {commenter}."
|
||||
|
@ -240,25 +243,25 @@ def test_notifications_and_events(mailbox):
|
|||
# Check correct events are generated
|
||||
comment_events = {e for e in ticket.events
|
||||
if e.event_type == EventType.comment}
|
||||
u1_events = {e for e in u1.events
|
||||
p1_events = {e for e in p1.events
|
||||
if e.event_type == EventType.user_mentioned}
|
||||
u2_events = {e for e in u2.events
|
||||
p2_events = {e for e in p2.events
|
||||
if e.event_type == EventType.user_mentioned}
|
||||
|
||||
assert len(comment_events) == 1
|
||||
assert len(u1_events) == 1
|
||||
assert len(u2_events) == 1
|
||||
assert len(p1_events) == 1
|
||||
assert len(p2_events) == 1
|
||||
|
||||
u1_mention = u1_events.pop()
|
||||
u2_mention = u2_events.pop()
|
||||
p1_mention = p1_events.pop()
|
||||
p2_mention = p2_events.pop()
|
||||
|
||||
assert u1_mention.comment == event.comment
|
||||
assert u1_mention.from_ticket == ticket
|
||||
assert u1_mention.by_user == commenter
|
||||
assert p1_mention.comment == event.comment
|
||||
assert p1_mention.from_ticket == ticket
|
||||
assert p1_mention.by_participant == commenter
|
||||
|
||||
assert u2_mention.comment == event.comment
|
||||
assert u2_mention.from_ticket == ticket
|
||||
assert u2_mention.by_user == commenter
|
||||
assert p2_mention.comment == event.comment
|
||||
assert p2_mention.from_ticket == ticket
|
||||
assert p2_mention.by_participant == commenter
|
||||
|
||||
assert len(t1.events) == 1
|
||||
assert len(t2.events) == 1
|
||||
|
@ -269,11 +272,11 @@ def test_notifications_and_events(mailbox):
|
|||
|
||||
assert t1_mention.comment == event.comment
|
||||
assert t1_mention.from_ticket == ticket
|
||||
assert t1_mention.by_user == commenter
|
||||
assert t1_mention.by_participant == commenter
|
||||
|
||||
assert t2_mention.comment == event.comment
|
||||
assert t2_mention.from_ticket == ticket
|
||||
assert t2_mention.by_user == commenter
|
||||
assert t2_mention.by_participant == commenter
|
||||
|
||||
def test_ticket_mention_pattern():
|
||||
def match(text):
|
||||
|
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
from itertools import chain
|
||||
from srht.database import db
|
||||
from tests.factories import UserFactory, TrackerFactory, TicketFactory
|
||||
from tests.factories import ParticipantFactory
|
||||
from todosrht.tickets import submit_ticket
|
||||
from todosrht.types import Ticket, EventType, TicketSubscription, Event
|
||||
from todosrht.urls import ticket_url
|
||||
|
@ -9,16 +10,16 @@ from todosrht.urls import ticket_url
|
|||
@pytest.mark.parametrize("submitter_subscribed", [True, False])
|
||||
def test_submit_ticket(client, mailbox, submitter_subscribed):
|
||||
tracker = TrackerFactory()
|
||||
submitter = UserFactory()
|
||||
submitter = ParticipantFactory()
|
||||
|
||||
subscriber = UserFactory()
|
||||
TicketSubscription(user=subscriber, tracker=tracker)
|
||||
subscriber = ParticipantFactory()
|
||||
TicketSubscription(participant=subscriber, tracker=tracker)
|
||||
|
||||
# `submitter_subscribed` parameter defines whether the submitter was
|
||||
# subscribed to the tracker prior to submitting the ticket
|
||||
# this affects whether a new subscription is created when submitting
|
||||
if submitter_subscribed:
|
||||
TicketSubscription(user=submitter, tracker=tracker)
|
||||
TicketSubscription(participant=submitter, tracker=tracker)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
@ -40,48 +41,44 @@ def test_submit_ticket(client, mailbox, submitter_subscribed):
|
|||
event = ticket.events[0]
|
||||
assert event.event_type == EventType.created
|
||||
assert event.ticket == ticket
|
||||
assert event.user == submitter
|
||||
assert event.participant == submitter
|
||||
|
||||
# Check subscriber got an email
|
||||
assert len(mailbox) == 1
|
||||
email = mailbox[0]
|
||||
assert email.to == subscriber.email
|
||||
assert email.to == subscriber.user.email
|
||||
assert title in email.subject
|
||||
assert ticket.ref() in email.subject
|
||||
assert description in email.body
|
||||
assert ticket_url(ticket) in email.body
|
||||
assert email.headers['From'].startswith(submitter.canonical_name)
|
||||
assert email.headers['From'].startswith(submitter.name)
|
||||
|
||||
# Check event notification is created for the subscriber
|
||||
assert len(subscriber.notifications) == 1
|
||||
notification = subscriber.notifications[0]
|
||||
assert len(subscriber.user.notifications) == 1
|
||||
notification = subscriber.user.notifications[0]
|
||||
assert notification.event == event
|
||||
|
||||
# Check submitter is subscribed to the ticket
|
||||
assert len(submitter.subscriptions) == 1
|
||||
subscription = submitter.subscriptions[0]
|
||||
if submitter_subscribed:
|
||||
assert subscription.tracker == tracker
|
||||
assert subscription.ticket is None
|
||||
next((s for s in submitter.subscriptions if s.tracker == tracker))
|
||||
else:
|
||||
assert subscription.tracker is None
|
||||
assert subscription.ticket == ticket
|
||||
next((s for s in submitter.subscriptions if s.ticket == ticket))
|
||||
|
||||
|
||||
def test_mentions_in_ticket_description(mailbox):
|
||||
owner = UserFactory()
|
||||
tracker = TrackerFactory(owner=owner)
|
||||
submitter = UserFactory()
|
||||
submitter = ParticipantFactory()
|
||||
|
||||
t1 = TicketFactory(tracker=tracker)
|
||||
t2 = TicketFactory(tracker=tracker)
|
||||
t3 = TicketFactory(tracker=tracker)
|
||||
|
||||
u1 = UserFactory()
|
||||
u2 = UserFactory()
|
||||
p1 = ParticipantFactory()
|
||||
p2 = ParticipantFactory()
|
||||
|
||||
subscriber = UserFactory()
|
||||
TicketSubscription(user=subscriber, tracker=tracker)
|
||||
subscriber = ParticipantFactory()
|
||||
TicketSubscription(participant=subscriber, tracker=tracker)
|
||||
|
||||
db.session.flush()
|
||||
tracker.next_ticket_id = t3.id + 1
|
||||
|
@ -91,11 +88,11 @@ def test_mentions_in_ticket_description(mailbox):
|
|||
description = f"""
|
||||
Testing mentioning
|
||||
---------------
|
||||
myself: {submitter.canonical_name}
|
||||
user one: {u1.canonical_name}
|
||||
user two: {u2.canonical_name}
|
||||
myself: {submitter.identifier}
|
||||
user one: {p1.identifier}
|
||||
user two: {p2.identifier}
|
||||
nonexistant user: ~hopefullythisuserdoesnotexist
|
||||
subscriber: {subscriber.canonical_name}
|
||||
subscriber: {subscriber.identifier}
|
||||
ticket one: #{t1.scoped_id}
|
||||
ticket two: {tracker.name}#{t2.scoped_id}
|
||||
ticket three: {owner.canonical_name}/{tracker.name}#{t3.scoped_id}
|
||||
|
@ -113,42 +110,42 @@ def test_mentions_in_ticket_description(mailbox):
|
|||
|
||||
for event in chain(t1.events, t2.events, t3.events):
|
||||
assert event.event_type == EventType.ticket_mentioned
|
||||
assert event.user is None
|
||||
assert event.participant is None
|
||||
assert event.comment is None
|
||||
assert event.from_ticket == ticket
|
||||
assert event.by_user == submitter
|
||||
assert event.by_participant == submitter
|
||||
|
||||
# Check user events, skip the ticket create event
|
||||
submitter_events = [e for e in submitter.events
|
||||
if e.event_type != EventType.created]
|
||||
|
||||
assert len(submitter_events) == 1
|
||||
assert len(u1.events) == 1
|
||||
assert len(u2.events) == 1
|
||||
assert len(p1.events) == 1
|
||||
assert len(p2.events) == 1
|
||||
assert len(subscriber.events) == 1
|
||||
|
||||
for event in chain(
|
||||
submitter_events, u1.events, u2.events, subscriber.events
|
||||
submitter_events, p1.events, p2.events, subscriber.events
|
||||
):
|
||||
assert event.event_type == EventType.user_mentioned
|
||||
assert event.ticket is None
|
||||
assert event.comment is None
|
||||
assert event.from_ticket == ticket
|
||||
assert event.by_user == submitter
|
||||
assert event.by_participant == submitter
|
||||
|
||||
# Check emails
|
||||
# Submitter should not have been emailed since they mentioned themselves
|
||||
assert len(mailbox) == 3
|
||||
|
||||
subscriber_email = next(e for e in mailbox if e.to == subscriber.email)
|
||||
u1_email = next(e for e in mailbox if e.to == u1.email)
|
||||
u2_email = next(e for e in mailbox if e.to == u2.email)
|
||||
subscriber_email = next(e for e in mailbox if e.to == subscriber.user.email)
|
||||
p1_email = next(e for e in mailbox if e.to == p1.user.email)
|
||||
p2_email = next(e for e in mailbox if e.to == p2.user.email)
|
||||
|
||||
# Subscriber should receive a notification that the ticket was created so
|
||||
# they're not sent a notification for the mention.
|
||||
assert subscriber_email.body.strip().startswith("Testing mentioning")
|
||||
|
||||
# Other mentioned users who are not subscribed should get a mention email
|
||||
expected = f"You were mentioned in {ticket.ref()} by {submitter}."
|
||||
assert u1_email.body.startswith(expected)
|
||||
assert u2_email.body.startswith(expected)
|
||||
expected = f"You were mentioned in {ticket.ref()} by {submitter.name}."
|
||||
assert p1_email.body.startswith(expected)
|
||||
assert p2_email.body.startswith(expected)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from srht.database import db
|
||||
from tests.factories import TrackerFactory, TicketFactory, UserFactory
|
||||
from tests.factories import ParticipantFactory
|
||||
from tests.utils import logged_in_as
|
||||
from todosrht.tickets import get_or_create_subscription
|
||||
from todosrht.types import TicketSeen, TicketSubscription
|
||||
|
@ -32,15 +33,15 @@ def test_mark_seen(client):
|
|||
assert second_time > first_time
|
||||
|
||||
def test_get_or_create_subscription():
|
||||
user1 = UserFactory()
|
||||
user2 = UserFactory()
|
||||
user3 = UserFactory()
|
||||
participant1 = ParticipantFactory()
|
||||
participant2 = ParticipantFactory()
|
||||
participant3 = ParticipantFactory()
|
||||
tracker = TrackerFactory()
|
||||
ticket = TicketFactory(tracker=tracker)
|
||||
|
||||
# Some existing subscriptions
|
||||
ts1 = TicketSubscription(user=user1, ticket=ticket)
|
||||
ts2 = TicketSubscription(user=user2, tracker=tracker)
|
||||
ts1 = TicketSubscription(participant=participant1, ticket=ticket)
|
||||
ts2 = TicketSubscription(participant=participant2, tracker=tracker)
|
||||
db.session.add(ts1)
|
||||
db.session.add(ts2)
|
||||
db.session.commit()
|
||||
|
@ -49,11 +50,11 @@ def test_get_or_create_subscription():
|
|||
assert set(tracker.subscriptions) == set([ts2])
|
||||
|
||||
# Return existing subs if they exist
|
||||
assert get_or_create_subscription(ticket, user1) == ts1
|
||||
assert get_or_create_subscription(ticket, user2) == ts2
|
||||
assert get_or_create_subscription(ticket, participant1) == ts1
|
||||
assert get_or_create_subscription(ticket, participant2) == ts2
|
||||
|
||||
# Create new ticket sub if none exists
|
||||
ts3 = get_or_create_subscription(ticket, user3)
|
||||
ts3 = get_or_create_subscription(ticket, participant3)
|
||||
db.session.commit()
|
||||
|
||||
assert set(ticket.subscriptions) == set([ts1, ts3])
|
||||
|
|
|
@ -11,7 +11,7 @@ from grp import getgrnam
|
|||
from todosrht.access import get_tracker, get_ticket
|
||||
from todosrht.types import TicketAccess, TicketResolution, Tracker, Ticket, User
|
||||
from todosrht.types import Label, TicketLabel, Event, EventType
|
||||
from todosrht.tickets import submit_ticket, add_comment
|
||||
from todosrht.tickets import add_comment, get_participant_for_user, submit_ticket
|
||||
from todosrht.webhooks import UserWebhook, TrackerWebhook, TicketWebhook
|
||||
from srht.validation import Validation
|
||||
import asyncio
|
||||
|
@ -105,7 +105,8 @@ class MailHandler:
|
|||
print("Rejecting email due to validation errors")
|
||||
return "550 " + ", ".join([e["reason"] for e in valid.errors])
|
||||
|
||||
ticket = submit_ticket(tracker, sender, title, desc)
|
||||
participant = get_participant_for_user(sender)
|
||||
ticket = submit_ticket(tracker, participant, title, desc)
|
||||
UserWebhook.deliver(UserWebhook.Events.ticket_create,
|
||||
ticket.to_dict(),
|
||||
UserWebhook.Subscription.user_id == sender.id)
|
||||
|
@ -122,6 +123,7 @@ class MailHandler:
|
|||
resolution = None
|
||||
resolve = reopen = False
|
||||
cmds = ["!resolve", "!reopen", "!assign", "!label", "!unlabel"]
|
||||
participant = get_participant_for_user(sender)
|
||||
if any(last_line.startswith(cmd) for cmd in cmds):
|
||||
cmd = shlex.split(last_line)
|
||||
body = body[:-len(last_line)-1].rstrip()
|
||||
|
@ -139,7 +141,7 @@ class MailHandler:
|
|||
return ("550 The label you requested does not exist on " +
|
||||
"this tracker.")
|
||||
if not TicketAccess.triage in access:
|
||||
print(f"Rejected, {sender.canonical_name} has insufficient " +
|
||||
print(f"Rejected, {participant.name} has insufficient " +
|
||||
f"permissions (have {access}, want triage)")
|
||||
return "550 You do not have permission to triage on this tracker."
|
||||
for label in labels:
|
||||
|
@ -147,10 +149,11 @@ class MailHandler:
|
|||
.filter(TicketLabel.label_id == label.id)
|
||||
.filter(TicketLabel.ticket_id == ticket.id)).first()
|
||||
event = Event()
|
||||
event.user_id = sender.id
|
||||
event.participant_id = participant.id
|
||||
event.ticket_id = ticket.id
|
||||
event.label_id = label.id
|
||||
if not ticket_label and cmd[0] == "!label":
|
||||
# TODO: only supported for user participants
|
||||
ticket_label = TicketLabel()
|
||||
ticket_label.ticket_id = ticket.id
|
||||
ticket_label.label_id = label.id
|
||||
|
@ -171,7 +174,7 @@ class MailHandler:
|
|||
# TODO: Remaining commands
|
||||
|
||||
if not required_access in access:
|
||||
print(f"Rejected, {sender.canonical_name} has insufficient " +
|
||||
print(f"Rejected, {participant.name} has insufficient " +
|
||||
f"permissions (have {access}, want {required_access})")
|
||||
return "550 You do not have permission to post on this tracker."
|
||||
|
||||
|
@ -180,7 +183,7 @@ class MailHandler:
|
|||
return "550 Comment must be between 3 and 16384 characters."
|
||||
|
||||
if body:
|
||||
event = add_comment(sender, ticket, text=body,
|
||||
event = add_comment(participant, ticket, text=body,
|
||||
resolution=resolution, resolve=resolve, reopen=reopen)
|
||||
TicketWebhook.deliver(TicketWebhook.Events.event_create,
|
||||
event.to_dict(),
|
||||
|
|
|
@ -10,6 +10,7 @@ def _get_permissions(tracker, ticket, name):
|
|||
return getattr(ticket, f"{name}_perms")
|
||||
return getattr(tracker, f"default_{name}_perms")
|
||||
|
||||
# TODO: get_access for any participant
|
||||
def get_access(tracker, ticket, user=None):
|
||||
user = user or current_user
|
||||
|
||||
|
@ -27,7 +28,7 @@ def get_access(tracker, ticket, user=None):
|
|||
return user_access.permissions
|
||||
|
||||
# Submitter
|
||||
if ticket and user.id == ticket.submitter_id:
|
||||
if ticket and user.id == ticket.submitter.user_id:
|
||||
return _get_permissions(tracker, ticket, "submitter")
|
||||
|
||||
# Any logged in user
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
"""Migrate users to participants
|
||||
|
||||
Revision ID: 4631a2317dd0
|
||||
Revises: d54ed600c4bf
|
||||
Create Date: 2019-08-21 11:09:46.626025
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4631a2317dd0'
|
||||
down_revision = 'd54ed600c4bf'
|
||||
|
||||
from alembic import op
|
||||
from enum import Enum
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy_utils as sau
|
||||
|
||||
class ParticipantType(Enum):
|
||||
user = "user"
|
||||
email = "email"
|
||||
external = "external"
|
||||
|
||||
def upgrade():
|
||||
op.create_table("participant",
|
||||
sa.Column("id", sa.Integer, primary_key=True),
|
||||
sa.Column("created", sa.DateTime, nullable=False),
|
||||
sa.Column("participant_type",
|
||||
sau.ChoiceType(ParticipantType, impl=sa.String()),
|
||||
nullable=False),
|
||||
sa.Column("user_id", sa.Integer,
|
||||
sa.ForeignKey("user.id", ondelete="CASCADE"), unique=True),
|
||||
sa.Column("email", sa.String, unique=True),
|
||||
sa.Column("email_name", sa.String),
|
||||
sa.Column("external_id", sa.String, unique=True),
|
||||
sa.Column("external_url", sa.String))
|
||||
|
||||
op.execute("""
|
||||
ALTER TABLE participant ALTER COLUMN created DROP NOT NULL;
|
||||
ALTER TABLE participant ALTER COLUMN participant_type DROP NOT NULL;
|
||||
|
||||
INSERT INTO participant(user_id)
|
||||
SELECT submitter_id AS user_id FROM ticket
|
||||
UNION SELECT user_id FROM event
|
||||
UNION SELECT by_user_id AS user_id FROM event
|
||||
UNION SELECT submitter_id AS user_id FROM ticket_comment
|
||||
UNION SELECT user_id FROM ticket_subscription;
|
||||
|
||||
UPDATE participant
|
||||
SET created = now() at time zone 'utc', participant_type = 'user';
|
||||
|
||||
ALTER TABLE participant ALTER COLUMN created SET NOT NULL;
|
||||
ALTER TABLE participant ALTER COLUMN participant_type SET NOT NULL;
|
||||
""")
|
||||
|
||||
op.add_column("ticket", sa.Column(
|
||||
"participant_id", sa.Integer, sa.ForeignKey("participant.id")))
|
||||
op.execute("""
|
||||
UPDATE ticket tk
|
||||
SET participant_id = p.id
|
||||
FROM participant p
|
||||
WHERE p.user_id = tk.submitter_id;
|
||||
""")
|
||||
op.drop_column("ticket", "submitter_id")
|
||||
op.alter_column("ticket", "participant_id",
|
||||
new_column_name="submitter_id", nullable=False)
|
||||
|
||||
op.add_column("event", sa.Column(
|
||||
"participant_id", sa.Integer, sa.ForeignKey("participant.id")))
|
||||
op.add_column("event", sa.Column(
|
||||
"by_participant_id", sa.Integer, sa.ForeignKey("participant.id")))
|
||||
op.execute("""
|
||||
UPDATE event ev
|
||||
SET participant_id = p.id
|
||||
FROM participant p
|
||||
WHERE p.user_id = ev.user_id;
|
||||
|
||||
UPDATE event ev
|
||||
SET by_participant_id = p.id
|
||||
FROM participant p
|
||||
WHERE p.user_id = ev.by_user_id;
|
||||
""")
|
||||
op.drop_column("event", "user_id")
|
||||
op.drop_column("event", "by_user_id")
|
||||
|
||||
op.add_column("ticket_comment", sa.Column(
|
||||
"participant_id", sa.Integer, sa.ForeignKey("participant.id")))
|
||||
op.execute("""
|
||||
UPDATE ticket_comment tc
|
||||
SET participant_id = p.id
|
||||
FROM participant p
|
||||
WHERE p.user_id = tc.submitter_id;
|
||||
""")
|
||||
op.drop_column("ticket_comment", "submitter_id")
|
||||
op.alter_column("ticket_comment", "participant_id",
|
||||
new_column_name="submitter_id", nullable=False)
|
||||
|
||||
op.add_column("ticket_subscription", sa.Column(
|
||||
"participant_id", sa.Integer, sa.ForeignKey("participant.id")))
|
||||
op.execute("""
|
||||
UPDATE ticket_subscription ts
|
||||
SET participant_id = p.id
|
||||
FROM participant p
|
||||
WHERE p.user_id = ts.user_id;
|
||||
""")
|
||||
op.drop_column("ticket_subscription", "user_id")
|
||||
op.drop_column("ticket_subscription", "email")
|
||||
op.drop_column("ticket_subscription", "webhook")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column("ticket", sa.Column(
|
||||
"submitter_user_id", sa.Integer, sa.ForeignKey("user.id")))
|
||||
op.execute("""
|
||||
UPDATE ticket tk
|
||||
SET submitter_user_id = p.user_id
|
||||
FROM participant p
|
||||
WHERE p.id = tk.submitter_id;
|
||||
""")
|
||||
op.drop_column("ticket", "submitter_id")
|
||||
op.alter_column("ticket", "submitter_user_id",
|
||||
new_column_name="submitter_id", nullable=False)
|
||||
|
||||
op.add_column("event", sa.Column(
|
||||
"user_id", sa.Integer, sa.ForeignKey("user.id")))
|
||||
op.add_column("event", sa.Column(
|
||||
"by_user_id", sa.Integer, sa.ForeignKey("user.id")))
|
||||
op.execute("""
|
||||
UPDATE event ev
|
||||
SET user_id = p.user_id
|
||||
FROM participant p
|
||||
WHERE p.id = ev.participant_id;
|
||||
|
||||
UPDATE event ev
|
||||
SET by_user_id = p.user_id
|
||||
FROM participant p
|
||||
WHERE p.user_id = ev.by_participant_id;
|
||||
""")
|
||||
op.drop_column("event", "participant_id")
|
||||
op.drop_column("event", "by_participant_id")
|
||||
|
||||
op.add_column("ticket_comment", sa.Column(
|
||||
"user_id", sa.Integer, sa.ForeignKey("user.id")))
|
||||
op.execute("""
|
||||
UPDATE ticket_comment tc
|
||||
SET user_id = p.user_id
|
||||
FROM participant p
|
||||
WHERE p.id = tc.submitter_id;
|
||||
""")
|
||||
op.drop_column("ticket_comment", "submitter_id")
|
||||
op.alter_column("ticket_comment", "user_id",
|
||||
new_column_name="submitter_id", nullable=False)
|
||||
|
||||
op.add_column("ticket_subscription", sa.Column(
|
||||
"user_id", sa.Integer, sa.ForeignKey("user.id")))
|
||||
op.execute("""
|
||||
UPDATE ticket_subscription ts
|
||||
SET user_id = p.user_id
|
||||
FROM participant p
|
||||
WHERE p.id = ts.participant_id;
|
||||
""")
|
||||
op.drop_column("ticket_subscription", "participant_id")
|
||||
|
||||
op.add_column("ticket_subscription",
|
||||
sa.Column("email", sa.Column(sa.Unicode(512)))
|
||||
op.add_column("ticket_subscription",
|
||||
sa.Column("webhook", sa.Column(sa.Unicode(1024)))
|
||||
|
||||
op.drop_table("participant")
|
||||
|
|
@ -4,7 +4,7 @@ from srht.database import db
|
|||
from srht.oauth import oauth, current_token
|
||||
from srht.validation import Validation
|
||||
from todosrht.access import get_tracker, get_ticket
|
||||
from todosrht.tickets import submit_ticket, add_comment
|
||||
from todosrht.tickets import add_comment, get_participant_for_user, submit_ticket
|
||||
from todosrht.blueprints.api import get_user
|
||||
from todosrht.types import Ticket, TicketAccess, TicketStatus, TicketResolution
|
||||
from todosrht.types import Event, EventType, Label, TicketLabel
|
||||
|
@ -53,7 +53,8 @@ def tracker_tickets_POST(username, tracker_name):
|
|||
if not valid.ok:
|
||||
return valid.response
|
||||
|
||||
ticket = submit_ticket(tracker, current_token.user, title, desc)
|
||||
participant = get_participant_for_user(current_token.user)
|
||||
ticket = submit_ticket(tracker, participant.id, title, desc)
|
||||
TrackerWebhook.deliver(TrackerWebhook.Events.ticket_create,
|
||||
ticket.to_dict(),
|
||||
TrackerWebhook.Subscription.tracker_id == tracker.id)
|
||||
|
@ -110,6 +111,7 @@ def tracker_ticket_by_id_PUT(username, tracker_name, ticket_id):
|
|||
abort(404)
|
||||
ticket, access = get_ticket(tracker, ticket_id, user=current_token.user)
|
||||
|
||||
participant = get_participant_for_user(current_token.user)
|
||||
required_access = TicketAccess.none
|
||||
valid = Validation(request)
|
||||
comment = resolution = None
|
||||
|
@ -152,7 +154,7 @@ def tracker_ticket_by_id_PUT(username, tracker_name, ticket_id):
|
|||
.filter(TicketLabel.label_id == label.id)).delete()
|
||||
event = Event()
|
||||
event.event_type = EventType.label_removed
|
||||
event.user_id = current_token.user_id
|
||||
event.participant_id = participant.id
|
||||
event.ticket_id = ticket.id
|
||||
event.label_id = label.id
|
||||
db.session.add(event)
|
||||
|
@ -176,7 +178,7 @@ def tracker_ticket_by_id_PUT(username, tracker_name, ticket_id):
|
|||
db.session.add(tl)
|
||||
event = Event()
|
||||
event.event_type = EventType.label_added
|
||||
event.user_id = current_token.user_id
|
||||
event.participant_id = participant.id
|
||||
event.ticket_id = ticket.id
|
||||
event.label_id = label.id
|
||||
db.session.add(event)
|
||||
|
@ -199,7 +201,7 @@ def tracker_ticket_by_id_PUT(username, tracker_name, ticket_id):
|
|||
|
||||
if comment or resolve or resolution or reopen:
|
||||
event = add_comment(
|
||||
user, ticket, comment, resolve, resolution, reopen)
|
||||
participant, ticket, comment, resolve, resolution, reopen)
|
||||
db.session.add(event)
|
||||
db.session.flush()
|
||||
events.append(events)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from flask import Blueprint, render_template, request, abort
|
||||
from flask_login import current_user
|
||||
from todosrht.access import get_tracker, get_access
|
||||
from todosrht.tickets import get_participant_for_user
|
||||
from todosrht.types import Tracker, Ticket, Event, EventNotification, EventType
|
||||
from todosrht.types import User
|
||||
from todosrht.types import User, Participant
|
||||
from srht.config import cfg
|
||||
from srht.flask import paginate_query, session
|
||||
from sqlalchemy import and_, or_
|
||||
|
@ -14,11 +15,12 @@ def filter_authorized_events(events):
|
|||
.join(Ticket, Ticket.id == Event.ticket_id)
|
||||
.join(Tracker, Tracker.id == Ticket.tracker_id))
|
||||
if current_user:
|
||||
participant = get_participant_for_user(current_user)
|
||||
events = (events.filter(
|
||||
or_(
|
||||
and_(
|
||||
Ticket.submitter_perms != None,
|
||||
Ticket.submitter_id == current_user.id,
|
||||
Ticket.submitter_id == participant.id,
|
||||
Ticket.submitter_perms > 0),
|
||||
and_(
|
||||
Ticket.user_perms != None,
|
||||
|
@ -28,7 +30,7 @@ def filter_authorized_events(events):
|
|||
Ticket.anonymous_perms > 0),
|
||||
and_(
|
||||
Ticket.submitter_perms == None,
|
||||
Ticket.submitter_id == current_user.id,
|
||||
Ticket.submitter_id == participant.id,
|
||||
Tracker.default_submitter_perms > 0),
|
||||
and_(
|
||||
Ticket.user_perms == None,
|
||||
|
@ -95,7 +97,8 @@ def user_GET(username):
|
|||
|
||||
# TODO: Join on stuff the user has explicitly been granted access to
|
||||
events = (Event.query
|
||||
.filter(Event.user_id == user.id)
|
||||
.join(Participant, Event.participant_id == Participant.id)
|
||||
.filter(Participant.user_id == user.id)
|
||||
.order_by(Event.created.desc()))
|
||||
if not current_user or current_user.id != user.id:
|
||||
events = filter_authorized_events(events)
|
||||
|
|
|
@ -8,10 +8,11 @@ from todosrht.access import get_tracker, get_ticket
|
|||
from todosrht.filters import invalidate_markup_cache
|
||||
from todosrht.search import find_usernames
|
||||
from todosrht.tickets import add_comment, mark_seen, assign, unassign
|
||||
from todosrht.tickets import get_participant_for_user
|
||||
from todosrht.trackers import get_recent_users
|
||||
from todosrht.types import Event, EventType, Label, TicketLabel
|
||||
from todosrht.types import TicketAccess, TicketResolution
|
||||
from todosrht.types import TicketSubscription, User
|
||||
from todosrht.types import TicketSubscription, User, Participant
|
||||
from todosrht.urls import ticket_url
|
||||
from todosrht.webhooks import TrackerWebhook, TicketWebhook
|
||||
|
||||
|
@ -24,10 +25,18 @@ def get_ticket_context(ticket, tracker, access):
|
|||
ticket_sub = None
|
||||
|
||||
if current_user:
|
||||
tracker_sub = TicketSubscription.query.filter_by(
|
||||
ticket=None, tracker=tracker, user=current_user).one_or_none()
|
||||
ticket_sub = TicketSubscription.query.filter_by(
|
||||
ticket=ticket, tracker=None, user=current_user).one_or_none()
|
||||
tracker_sub = (TicketSubscription.query
|
||||
.join(Participant)
|
||||
.filter(TicketSubscription.ticket_id == None)
|
||||
.filter(TicketSubscription.tracker_id == tracker.id)
|
||||
.filter(Participant.user_id == current_user.id)
|
||||
).one_or_none()
|
||||
ticket_sub = (TicketSubscription.query
|
||||
.join(Participant)
|
||||
.filter(TicketSubscription.ticket_id == ticket.id)
|
||||
.filter(TicketSubscription.tracker_id == None)
|
||||
.filter(Participant.user_id == current_user.id)
|
||||
).one_or_none()
|
||||
|
||||
return {
|
||||
"tracker": tracker,
|
||||
|
@ -76,9 +85,10 @@ def enable_notifications(owner, name, ticket_id):
|
|||
if sub:
|
||||
return redirect(ticket_url(ticket))
|
||||
|
||||
participant = get_participant_for_user(current_user)
|
||||
sub = TicketSubscription()
|
||||
sub.ticket_id = ticket.id
|
||||
sub.user_id = current_user.id
|
||||
sub.participant_id = participant.id
|
||||
db.session.add(sub)
|
||||
db.session.commit()
|
||||
return redirect(ticket_url(ticket))
|
||||
|
@ -142,7 +152,8 @@ def ticket_comment_POST(owner, name, ticket_id):
|
|||
ctx = get_ticket_context(ticket, tracker, access)
|
||||
return render_template("ticket.html", **ctx, **valid.kwargs)
|
||||
|
||||
event = add_comment(current_user, ticket,
|
||||
participant = get_participant_for_user(current_user)
|
||||
event = add_comment(participant, ticket,
|
||||
text=text, resolve=resolve, resolution=resolution, reopen=reopen)
|
||||
|
||||
TicketWebhook.deliver(TicketWebhook.Events.event_create,
|
||||
|
@ -241,9 +252,10 @@ def ticket_add_label(owner, name, ticket_id):
|
|||
ticket_label.label_id = label.id
|
||||
ticket_label.user_id = current_user.id
|
||||
|
||||
participant = get_participant_for_user(current_user)
|
||||
event = Event()
|
||||
event.event_type = EventType.label_added
|
||||
event.user_id = current_user.id
|
||||
event.participant_id = participant.id
|
||||
event.ticket_id = ticket.id
|
||||
event.label_id = label.id
|
||||
|
||||
|
@ -281,9 +293,10 @@ def ticket_remove_label(owner, name, ticket_id, label_id):
|
|||
.filter(TicketLabel.ticket_id == ticket.id)).first()
|
||||
|
||||
if ticket_label:
|
||||
participant = get_participant_for_user(current_user)
|
||||
event = Event()
|
||||
event.event_type = EventType.label_removed
|
||||
event.user_id = current_user.id
|
||||
event.participant_id = participant.id
|
||||
event.ticket_id = ticket.id
|
||||
event.label_id = label.id
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ from todosrht import color
|
|||
from todosrht.access import get_tracker
|
||||
from todosrht.search import apply_search
|
||||
from todosrht.tickets import get_last_seen_times, get_comment_counts
|
||||
from todosrht.tickets import submit_ticket
|
||||
from todosrht.tickets import get_participant_for_user, submit_ticket
|
||||
from todosrht.trackers import get_recent_users
|
||||
from todosrht.types import Event, UserAccess
|
||||
from todosrht.types import Label, TicketLabel
|
||||
from todosrht.types import TicketSubscription, User
|
||||
from todosrht.types import TicketSubscription, User, Participant
|
||||
from todosrht.types import Tracker, Ticket, TicketAccess
|
||||
from todosrht.urls import tracker_url, ticket_url
|
||||
from todosrht.webhooks import TrackerWebhook, UserWebhook
|
||||
|
@ -43,9 +43,10 @@ def create_POST():
|
|||
tracker.to_dict(),
|
||||
UserWebhook.Subscription.user_id == tracker.owner_id)
|
||||
|
||||
participant = get_participant_for_user(current_user)
|
||||
sub = TicketSubscription()
|
||||
sub.tracker_id = tracker.id
|
||||
sub.user_id = current_user.id
|
||||
sub.participant_id = participant.id
|
||||
db.session.add(sub)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -63,9 +64,10 @@ def return_tracker(tracker, access, **kwargs):
|
|||
is_subscribed = False
|
||||
if current_user:
|
||||
sub = (TicketSubscription.query
|
||||
.join(Participant)
|
||||
.filter(TicketSubscription.tracker_id == tracker.id)
|
||||
.filter(TicketSubscription.ticket_id == None)
|
||||
.filter(TicketSubscription.user_id == current_user.id)
|
||||
.filter(Participant.user_id == current_user.id)
|
||||
).one_or_none()
|
||||
is_subscribed = bool(sub)
|
||||
|
||||
|
@ -122,9 +124,10 @@ def enable_notifications(owner, name):
|
|||
if sub:
|
||||
return redirect(tracker_url(tracker))
|
||||
|
||||
participant = get_participant_for_user(current_user)
|
||||
sub = TicketSubscription()
|
||||
sub.tracker_id = tracker.id
|
||||
sub.user_id = current_user.id
|
||||
sub.participant_id = participant.id
|
||||
db.session.add(sub)
|
||||
db.session.commit()
|
||||
return redirect(tracker_url(tracker))
|
||||
|
@ -370,7 +373,8 @@ def tracker_submit_POST(owner, name):
|
|||
return return_tracker(tracker, access, **valid.kwargs), 400
|
||||
|
||||
# TODO: Handle unique constraint failure (contention) and retry?
|
||||
ticket = submit_ticket(tracker, current_user, title, desc)
|
||||
participant = get_participant_for_user(current_user)
|
||||
ticket = submit_ticket(tracker, participant, title, desc)
|
||||
|
||||
UserWebhook.deliver(UserWebhook.Events.ticket_create,
|
||||
ticket.to_dict(),
|
||||
|
|
|
@ -5,6 +5,7 @@ import textwrap
|
|||
from flask_login import current_user
|
||||
from srht.config import cfg, cfgi
|
||||
from srht.email import send_email, lookup_key
|
||||
from todosrht.types import ParticipantType
|
||||
|
||||
origin = cfg("todo.sr.ht", "origin")
|
||||
|
||||
|
@ -15,13 +16,15 @@ def format_lines(text, quote=False):
|
|||
|
||||
def notify(sub, template, subject, headers, **kwargs):
|
||||
encrypt_key = None
|
||||
if sub.email:
|
||||
to = sub.email
|
||||
elif sub.user:
|
||||
to = sub.user.email
|
||||
encrypt_key = lookup_key(sub.user.username, sub.user.oauth_token)
|
||||
to = sub.participant
|
||||
if to.participant_type == ParticipantType.email:
|
||||
to = to.email
|
||||
elif to.participant_type == ParticipantType.user:
|
||||
user = to.user
|
||||
to = user.email
|
||||
encrypt_key = lookup_key(user.username, user.oauth_token)
|
||||
else:
|
||||
return # TODO
|
||||
return
|
||||
with open(os.path.join(os.path.dirname(__file__), "emails", template)) as f:
|
||||
body = html.unescape(
|
||||
pystache.render(f.read(), {
|
||||
|
|
|
@ -27,10 +27,11 @@ def render_markup(tracker, text):
|
|||
users = find_mentioned_users(text)
|
||||
tickets = find_mentioned_tickets(tracker, text)
|
||||
|
||||
users_map = {str(u): u for u in users}
|
||||
users_map = {u.identifier: u for u in users}
|
||||
tickets_map = {t.ref(): t for t in tickets}
|
||||
|
||||
def urlize_user(match):
|
||||
# TODO: Handle mentions for non-user participants
|
||||
username = match.group(0)
|
||||
if username in users_map:
|
||||
url = urls.user_url(users_map[username])
|
||||
|
|
|
@ -29,6 +29,7 @@ class TodoApp(SrhtFlask):
|
|||
self.add_template_filter(filters.render_ticket_description)
|
||||
self.add_template_filter(urls.label_add_url)
|
||||
self.add_template_filter(urls.label_search_url)
|
||||
self.add_template_filter(urls.participant_url)
|
||||
self.add_template_filter(urls.ticket_assign_url)
|
||||
self.add_template_filter(urls.ticket_edit_url)
|
||||
self.add_template_filter(urls.ticket_unassign_url)
|
||||
|
|
|
@ -10,35 +10,35 @@
|
|||
</h4>
|
||||
<p>
|
||||
{% if EventType.created in event.event_type %}
|
||||
Ticket created by <a href="{{ event.ticket.submitter|user_url }}">
|
||||
Ticket created by <a href="{{ event.ticket.submitter|participant_url }}">
|
||||
{{ event.ticket.submitter }}
|
||||
</a>
|
||||
{% elif EventType.comment in event.event_type %}
|
||||
Comment by <a href="{{ event.comment.submitter|user_url }}">
|
||||
Comment by <a href="{{ event.comment.submitter|participant_url }}">
|
||||
{{ event.comment.submitter }}
|
||||
</a>
|
||||
{% elif EventType.label_added in event.event_type %}
|
||||
{{ event.label|label_badge(cls="small") }} added by
|
||||
<a href="{{ event.user|user_url }}">{{ event.user }}</a>
|
||||
<a href="{{ event.participant|participant_url }}">{{ event.participant }}</a>
|
||||
{% elif EventType.label_removed in event.event_type %}
|
||||
{{ event.label|label_badge(cls="small") }} removed by
|
||||
<a href="{{ event.user|user_url }}">{{ event.user }}</a>
|
||||
<a href="{{ event.participant|participant_url }}">{{ event.participant }}</a>
|
||||
{% elif EventType.assigned_user in event.event_type %}
|
||||
<a href="{{event.by_user|user_url}}">
|
||||
{{event.by_user}}
|
||||
<a href="{{event.by_participant|participant_url}}">
|
||||
{{event.by_participant}}
|
||||
</a>
|
||||
assigned
|
||||
<a href="{{event.user|user_url}}">
|
||||
{{event.user}}
|
||||
<a href="{{event.participant|participant_url}}">
|
||||
{{event.participant}}
|
||||
</a>
|
||||
to #{{event.ticket.scoped_id}}
|
||||
{% elif EventType.unassigned_user in event.event_type %}
|
||||
<a href="{{event.by_user|user_url}}">
|
||||
{{event.by_user}}
|
||||
<a href="{{event.by_user|participant_url}}">
|
||||
{{event.by_participant}}
|
||||
</a>
|
||||
unassigned
|
||||
<a href="{{event.user|user_url}}">
|
||||
{{event.user}}
|
||||
<a href="{{event.participant|participant_url}}">
|
||||
{{event.participant}}
|
||||
</a>
|
||||
from #{{event.ticket.scoped_id}}
|
||||
{% endif %}
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
</dd>
|
||||
<dt class="col-md-3">Submitter</dt>
|
||||
<dd class="col-md-9">
|
||||
<a id="submitter-field" href="{{ ticket.submitter|user_url }}">
|
||||
<a id="submitter-field" href="{{ ticket.submitter|participant_url }}">
|
||||
{{ ticket.submitter }}
|
||||
</a>
|
||||
</dd>
|
||||
|
@ -241,8 +241,8 @@
|
|||
EventType.unassigned_user,
|
||||
EventType.ticket_mentioned,
|
||||
] %}
|
||||
<a href="{{ event.user|user_url }}">
|
||||
{{ event.user }}
|
||||
<a href="{{ event.participant|participant_url }}">
|
||||
{{ event.participant }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if EventType.status_change in event.event_type %}
|
||||
|
@ -269,26 +269,26 @@
|
|||
removed {{ event.label|label_badge(cls="small") }}
|
||||
{% endif %}
|
||||
{% if EventType.assigned_user in event.event_type %}
|
||||
<a href="{{event.by_user|user_url}}">
|
||||
{{event.by_user}}
|
||||
<a href="{{event.by_participant|participant_url}}">
|
||||
{{event.by_participant}}
|
||||
</a>
|
||||
assigned
|
||||
<a href="{{event.user|user_url}}">
|
||||
{{event.user}}
|
||||
<a href="{{event.participant|participant_url}}">
|
||||
{{event.participant}}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if EventType.unassigned_user in event.event_type %}
|
||||
<a href="{{event.by_user|user_url}}">
|
||||
{{event.by_user}}
|
||||
<a href="{{event.by_participant|participant_url}}">
|
||||
{{event.by_participant}}
|
||||
</a>
|
||||
unassigned
|
||||
<a href="{{event.user|user_url}}">
|
||||
{{event.user}}
|
||||
<a href="{{event.participant|participant_url}}">
|
||||
{{event.participant}}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if EventType.ticket_mentioned in event.event_type %}
|
||||
<a href="{{ event.by_user|user_url }}">
|
||||
{{ event.by_user }}
|
||||
<a href="{{ event.by_user|participant_url }}">
|
||||
{{ event.by_participant }}
|
||||
</a>
|
||||
{% set relation = event.from_ticket %}
|
||||
{% if relation.status == TicketStatus.resolved and
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
</div>
|
||||
<div class="updated">{{ ticket.updated | date }}</div>
|
||||
<div class="submitter">
|
||||
<a href="{{ ticket.submitter|user_url }}">
|
||||
<a href="{{ ticket.submitter|participant_url }}">
|
||||
{{ ticket.submitter }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ 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, Tracker
|
||||
from todosrht.types import Participant, ParticipantType
|
||||
from todosrht.urls import ticket_url
|
||||
from sqlalchemy import func, or_, and_
|
||||
|
||||
|
@ -52,11 +53,23 @@ TICKET_URL_PATTERN = re.compile(f"""
|
|||
\\b # Word boundary
|
||||
""", re.VERBOSE)
|
||||
|
||||
def get_participant_for_user(user):
|
||||
participant = Participant.query.filter(
|
||||
Participant.user_id == user.id).one_or_none()
|
||||
if not participant:
|
||||
participant = Participant()
|
||||
participant.user_id = user.id
|
||||
participant.participant_type = ParticipantType.user
|
||||
db.session.add(participant)
|
||||
db.session.flush()
|
||||
return participant
|
||||
|
||||
def find_mentioned_users(text):
|
||||
# TODO: Find mentioned email addresses as well
|
||||
usernames = re.findall(USER_MENTION_PATTERN, text)
|
||||
users = User.query.filter(User.username.in_(usernames)).all()
|
||||
return set(users)
|
||||
participants = set([get_participant_for_user(u) for u in set(users)])
|
||||
return participants
|
||||
|
||||
def find_mentioned_tickets(tracker, text):
|
||||
filters = or_()
|
||||
|
@ -85,21 +98,20 @@ def find_mentioned_tickets(tracker, text):
|
|||
.filter(filters)
|
||||
.all())
|
||||
|
||||
def _create_comment(ticket, user, text):
|
||||
def _create_comment(ticket, participant, text):
|
||||
comment = TicketComment()
|
||||
comment.text = text
|
||||
# TODO: anonymous comments (when configured appropriately)
|
||||
comment.submitter_id = user.id
|
||||
comment.submitter_id = participant.id
|
||||
comment.ticket_id = ticket.id
|
||||
|
||||
db.session.add(comment)
|
||||
db.session.flush()
|
||||
return comment
|
||||
|
||||
def _create_comment_event(ticket, user, comment, status_change):
|
||||
def _create_comment_event(ticket, participant, comment, status_change):
|
||||
event = Event()
|
||||
event.event_type = 0
|
||||
event.user_id = user.id
|
||||
event.participant_id = participant.id
|
||||
event.ticket_id = ticket.id
|
||||
|
||||
if comment:
|
||||
|
@ -117,17 +129,20 @@ def _create_comment_event(ticket, user, comment, status_change):
|
|||
db.session.flush()
|
||||
return event
|
||||
|
||||
def _create_event_notification(user, event):
|
||||
def _create_event_notification(participant, event):
|
||||
if participant.participant_type != ParticipantType.user:
|
||||
return None # We only record notifications for registered users
|
||||
notification = EventNotification()
|
||||
notification.user_id = user.id
|
||||
notification.user_id = participant.user.id
|
||||
notification.event_id = event.id
|
||||
db.session.add(notification)
|
||||
return notification
|
||||
|
||||
def _send_comment_notification(subscription, ticket, user, comment, resolution):
|
||||
def _send_comment_notification(subscription, ticket,
|
||||
participant, comment, resolution):
|
||||
subject = "Re: {}: {}".format(ticket.ref(), ticket.title)
|
||||
headers = {
|
||||
"From": "~{} <{}>".format(user.username, notify_from),
|
||||
"From": "{} <{}>".format(participant.name, notify_from),
|
||||
"Reply-To": f"{ticket.ref()} <{ticket.ref(email=True)}@{posting_domain}>",
|
||||
"Sender": smtp_user,
|
||||
}
|
||||
|
@ -156,45 +171,46 @@ def _change_ticket_status(ticket, resolve, resolution, reopen):
|
|||
if reopen:
|
||||
ticket.status = TicketStatus.reported
|
||||
|
||||
return StatusChange(
|
||||
old_status, ticket.status, old_resolution, ticket.resolution)
|
||||
return StatusChange(old_status, ticket.status,
|
||||
old_resolution, ticket.resolution)
|
||||
|
||||
def _send_comment_notifications(user, ticket, event, comment, resolution):
|
||||
def _send_comment_notifications(
|
||||
participant, ticket, event, comment, resolution):
|
||||
"""
|
||||
Notify users subscribed to the ticket or tracker.
|
||||
Returns a list of notified users.
|
||||
"""
|
||||
# Find subscribers, eliminate duplicates
|
||||
subscriptions = {sub.user: sub
|
||||
subscriptions = {sub.participant: sub
|
||||
for sub in ticket.tracker.subscriptions + ticket.subscriptions}
|
||||
|
||||
# Subscribe commenter if not already subscribed
|
||||
if user not in subscriptions:
|
||||
if participant not in subscriptions:
|
||||
subscription = TicketSubscription()
|
||||
subscription.ticket_id = ticket.id
|
||||
subscription.user_id = user.id
|
||||
subscription.participant_id = participant.id
|
||||
db.session.add(subscription)
|
||||
subscriptions[user] = subscription
|
||||
subscriptions[participant] = subscription
|
||||
|
||||
for subscriber, subscription in subscriptions.items():
|
||||
_create_event_notification(subscriber, event)
|
||||
if subscriber != user:
|
||||
if subscriber != participant:
|
||||
_send_comment_notification(
|
||||
subscription, ticket, user, comment, resolution)
|
||||
subscription, ticket, participant, comment, resolution)
|
||||
|
||||
return subscriptions.keys()
|
||||
|
||||
def _send_mention_notification(sub, submitter, text, ticket, comment=None):
|
||||
subject = "{}: {}".format(ticket.ref(), ticket.title)
|
||||
headers = {
|
||||
"From": "~{} <{}>".format(submitter.username, notify_from),
|
||||
"From": "{} <{}>".format(submitter.name, notify_from),
|
||||
"Reply-To": f"{ticket.ref()} <{ticket.ref(email=True)}@{posting_domain}>",
|
||||
"Sender": smtp_user,
|
||||
}
|
||||
|
||||
context = {
|
||||
"text": format_lines(text),
|
||||
"submitter": submitter.canonical_name,
|
||||
"submitter": submitter.name,
|
||||
"ticket_ref": ticket.ref(),
|
||||
"ticket_url": ticket_url(ticket, comment),
|
||||
}
|
||||
|
@ -206,15 +222,15 @@ def _handle_mentions(ticket, submitter, text, notified_users, comment=None):
|
|||
"""
|
||||
Create events for mentioned tickets and users and notify mentioned users.
|
||||
"""
|
||||
mentioned_users = find_mentioned_users(text)
|
||||
mentioned_participants = find_mentioned_users(text)
|
||||
mentioned_tickets = find_mentioned_tickets(ticket.tracker, text)
|
||||
|
||||
for user in mentioned_users:
|
||||
for participant in mentioned_participants:
|
||||
db.session.add(Event(
|
||||
event_type=EventType.user_mentioned,
|
||||
user=user,
|
||||
participant=participant,
|
||||
from_ticket=ticket,
|
||||
by_user=submitter,
|
||||
by_participant=submitter,
|
||||
comment=comment,
|
||||
))
|
||||
|
||||
|
@ -223,20 +239,20 @@ def _handle_mentions(ticket, submitter, text, notified_users, comment=None):
|
|||
event_type=EventType.ticket_mentioned,
|
||||
ticket=mentioned_ticket,
|
||||
from_ticket=ticket,
|
||||
by_user=submitter,
|
||||
by_participant=submitter,
|
||||
comment=comment,
|
||||
))
|
||||
|
||||
# Notify users who are mentioned, but only if they haven't already received
|
||||
# a notification due to being subscribed to the event or tracker
|
||||
# Also don't notify the submitter if they mention themselves.
|
||||
to_notify_users = mentioned_users - set(notified_users) - set([submitter])
|
||||
for user in to_notify_users:
|
||||
sub = get_or_create_subscription(ticket, user)
|
||||
to_notify = mentioned_participants - set(notified_users) - set([submitter])
|
||||
for target in to_notify:
|
||||
sub = get_or_create_subscription(ticket, target)
|
||||
_send_mention_notification(sub, submitter, text, ticket, comment)
|
||||
|
||||
|
||||
def add_comment(user, ticket,
|
||||
def add_comment(submitter, ticket,
|
||||
text=None, resolve=False, resolution=None, reopen=False):
|
||||
"""
|
||||
Comment on a ticket, optionally resolve or reopen the ticket.
|
||||
|
@ -247,18 +263,18 @@ def add_comment(user, ticket,
|
|||
if resolve:
|
||||
assert resolution is not None
|
||||
|
||||
comment = _create_comment(ticket, user, text) if text else None
|
||||
comment = _create_comment(ticket, submitter, text) if text else None
|
||||
status_change = _change_ticket_status(ticket, resolve, resolution, reopen)
|
||||
event = _create_comment_event(ticket, user, comment, status_change)
|
||||
notified_users = _send_comment_notifications(
|
||||
user, ticket, event, comment, resolution)
|
||||
event = _create_comment_event(ticket, submitter, comment, status_change)
|
||||
notified_participants = _send_comment_notifications(
|
||||
submitter, ticket, event, comment, resolution)
|
||||
|
||||
if comment and comment.text:
|
||||
_handle_mentions(
|
||||
ticket,
|
||||
comment.submitter,
|
||||
comment.text,
|
||||
notified_users,
|
||||
notified_participants,
|
||||
comment,
|
||||
)
|
||||
|
||||
|
@ -279,24 +295,27 @@ def mark_seen(ticket, user):
|
|||
|
||||
return seen
|
||||
|
||||
def get_or_create_subscription(ticket, user):
|
||||
def get_or_create_subscription(ticket, participant):
|
||||
"""
|
||||
If user is subscribed to ticket or tracker, returns that subscription,
|
||||
otherwise subscribes the user to the ticket and returns that one.
|
||||
If participant is subscribed to ticket or tracker, returns that
|
||||
subscription, otherwise subscribes the user to the ticket and returns that
|
||||
one.
|
||||
"""
|
||||
subscription = TicketSubscription.query.filter(
|
||||
(TicketSubscription.user == user) & (
|
||||
(TicketSubscription.participant == participant) & (
|
||||
(TicketSubscription.ticket == ticket) |
|
||||
(TicketSubscription.tracker == ticket.tracker)
|
||||
)
|
||||
).first()
|
||||
|
||||
if not subscription:
|
||||
subscription = TicketSubscription(ticket=ticket, user=user)
|
||||
subscription = TicketSubscription(
|
||||
ticket=ticket, participant=participant)
|
||||
db.session.add(subscription)
|
||||
|
||||
return subscription
|
||||
|
||||
# TODO: support arbitrary participants being assigned to tickets
|
||||
def notify_assignee(subscription, ticket, assigner, assignee):
|
||||
"""
|
||||
Sends a notification email to the person who was assigned to the issue.
|
||||
|
@ -334,15 +353,18 @@ def assign(ticket, assignee, assigner):
|
|||
)
|
||||
db.session.add(ticket_assignee)
|
||||
|
||||
subscription = get_or_create_subscription(ticket, assignee)
|
||||
assignee_participant = get_participant_for_user(assignee)
|
||||
assigner_participant = get_participant_for_user(assigner)
|
||||
|
||||
subscription = get_or_create_subscription(ticket, assignee_participant)
|
||||
if assigner != assignee:
|
||||
notify_assignee(subscription, ticket, assigner, assignee)
|
||||
|
||||
event = Event()
|
||||
event.event_type = EventType.assigned_user
|
||||
event.user_id = assignee.id
|
||||
event.participant_id = assignee_participant.id
|
||||
event.ticket_id = ticket.id
|
||||
event.by_user_id = assigner.id
|
||||
event.by_user_id = assigner_participant.id
|
||||
db.session.add(event)
|
||||
|
||||
return ticket_assignee
|
||||
|
@ -357,11 +379,14 @@ def unassign(ticket, assignee, assigner):
|
|||
|
||||
db.session.delete(ticket_assignee)
|
||||
|
||||
assignee_participant = get_participant_for_user(assignee)
|
||||
assigner_participant = get_participant_for_user(assigner)
|
||||
|
||||
event = Event()
|
||||
event.event_type = EventType.unassigned_user
|
||||
event.user_id = assignee.id
|
||||
event.participant_id = assignee_participant.id
|
||||
event.ticket_id = ticket.id
|
||||
event.by_user_id = assigner.id
|
||||
event.by_user_id = assigner_participant.id
|
||||
db.session.add(event)
|
||||
|
||||
def get_last_seen_times(user, tickets):
|
||||
|
@ -381,7 +406,7 @@ def get_comment_counts(tickets):
|
|||
def _send_new_ticket_notification(subscription, ticket):
|
||||
subject = f"{ticket.ref()}: {ticket.title}"
|
||||
headers = {
|
||||
"From": "~{} <{}>".format(ticket.submitter.username, notify_from),
|
||||
"From": "{} <{}>".format(ticket.submitter.name, notify_from),
|
||||
"Reply-To": f"{ticket.ref()} <{ticket.ref(email=True)}@{posting_domain}>",
|
||||
"Sender": smtp_user,
|
||||
}
|
||||
|
@ -403,7 +428,8 @@ def submit_ticket(tracker, submitter, title, description):
|
|||
tracker.next_ticket_id += 1
|
||||
tracker.updated = datetime.utcnow()
|
||||
|
||||
event = Event(event_type=EventType.created, user=submitter, ticket=ticket)
|
||||
event = Event(event_type=EventType.created,
|
||||
participant=submitter, ticket=ticket)
|
||||
db.session.add(event)
|
||||
db.session.flush()
|
||||
|
||||
|
@ -412,11 +438,11 @@ def submit_ticket(tracker, submitter, title, description):
|
|||
|
||||
# Send notifications
|
||||
for sub in tracker.subscriptions:
|
||||
_create_event_notification(sub.user, event)
|
||||
if sub.user != submitter:
|
||||
_create_event_notification(sub.participant, event)
|
||||
if sub.participant != submitter:
|
||||
_send_new_ticket_notification(sub, ticket)
|
||||
|
||||
notified_users = [sub.user for sub in tracker.subscriptions]
|
||||
notified_users = [sub.participant for sub in tracker.subscriptions]
|
||||
_handle_mentions(
|
||||
ticket,
|
||||
ticket.submitter,
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from srht.database import db
|
||||
from todosrht.types import Event, Ticket, User
|
||||
from todosrht.types import Event, Ticket, User, Participant
|
||||
|
||||
|
||||
def get_recent_users(tracker, limit=20):
|
||||
"""Find users who recently interacted with a tracker."""
|
||||
|
||||
recent_user_events = (db.session.query(Event.id, User.username)
|
||||
.join(User, User.id == Event.user_id)
|
||||
recent_user_events = (db.session
|
||||
.query(Event.id, Participant.id, User.username)
|
||||
.join(Participant, Participant.id == Event.participant_id)
|
||||
.join(User, User.id == Participant.user_id)
|
||||
.join(Ticket, Ticket.id == Event.ticket_id)
|
||||
.filter(Ticket.tracker_id == tracker.id)
|
||||
.order_by(Event.created.desc())
|
||||
|
|
|
@ -10,12 +10,14 @@ class OAuthToken(Base, ExternalOAuthTokenMixin):
|
|||
|
||||
from todosrht.types.ticketaccess import TicketAccess
|
||||
from todosrht.types.ticketstatus import TicketStatus, TicketResolution
|
||||
from todosrht.types.tracker import Tracker
|
||||
from todosrht.types.ticketseen import TicketSeen
|
||||
from todosrht.types.ticket import Ticket
|
||||
from todosrht.types.ticketsubscription import TicketSubscription
|
||||
from todosrht.types.ticketcomment import TicketComment
|
||||
from todosrht.types.ticketassignee import TicketAssignee
|
||||
|
||||
from todosrht.types.event import Event, EventType, EventNotification
|
||||
from todosrht.types.label import Label, TicketLabel
|
||||
from todosrht.types.participant import Participant, ParticipantType
|
||||
from todosrht.types.ticket import Ticket
|
||||
from todosrht.types.ticketassignee import TicketAssignee
|
||||
from todosrht.types.ticketcomment import TicketComment
|
||||
from todosrht.types.ticketseen import TicketSeen
|
||||
from todosrht.types.ticketsubscription import TicketSubscription
|
||||
from todosrht.types.tracker import Tracker
|
||||
from todosrht.types.useraccess import UserAccess
|
||||
|
|
|
@ -31,9 +31,9 @@ class Event(Base):
|
|||
new_status = sa.Column(FlagType(TicketStatus), default=0)
|
||||
new_resolution = sa.Column(FlagType(TicketResolution), default=0)
|
||||
|
||||
user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"))
|
||||
user = sa.orm.relationship("User",
|
||||
backref=sa.orm.backref("events"), foreign_keys=[user_id])
|
||||
participant_id = sa.Column(sa.Integer, sa.ForeignKey("participant.id"))
|
||||
participant = sa.orm.relationship("Participant",
|
||||
backref=sa.orm.backref("events"), foreign_keys=[participant_id])
|
||||
|
||||
ticket_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("ticket.id", ondelete="CASCADE"))
|
||||
|
@ -50,8 +50,9 @@ class Event(Base):
|
|||
label = sa.orm.relationship("Label",
|
||||
backref=sa.orm.backref("events", cascade="all, delete-orphan"))
|
||||
|
||||
by_user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"))
|
||||
by_user = sa.orm.relationship("User", foreign_keys=[by_user_id])
|
||||
by_participant_id = sa.Column(sa.Integer, sa.ForeignKey("participant.id"))
|
||||
by_participant = sa.orm.relationship(
|
||||
"Participant", foreign_keys=[by_participant_id])
|
||||
|
||||
from_ticket_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("ticket.id", ondelete="CASCADE"))
|
||||
|
@ -71,15 +72,15 @@ class Event(Base):
|
|||
"new_status": self.new_status.name if self.new_status else None,
|
||||
"new_resolution": self.new_resolution.name
|
||||
if self.new_resolution else None,
|
||||
"user": self.user.to_dict(short=True)
|
||||
if self.user else None,
|
||||
"user": self.participant.to_dict(short=True)
|
||||
if self.participant else None,
|
||||
"ticket": self.ticket.to_dict(short=True)
|
||||
if self.ticket else None,
|
||||
"comment": self.comment.to_dict(short=True)
|
||||
if self.comment else None,
|
||||
"label": self.label.name if self.label else None,
|
||||
"by_user": self.by_user.to_dict(short=True)
|
||||
if self.by_user else None,
|
||||
"by_user": self.by_participant.to_dict(short=True)
|
||||
if self.by_participant else None,
|
||||
"from_ticket": self.from_ticket.to_dict(short=True)
|
||||
if self.from_ticket else None,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy_utils as sau
|
||||
from srht.database import Base
|
||||
from enum import Enum
|
||||
|
||||
class ParticipantType(Enum):
|
||||
user = "user"
|
||||
email = "email"
|
||||
external = "external"
|
||||
|
||||
class Participant(Base):
|
||||
__tablename__ = "participant"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
created = sa.Column(sa.DateTime, nullable=False)
|
||||
|
||||
participant_type = sa.Column(sau.ChoiceType(
|
||||
ParticipantType, impl=sa.String()), nullable=False)
|
||||
|
||||
# ParticipantType.user
|
||||
user_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("user.id", ondelete="CASCADE"),
|
||||
unique=True)
|
||||
user = sa.orm.relationship('User')
|
||||
|
||||
# ParticipantType.email
|
||||
email = sa.Column(sa.String, unique=True)
|
||||
email_name = sa.Column(sa.String)
|
||||
|
||||
# ParticipantType.external
|
||||
external_id = sa.Column(sa.String, unique=True)
|
||||
external_url = sa.Column(sa.String)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Returns a human-friendly display name for this participant"""
|
||||
if self.participant_type == ParticipantType.user:
|
||||
return self.user.canonical_name
|
||||
elif self.participant_type == ParticipantType.email:
|
||||
return self.email_name or self.email
|
||||
elif self.participant_type == ParticipantType.external:
|
||||
return self.external_id
|
||||
assert False
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""Returns a human-friendly unique identifier for this participant"""
|
||||
if self.participant_type == ParticipantType.user:
|
||||
return self.user.canonical_name
|
||||
elif self.participant_type == ParticipantType.email:
|
||||
return self.email
|
||||
elif self.participant_type == ParticipantType.external:
|
||||
return self.external_id
|
||||
assert False
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Participant {self.id} [{self.participant_type.value}]>"
|
||||
|
||||
def to_dict(self, short=False):
|
||||
if self.participant_type == ParticipantType.user:
|
||||
return {
|
||||
"type": "user",
|
||||
**self.user.to_dict(short),
|
||||
}
|
||||
elif self.participant_type == ParticipantType.email:
|
||||
return {
|
||||
"type": "email",
|
||||
"address": self.email,
|
||||
"name": self.email_name,
|
||||
}
|
||||
elif self.participant_type == ParticipantType.external:
|
||||
return {
|
||||
"type": "external",
|
||||
"external_id": self.external_id,
|
||||
"external_url": self.external_url,
|
||||
}
|
||||
assert False
|
|
@ -28,8 +28,8 @@ class Ticket(Base):
|
|||
remote_side=[id])
|
||||
|
||||
submitter_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("user.id"), nullable=False)
|
||||
submitter = sa.orm.relationship("User",
|
||||
sa.ForeignKey("participant.id"), nullable=False)
|
||||
submitter = sa.orm.relationship("Participant",
|
||||
backref=sa.orm.backref("submitted", cascade="all, delete-orphan"))
|
||||
|
||||
title = sa.Column(sa.Unicode(2048), nullable=False)
|
||||
|
|
|
@ -10,8 +10,8 @@ class TicketComment(Base):
|
|||
updated = sa.Column(sa.DateTime, nullable=False)
|
||||
|
||||
submitter_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("user.id"), nullable=False)
|
||||
submitter = sa.orm.relationship("User")
|
||||
sa.ForeignKey("participant.id"), nullable=False)
|
||||
submitter = sa.orm.relationship("Participant")
|
||||
|
||||
ticket_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("ticket.id", ondelete="CASCADE"),
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy_utils as sau
|
||||
from srht.database import Base
|
||||
from srht.flagtype import FlagType
|
||||
from enum import Enum
|
||||
|
||||
class TicketSubscription(Base):
|
||||
"""One of user, email, or webhook will be valid. The rest will be null."""
|
||||
|
@ -25,11 +23,11 @@ class TicketSubscription(Base):
|
|||
cascade="all, delete-orphan"))
|
||||
"""Used for subscriptions to specific tickets"""
|
||||
|
||||
user_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("user.id"))
|
||||
user = sa.orm.relationship("User",
|
||||
participant_id = sa.Column(sa.Integer,
|
||||
sa.ForeignKey("participant.id"))
|
||||
participant = sa.orm.relationship("Participant",
|
||||
backref=sa.orm.backref("subscriptions"))
|
||||
|
||||
email = sa.Column(sa.Unicode(512))
|
||||
|
||||
webhook = sa.Column(sa.Unicode(1024))
|
||||
def __repr__(self):
|
||||
return (f"<TicketSubscription {self.id} {self.participant}; " +
|
||||
f"tk: {self.ticket_id}; tr: {self.tracker_id}>")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from flask import has_app_context, url_for
|
||||
from jinja2.utils import unicode_urlencode
|
||||
from todosrht.types import ParticipantType
|
||||
|
||||
def tracker_url(tracker):
|
||||
return url_for("tracker.tracker_GET",
|
||||
|
@ -68,5 +69,16 @@ def label_remove_url(label, ticket):
|
|||
ticket_id=ticket.scoped_id,
|
||||
label_id=label.id)
|
||||
|
||||
def participant_url(participant):
|
||||
if participant.participant_type == ParticipantType.user:
|
||||
return url_for("html.user_GET", username=participant.user.username)
|
||||
elif participant.participant_type == ParticipantType.email:
|
||||
if participant.email_name:
|
||||
return f"mailto:{participant.email_name} <{participant.email}>"
|
||||
else:
|
||||
return f"mailto:{participant.email}"
|
||||
elif participant.participant_type == ParticipantType.external:
|
||||
return participant.external_url
|
||||
|
||||
def user_url(user):
|
||||
return url_for("html.user_GET", username=user.username)
|
||||
|
|
Loading…
Reference in New Issue