-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/history rooms endpoint (#294)
* feat: add history app module for history related code * feat: remove unused files * feat: add module to settings * feat: add History Room endpoint to be used instead of the contacts endpoint
- Loading branch information
1 parent
5ed60a5
commit e3d4c1a
Showing
17 changed files
with
372 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class EventDrivenConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "chats.apps.history" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
from django.core.exceptions import ObjectDoesNotExist | ||
from django.db.models import Q | ||
from django.utils.translation import gettext_lazy as _ | ||
from django_filters import rest_framework as filters | ||
from rest_framework import exceptions | ||
|
||
from chats.apps.rooms.models import Room | ||
|
||
|
||
class HistoryRoomFilter(filters.FilterSet): | ||
class Meta: | ||
model = Room | ||
fields = [] | ||
|
||
contact = filters.CharFilter( | ||
field_name="contact", | ||
required=False, | ||
method="filter_contact", | ||
help_text=_("Contact's External ID"), | ||
) | ||
|
||
project = filters.CharFilter( | ||
field_name="project", | ||
required=True, | ||
method="filter_project", | ||
help_text=_("Projects's UUID"), | ||
) | ||
|
||
sector = filters.CharFilter( | ||
field_name="sector", | ||
required=False, | ||
method="filter_sector", | ||
help_text=_("Sector's UUID"), | ||
) | ||
|
||
tag = filters.CharFilter( | ||
required=False, | ||
method="filter_tags", | ||
help_text=_("Room Tags"), | ||
) | ||
created_on = filters.DateFromToRangeFilter( | ||
field_name="created_on", | ||
required=False, | ||
help_text=_("Room created on"), | ||
) | ||
ended_at = filters.DateFromToRangeFilter( | ||
field_name="ended_at", | ||
required=False, | ||
help_text=_("Room ended at"), | ||
) | ||
|
||
def filter_project(self, queryset, name, value): | ||
qs = queryset | ||
user = self.request.user | ||
try: | ||
user_permission = user.project_permissions.get(project=value) | ||
except ObjectDoesNotExist: | ||
raise exceptions.APIException( | ||
detail="Access denied! Make sure you have the right permission to access this project" | ||
) | ||
|
||
queue_ids = user_permission.queue_ids | ||
|
||
contacts_blocklist = user_permission.project.history_contacts_blocklist | ||
if contacts_blocklist: | ||
qs = qs.exclude(contact__external_id__in=contacts_blocklist) | ||
|
||
return qs.filter( | ||
Q(queue__in=queue_ids) | Q(user=user, queue__sector__project=value), | ||
is_active=False, | ||
ended_at__isnull=False, | ||
) | ||
|
||
def filter_sector(self, queryset, name, value): | ||
return queryset.filter(queue__sector__uuid=value) | ||
|
||
def filter_tags(self, queryset, name, value): | ||
values = value.split(",") | ||
return queryset.filter(tags__name__in=values) | ||
|
||
def filter_contact(self, queryset, name, value): | ||
return queryset.filter(contact__external_id=value) |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from rest_framework import serializers | ||
|
||
from chats.apps.api.v1.accounts.serializers import UserNameSerializer | ||
from chats.apps.api.v1.contacts.serializers import ContactSimpleSerializer | ||
from chats.apps.api.v1.sectors.serializers import TagSimpleSerializer | ||
from chats.apps.rooms.models import Room | ||
|
||
|
||
class RoomHistorySerializer(serializers.ModelSerializer): | ||
user = UserNameSerializer(many=False, read_only=True) | ||
contact = ContactSimpleSerializer(many=False, read_only=True) | ||
tags = TagSimpleSerializer(many=True, read_only=True) | ||
|
||
class Meta: | ||
model = Room | ||
fields = [ | ||
"uuid", | ||
"created_on", | ||
"ended_at", | ||
"user", | ||
"contact", | ||
"tags", | ||
] | ||
|
||
|
||
class RoomBasicSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = Room | ||
fields = [ | ||
"uuid", | ||
"ended_at", | ||
] | ||
|
||
|
||
class RoomDetailSerializer(serializers.ModelSerializer): | ||
user = UserNameSerializer(many=False, read_only=True) | ||
contact = ContactSimpleSerializer(many=False, read_only=True) | ||
tags = TagSimpleSerializer(many=True, read_only=True) | ||
|
||
class Meta: | ||
model = Room | ||
fields = [ | ||
"uuid", | ||
"custom_fields", | ||
"urn", | ||
"created_on", | ||
"ended_at", | ||
"user", | ||
"contact", | ||
"tags", | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
from django.urls import reverse | ||
from django.utils import timezone | ||
from rest_framework import status | ||
|
||
from chats.core.tests.test_base import BaseAPIChatsTestCase | ||
|
||
|
||
class TestHistoryRoomViewsets(BaseAPIChatsTestCase): | ||
def _list_request(self, token, data): | ||
url = reverse("history_room-list") | ||
client = self.client | ||
client.credentials(HTTP_AUTHORIZATION="Token " + token.key) | ||
response = client.get(url, format="json", data=data) | ||
results = response.json().get("results") | ||
return response, results | ||
|
||
def test_admin_list_within_its_project(self): | ||
payload = {"project": str(self.project.uuid)} | ||
self.deactivate_rooms() | ||
response = self._list_request(token=self.admin_token, data=payload)[0] | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(response.json().get("count"), self.count_project_1_contact) | ||
|
||
def test_manager_list_within_its_sectors(self): | ||
payload = {"project": str(self.project.uuid)} | ||
self.deactivate_rooms() | ||
response = self._list_request(token=self.manager_token, data=payload)[0] | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(response.json().get("count"), 3) | ||
|
||
def test_agent_list_within_its_rooms(self): | ||
payload = {"project": str(self.project.uuid)} | ||
self.deactivate_rooms() | ||
response, _ = self._list_request(token=self.agent_token, data=payload) | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(response.json().get("count"), 2) | ||
|
||
def test_basic_list(self): | ||
payload = { | ||
"project": str(self.project.uuid), | ||
"basic": True, | ||
"contact": self.contact.external_id, | ||
} | ||
self.deactivate_rooms() | ||
response = self._list_request(token=self.admin_token, data=payload)[0] | ||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(response.json().get("count"), 1) | ||
self.assertEqual(len(response.json().get("results")[0]), 2) | ||
|
||
def test_admin_list_with_blocked_contacts(self): | ||
self.project.add_contact_to_history_blocklist(self.contact_2.external_id) | ||
|
||
payload = {"project": str(self.project.uuid)} | ||
self.deactivate_rooms() | ||
response = self._list_request(token=self.admin_token, data=payload)[0] | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertEqual(response.json().get("count"), self.count_project_1_contact - 1) | ||
|
||
def test_retrieve_room_ok(self): | ||
""" | ||
Ensure we can retrieve a contact | ||
""" | ||
client = self.client | ||
client.credentials(HTTP_AUTHORIZATION="Token " + self.admin_token.key) | ||
self.contact.rooms.update(is_active=False, ended_at=timezone.now()) | ||
url = ( | ||
reverse("history_room-detail", kwargs={"pk": str(self.room_1.pk)}) | ||
+ f"?project={str(self.project.uuid)}" | ||
) | ||
response = client.get( | ||
url, | ||
format="json", | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
|
||
def test_retrieve_contact_no_closed_rooms(self): | ||
""" | ||
Ensure we can retrieve a contact | ||
""" | ||
client = self.client | ||
client.credentials(HTTP_AUTHORIZATION="Token " + self.admin_token.key) | ||
url = ( | ||
reverse("history_room-detail", kwargs={"pk": str(self.room_1.pk)}) | ||
+ f"?project={str(self.project.uuid)}" | ||
) | ||
response = self.client.get( | ||
url, | ||
format="json", | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) | ||
|
||
def test_retrieve_contact_unauthorized(self): | ||
""" | ||
Ensure we can retrieve a contact | ||
""" | ||
client = self.client | ||
client.credentials(HTTP_AUTHORIZATION="Token " + self.agent_token.key) | ||
self.contact_2.rooms.update(is_active=False, ended_at=timezone.now()) | ||
url = ( | ||
reverse("history_room-detail", kwargs={"pk": str(self.room_2.pk)}) | ||
+ f"?project={str(self.project.uuid)}" | ||
) | ||
response = client.get(url, format="json") | ||
|
||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) | ||
|
||
def test_handling_not_allowed_http_methods(self): | ||
url = reverse("history_room-detail", kwargs={"pk": 1}) | ||
client = self.client | ||
client.credentials(HTTP_AUTHORIZATION="Token " + self.admin_token.key) | ||
post = client.post(url, format="json") | ||
put = client.put(url, format="json") | ||
delete = client.delete(url, format="json") | ||
actions = [post, put, delete] | ||
|
||
for action in actions: | ||
self.assertEqual(action.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .history_rooms import HistoryRoomViewset # NoQA |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from django_filters.rest_framework import DjangoFilterBackend | ||
from rest_framework.filters import OrderingFilter, SearchFilter | ||
from rest_framework.permissions import IsAuthenticated | ||
from rest_framework.viewsets import ReadOnlyModelViewSet | ||
|
||
from chats.apps.rooms.models import Room | ||
|
||
from ..filters.rooms_filter import HistoryRoomFilter | ||
from ..serializers.rooms import ( | ||
RoomBasicSerializer, | ||
RoomDetailSerializer, | ||
RoomHistorySerializer, | ||
) | ||
from .permissions import CanRetrieveRoomHistory | ||
|
||
|
||
class HistoryRoomViewset(ReadOnlyModelViewSet): | ||
queryset = Room.objects.all() | ||
serializer_class = RoomHistorySerializer | ||
filter_backends = [ | ||
DjangoFilterBackend, | ||
SearchFilter, | ||
OrderingFilter, | ||
] | ||
filterset_class = HistoryRoomFilter | ||
permission_classes = [IsAuthenticated] | ||
search_fields = [ | ||
"contact__name", | ||
"urn", | ||
"user__first_name", | ||
"user__last_name", | ||
"user__email", | ||
] | ||
ordering = ["-ended_at"] | ||
|
||
def get_permissions(self): | ||
permission_classes = self.permission_classes | ||
|
||
if self.action == "retrieve": | ||
permission_classes = (IsAuthenticated, CanRetrieveRoomHistory) | ||
return [permission() for permission in permission_classes] | ||
|
||
def get_serializer_class(self): | ||
if self.request.GET.get("basic", None): | ||
return RoomBasicSerializer | ||
if self.action == "retrieve": | ||
return RoomDetailSerializer | ||
return super().get_serializer_class() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
from django.contrib.auth.models import AnonymousUser | ||
from rest_framework import permissions | ||
|
||
from chats.apps.projects.models import ProjectPermission | ||
from chats.core.permissions import GetPermission | ||
|
||
|
||
class CanRetrieveRoomHistory(permissions.BasePermission): | ||
def has_permission(self, request, view): | ||
if view.action in ["list", "create"]: | ||
permission = GetPermission(request).permission | ||
return permission.is_admin | ||
|
||
return super().has_permission(request, view) | ||
|
||
def has_object_permission(self, request, view, obj) -> bool: | ||
if isinstance(request.user, AnonymousUser): | ||
return False | ||
try: | ||
perm = obj.get_permission(request.user) | ||
except ProjectPermission.DoesNotExist: | ||
return False | ||
return perm.is_admin |
Oops, something went wrong.