From 64d7f03d3de12a6cd5e2158cc81010ec86a0bcef Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:40:58 -0300 Subject: [PATCH 1/9] Feature/agent signature (#259) * feat: add new sign_messages flag to the sector endpoint * feat: makemigrations * feat: fix sing_messages name on update_msg_text_with_signature * feat: add sign_messages field to other sector serializers :) * feat: use only first name when signing messages * feat: skip two lines when signing --- .../apps/api/v1/external/rooms/serializers.py | 4 + chats/apps/api/v1/sectors/serializers.py | 2 + .../apps/dashboard/tests/test_serializers.py | 10 +- chats/apps/msgs/models.py | 11 ++ .../rooms/tests/test_viewsets_external.py | 41 +++++++ .../migrations/0006_sector_open_offline.py | 20 +++ .../migrations/0007_sector_sign_messages.py | 18 +++ chats/apps/sectors/models.py | 28 ++++- chats/fixtures/fixture_sector.json | 116 ++++++++++++++++++ 9 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 chats/apps/sectors/migrations/0006_sector_open_offline.py create mode 100644 chats/apps/sectors/migrations/0007_sector_sign_messages.py diff --git a/chats/apps/api/v1/external/rooms/serializers.py b/chats/apps/api/v1/external/rooms/serializers.py index 9ef862c3..ae9fb550 100644 --- a/chats/apps/api/v1/external/rooms/serializers.py +++ b/chats/apps/api/v1/external/rooms/serializers.py @@ -188,6 +188,10 @@ def check_work_time(self, sector, created_on): raise ValidationError( {"detail": _("Contact cannot be done outside working hours")} ) + elif sector.validate_agent_status() is False: + raise ValidationError( + {"detail": _("Contact cannot be done when agents are offline")} + ) def handle_urn(self, validated_data): is_anon = validated_data.pop("is_anon", False) diff --git a/chats/apps/api/v1/sectors/serializers.py b/chats/apps/api/v1/sectors/serializers.py index 5a9136f8..bfd0ccc0 100644 --- a/chats/apps/api/v1/sectors/serializers.py +++ b/chats/apps/api/v1/sectors/serializers.py @@ -40,6 +40,7 @@ class Meta: "work_end", "is_deleted", "can_trigger_flows", + "sign_messages", ] extra_kwargs = {field: {"required": False} for field in fields} @@ -76,6 +77,7 @@ class Meta: "work_end", "rooms_limit", "can_trigger_flows", + "sign_messages", ] diff --git a/chats/apps/dashboard/tests/test_serializers.py b/chats/apps/dashboard/tests/test_serializers.py index 7acabe12..0b366462 100644 --- a/chats/apps/dashboard/tests/test_serializers.py +++ b/chats/apps/dashboard/tests/test_serializers.py @@ -39,8 +39,8 @@ def test_field_value_from_dashboard_agent_serializer(self): context={"is_weni_admin": True}, ) - self.assertEqual(instance[0]["first_name"], "") - self.assertEqual(instance[0]["email"], "amywong@chats.weni.ai") - self.assertEqual(instance[0]["agent_status"], "OFFLINE") - self.assertEqual(instance[0]["closed_rooms"], 0) - self.assertEqual(instance[0]["opened_rooms"], 1) + self.assertEqual(instance[2]["first_name"], "") + self.assertEqual(instance[2]["email"], "amywong@chats.weni.ai") + self.assertEqual(instance[2]["agent_status"], "OFFLINE") + self.assertEqual(instance[2]["closed_rooms"], 0) + self.assertEqual(instance[2]["opened_rooms"], 1) diff --git a/chats/apps/msgs/models.py b/chats/apps/msgs/models.py index 91385b1c..0e9864a5 100644 --- a/chats/apps/msgs/models.py +++ b/chats/apps/msgs/models.py @@ -62,6 +62,10 @@ def serialized_ws_data(self) -> dict: return dict(MessageWSSerializer(self).data) + @property + def signed_text(self): + return f"{self.user.first_name}:\n\n{self.text}" + def get_authorization(self, user): return self.room.get_authorization(user) @@ -71,11 +75,18 @@ def media(self): def get_sender(self): return self.user or self.contact + def update_msg_text_with_signature(self, msg_data: dict): + if self.user and self.room.queue.sector.sign_messages and self.text: + msg_data["text"] = self.signed_text + return msg_data + def notify_room(self, action: str, callback: bool = False): """ """ data = self.serialized_ws_data self.room.base_notification(content=data, action=f"msg.{action}") if self.room.callback_url and callback: + data = self.update_msg_text_with_signature(data) + requests.post( self.room.callback_url, data=json.dumps( diff --git a/chats/apps/rooms/tests/test_viewsets_external.py b/chats/apps/rooms/tests/test_viewsets_external.py index 524cc589..4b736136 100644 --- a/chats/apps/rooms/tests/test_viewsets_external.py +++ b/chats/apps/rooms/tests/test_viewsets_external.py @@ -372,3 +372,44 @@ def test_add_agent_to_nonexistent_room(self): ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + +class RoomsRoutingExternalTests(APITestCase): + fixtures = [ + "chats/fixtures/fixture_sector.json", + ] + + def _create_room(self, token: str, data: dict): + url = reverse("external_rooms-list") + client = self.client + client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") + + return client.post(url, data=data, format="json") + + def test_create_external_room_can_open_offline(self): + data = { + "queue_uuid": "8590ad29-5629-448c-bfb6-1bfd5219b8ec", + "contact": { + "external_id": "953fdcc9-1f6f-4abd-b90e-10a35c1cc825", + "name": "Foo Bar", + "email": "FooBar@weni.ai", + "phone": "+250788123123", + "custom_fields": {}, + }, + } + response = self._create_room("b5fab78a-4836-468c-96c4-f5b0bba3303a", data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_external_room_cannot_open_offline(self): + data = { + "queue_uuid": "605e21b0-4177-4eae-9cfb-529d9972a192", + "contact": { + "external_id": "a5ff0cd3-0bcd-4e91-8120-8718128cb1d9", + "name": "Foo Bar", + "email": "FooBar@weni.ai", + "phone": "+250788123123", + "custom_fields": {}, + }, + } + response = self._create_room("b5fab78a-4836-468c-96c4-f5b0bba3303a", data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/chats/apps/sectors/migrations/0006_sector_open_offline.py b/chats/apps/sectors/migrations/0006_sector_open_offline.py new file mode 100644 index 00000000..0d253fdf --- /dev/null +++ b/chats/apps/sectors/migrations/0006_sector_open_offline.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.2 on 2023-06-27 13:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sectors", "0005_remove_sector_rooms_limit_greater_than_zero"), + ] + + operations = [ + migrations.AddField( + model_name="sector", + name="open_offline", + field=models.BooleanField( + default=True, verbose_name="Open room when all agents are offline?" + ), + ), + ] diff --git a/chats/apps/sectors/migrations/0007_sector_sign_messages.py b/chats/apps/sectors/migrations/0007_sector_sign_messages.py new file mode 100644 index 00000000..d18930d0 --- /dev/null +++ b/chats/apps/sectors/migrations/0007_sector_sign_messages.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.2 on 2023-07-19 17:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sectors", "0006_sector_open_offline"), + ] + + operations = [ + migrations.AddField( + model_name="sector", + name="sign_messages", + field=models.BooleanField(default=False, verbose_name="Sign messages?"), + ), + ] diff --git a/chats/apps/sectors/models.py b/chats/apps/sectors/models.py index 20e3daec..77de3f52 100644 --- a/chats/apps/sectors/models.py +++ b/chats/apps/sectors/models.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from django.db import models -from django.db.models import F, Q, Value +from django.db.models import Count, F, Q, Value from django.db.models.functions import Concat from django.utils.translation import gettext_lazy as _ @@ -30,7 +30,11 @@ class Sector(BaseSoftDeleteModel, BaseModel): ), default=False, ) + sign_messages = models.BooleanField(_("Sign messages?"), default=False) is_deleted = models.BooleanField(_("is deleted?"), default=False) + open_offline = models.BooleanField( + _("Open room when all agents are offline?"), default=True + ) class Meta: verbose_name = _("Sector") @@ -123,6 +127,28 @@ def contact_count(self): # ) return 0 + def validate_agent_status(self, queue=None): + if self.open_offline: + return True + is_online = False + if queue: + is_online = queue.authorizations.filter( + permission__status="ONLINE" + ).exists() + else: + is_online = ( + self.queues.annotate( + online_count=Count( + "authorizations", + filter=Q(authorizations__permission__status="ONLINE"), + ) + ) + .filter(online_count__gt=0) + .exists() + ) + + return is_online + def is_attending(self, created_on): tz = pendulum.timezone(str(self.project.timezone)) created_on = pendulum.parse(str(created_on)).in_timezone(tz) diff --git a/chats/fixtures/fixture_sector.json b/chats/fixtures/fixture_sector.json index 85dfe081..c33998f0 100644 --- a/chats/fixtures/fixture_sector.json +++ b/chats/fixtures/fixture_sector.json @@ -10,6 +10,47 @@ "date_format": "D" } }, + { + "model": "projects.project", + "pk": "e7e5163d-4ee2-42da-a804-c252fda989b0", + "fields": { + "created_on": "2022-08-23T20:08:48.462Z", + "modified_on": "2022-08-23T20:08:48.462Z", + "name": "Weni routing", + "timezone": "America/Sao_Paulo", + "date_format": "D" + } + }, + { + "model": "sectors.sector", + "pk": "ae4b17b4-6ff1-44f4-b77b-182b65f0098a", + "fields": { + "created_on": "2022-08-24T20:59:49.115Z", + "modified_on": "2022-08-24T20:59:49.115Z", + "name": "Don't open offline", + "project": "e7e5163d-4ee2-42da-a804-c252fda989b0", + "rooms_limit": 5, + "open_offline": false, + "work_start": "08:00:00", + "work_end": "17:00:00", + "is_deleted": false + } + }, + { + "model": "sectors.sector", + "pk": "ab6e9380-a384-4f9f-ac21-c1131029c59b", + "fields": { + "created_on": "2022-08-24T20:59:49.115Z", + "modified_on": "2022-08-24T20:59:49.115Z", + "name": "Open offline", + "project": "e7e5163d-4ee2-42da-a804-c252fda989b0", + "rooms_limit": 5, + "open_offline": true, + "work_start": "08:00:00", + "work_end": "17:00:00", + "is_deleted": false + } + }, { "model": "sectors.sector", "pk": "21aecf8c-0c73-4059-ba82-4343e0cc627c", @@ -111,6 +152,17 @@ "user_permissions": [] } }, + { + "model": "projects.projectpermission", + "pk": "b5fab78a-4836-468c-96c4-f5b0bba3303a", + "fields": { + "created_on": "2022-08-24T20:28:05.281Z", + "modified_on": "2022-08-24T20:28:05.281Z", + "project": "e7e5163d-4ee2-42da-a804-c252fda989b0", + "user": "amywong@chats.weni.ai", + "role": 1 + } + }, { "model": "projects.projectpermission", "pk": "ce3f052c-e71d-402c-b02e-1dfaca8b3d45", @@ -144,6 +196,28 @@ "role": 1 } }, + { + "model": "sectors.sectorauthorization", + "pk": "c5378e06-ee3b-4cd4-a9c7-62410f6a45d4", + "fields": { + "created_on": "2022-08-24T21:13:48.359Z", + "modified_on": "2022-08-24T21:13:48.359Z", + "permission": "b5fab78a-4836-468c-96c4-f5b0bba3303a", + "sector": "ae4b17b4-6ff1-44f4-b77b-182b65f0098a", + "role": 1 + } + }, + { + "model": "sectors.sectorauthorization", + "pk": "2a2571be-294e-4bd7-ba3a-8db6b182de82", + "fields": { + "created_on": "2022-08-24T21:13:48.359Z", + "modified_on": "2022-08-24T21:13:48.359Z", + "permission": "b5fab78a-4836-468c-96c4-f5b0bba3303a", + "sector": "ab6e9380-a384-4f9f-ac21-c1131029c59b", + "role": 1 + } + }, { "model": "sectors.sectorauthorization", "pk": "00185928-ef97-47b8-9295-5462ff797a4e", @@ -166,6 +240,26 @@ "role": 1 } }, + { + "model": "queues.queue", + "pk": "605e21b0-4177-4eae-9cfb-529d9972a192", + "fields": { + "created_on": "2022-08-24T21:40:15.274Z", + "modified_on": "2022-08-24T21:40:15.274Z", + "sector": "ae4b17b4-6ff1-44f4-b77b-182b65f0098a", + "name": "DON'T OPEN OFFLINE" + } + }, + { + "model": "queues.queue", + "pk": "8590ad29-5629-448c-bfb6-1bfd5219b8ec", + "fields": { + "created_on": "2022-08-24T21:40:15.274Z", + "modified_on": "2022-08-24T21:40:15.274Z", + "sector": "ab6e9380-a384-4f9f-ac21-c1131029c59b", + "name": "OPEN OFFLINE" + } + }, { "model": "queues.queue", "pk": "f2519480-7e58-4fc4-9894-9ab1769e29cf", @@ -186,6 +280,28 @@ "name": "FRONTEND" } }, + { + "model": "queues.queueauthorization", + "pk": "355297c1-448f-4357-b4b1-97df80c240b4", + "fields": { + "created_on": "2022-08-25T04:55:18.502Z", + "modified_on": "2022-08-25T04:55:18.502Z", + "queue": "605e21b0-4177-4eae-9cfb-529d9972a192", + "role": 1, + "permission": "b5fab78a-4836-468c-96c4-f5b0bba3303a" + } + }, + { + "model": "queues.queueauthorization", + "pk": "45c6658e-966d-4222-b865-56710922335a", + "fields": { + "created_on": "2022-08-25T04:55:18.502Z", + "modified_on": "2022-08-25T04:55:18.502Z", + "queue": "8590ad29-5629-448c-bfb6-1bfd5219b8ec", + "role": 1, + "permission": "b5fab78a-4836-468c-96c4-f5b0bba3303a" + } + }, { "model": "queues.queueauthorization", "pk": "3717f056-7ea5-4d38-80f5-ba907132807c", From 4358ce76aa94e68fb458da216bc585d8f48ebf50 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:50:02 -0300 Subject: [PATCH 2/9] Feature/excel option in data exporter (#260) * feature: resolving bugs in excel exporter from dashboard. * feature: resolving flake8 warnings. * feature: resolving flake8 warnings. --------- Co-authored-by: AlanJaeger --- chats/apps/api/v1/dashboard/viewsets.py | 116 ++++++++++++++++-------- chats/core/excel_storage.py | 8 ++ 2 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 chats/core/excel_storage.py diff --git a/chats/apps/api/v1/dashboard/viewsets.py b/chats/apps/api/v1/dashboard/viewsets.py index 212e8962..c2ac2872 100644 --- a/chats/apps/api/v1/dashboard/viewsets.py +++ b/chats/apps/api/v1/dashboard/viewsets.py @@ -5,10 +5,7 @@ from rest_framework.response import Response from chats.apps.api.v1.dashboard.presenter import ( - get_agents_data, get_export_data, - get_general_data, - get_sector_data, ) from chats.apps.api.v1.dashboard.serializers import ( DashboardRawDataSerializer, @@ -19,6 +16,10 @@ from chats.apps.api.v1.permissions import HasDashboardAccess from chats.apps.projects.models import Project +import io +from chats.core.excel_storage import ExcelStorage +import json + class DashboardLiveViewset(viewsets.GenericViewSet): lookup_field = "uuid" @@ -106,15 +107,34 @@ def export(self, request, *args, **kwargs): project = self.get_object() filter = request.query_params dataset = get_export_data(project, filter) + data_frame_rooms = pandas.DataFrame(dataset) - filename = "dashboard_export_data" + filename = "dashboard_rooms_export_data" if "xls" in filter: - response = HttpResponse( - content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ) - response["Content-Disposition"] = ( - 'attachment; filename="' + filename + ".xls" + excel_rooms_buffer = io.BytesIO() + with pandas.ExcelWriter(excel_rooms_buffer, engine="xlsxwriter") as writer: + data_frame_rooms.to_excel( + writer, + sheet_name="rooms_infos", + startrow=1, + startcol=0, + index=False, + ) + excel_rooms_buffer.seek(0) # Move o cursor para o início do buffer + storage = ExcelStorage() + + bytes_archive = excel_rooms_buffer.getvalue() + + with storage.open(filename + ".xlsx", "wb") as up_file: + up_file.write(bytes_archive) + file_url = storage.url(up_file.name) + + data = {"path_file": file_url} + + return HttpResponse( + json.dumps(data), + content_type="application/javascript; charset=utf8", ) else: response = HttpResponse(content_type="text/csv") @@ -122,19 +142,19 @@ def export(self, request, *args, **kwargs): 'attachment; filename="' + filename + ".csv" ) - table = pandas.DataFrame(dataset) - table.rename( - columns={ - 0: "Queue Name", - 1: "Waiting Time", - 2: "Response Time", - 3: "Interaction Time", - 4: "Open", - }, - inplace=True, - ) - table.to_csv(response, encoding="utf-8", index=False) - return response + table = pandas.DataFrame(dataset) + table.rename( + columns={ + 0: "Queue Name", + 1: "Waiting Time", + 2: "Response Time", + 3: "Interaction Time", + 4: "Open", + }, + inplace=True, + ) + table.to_csv(response, encoding="utf-8", index=False) + return response @action( detail=True, @@ -149,24 +169,28 @@ def export_dashboard(self, request, *args, **kwargs): project = self.get_object() filter = request.query_params - general_dataset = get_general_data(project, filter) - userinfo_dataset = get_agents_data(project, filter) - sector_dataset = get_sector_data(project, filter) + user_info_context = {} + user_info_context["filters"] = request.query_params + + # General data + general_dataset = dashboard_general_data(context=filter, project=project) + raw_dataset = DashboardRawDataSerializer(instance=project, context=filter) + combined_dataset = {**general_dataset, **raw_dataset.data} + + # Agents Data + userinfo_dataset = dashboard_agents_data(context=filter, project=project) + # # Sectors Data + sector_dataset = dashboard_division_data(context=filter, project=project) filename = "dashboard_export_data" - data_frame = pandas.DataFrame([general_dataset]) - data_frame_1 = pandas.read_json(userinfo_dataset) - data_frame_2 = pandas.read_json(sector_dataset) + data_frame = pandas.DataFrame([combined_dataset]) + data_frame_1 = pandas.DataFrame(userinfo_dataset) + data_frame_2 = pandas.DataFrame(sector_dataset) if "xls" in filter: - response = HttpResponse( - content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ) - response["Content-Disposition"] = ( - 'attachment; filename="' + filename + ".xlsx" - ) - with pandas.ExcelWriter(response, engine="xlsxwriter") as writer: + excel_buffer = io.BytesIO() + with pandas.ExcelWriter(excel_buffer, engine="xlsxwriter") as writer: data_frame.to_excel( writer, sheet_name="dashboard_infos", @@ -188,7 +212,21 @@ def export_dashboard(self, request, *args, **kwargs): startcol=0, index=False, ) - return response + excel_buffer.seek(0) # Move o cursor para o início do buffer + storage = ExcelStorage() + + bytes_archive = excel_buffer.getvalue() + + with storage.open(filename + ".xlsx", "wb") as up_file: + up_file.write(bytes_archive) + file_url = storage.url(up_file.name) + + data = {"path_file": file_url} + + return HttpResponse( + json.dumps(data), + content_type="application/javascript; charset=utf8", + ) else: response = HttpResponse(content_type="text/csv") @@ -196,8 +234,8 @@ def export_dashboard(self, request, *args, **kwargs): 'attachment; filename="' + filename + ".csv" ) - data_frame.to_csv(response, index=False) - data_frame_1.to_csv(response, index=False, mode="a") - data_frame_2.to_csv(response, index=False, mode="a") + data_frame.to_csv(response, index=False, sep=";") + data_frame_1.to_csv(response, index=False, mode="a", sep=";") + data_frame_2.to_csv(response, index=False, mode="a", sep=";") return response diff --git a/chats/core/excel_storage.py b/chats/core/excel_storage.py new file mode 100644 index 00000000..e4aee6ee --- /dev/null +++ b/chats/core/excel_storage.py @@ -0,0 +1,8 @@ +from storages.backends.s3boto3 import S3Boto3Storage + + +class ExcelStorage(S3Boto3Storage): # pragma: no cover + def get_default_settings(self): + default_settings = super().get_default_settings() + default_settings["location"] = "dashboard_data/excel" + return default_settings From 32e8ee2e0d1f70809bb54dee32d56e3f9057fef5 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:24:05 -0300 Subject: [PATCH 3/9] feat: verify objects permission on external endpoint (#255) * feat: verify objects permission on external endpoint * feat: add room to name options on ValidatePermissionRequest * feat: use request.auth instead of querying again --- chats/apps/api/v1/external/permissions.py | 79 ++++++++++++++++++-- chats/apps/api/v1/external/rooms/viewsets.py | 5 +- chats/apps/dashboard/models.py | 4 + chats/apps/msgs/models.py | 8 ++ chats/apps/projects/models.py | 4 + chats/apps/queues/models.py | 8 ++ chats/apps/rooms/models.py | 4 + chats/apps/sectors/models.py | 8 ++ 8 files changed, 110 insertions(+), 10 deletions(-) diff --git a/chats/apps/api/v1/external/permissions.py b/chats/apps/api/v1/external/permissions.py index 64c06581..318e014e 100644 --- a/chats/apps/api/v1/external/permissions.py +++ b/chats/apps/api/v1/external/permissions.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ObjectDoesNotExist from rest_framework import permissions from chats.apps.projects.models import ProjectPermission @@ -5,11 +6,77 @@ class IsAdminPermission(permissions.BasePermission): def has_permission(self, request, view): # pragma: no cover - auth_header = request.META.get("HTTP_AUTHORIZATION") + if view.action in ["list", "create"]: + try: + permission = request.auth + project = permission.project + + validation = ValidatePermissionRequest( + request_data=request.data or request.query_params, project=project + ) + + return validation.is_valid + except (AttributeError, IndexError, ProjectPermission.DoesNotExist): + return False + + return super().has_permission(request, view) + + def has_object_permission(self, request, view, obj): + """ + Return `True` if permission is granted, `False` otherwise. + """ + try: + permission = request.auth + project = obj.project + except ProjectPermission.DoesNotExist: + return False + return permission.project == project + + +LEVEL_NAME_MAPPING = { + "project_uuid": "project", + "project": "project", + "sector_uuid": "pk", + "sector": "pk", + "queue_uuid": "queues", + "queue": "queues", + "room__uuid": "queues__rooms", + "room": "queues__rooms", +} + + +class ValidatePermissionRequest: + def __init__(self, request_data, project) -> None: + self.project = project + self.data = request_data + self.queryset = {} + + # Get the instance type and pk + for key in [ + "project", + "sector", + "queue", + "project_uuid", + "sector_uuid", + "queue_uuid", + "room__uuid", + "room", + ]: + self.level_name = LEVEL_NAME_MAPPING[key] + self.level_id = self.data.get(key, None) + if self.level_name != "project" and self.level_id is not None: + self.queryset = {self.level_name: self.level_id} + break + elif self.level_name == "project" and self.level_id is not None: + break + + @property + def is_valid(self): try: - auth_token = auth_header.split()[1] - return ( - True if ProjectPermission.objects.get(pk=auth_token, role=1) else False - ) - except (AttributeError, IndexError, ProjectPermission.DoesNotExist): + if self.level_name == "project": + return str(self.project.pk) == self.level_id + if self.queryset != {}: + return self.project.sectors.filter(**self.queryset).exists() + except ObjectDoesNotExist: return False + return False diff --git a/chats/apps/api/v1/external/rooms/viewsets.py b/chats/apps/api/v1/external/rooms/viewsets.py index d52967b2..c5014227 100644 --- a/chats/apps/api/v1/external/rooms/viewsets.py +++ b/chats/apps/api/v1/external/rooms/viewsets.py @@ -187,9 +187,6 @@ def partial_update(self, request, pk=None): class CustomFieldsUserExternalViewSet(viewsets.ViewSet): serializer_class = RoomFlowSerializer - permission_classes = [ - IsAdminPermission, - ] authentication_classes = [ProjectAdminAuthentication] def partial_update(self, request, pk=None): @@ -200,7 +197,7 @@ def partial_update(self, request, pk=None): return Response( {"Detail": "No contact id on the request"}, status.HTTP_400_BAD_REQUEST ) - request_permission = self.request.auth + request_permission = request.auth project = request_permission.project response = FlowRESTClient().create_contact( diff --git a/chats/apps/dashboard/models.py b/chats/apps/dashboard/models.py index 14dfbdbf..2c269627 100644 --- a/chats/apps/dashboard/models.py +++ b/chats/apps/dashboard/models.py @@ -23,3 +23,7 @@ class Meta: def __str__(self): return self.room.queue.name + + @property + def project(self): + return self.room.project diff --git a/chats/apps/msgs/models.py b/chats/apps/msgs/models.py index 0e9864a5..7a6eeb69 100644 --- a/chats/apps/msgs/models.py +++ b/chats/apps/msgs/models.py @@ -98,6 +98,10 @@ def notify_room(self, action: str, callback: bool = False): headers={"content-type": "application/json"}, ) + @property + def project(self): + return self.room.project + class MessageMedia(BaseModel): message = models.ForeignKey( @@ -157,3 +161,7 @@ def callback(self): def notify_room(self, *args, **kwargs): """ """ self.message.notify_room(*args, **kwargs) + + @property + def project(self): + return self.message.project diff --git a/chats/apps/projects/models.py b/chats/apps/projects/models.py index 3025e3a5..7b145363 100644 --- a/chats/apps/projects/models.py +++ b/chats/apps/projects/models.py @@ -351,3 +351,7 @@ class Meta: def __str__(self): return self.receiver_type + ": " + self.external_id + + @property + def project(self): + return self.flow_start.project diff --git a/chats/apps/queues/models.py b/chats/apps/queues/models.py index a1643a7e..692abcfd 100644 --- a/chats/apps/queues/models.py +++ b/chats/apps/queues/models.py @@ -86,6 +86,10 @@ def set_queue_authorization(self, user, role: int): ) return sector_auth + @property + def project(self): + return self.sector.project + class QueueAuthorization(BaseModel): ROLE_NOT_SETTED = 0 @@ -132,6 +136,10 @@ def get_permission(self, user): def sector(self): return self.queue.sector + @property + def project(self): + return self.queue.project + @property def is_agent(self): return self.role == self.ROLE_AGENT diff --git a/chats/apps/rooms/models.py b/chats/apps/rooms/models.py index 5477b34c..d7472d3a 100644 --- a/chats/apps/rooms/models.py +++ b/chats/apps/rooms/models.py @@ -103,6 +103,10 @@ def get_is_waiting(self): check_flowstarts = self.flowstarts.filter(is_deleted=False).exists() return check_messages or check_flowstarts + @property + def project(self): + return self.queue.project + @property def last_contact_message(self): return ( diff --git a/chats/apps/sectors/models.py b/chats/apps/sectors/models.py index 77de3f52..7302b8b2 100644 --- a/chats/apps/sectors/models.py +++ b/chats/apps/sectors/models.py @@ -251,6 +251,10 @@ def notify_user(self, action): action=f"sector_authorization.{action}", ) + @property + def project(self): + return self.sector.project + class SectorTag(BaseModel): name = models.CharField(_("Name"), max_length=120) @@ -277,3 +281,7 @@ class Meta: def __str__(self): return self.name + + @property + def project(self): + return self.sector.project From 114e0fd19801f84f1297efe07bf57b5e5a7a8163 Mon Sep 17 00:00:00 2001 From: alandavl Date: Wed, 26 Jul 2023 17:17:39 -0300 Subject: [PATCH 4/9] feature: frontend endpoint for edit custom fields (#261) --- chats/apps/api/v1/external/rooms/viewsets.py | 47 ++++++++-------- chats/apps/api/v1/rooms/serializers.py | 5 ++ chats/apps/api/v1/rooms/viewsets.py | 54 ++++++++++++++----- chats/apps/api/v1/sectors/serializers.py | 2 + chats/apps/rooms/views.py | 39 ++++++++++++++ .../0008_sector_can_edit_custom_fields.py | 20 +++++++ chats/apps/sectors/models.py | 3 ++ 7 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 chats/apps/rooms/views.py create mode 100644 chats/apps/sectors/migrations/0008_sector_can_edit_custom_fields.py diff --git a/chats/apps/api/v1/external/rooms/viewsets.py b/chats/apps/api/v1/external/rooms/viewsets.py index c5014227..ee0ac7b3 100644 --- a/chats/apps/api/v1/external/rooms/viewsets.py +++ b/chats/apps/api/v1/external/rooms/viewsets.py @@ -12,9 +12,13 @@ ) from chats.apps.api.v1.external.permissions import IsAdminPermission from chats.apps.api.v1.external.rooms.serializers import RoomFlowSerializer -from chats.apps.api.v1.internal.rest_clients.flows_rest_client import FlowRESTClient from chats.apps.dashboard.models import RoomMetrics from chats.apps.rooms.models import Room +from chats.apps.rooms.views import ( + get_editable_custom_fields_room, + update_custom_fields, + update_flows_custom_fields, +) def add_user_or_queue_to_room(instance, request): @@ -197,34 +201,27 @@ def partial_update(self, request, pk=None): return Response( {"Detail": "No contact id on the request"}, status.HTTP_400_BAD_REQUEST ) - request_permission = request.auth - project = request_permission.project - - response = FlowRESTClient().create_contact( - project=project, data=data, contact_id=pk - ) - if response.status_code not in [status.HTTP_200_OK]: + elif not custom_fields_update: return Response( - { - "Detail": f"[{response.status_code}]\n" - + f"Error updating custom fields on flows. Exception: {response.content}" - }, - status.HTTP_404_NOT_FOUND, + {"Detail": "No custom fields the request"}, status.HTTP_400_BAD_REQUEST ) + request_permission = self.request.auth + project = request_permission.project - room = Room.objects.filter( - contact__external_id=pk, - queue__sector__project=project, - is_active=True, - ).update(custom_fields=custom_fields_update) + room = get_editable_custom_fields_room( + { + "contact__external_id": pk, + "queue__sector__project": project, + "is_active": "True", + } + ) + update_flows_custom_fields( + project=room.queue.sector.project, + data=data, + contact_id=room.contact.external_id, + ) - if not room: - return Response( - { - "Detail": "Contact with the given id was not found, it does not exist or it is deleted" - }, - status.HTTP_404_NOT_FOUND, - ) + update_custom_fields(room, custom_fields_update) return Response( {"Detail": "Custom Field edited with success"}, diff --git a/chats/apps/api/v1/rooms/serializers.py b/chats/apps/api/v1/rooms/serializers.py index 1066d62f..2e6476d6 100644 --- a/chats/apps/api/v1/rooms/serializers.py +++ b/chats/apps/api/v1/rooms/serializers.py @@ -32,6 +32,7 @@ class RoomSerializer(serializers.ModelSerializer): is_24h_valid = serializers.SerializerMethodField() flowstart_data = serializers.SerializerMethodField() last_interaction = serializers.DateTimeField(read_only=True) + can_edit_custom_fields = serializers.SerializerMethodField() class Meta: model = Room @@ -44,6 +45,7 @@ class Meta: "linked_user", "is_24h_valid", "last_interaction", + "can_edit_custom_fields", ] def get_is_24h_valid(self, room: Room) -> bool: @@ -80,6 +82,9 @@ def get_last_message(self, room: Room): ) return "" if last_message is None else last_message.text + def get_can_edit_custom_fields(self, room: Room): + return room.queue.sector.can_edit_custom_fields + class TransferRoomSerializer(serializers.ModelSerializer): user = UserSerializer(many=False, required=False, read_only=True) diff --git a/chats/apps/api/v1/rooms/viewsets.py b/chats/apps/api/v1/rooms/viewsets.py index 5889e196..4cd3b9a5 100644 --- a/chats/apps/api/v1/rooms/viewsets.py +++ b/chats/apps/api/v1/rooms/viewsets.py @@ -20,6 +20,11 @@ from chats.apps.dashboard.models import RoomMetrics from chats.apps.msgs.models import Message from chats.apps.rooms.models import Room +from chats.apps.rooms.views import ( + get_editable_custom_fields_room, + update_custom_fields, + update_flows_custom_fields, +) class RoomViewset( @@ -47,12 +52,6 @@ def get_permissions(self): permissions.IsAuthenticated, api_permissions.IsQueueAgent, ) - elif self.action == "list" and self.request.query_params.get("email"): - permission_classes = ( - permissions.IsAuthenticated, - api_permissions.AnySectorManagerPermission, - ) - return [permission() for permission in permission_classes] def get_queryset(self): @@ -214,16 +213,47 @@ def perform_update(self, serializer): msg.notify_room("create") if old_user is None and user: # queued > agent - instance.notify_queue("update", transferred_by=self.request.user.email) + instance.notify_queue("update") elif old_user is not None: - instance.notify_user( - "update", user=old_user, transferred_by=self.request.user.email - ) + instance.notify_user("update", user=old_user) if queue: # agent > queue - instance.notify_queue("update", transferred_by=self.request.user.email) + instance.notify_queue("update") else: # agent > agent - instance.notify_user("update", transferred_by=self.request.user.email) + instance.notify_user("update") def perform_destroy(self, instance): instance.notify_room("destroy", callback=True) super().perform_destroy(instance) + + @action( + detail=True, + methods=["PATCH"], + ) + def update_custom_fields(self, request, pk=None): + custom_fields_update = request.data + data = {"fields": custom_fields_update} + + if pk is None: + return Response( + {"Detail": "No room on the request"}, status.HTTP_400_BAD_REQUEST + ) + elif not custom_fields_update: + return Response( + {"Detail": "No custom fields on the request"}, + status.HTTP_400_BAD_REQUEST, + ) + + room = get_editable_custom_fields_room({"uuid": pk, "is_active": "True"}) + + update_flows_custom_fields( + project=room.queue.sector.project, + data=data, + contact_id=room.contact.external_id, + ) + + update_custom_fields(room, custom_fields_update) + + return Response( + {"Detail": "Custom Field edited with success"}, + status.HTTP_200_OK, + ) diff --git a/chats/apps/api/v1/sectors/serializers.py b/chats/apps/api/v1/sectors/serializers.py index bfd0ccc0..a1122baf 100644 --- a/chats/apps/api/v1/sectors/serializers.py +++ b/chats/apps/api/v1/sectors/serializers.py @@ -41,6 +41,7 @@ class Meta: "is_deleted", "can_trigger_flows", "sign_messages", + "can_edit_custom_fields", ] extra_kwargs = {field: {"required": False} for field in fields} @@ -78,6 +79,7 @@ class Meta: "rooms_limit", "can_trigger_flows", "sign_messages", + "can_edit_custom_fields", ] diff --git a/chats/apps/rooms/views.py b/chats/apps/rooms/views.py new file mode 100644 index 00000000..8c8902ce --- /dev/null +++ b/chats/apps/rooms/views.py @@ -0,0 +1,39 @@ +from rest_framework import status +from rest_framework.exceptions import APIException + +from chats.apps.api.v1.internal.rest_clients.flows_rest_client import FlowRESTClient +from chats.apps.rooms.models import Room + + +def update_custom_fields(room: Room, custom_fields_update: dict): + room.custom_fields.update(custom_fields_update) + room.save() + + +def update_flows_custom_fields(project, data, contact_id): + response = FlowRESTClient().create_contact( + project=project, + data=data, + contact_id=contact_id, + ) + if response.status_code not in [status.HTTP_200_OK]: + raise APIException( + { + "Detail": f"[{response.status_code}]\n" + + f"Error updating custom fields on flows. Exception: {response.content}" + }, + ) + + +def get_editable_custom_fields_room(room_filter: dict) -> Room: + try: + room = Room.objects.get(**room_filter) + except Room.DoesNotExist: + raise APIException(detail="Active room not found.") + + if not room.queue.sector.can_edit_custom_fields: + raise APIException( + detail="Access denied! You can't edit custom fields in this sector." + ) + + return room diff --git a/chats/apps/sectors/migrations/0008_sector_can_edit_custom_fields.py b/chats/apps/sectors/migrations/0008_sector_can_edit_custom_fields.py new file mode 100644 index 00000000..2e8e561d --- /dev/null +++ b/chats/apps/sectors/migrations/0008_sector_can_edit_custom_fields.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.2 on 2023-07-25 20:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sectors", "0007_sector_sign_messages"), + ] + + operations = [ + migrations.AddField( + model_name="sector", + name="can_edit_custom_fields", + field=models.BooleanField( + default=False, verbose_name="Can edit custom fields?" + ), + ), + ] diff --git a/chats/apps/sectors/models.py b/chats/apps/sectors/models.py index 7302b8b2..e971ae66 100644 --- a/chats/apps/sectors/models.py +++ b/chats/apps/sectors/models.py @@ -35,6 +35,9 @@ class Sector(BaseSoftDeleteModel, BaseModel): open_offline = models.BooleanField( _("Open room when all agents are offline?"), default=True ) + can_edit_custom_fields = models.BooleanField( + _("Can edit custom fields?"), default=False + ) class Meta: verbose_name = _("Sector") From 16ddb6afdf6214446824d6b449c80f9cbda4534f Mon Sep 17 00:00:00 2001 From: alandavl Date: Wed, 26 Jul 2023 17:41:12 -0300 Subject: [PATCH 5/9] feature: more unit tests for dashboard (#250) * feature: more unit tests for dashboard * feature: resolving duplicity in unit tests --- .../apps/dashboard/tests/test_serializers.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/chats/apps/dashboard/tests/test_serializers.py b/chats/apps/dashboard/tests/test_serializers.py index 0b366462..e8add10c 100644 --- a/chats/apps/dashboard/tests/test_serializers.py +++ b/chats/apps/dashboard/tests/test_serializers.py @@ -1,6 +1,8 @@ from django.test import TestCase - -from chats.apps.api.v1.dashboard.serializers import dashboard_agents_data +from chats.apps.api.v1.dashboard.serializers import ( + dashboard_general_data, + dashboard_agents_data, +) from chats.apps.projects.models import Project @@ -20,6 +22,20 @@ class SerializerTests(TestCase): def setUp(self): self.project = Project.objects.get(pk="34a93b52-231e-11ed-861d-0242ac120002") + def test_active_chats_function_passing_sector(self): + serializer = dashboard_general_data( + context={"sector": "d49049f0-c601-4e05-a293-98c1dea5fe4f"}, + project=self.project, + ) + self.assertEqual(serializer["active_chats"], 0) + + def test_active_chats_function_without_filter(self): + serializer = dashboard_general_data( + context={}, + project=self.project, + ) + self.assertEqual(serializer["active_chats"], 1) + def test_returned_fields_from_dashboard_agent_serializer(self): project = Project.objects.get(uuid="34a93b52-231e-11ed-861d-0242ac120002") instance = dashboard_agents_data( From ef7ae139d0d21f2a172bf5b64da36f1fde70ffdb Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Wed, 26 Jul 2023 18:03:34 -0300 Subject: [PATCH 6/9] Feature/refactor room list (#257) * feat: move 'unread_msgs' field to an annotation instead of SerializerMethodField * feat: update room serializers and list action * feat: update room serializers and list action * feat: change is_waiting rules * feat: fix tests * feat: fix is_24h_valid condition to check urn * feat: lint --- chats/apps/api/v1/accounts/serializers.py | 10 ++++ chats/apps/api/v1/contacts/serializers.py | 11 ++++- .../apps/api/v1/external/msgs/serializers.py | 5 +- .../apps/api/v1/external/rooms/serializers.py | 2 +- chats/apps/api/v1/projects/viewsets.py | 4 +- chats/apps/api/v1/queues/serializers.py | 7 ++- chats/apps/api/v1/rooms/serializers.py | 37 +++++--------- chats/apps/api/v1/rooms/viewsets.py | 49 ++++++++++++++++++- chats/apps/msgs/models.py | 2 +- chats/apps/rooms/models.py | 2 +- chats/apps/rooms/tests/test_viewsets.py | 4 +- 11 files changed, 95 insertions(+), 38 deletions(-) diff --git a/chats/apps/api/v1/accounts/serializers.py b/chats/apps/api/v1/accounts/serializers.py index 69659889..514e91a2 100644 --- a/chats/apps/api/v1/accounts/serializers.py +++ b/chats/apps/api/v1/accounts/serializers.py @@ -38,6 +38,16 @@ def get_last_interaction(self, user: User): return user.last_interaction +class UserSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + "first_name", + "last_name", + "email", + ] + + class UserNameSerializer(serializers.ModelSerializer): class Meta: model = User diff --git a/chats/apps/api/v1/contacts/serializers.py b/chats/apps/api/v1/contacts/serializers.py index 7c1d0f92..0bde3015 100644 --- a/chats/apps/api/v1/contacts/serializers.py +++ b/chats/apps/api/v1/contacts/serializers.py @@ -4,7 +4,6 @@ class ContactSerializer(serializers.ModelSerializer): - room = serializers.SerializerMethodField() class Meta: @@ -87,3 +86,13 @@ class ContactWSSerializer(serializers.ModelSerializer): class Meta: model = Contact fields = "__all__" + + +class ContactSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = Contact + fields = [ + "uuid", + "external_id", + "name", + ] diff --git a/chats/apps/api/v1/external/msgs/serializers.py b/chats/apps/api/v1/external/msgs/serializers.py index 4023fbc7..59d1f81f 100644 --- a/chats/apps/api/v1/external/msgs/serializers.py +++ b/chats/apps/api/v1/external/msgs/serializers.py @@ -73,15 +73,14 @@ def create(self, validated_data): if direction == "incoming": validated_data["contact"] = room.contact - is_waiting = room.get_is_waiting() - was_24h_valid = room.is_24h_valid + was_24h_valid = room.validate_24h msg = super().create(validated_data) media_list = [MessageMedia(**media_data, message=msg) for media_data in medias] medias = MessageMedia.objects.bulk_create(media_list) if direction == "incoming": validated_data["contact"] = room.contact - if is_waiting: + if room.is_waiting: room.is_waiting = False room.save() room.notify_room("update") diff --git a/chats/apps/api/v1/external/rooms/serializers.py b/chats/apps/api/v1/external/rooms/serializers.py index ae9fb550..e6bffc99 100644 --- a/chats/apps/api/v1/external/rooms/serializers.py +++ b/chats/apps/api/v1/external/rooms/serializers.py @@ -154,7 +154,7 @@ def create(self, validated_data): contact, queue, user, groups, created, flow_uuid, project ) - room = Room.objects.create(**validated_data, contact=contact, queue=queue) + room = Room.objects.create(**validated_data, contact=contact, queue=queue, is_waiting=True) RoomMetrics.objects.create(room=room) return room diff --git a/chats/apps/api/v1/projects/viewsets.py b/chats/apps/api/v1/projects/viewsets.py index da1ebd8c..99e235e2 100644 --- a/chats/apps/api/v1/projects/viewsets.py +++ b/chats/apps/api/v1/projects/viewsets.py @@ -262,7 +262,7 @@ def start_flow(self, request, *args, **kwargs): {"Detail": "There already is an active flow start for this room"}, status.HTTP_400_BAD_REQUEST, ) - if not room.is_24h_valid: + if not room.validate_24h: flow_start_data["room"] = room room.request_callback(room.serialized_ws_data) except (ObjectDoesNotExist, ValidationError): @@ -275,6 +275,8 @@ def start_flow(self, request, *args, **kwargs): chats_flow_start.external_id = flow_start.get("uuid") chats_flow_start.name = flow_start.get("flow").get("name") chats_flow_start.save() + room.is_waiting = True + room.save() if chats_flow_start.room: room.notify_room("update") return Response(flow_start, status.HTTP_200_OK) diff --git a/chats/apps/api/v1/queues/serializers.py b/chats/apps/api/v1/queues/serializers.py index 1a5be069..6ff39767 100644 --- a/chats/apps/api/v1/queues/serializers.py +++ b/chats/apps/api/v1/queues/serializers.py @@ -6,7 +6,6 @@ class QueueSerializer(serializers.ModelSerializer): - sector_name = serializers.CharField(source="sector.name", read_only=True) class Meta: @@ -99,3 +98,9 @@ class Meta: def get_user(self, auth): return UserSerializer(auth.permission.user).data + + +class QueueSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = Queue + fields = ["uuid", "name"] diff --git a/chats/apps/api/v1/rooms/serializers.py b/chats/apps/api/v1/rooms/serializers.py index 2e6476d6..d2c924b8 100644 --- a/chats/apps/api/v1/rooms/serializers.py +++ b/chats/apps/api/v1/rooms/serializers.py @@ -2,9 +2,12 @@ from rest_framework import serializers from chats.apps.accounts.models import User -from chats.apps.api.v1.accounts.serializers import UserSerializer -from chats.apps.api.v1.contacts.serializers import ContactRelationsSerializer -from chats.apps.api.v1.queues.serializers import QueueSerializer +from chats.apps.api.v1.accounts.serializers import UserSerializer, UserSimpleSerializer +from chats.apps.api.v1.contacts.serializers import ( + ContactRelationsSerializer, + ContactSimpleSerializer, +) +from chats.apps.api.v1.queues.serializers import QueueSerializer, QueueSimpleSerializer from chats.apps.api.v1.sectors.serializers import DetailSectorTagSerializer from chats.apps.queues.models import Queue from chats.apps.rooms.models import Room @@ -21,15 +24,14 @@ class RoomMessageStatusSerializer(serializers.Serializer): class RoomSerializer(serializers.ModelSerializer): - user = UserSerializer(many=False, read_only=True) - contact = ContactRelationsSerializer(many=False, read_only=True) - queue = QueueSerializer(many=False, read_only=True) + user = UserSimpleSerializer(many=False, read_only=True) + contact = ContactSimpleSerializer(many=False, read_only=True) + queue = QueueSimpleSerializer(many=False, read_only=True) tags = DetailSectorTagSerializer(many=True, read_only=True) - unread_msgs = serializers.SerializerMethodField() + unread_msgs = serializers.IntegerField(required=False, default=0) + linked_user = serializers.CharField(read_only=True) + is_24h_valid = serializers.BooleanField(default=True) last_message = serializers.SerializerMethodField() - is_waiting = serializers.SerializerMethodField() - linked_user = serializers.SerializerMethodField() - is_24h_valid = serializers.SerializerMethodField() flowstart_data = serializers.SerializerMethodField() last_interaction = serializers.DateTimeField(read_only=True) can_edit_custom_fields = serializers.SerializerMethodField() @@ -48,9 +50,6 @@ class Meta: "can_edit_custom_fields", ] - def get_is_24h_valid(self, room: Room) -> bool: - return room.is_24h_valid - def get_flowstart_data(self, room: Room) -> bool: try: flowstart = room.flowstarts.get(is_deleted=False) @@ -62,18 +61,6 @@ def get_flowstart_data(self, room: Room) -> bool: "created_on": flowstart.created_on, } - def get_linked_user(self, room: Room): - try: - return room.contact.get_linked_user(room.queue.sector.project).full_name - except AttributeError: - return "" - - def get_is_waiting(self, room: Room): - return room.get_is_waiting() - - def get_unread_msgs(self, room: Room): - return room.messages.filter(seen=False).count() - def get_last_message(self, room: Room): last_message = ( room.messages.order_by("-created_on") diff --git a/chats/apps/api/v1/rooms/viewsets.py b/chats/apps/api/v1/rooms/viewsets.py index 4cd3b9a5..bb3329f8 100644 --- a/chats/apps/api/v1/rooms/viewsets.py +++ b/chats/apps/api/v1/rooms/viewsets.py @@ -1,12 +1,26 @@ import json +from datetime import timedelta from django.conf import settings -from django.db.models import F, Max, Sum +from django.db.models import ( + BooleanField, + Case, + CharField, + Count, + F, + Max, + Q, + Sum, + Value, + When, +) +from django.db.models.functions import Concat from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters, mixins, permissions, status from rest_framework.decorators import action from rest_framework.filters import OrderingFilter +from rest_framework.pagination import CursorPagination from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -44,6 +58,8 @@ class RoomViewset( search_fields = ["contact__name", "urn"] ordering_fields = "__all__" ordering = ["user", "-last_interaction"] + pagination_class = CursorPagination + pagination_class.page_size_query_param = "limit" def get_permissions(self): permission_classes = [permissions.IsAuthenticated] @@ -58,7 +74,36 @@ def get_queryset(self): if self.action != "list": self.filterset_class = None qs = super().get_queryset() - return qs.annotate(last_interaction=Max("messages__created_on")) + + last_24h = timezone.now() - timedelta(days=1) + + qs = qs.annotate( + last_interaction=Max("messages__created_on"), + unread_msgs=Count("messages", filter=Q(messages__seen=False)), + linked_user=Concat( + "contact__linked_users__user__first_name", + Value(" "), + "contact__linked_users__user__last_name", + filter=Q(contact__linked_users__project=F("queue__sector__project")), + output_field=CharField(), + ), + last_contact_interaction=Max( + "messages__created_on", filter=Q(messages__contact__isnull=False) + ), + is_24h_valid=Case( + When( + Q( + urn__startswith="whatsapp", + last_contact_interaction__lt=last_24h, + ), + then=False, + ), + default=True, + output_field=BooleanField(), + ), + ) + + return qs def get_serializer_class(self): if "update" in self.action: diff --git a/chats/apps/msgs/models.py b/chats/apps/msgs/models.py index 7a6eeb69..b2a4b79e 100644 --- a/chats/apps/msgs/models.py +++ b/chats/apps/msgs/models.py @@ -45,7 +45,7 @@ class Meta: def save(self, *args, **kwargs) -> None: if self.room.is_active is False: raise ValidationError({"detail": _("Closed rooms cannot receive messages")}) - if self.room.is_24h_valid is False and self.user is not None: + if self.room.validate_24h is False and self.user is not None: raise ValidationError( { "detail": _( diff --git a/chats/apps/rooms/models.py b/chats/apps/rooms/models.py index d7472d3a..32043d2c 100644 --- a/chats/apps/rooms/models.py +++ b/chats/apps/rooms/models.py @@ -122,7 +122,7 @@ def trigger_default_message(self): sent_message.notify_room("create", True) @property - def is_24h_valid(self) -> bool: + def validate_24h(self) -> bool: """Validates is the last contact message was sent more than a day ago""" if not self.urn.startswith("whatsapp"): return True diff --git a/chats/apps/rooms/tests/test_viewsets.py b/chats/apps/rooms/tests/test_viewsets.py index 685b0ad3..fa0e1496 100644 --- a/chats/apps/rooms/tests/test_viewsets.py +++ b/chats/apps/rooms/tests/test_viewsets.py @@ -113,14 +113,14 @@ def _ok_list_rooms(self, token, rooms: list, data: dict): response, results = self._request_list_rooms(token, data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json().get("count"), len(rooms)) + self.assertEqual(len(response.json().get("results")), len(rooms)) for result in results: self.assertIn(result.get("uuid"), rooms) def _not_ok_list_rooms(self, token, data: dict): response, _ = self._request_list_rooms(token, data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json().get("count"), 0) + self.assertEqual(len(response.json().get("results")), 0) def test_list_rooms_given_agents(self): self._ok_list_rooms( From 086d8fb603089eb22da1ddc5a26c8e8a747b4fc4 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Wed, 26 Jul 2023 18:12:51 -0300 Subject: [PATCH 7/9] Feateure/metrics tasks (#248) * feat: Add celery settings * feat: fix celery settings * feat: update lock file * feat: add close metrics task * feat: add custom queue to run the close metrics task * feat: change default value of metrics custom queue to celery * feat: make it a choice to run the tasks in celery or synchronous * feat: remove apm from docker compose * feat: add debug task --- chats/__init__.py | 5 + chats/apps/api/v1/external/rooms/viewsets.py | 6 + chats/apps/api/v1/rooms/viewsets.py | 41 +--- chats/apps/dashboard/tasks.py | 36 +++ chats/apps/rooms/views.py | 12 + chats/celery.py | 22 ++ chats/settings.py | 25 +- poetry.lock | 229 ++++++++++++++++++- pyproject.toml | 3 + 9 files changed, 331 insertions(+), 48 deletions(-) create mode 100644 chats/apps/dashboard/tasks.py create mode 100644 chats/celery.py diff --git a/chats/__init__.py b/chats/__init__.py index e69de29b..5568b6d7 100644 --- a/chats/__init__.py +++ b/chats/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/chats/apps/api/v1/external/rooms/viewsets.py b/chats/apps/api/v1/external/rooms/viewsets.py index ee0ac7b3..58767370 100644 --- a/chats/apps/api/v1/external/rooms/viewsets.py +++ b/chats/apps/api/v1/external/rooms/viewsets.py @@ -1,5 +1,6 @@ import json +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import IntegrityError from django.utils import timezone @@ -15,6 +16,7 @@ from chats.apps.dashboard.models import RoomMetrics from chats.apps.rooms.models import Room from chats.apps.rooms.views import ( + close_room, get_editable_custom_fields_room, update_custom_fields, update_flows_custom_fields, @@ -67,6 +69,10 @@ def close( instance.close(None, "agent") serialized_data = RoomFlowSerializer(instance=instance) instance.notify_queue("close") + if not settings.ACTIVATE_CALC_METRICS: + return Response(serialized_data.data, status=status.HTTP_200_OK) + + close_room(str(instance.pk)) return Response(serialized_data.data, status=status.HTTP_200_OK) def create(self, request, *args, **kwargs): diff --git a/chats/apps/api/v1/rooms/viewsets.py b/chats/apps/api/v1/rooms/viewsets.py index bb3329f8..3083aac4 100644 --- a/chats/apps/api/v1/rooms/viewsets.py +++ b/chats/apps/api/v1/rooms/viewsets.py @@ -32,9 +32,9 @@ TransferRoomSerializer, ) from chats.apps.dashboard.models import RoomMetrics -from chats.apps.msgs.models import Message from chats.apps.rooms.models import Room from chats.apps.rooms.views import ( + close_room, get_editable_custom_fields_room, update_custom_fields, update_flows_custom_fields, @@ -161,44 +161,7 @@ def close( if not settings.ACTIVATE_CALC_METRICS: return Response(serialized_data.data, status=status.HTTP_200_OK) - messages_contact = ( - Message.objects.filter(room=instance, contact__isnull=False) - .order_by("created_on") - .first() - ) - messages_agent = ( - Message.objects.filter(room=instance, user__isnull=False) - .order_by("created_on") - .first() - ) - - time_message_contact = 0 - time_message_agent = 0 - - if messages_agent and messages_contact: - time_message_agent = messages_agent.created_on.timestamp() - time_message_contact = messages_contact.created_on.timestamp() - else: - time_message_agent = 0 - time_message_contact = 0 - - difference_time = time_message_agent - time_message_contact - - interation_time = ( - Room.objects.filter(pk=instance.pk) - .aggregate( - avg_time=Sum( - F("ended_at") - F("created_on"), - ) - )["avg_time"] - .total_seconds() - ) - - metric_room = RoomMetrics.objects.get_or_create(room=instance)[0] - metric_room.message_response_time = difference_time - metric_room.interaction_time = interation_time - metric_room.save() - + close_room(str(instance.pk)) return Response(serialized_data.data, status=status.HTTP_200_OK) def perform_create(self, serializer): diff --git a/chats/apps/dashboard/tasks.py b/chats/apps/dashboard/tasks.py new file mode 100644 index 00000000..726ccd07 --- /dev/null +++ b/chats/apps/dashboard/tasks.py @@ -0,0 +1,36 @@ +from chats.apps.dashboard.models import RoomMetrics +from chats.apps.rooms.models import Room +from chats.celery import app + + +def generate_metrics(room: str): + room = Room.objects.get(pk=room) + messages_contact = ( + room.messages.filter(contact__isnull=False).order_by("created_on").first() + ) + messages_agent = ( + room.messages.filter(user__isnull=False).order_by("created_on").first() + ) + + time_message_contact = 0 + time_message_agent = 0 + + if messages_agent and messages_contact: + time_message_agent = messages_agent.created_on.timestamp() + time_message_contact = messages_contact.created_on.timestamp() + else: + time_message_agent = 0 + time_message_contact = 0 + + difference_time = time_message_agent - time_message_contact + interaction_time = room.ended_at - room.created_on + + metric_room = RoomMetrics.objects.get_or_create(room=room)[0] + metric_room.message_response_time = difference_time + metric_room.interaction_time = interaction_time.total_seconds() + metric_room.save() + + +@app.task(name="close_metrics") +def close_metrics(room: str): + generate_metrics(room) diff --git a/chats/apps/rooms/views.py b/chats/apps/rooms/views.py index 8c8902ce..639c7fb8 100644 --- a/chats/apps/rooms/views.py +++ b/chats/apps/rooms/views.py @@ -1,10 +1,22 @@ from rest_framework import status from rest_framework.exceptions import APIException +from django.conf import settings +from django.db import transaction +from chats.apps.dashboard.tasks import close_metrics, generate_metrics from chats.apps.api.v1.internal.rest_clients.flows_rest_client import FlowRESTClient from chats.apps.rooms.models import Room +def close_room(room_pk: str): + if settings.USE_CELERY: + transaction.on_commit( + lambda: close_metrics.apply_async( + args=[room_pk], queue=settings.METRICS_CUSTOM_QUEUE + ) + ) + generate_metrics(room_pk) + def update_custom_fields(room: Room, custom_fields_update: dict): room.custom_fields.update(custom_fields_update) room.save() diff --git a/chats/celery.py b/chats/celery.py new file mode 100644 index 00000000..7147f189 --- /dev/null +++ b/chats/celery.py @@ -0,0 +1,22 @@ +import os + +from celery import Celery + +# Set the default Django settings module for the "celery" program. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chats.settings") + +app = Celery("chats") + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace="CELERY" means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object("django.conf:settings", namespace="CELERY") + +# Load task modules from all registered Django apps. +app.autodiscover_tasks() + + +@app.task(bind=True) +def debug_task(self): + print("Request: {0!r}".format(self.request)) diff --git a/chats/settings.py b/chats/settings.py index d3e9ccb0..e8571da5 100644 --- a/chats/settings.py +++ b/chats/settings.py @@ -72,6 +72,8 @@ "rest_framework", "rest_framework.authtoken", "corsheaders", + "django_celery_beat", + "django_celery_results", ] MIDDLEWARE = [ @@ -105,16 +107,17 @@ WSGI_APPLICATION = "chats.wsgi.application" -# channels ASGI_APPLICATION = "chats.asgi.application" +REDIS_URL = env.str("CHANNEL_LAYERS_REDIS", default="redis://localhost:6379/1") + +# channels + CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer", "CONFIG": { - "hosts": [ - env.str("CHANNEL_LAYERS_REDIS", default="redis://127.0.0.1:6379/1") - ], + "hosts": [REDIS_URL], }, }, } @@ -122,7 +125,7 @@ CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": env.str("CHANNEL_LAYERS_REDIS", default="redis://127.0.0.1:6379/1"), + "LOCATION": REDIS_URL, "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, } } @@ -353,3 +356,15 @@ CHATS_FLOWS_TAG = env.str("CHATS_FLOWS_TAG", default="chats") CHATS_CACHE_TIME = env.int("CHATS_CACHE_TIME", default=1 * 60 * 60) + +# Celery + +METRICS_CUSTOM_QUEUE = env("METRICS_CUSTOM_QUEUE", default="celery") + +USE_CELERY = env.bool("USE_CELERY", default=False) +CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default=REDIS_URL) +CELERY_RESULT_BACKEND = env.str("CELERY_RESULT_BACKEND", default="django-db") +CELERY_ACCEPT_CONTENT = ["application/json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" +CELERY_TIMEZONE = TIME_ZONE diff --git a/poetry.lock b/poetry.lock index dee022de..e1b0062f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,14 @@ +[[package]] +name = "amqp" +version = "5.1.1" +description = "Low-level AMQP client for Python (fork of amqplib)." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +vine = ">=5.0.0" + [[package]] name = "anyio" version = "3.6.1" @@ -106,6 +117,9 @@ category = "main" optional = false python-versions = ">=3.6" +[package.dependencies] +tzdata = {version = "*", optional = true, markers = "extra == \"tzdata\""} + [package.extras] tzdata = ["tzdata"] @@ -129,6 +143,14 @@ test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr toml = ["tomli (>=1.1.0)"] yaml = ["pyyaml"] +[[package]] +name = "billiard" +version = "4.1.0" +description = "Python multiprocessing fork with improvements and bugfixes" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "black" version = "22.10.0" @@ -183,6 +205,59 @@ urllib3 = ">=1.25.4,<1.27" [package.extras] crt = ["awscrt (==0.14.0)"] +[[package]] +name = "celery" +version = "5.3.1" +description = "Distributed Task Queue." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +"backports.zoneinfo" = {version = ">=0.2.1", markers = "python_version < \"3.9\""} +billiard = ">=4.1.0,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.3.1,<6.0" +python-dateutil = ">=2.8.2" +tzdata = ">=2022.7" +vine = ">=5.0.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.1)"] +auth = ["cryptography (==41.0.1)"] +azureblockblob = ["azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb (==1.14.2)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elasticsearch (<8.0)"] +eventlet = ["eventlet (>=0.32.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] +mongodb = ["pymongo[srv] (>=4.0.2)"] +msgpack = ["msgpack (==1.0.5)"] +pymemcache = ["python-memcached (==1.59)"] +pyro = ["pyro4 (==4.82)"] +pytest = ["pytest-celery (==0.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5)"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem (==4.1.4)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "urllib3 (>=1.26.16)", "pycurl (>=7.43.0.5)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.21.0)"] + [[package]] name = "certifi" version = "2022.9.24" @@ -266,6 +341,46 @@ python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-didyoumean" +version = "0.3.0" +description = "Enables git-like *did-you-mean* feature in click" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest-cov (>=4.0.0)", "pytest (>=7.2.1)", "tox (>=4.4.3)"] + [[package]] name = "colorama" version = "0.4.5" @@ -310,6 +425,17 @@ python-versions = ">=3.7" [package.extras] toml = ["tomli"] +[[package]] +name = "cron-descriptor" +version = "1.4.0" +description = "A Python library that converts cron expressions into human readable strings." +category = "main" +optional = false +python-versions = "*" + +[package.extras] +dev = ["polib"] + [[package]] name = "cryptography" version = "38.0.1" @@ -363,6 +489,35 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-celery-beat" +version = "2.5.0" +description = "Database-backed Periodic Tasks." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +celery = ">=5.2.3,<6.0" +cron-descriptor = ">=1.2.32" +Django = ">=2.2,<5.0" +django-timezone-field = ">=5.0" +python-crontab = ">=2.3.4" +tzdata = "*" + +[[package]] +name = "django-celery-results" +version = "2.5.1" +description = "Celery result backends for Django." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +celery = ">=5.2.7,<6.0" +Django = ">=3.2.18" + [[package]] name = "django-cors-headers" version = "3.13.0" @@ -753,6 +908,37 @@ dev = ["pytest", "tox"] docs = ["Sphinx (>=1.0)", "sphinx-rtd-theme (>=1.0)"] tests = ["coverage (>=4.0)", "flake8 (<4)", "isort", "mypy", "pytest-cov", "pytest-flake8 (>=0.5)", "pytest (>=2.8.0)", "types-pyopenssl", "types-pyrfc3339", "types-requests", "types-setuptools"] +[[package]] +name = "kombu" +version = "5.3.1" +description = "Messaging library for Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +"backports.zoneinfo" = {version = ">=0.2.1", extras = ["tzdata"], markers = "python_version < \"3.9\""} +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} +vine = "*" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (==2.1.1)"] +consul = ["python-consul2"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=4.1.1)"] +msgpack = ["msgpack"] +pyro = ["pyro4"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=4.5.2)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "urllib3 (>=1.26.16)", "pycurl (>=7.43.0.5)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + [[package]] name = "lazy-object-proxy" version = "1.9.0" @@ -1058,7 +1244,7 @@ twisted = ["twisted"] name = "prompt-toolkit" version = "3.0.38" description = "Library for building powerful interactive command lines in Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.7.0" @@ -1224,6 +1410,21 @@ wcwidth = "*" checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "python-crontab" +version = "2.7.1" +description = "Python Crontab API" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +python-dateutil = "*" + +[package.extras] +cron-description = ["cron-descriptor"] +cron-schedule = ["croniter"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1493,7 +1694,7 @@ python-versions = ">=3.7" [[package]] name = "tzdata" -version = "2022.4" +version = "2022.7" description = "Provider of IANA time zone data" category = "main" optional = false @@ -1555,6 +1756,14 @@ dev = ["Cython (>=0.29.32,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0 docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] test = ["flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=22.0.0,<22.1.0)", "mypy (>=0.800)", "Cython (>=0.29.32,<0.30.0)", "aiohttp"] +[[package]] +name = "vine" +version = "5.0.0" +description = "Promises, promises, promises." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "virtualenv" version = "20.21.0" @@ -1587,7 +1796,7 @@ anyio = ">=3.0.0,<4" name = "wcwidth" version = "0.2.5" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -1618,9 +1827,10 @@ python-versions = ">=3.6" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "f80642b458f4d6362efb80fb3e2bd1d4141fdc66d3cd0c7f9b274006505eec1f" +content-hash = "993ed6fb432a336721275dbda4dfd9ae8252f6cc8a33642db003a6c37681318b" [metadata.files] +amqp = [] anyio = [] appnope = [] asgiref = [] @@ -1632,9 +1842,11 @@ attrs = [] backcall = [] "backports.zoneinfo" = [] bandit = [] +billiard = [] black = [] boto3 = [] botocore = [] +celery = [] certifi = [] cffi = [] cfgv = [] @@ -1642,14 +1854,20 @@ channels = [] channels-redis = [] charset-normalizer = [] click = [] +click-didyoumean = [] +click-plugins = [] +click-repl = [] colorama = [] coreapi = [] coreschema = [] coverage = [] +cron-descriptor = [] cryptography = [] decorator = [] distlib = [] django = [] +django-celery-beat = [] +django-celery-results = [] django-cors-headers = [] django-csp = [] django-environ = [] @@ -1679,6 +1897,7 @@ jedi = [] jinja2 = [] jmespath = [] josepy = [] +kombu = [] lazy-object-proxy = [] markdown-it-py = [] markupsafe = [] @@ -1720,6 +1939,7 @@ pymongo = [] pyopenssl = [] pyparsing = [] pytest = [] +python-crontab = [] python-dateutil = [] python-dotenv = [] python-magic = [] @@ -1748,6 +1968,7 @@ uritemplate = [] urllib3 = [] uvicorn = [] uvloop = [] +vine = [] virtualenv = [] watchfiles = [] wcwidth = [] diff --git a/pyproject.toml b/pyproject.toml index 79e19aec..8bb612f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ openpyxl = "^3.1.2" XlsxWriter = "^3.1.0" django-redis = "^5.2.0" elastic-apm = "^6.10.1" +celery = "^5.3.1" +django-celery-results = "^2.5.1" +django-celery-beat = "^2.5.0" [tool.poetry.dev-dependencies] pytest = "^5.2" From 423a9357356b5eaa9cae98b8c3b9a86b931310e6 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Thu, 27 Jul 2023 11:28:41 -0300 Subject: [PATCH 8/9] Hotfix/flow start room (#262) * feat: move room uptado to the try except block * feat: move room uptado to the try except block --- chats/apps/api/v1/projects/viewsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chats/apps/api/v1/projects/viewsets.py b/chats/apps/api/v1/projects/viewsets.py index 99e235e2..38c5ad48 100644 --- a/chats/apps/api/v1/projects/viewsets.py +++ b/chats/apps/api/v1/projects/viewsets.py @@ -275,9 +275,9 @@ def start_flow(self, request, *args, **kwargs): chats_flow_start.external_id = flow_start.get("uuid") chats_flow_start.name = flow_start.get("flow").get("name") chats_flow_start.save() - room.is_waiting = True - room.save() if chats_flow_start.room: + room.is_waiting = True + room.save() room.notify_room("update") return Response(flow_start, status.HTTP_200_OK) From 5af7ad563282b67a506e51b581b644727889f090 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:39:41 -0300 Subject: [PATCH 9/9] feat: return sector field on the queue simple serializer (#263) --- chats/apps/api/v1/queues/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chats/apps/api/v1/queues/serializers.py b/chats/apps/api/v1/queues/serializers.py index 6ff39767..11a2e984 100644 --- a/chats/apps/api/v1/queues/serializers.py +++ b/chats/apps/api/v1/queues/serializers.py @@ -103,4 +103,4 @@ def get_user(self, auth): class QueueSimpleSerializer(serializers.ModelSerializer): class Meta: model = Queue - fields = ["uuid", "name"] + fields = ["uuid", "name", "sector"]