Skip to content

Commit

Permalink
Some cleanup to tickets list view
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Oct 29, 2024
1 parent a385918 commit 152766e
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 102 deletions.
25 changes: 11 additions & 14 deletions temba/tickets/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ class TicketFolder(metaclass=ABCMeta):
icon = None
verbose_name = None

def get_queryset(self, org, user, ordered):
def get_queryset(self, org, user, *, ordered: bool):
qs = org.tickets.all()

if ordered:
Expand All @@ -325,60 +325,57 @@ def all(cls):

class MineFolder(TicketFolder):
"""
Tickets assigned to the current user
Tickets assigned to the current user.
"""

slug = "mine"
name = _("My Tickets")
icon = "tickets_mine"

def get_queryset(self, org, user, ordered):
return super().get_queryset(org, user, ordered).filter(assignee=user)
def get_queryset(self, org, user, *, ordered: bool):
return super().get_queryset(org, user, ordered=ordered).filter(assignee=user)


class UnassignedFolder(TicketFolder):
"""
Tickets not assigned to any user
Tickets not assigned to any user.
"""

slug = "unassigned"
name = _("Unassigned")
verbose_name = _("Unassigned Tickets")
icon = "tickets_unassigned"

def get_queryset(self, org, user, ordered):
return super().get_queryset(org, user, ordered).filter(assignee=None)
def get_queryset(self, org, user, *, ordered: bool):
return super().get_queryset(org, user, ordered=ordered).filter(assignee=None)


class AllFolder(TicketFolder):
"""
All tickets
All tickets the user can access.
"""

slug = "all"
name = _("All")
verbose_name = _("All Tickets")
icon = "tickets_all"

def get_queryset(self, org, user, ordered):
return super().get_queryset(org, user, ordered)


FOLDERS = {f.slug: f() for f in TicketFolder.__subclasses__()}


class TopicFolder(TicketFolder):
"""
Tickets with a specific topic
Wraps a topic so we can use it like a folder.
"""

def __init__(self, topic: Topic):
self.slug = topic.uuid
self.name = topic.name
self.topic = topic

def get_queryset(self, org, user, ordered):
return super().get_queryset(org, user, ordered).filter(topic=self.topic)
def get_queryset(self, org, user, *, ordered: bool):
return super().get_queryset(org, user, ordered=ordered).filter(topic=self.topic)


class TicketCount(SquashableModel):
Expand Down
31 changes: 16 additions & 15 deletions temba/tickets/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,34 +590,35 @@ def test_list(self):
self.assertRequestDisallowed(list_url, [None])
self.assertListFetch(list_url, [self.user, self.editor, self.admin, self.agent], context_objects=[])

# can hit this page with a uuid
# TODO: work out reverse for deep link
# deep_link = reverse(
# "tickets.ticket_list", kwargs={"folder": "all", "status": "open", "uuid": str(ticket.uuid)}
# )

# link to our ticket within the All folder
deep_link = f"{list_url}all/open/{str(ticket.uuid)}/"
self.assertContentMenu(deep_link, self.admin, ["Edit", "Add Note", "Start Flow"])

response = self.assertListFetch(deep_link, [self.user, self.editor, self.admin, self.agent], context_objects=[])
self.assertEqual("all", response.context["folder"])
self.assertEqual("open", response.context["status"])

# our ticket exists on the first page, so it'll get flagged to be focused
self.assertEqual(str(ticket.uuid), response.context["nextUUID"])

# deep link into a page that doesn't have our ticket
deep_link = f"{list_url}all/closed/{str(ticket.uuid)}/"
# we have a specific ticket so we should show context menu for it
self.assertContentMenu(deep_link, self.admin, ["Edit", "Add Note", "Start Flow"])

self.login(self.admin)
# try to link to our ticket but with mismatched status
deep_link = f"{list_url}all/closed/{str(ticket.uuid)}/"

response = self.client.get(deep_link)
response = self.assertListFetch(deep_link, [self.agent], context_objects=[])

# now our ticket is listed as the uuid and we were redirected to all/open
# now our ticket is listed as the uuid and we were redirected to All folder with Open status
self.assertEqual("all", response.context["folder"])
self.assertEqual("open", response.context["status"])
self.assertEqual(str(ticket.uuid), response.context["uuid"])

# bad topic should give a 404
# and again we have a specific ticket so we should show context menu for it
self.assertContentMenu(deep_link, self.admin, ["Edit", "Add Note", "Start Flow"])

# non-existent topic should give a 404
bad_topic_link = f"{list_url}{uuid4()}/open/{str(ticket.uuid)}/"
response = self.client.get(bad_topic_link)
response = self.requestView(bad_topic_link, self.agent)
self.assertEqual(404, response.status_code)

response = self.client.get(
Expand All @@ -637,7 +638,7 @@ def test_list(self):

# closed our tickets don't get extra menu options
ticket.status = Ticket.STATUS_CLOSED
ticket.save()
ticket.save(update_fields=("status",))
deep_link = f"{list_url}all/closed/{str(ticket.uuid)}/"
self.assertContentMenu(deep_link, self.admin, [])

Expand Down
148 changes: 75 additions & 73 deletions temba/tickets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def derive_menu(self):

class List(SpaMixin, ContextMenuMixin, OrgPermsMixin, NotificationTargetMixin, SmartListView):
"""
A placeholder view for the ticket handling frontend components which fetch tickets from the endpoint below
Placeholder view for the ticketing frontend components which fetch tickets from the folders view below.
"""

@classmethod
Expand All @@ -226,105 +226,107 @@ def derive_url_pattern(cls, path, action):
return rf"^ticket/((?P<folder>{folders}|{UUID_REGEX.pattern})/((?P<status>open|closed)/((?P<uuid>[a-z0-9\-]+)/)?)?)?$"

def get_notification_scope(self) -> tuple:
folder, status, _, _ = self.tickets_path
if folder == UnassignedFolder.slug and status == "open":
folder, status, ticket, in_page = self.tickets_path

if folder.slug == UnassignedFolder.slug and status == Ticket.STATUS_OPEN:
return "tickets:opened", ""
elif folder == MineFolder.slug and status == "open":
elif folder.slug == MineFolder.slug and status == Ticket.STATUS_OPEN:
return "tickets:activity", ""
return "", ""

def derive_menu_path(self):
return f"/ticket/{self.kwargs.get('folder', 'mine')}/"
folder, status, ticket, in_page = self.tickets_path

return f"/ticket/{folder.slug}/"

@cached_property
def tickets_path(self) -> tuple:
def tickets_path(self) -> tuple[TicketFolder, str, Ticket, bool]:
"""
Returns tuple of folder, status, ticket uuid, and whether that ticket exists in first page of tickets
Returns tuple of folder, status, ticket, and whether that ticket exists in first page of tickets
"""
folder = self.kwargs.get("folder")
status = self.kwargs.get("status")
uuid = self.kwargs.get("uuid")

# get requested folder, defaulting to Mine
folder = TicketFolder.from_slug(self.request.org, self.kwargs.get("folder", MineFolder.slug))
if not folder:
raise Http404()

status = Ticket.STATUS_OPEN if self.kwargs.get("status", "open") == "open" else Ticket.STATUS_CLOSED
ticket = None
in_page = False

# if we have a uuid make sure it is in our first page of tickets
if uuid:
status_code = Ticket.STATUS_OPEN if status == "open" else Ticket.STATUS_CLOSED
# is the request for a specific ticket?
if uuid := self.kwargs.get("uuid"):
org = self.request.org
user = self.request.user
ticket_folder = TicketFolder.from_slug(org, folder)

if not ticket_folder:
raise Http404()
# is the ticket in the first page from of current folder?
for t in list(folder.get_queryset(org, user, ordered=True).filter(status=status)[:25]):
if str(t.uuid) == uuid:
ticket = t
in_page = True
break

tickets = list(ticket_folder.get_queryset(org, user, True).filter(status=status_code)[:25])
# if not, see if we can access it in the All tickets folder and if so switch to that
if not in_page:
all_folder = TicketFolder.from_slug(self.request.org, AllFolder.slug)
ticket = all_folder.get_queryset(org, user, ordered=False).filter(uuid=uuid).first()

found = list(filter(lambda t: str(t.uuid) == uuid, tickets))
if found:
in_page = True
else:
# if it's not, switch our folder to everything with that ticket's state
ticket = org.tickets.filter(uuid=uuid).first()
if ticket:
folder = AllFolder.slug
status = "open" if ticket.status == Ticket.STATUS_OPEN else "closed"
folder = all_folder
status = ticket.status

return folder or MineFolder.slug, status or "open", uuid, in_page
return folder, status, ticket, in_page

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

folder, status, uuid, in_page = self.tickets_path
context["folder"] = folder
context["status"] = status
context["has_tickets"] = self.request.org.tickets.exists()
folder, status, ticket, in_page = self.tickets_path

folder = TicketFolder.from_slug(self.request.org, folder)
context["title"] = folder.name
context["folder"] = folder.slug
context["status"] = "open" if status == Ticket.STATUS_OPEN else "closed"
context["has_tickets"] = self.request.org.tickets.exists()

if uuid:
context["nextUUID" if in_page else "uuid"] = uuid
if ticket:
context["nextUUID" if in_page else "uuid"] = str(ticket.uuid)

return context

def build_context_menu(self, menu):
uuid = self.kwargs.get("uuid")
if uuid:
ticket = self.request.org.tickets.filter(uuid=uuid).first()
if ticket:
if ticket.status == Ticket.STATUS_OPEN:
if self.has_org_perm("tickets.ticket_update"):
menu.add_modax(
_("Edit"),
"edit-ticket",
f"{reverse('tickets.ticket_update', args=[ticket.uuid])}",
title=_("Edit Ticket"),
on_submit="handleTicketEditComplete()",
)

if self.has_org_perm("tickets.ticket_note"):
menu.add_modax(
_("Add Note"),
"add-note",
f"{reverse('tickets.ticket_note', args=[ticket.uuid])}",
on_submit="handleNoteAdded()",
)

# we don't want to show start flow if interrupt was given as an option
interrupt_added = False
if self.has_org_perm("contacts.contact_interrupt") and ticket.contact.current_flow:
menu.add_url_post(
_("Interrupt"), reverse("contacts.contact_interrupt", args=(ticket.contact.id,))
)
interrupt_added = True

if not interrupt_added and self.has_org_perm("flows.flow_start"):
menu.add_modax(
_("Start Flow"),
"start-flow",
f"{reverse('flows.flow_start')}?c={ticket.contact.uuid}",
disabled=True,
on_submit="handleFlowStarted()",
)
folder, status, ticket, in_page = self.tickets_path

if ticket and ticket.status == Ticket.STATUS_OPEN:
if self.has_org_perm("tickets.ticket_update"):
menu.add_modax(
_("Edit"),
"edit-ticket",
f"{reverse('tickets.ticket_update', args=[ticket.uuid])}",
title=_("Edit Ticket"),
on_submit="handleTicketEditComplete()",
)

if self.has_org_perm("tickets.ticket_note"):
menu.add_modax(
_("Add Note"),
"add-note",
f"{reverse('tickets.ticket_note', args=[ticket.uuid])}",
on_submit="handleNoteAdded()",
)

# we don't want to show start flow if interrupt was given as an option
interrupt_added = False
if self.has_org_perm("contacts.contact_interrupt") and ticket.contact.current_flow:
menu.add_url_post(_("Interrupt"), reverse("contacts.contact_interrupt", args=(ticket.contact.id,)))
interrupt_added = True

if not interrupt_added and self.has_org_perm("flows.flow_start"):
menu.add_modax(
_("Start Flow"),
"start-flow",
f"{reverse('flows.flow_start')}?c={ticket.contact.uuid}",
disabled=True,
on_submit="handleFlowStarted()",
)

def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).none()
Expand Down Expand Up @@ -374,7 +376,7 @@ def get_queryset(self, **kwargs):

# fetching new activity gets a different order later
ordered = False if after else True
qs = self.folder.get_queryset(org, user, ordered).filter(status=status)
qs = self.folder.get_queryset(org, user, ordered=ordered).filter(status=status)

# all new activity
after = int(self.request.GET.get("after", 0))
Expand All @@ -395,7 +397,7 @@ def get_queryset(self, **kwargs):

if count == self.paginate_by:
last_ticket = qs[len(qs) - 1]
qs = self.folder.get_queryset(org, user, ordered).filter(
qs = self.folder.get_queryset(org, user, ordered=ordered).filter(
status=status, last_activity_on__gte=last_ticket.last_activity_on
)

Expand Down

0 comments on commit 152766e

Please sign in to comment.