- Implement tests for ticket approval panel to verify approval request logic and visibility based on user roles. - Add functionality to handle ticket movement based on server response for both tickets and leads. - Introduce new tests for duplicate checking API to ensure correct handling of duplicates and merging logic. - Remove outdated permissions tests that are no longer relevant. - Update ticket board and lead board providers to include 'can_move' field for better control over ticket actions. - Refactor existing tests to improve coverage and ensure accurate representation of API responses.
130 lines
5 KiB
Python
130 lines
5 KiB
Python
"""Watch / unwatch / list-watchers REST endpoints.
|
|
|
|
URL surface (mounted under /api/cases/):
|
|
POST /<id>/watch/: subscribe the requesting profile (idempotent)
|
|
DELETE /<id>/watch/: unsubscribe
|
|
GET /<id>/watchers/: list everyone watching the case, plus
|
|
`is_current_user_watching` for the caller
|
|
GET /watching/: cases the current profile watches
|
|
|
|
Authorisation: a user who can see the case can also watch it, using the one
|
|
rule in `cases.access.visible_cases_qs`, admins see everything; non-admins
|
|
see cases they created OR are assigned to OR already watch (so a watcher who
|
|
was un-assigned can still unsubscribe). This module used to keep its own copy
|
|
of that query, which is how the case detail view came to disagree with it.
|
|
"""
|
|
|
|
from django.db import IntegrityError
|
|
from django.shortcuts import get_object_or_404
|
|
from rest_framework import status
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from cases.access import visible_cases_qs as _visible_cases_qs
|
|
from cases.models import Case, CaseWatcher
|
|
from common.permissions import HasOrgContext
|
|
|
|
|
|
class WatchView(APIView):
|
|
"""POST/DELETE /api/cases/<pk>/watch/"""
|
|
|
|
permission_classes = (IsAuthenticated, HasOrgContext)
|
|
|
|
def post(self, request, pk, *args, **kwargs):
|
|
case = get_object_or_404(_visible_cases_qs(request.profile), pk=pk)
|
|
try:
|
|
obj, created = CaseWatcher.objects.get_or_create(
|
|
case=case,
|
|
profile=request.profile,
|
|
defaults={
|
|
"org": case.org,
|
|
"subscribed_via": "manual",
|
|
},
|
|
)
|
|
except IntegrityError:
|
|
obj = CaseWatcher.objects.get(case=case, profile=request.profile)
|
|
created = False
|
|
return Response(
|
|
{"watching": True, "subscribed_via": obj.subscribed_via},
|
|
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
|
|
)
|
|
|
|
def delete(self, request, pk, *args, **kwargs):
|
|
case = get_object_or_404(_visible_cases_qs(request.profile), pk=pk)
|
|
CaseWatcher.objects.filter(case=case, profile=request.profile).delete()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class WatchersListView(APIView):
|
|
"""GET /api/cases/<pk>/watchers/"""
|
|
|
|
permission_classes = (IsAuthenticated, HasOrgContext)
|
|
|
|
def get(self, request, pk, *args, **kwargs):
|
|
case = get_object_or_404(_visible_cases_qs(request.profile), pk=pk)
|
|
rows = (
|
|
CaseWatcher.objects.filter(case=case)
|
|
.select_related("profile__user")
|
|
.order_by("created_at")
|
|
)
|
|
watchers = [
|
|
{
|
|
"id": str(row.id),
|
|
"profile_id": str(row.profile_id),
|
|
"user_id": str(row.profile.user_id) if row.profile.user_id else None,
|
|
"name": (row.profile.user.email if row.profile.user else "") or "",
|
|
"email": row.profile.user.email if row.profile.user else "",
|
|
"subscribed_via": row.subscribed_via,
|
|
"created_at": row.created_at,
|
|
}
|
|
for row in rows
|
|
]
|
|
# The caller's own watch state, server-derived: both clients show
|
|
# Watch or Unwatch from this rather than matching ids themselves.
|
|
is_watching = any(row.profile_id == request.profile.id for row in rows)
|
|
return Response(
|
|
{
|
|
"watchers": watchers,
|
|
"count": len(watchers),
|
|
"is_current_user_watching": is_watching,
|
|
}
|
|
)
|
|
|
|
|
|
class WatchingListView(APIView):
|
|
"""GET /api/cases/watching/
|
|
|
|
Returns the list of cases the current profile watches. Accepts the same
|
|
query-param filters as `CaseListView` (status, priority, account,
|
|
case_type, assigned_to[], tags[], search, created_at__gte/lte,
|
|
sla_breached, ordering) so the mobile filter chips work identically in
|
|
watching mode.
|
|
"""
|
|
|
|
permission_classes = (IsAuthenticated, HasOrgContext)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
from cases.serializer import CaseSerializer, parent_access_context
|
|
from cases.views import apply_case_list_filters
|
|
|
|
# The same related rows `/cases/` loads up front, so a row's account,
|
|
# people, teams and tags are not each a query of their own.
|
|
cases = (
|
|
Case.objects.filter(org=request.profile.org, watchers=request.profile)
|
|
.select_related("account", "org", "created_by", "parent")
|
|
.prefetch_related("assigned_to__user", "contacts", "teams", "tags")
|
|
.order_by("-created_at")
|
|
.distinct()
|
|
)
|
|
cases = apply_case_list_filters(cases, request.query_params)
|
|
return Response(
|
|
{
|
|
"cases": CaseSerializer(
|
|
cases,
|
|
many=True,
|
|
context=parent_access_context(request.profile, cases),
|
|
).data,
|
|
"count": cases.count(),
|
|
}
|
|
)
|