From 5f222716b42c20958894c88a2728c074189f7e16 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:12:06 -0300 Subject: [PATCH 001/101] feat: Create Statistic endpoint (#121) --- weni/internal/statistic/tests.py | 55 ++++++++++++++++++++++++++++++++ weni/internal/statistic/urls.py | 8 +++++ weni/internal/statistic/views.py | 24 ++++++++++++++ weni/internal/urls.py | 3 ++ 4 files changed, 90 insertions(+) create mode 100644 weni/internal/statistic/tests.py create mode 100644 weni/internal/statistic/urls.py create mode 100644 weni/internal/statistic/views.py diff --git a/weni/internal/statistic/tests.py b/weni/internal/statistic/tests.py new file mode 100644 index 000000000..555f234d6 --- /dev/null +++ b/weni/internal/statistic/tests.py @@ -0,0 +1,55 @@ +from abc import ABC, abstractmethod + +from django.contrib.auth.models import Group +from django.urls import reverse +from django.utils.http import urlencode +from django.contrib.auth.models import User + +from temba.api.models import APIToken +from temba.flows.models import Flow +from temba.orgs.models import Org +from temba.tests import TembaTest + + +class TembaRequestMixin(ABC): + def reverse(self, viewname, kwargs=None, query_params=None): + url = reverse(viewname, kwargs=kwargs) + + if query_params: + return "%s?%s" % (url, urlencode(query_params)) + + return url + + def request_detail(self, uuid): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + @abstractmethod + def get_url_namespace(self): + ... + + +class StatisticTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + super().setUp() + + def test_retrieve(self): + user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + org = Org.objects.create(name="Temba", timezone="Africa/Kigali", created_by=user, modified_by=user) + + Flow.create(org=org, name="Flow test", user=user) + + active_flows = org.flows.filter(is_active=True, is_archived=False).exclude(is_system=True).count() + active_classifiers = org.classifiers.filter(is_active=True).count() + active_contacts = org.contacts.filter(is_active=True).count() + + statistic_request = self.request_detail(uuid=str(org.uuid)).json() + + self.assertEqual(statistic_request["active_flows"], active_flows) + self.assertEqual(statistic_request["active_classifiers"], active_classifiers) + self.assertEqual(statistic_request["active_contacts"], active_contacts) + + def get_url_namespace(self): + return "statistic-detail" diff --git a/weni/internal/statistic/urls.py b/weni/internal/statistic/urls.py new file mode 100644 index 000000000..5fa3866da --- /dev/null +++ b/weni/internal/statistic/urls.py @@ -0,0 +1,8 @@ +from rest_framework import routers + +from .views import StatisticEndpoint + +router = routers.DefaultRouter() +router.register(r"statistic", StatisticEndpoint, basename="statistic") + +urlpatterns = router.urls diff --git a/weni/internal/statistic/views.py b/weni/internal/statistic/views.py new file mode 100644 index 000000000..97ae78a2d --- /dev/null +++ b/weni/internal/statistic/views.py @@ -0,0 +1,24 @@ +from django.shortcuts import get_object_or_404 + +from rest_framework.response import Response +from rest_framework.mixins import RetrieveModelMixin +from rest_framework import status + +from temba.orgs.models import Org + +from weni.internal.views import InternalGenericViewSet + + +class StatisticEndpoint(RetrieveModelMixin, InternalGenericViewSet): + lookup_field = "uuid" + + def retrieve(self, request, uuid=None): + org = get_object_or_404(Org, uuid=uuid) + + response = { + "active_flows": org.flows.filter(is_active=True, is_archived=False).exclude(is_system=True).count(), + "active_classifiers": org.classifiers.filter(is_active=True).count(), + "active_contacts": org.contacts.filter(is_active=True).count(), + } + + return Response(data=response, status=status.HTTP_200_OK) diff --git a/weni/internal/urls.py b/weni/internal/urls.py index 2587ed947..6d02d4025 100644 --- a/weni/internal/urls.py +++ b/weni/internal/urls.py @@ -14,6 +14,7 @@ from weni.internal.flows.urls import urlpatterns as flows_urls from weni.internal.users.urls import urlpatterns as users_urls from weni.internal.tickets.urls import urlpatterns as tickets_urls +from weni.internal.statistic.urls import urlpatterns as statistics_urls internal_urlpatterns = [] @@ -21,5 +22,7 @@ internal_urlpatterns += flows_urls internal_urlpatterns += users_urls internal_urlpatterns += tickets_urls +internal_urlpatterns += statistics_urls + urlpatterns = [path("internals/", include(internal_urlpatterns))] From 543c67484d5b83f2a45eaab66f88147d74170549 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:12:51 -0300 Subject: [PATCH 002/101] [Conversion gRPC-REST] User and User-Permisson endpoint (#143) * Org and User APIToken internal endpoint (#151) * feat: Create User's internal api app feat: Add endpoint that allows retrieving API token from specified org and user * feat: Adds new internal users app to internal routes * feat: Add User and User-Permisson REST endpoint * fix: Add internal authetication * fix: change lookupfield from org_id to org_uuid and user_id to email * fix: change lookupfield from org_id to org_uuid and user_id to email modified: weni/internal/users/views.py * fix: user and user-permission urls fix Co-authored-by: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> --- weni/internal/users/serializers.py | 19 +- weni/internal/users/tests.py | 278 +++++++++++++++++++++++++++++ weni/internal/users/urls.py | 19 +- weni/internal/users/views.py | 120 ++++++++++++- 4 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 weni/internal/users/tests.py diff --git a/weni/internal/users/serializers.py b/weni/internal/users/serializers.py index aee7aead0..3b342c19a 100644 --- a/weni/internal/users/serializers.py +++ b/weni/internal/users/serializers.py @@ -1,8 +1,25 @@ +from django.contrib.auth import get_user_model + from rest_framework import serializers -from weni import serializers as weni_serializers +from weni.grpc.core import serializers as weni_serializers + +User = get_user_model() class UserAPITokenSerializer(serializers.Serializer): user = weni_serializers.UserEmailRelatedField(required=True) org = weni_serializers.OrgUUIDRelatedField(required=True) + + +class UserPermissionSerializer(serializers.Serializer): + administrator = serializers.BooleanField(default=False) + viewer = serializers.BooleanField(default=False) + editor = serializers.BooleanField(default=False) + surveyor = serializers.BooleanField(default=False) + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "email", "username", "first_name", "last_name", "date_joined", "is_active", "is_superuser"] diff --git a/weni/internal/users/tests.py b/weni/internal/users/tests.py new file mode 100644 index 000000000..74460410d --- /dev/null +++ b/weni/internal/users/tests.py @@ -0,0 +1,278 @@ +import json +from abc import ABC, abstractmethod + +from django.contrib.auth.models import User +from django.conf import settings +from django.contrib.auth.models import Group +from django.urls import reverse +from django.utils.http import urlencode + +from temba.api.models import APIToken + +from temba.tests import TembaTest +from temba.orgs.models import Org + + +class TembaRequestMixin(ABC): + def reverse(self, viewname, kwargs=None, query_params=None): + url = reverse(viewname, kwargs=kwargs) + + if query_params: + return "%s?%s" % (url, urlencode(query_params)) + else: + return url + + def request_get(self, **query_params): + url = self.reverse(self.get_url_namespace(), query_params=query_params) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_detail(self, **kwargs): + url = self.reverse(self.get_url_namespace(), query_params=kwargs) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_patch(self, data, **kwargs): + url = self.reverse(self.get_url_namespace(), query_params=kwargs) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.patch( + f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + def request_post(self, data): + url = reverse(self.get_url_namespace()) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.post( + url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + def request_delete(self, data, **kwargs): + url = self.reverse(self.get_url_namespace(), query_params=kwargs) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.delete( + f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + @abstractmethod + def get_url_namespace(self): + ... + + +class UserPermissionUpdateDestroyTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + super().setUp() + + def test_user_permission_destroy(self): + org = Org.objects.first() + user = User.objects.first() + + destroy_wrong_permission = self.request_delete( + data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="adm") + ) + self.assertEqual(destroy_wrong_permission.status_code, 400) + self.assertEqual(destroy_wrong_permission.json()[0], "adm is not a valid permission!") + + self.request_patch(data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="viewer")) + user_permissions = self._get_user_permissions(org=org, user=user) + + self.request_delete(data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="viewer")) + user_permissions_removed = self._get_user_permissions(org=org, user=user) + + self.assertFalse(user_permissions_removed.get("viewer", False)) + self.assertNotEquals(user_permissions, user_permissions_removed) + + def test_user_permission_update(self): + org = Org.objects.first() + user = User.objects.first() + + update_wrong_permission_response = self.request_patch( + data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="adm") + ) + self.assertEqual(update_wrong_permission_response.status_code, 400) + self.assertEqual(update_wrong_permission_response.json()[0], "adm is not a valid permission!") + + update_response = self.request_patch( + data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="administrator") + ).json() + user_permissions = self._get_user_permissions(org, user) + + self.assertTrue(user_permissions.get("administrator")) + self.assertTrue(self._permission_is_unique_true(update_response, "administrator")) + + update_response = self.request_patch( + data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="viewer") + ).json() + user_permissions = self._get_user_permissions(org, user) + + self.assertTrue(user_permissions.get("viewer")) + self.assertTrue(self._permission_is_unique_true(update_response, "viewer")) + + update_response = self.request_patch( + data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="editor") + ).json() + user_permissions = self._get_user_permissions(org, user) + + self.assertTrue(user_permissions.get("editor")) + self.assertTrue(self._permission_is_unique_true(update_response, "editor")) + + update_response = self.request_patch( + data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="surveyor") + ).json() + user_permissions = self._get_user_permissions(org, user) + + self.assertTrue(user_permissions.get("surveyor")) + self.assertTrue(self._permission_is_unique_true(update_response, "surveyor")) + + def _get_permissions(self, org: Org) -> dict: + return { + "administrator": org.administrators, + "viewer": org.viewers, + "editor": org.editors, + "surveyor": org.surveyors, + } + + def _get_user_permissions(self, org: Org, user: User) -> dict: + permissions = {} + org_permissions = self._get_permissions(org) + + for perm_name, org_field in org_permissions.items(): + if org_field.filter(pk=user.id).exists(): + permissions[perm_name] = True + + return permissions + + def _permission_is_unique_true(self, response, permission: str) -> bool: + permissions = { + "administrator": response.get("administrator"), + "viewer": response.get("viewer"), + "editor": response.get("editor"), + "surveyor": response.get("surveyor"), + } + false_valeues = [key for key, value in permissions.items() if not value] + + return len(false_valeues) == 3 and permission not in false_valeues + + def get_url_namespace(self): + return "user_permission" + + +class UserPermissionRetrieveTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + super().setUp() + + def test_user_permission_retrieve(self): + org = Org.objects.first() + user = User.objects.first() + + response_wrong_org = self.request_detail( + org_uuid="f7e70145-6d17-4384-a1f2-d6981397866a", user_email="wrong@weni.ai" + ) + + self.assertEqual(response_wrong_org.status_code, 404) + self.assertEqual(response_wrong_org.json().get("detail"), "Not found.") + + org.administrators.add(user) + + response_wrong_user = self.request_detail(org_uuid=org.uuid, user_email=0) + + self.assertEqual(response_wrong_user.status_code, 404) + self.assertEqual(response_wrong_user.json().get("detail"), "Not found.") + + response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + + self.assertTrue(response.get("administrator")) + self.assertTrue(self.permission_is_unique_true(response, "administrator")) + + org.administrators.remove(user) + org.viewers.add(user) + + response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + + self.assertTrue(response.get("viewer")) + self.assertTrue(self.permission_is_unique_true(response, "viewer")) + + org.viewers.remove(user) + org.editors.add(user) + + response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + + self.assertTrue(response.get("editor")) + self.assertTrue(self.permission_is_unique_true(response, "editor")) + + org.editors.remove(user) + org.surveyors.add(user) + + response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + + self.assertTrue(response.get("surveyor")) + self.assertTrue(self.permission_is_unique_true(response, "surveyor")) + + def permission_is_unique_true(self, response, permission: str) -> bool: + permissions = { + "administrator": response.get("administrator"), + "viewer": response.get("viewer"), + "editor": response.get("editor"), + "surveyor": response.get("surveyor"), + } + false_valeues = [key for key, value in permissions.items() if not value] + + return len(false_valeues) == 3 and permission not in false_valeues + + def get_url_namespace(self): + return "user_permission" + + +class UserUpdateTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + super().setUp() + + def test_user_language_update(self): + languages = [language[0] for language in settings.LANGUAGES] + + bad_language_response = self.request_patch(data={"language": "wrong"}, email=self.admin.email) + self.assertEqual(bad_language_response.status_code, 400) + + for language in languages: + self.request_patch(data={"language": language}, email=self.admin.email) + + user_language = User.objects.get(id=self.admin.id).get_settings().language + self.assertEqual(user_language, language) + + def test_update_user_lang_with_non_existent_user(self): + bad_user_response = self.request_patch(data={"language": "wrong"}, email="ssd") + self.assertEqual(bad_user_response.status_code, 404) + self.assertEqual(bad_user_response.json().get("detail"), "Not found.") + + def get_url_namespace(self): + return "flow_users-detail" + + +class UserRetrieveByEmailTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + super().setUp() + + def test_retrive_user_by_email(self): + response = self.request_get(email=self.admin.email).json() + response_user = User.objects.get(id=response.get("id")) + + self.assertEqual(response_user, self.admin) + + def get_url_namespace(self): + return "flow_users-detail" diff --git a/weni/internal/users/urls.py b/weni/internal/users/urls.py index 98a5381fb..8a07e6b02 100644 --- a/weni/internal/users/urls.py +++ b/weni/internal/users/urls.py @@ -1,10 +1,27 @@ +from django.urls import path + from rest_framework import routers -from weni.internal.users.views import UserViewSet +from weni.internal.users.views import UserViewSet, UserEndpoint, UserPermissionEndpoint router = routers.DefaultRouter() router.register(r"users", UserViewSet, basename="users") +flows_router = [ + path( + "flows-users/", UserEndpoint.as_view({"get": "retrieve", "patch": "partial_update"}), name="flow_users-detail" + ), +] + +user_permission_router = [ + path( + "user-permission/", + UserPermissionEndpoint.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}), + name="user_permission", + ), +] urlpatterns = router.urls +urlpatterns += flows_router +urlpatterns += user_permission_router diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index d85d9f693..dc3e86d80 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -1,21 +1,29 @@ -import imp from typing import TYPE_CHECKING +from django.conf import settings +from django.shortcuts import get_object_or_404 +from django.contrib.auth import get_user_model + +from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework import exceptions +from rest_framework import mixins +from rest_framework.exceptions import ValidationError from weni.internal.views import InternalGenericViewSet -from weni.internal.users.serializers import UserAPITokenSerializer +from weni.internal.users.serializers import UserAPITokenSerializer, UserSerializer, UserPermissionSerializer from temba.api.models import APIToken +from temba.orgs.models import Org if TYPE_CHECKING: from rest_framework.request import Request +User = get_user_model() -class UserViewSet(InternalGenericViewSet): +class UserViewSet(InternalGenericViewSet): @action(detail=False, methods=["GET"], url_path="api-token", serializer_class=UserAPITokenSerializer) def api_token(self, request: "Request", **kwargs): @@ -28,3 +36,109 @@ def api_token(self, request: "Request", **kwargs): raise exceptions.PermissionDenied() return Response(dict(user=api_token.user.email, org=api_token.org.uuid, api_token=api_token.key)) + + +class UserPermissionEndpoint(InternalGenericViewSet): + serializer_class = UserPermissionSerializer + # lookup_field = "org_id" + + def retrieve(self, request): + org = get_object_or_404(Org, uuid=request.query_params.get("org_uuid")) + user = get_object_or_404(User, email=request.query_params.get("user_email")) + + permissions = self._get_user_permissions(org, user) + serializer = self.get_serializer(permissions) + + return Response(serializer.data) + + def partial_update(self, request): + org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) + user = get_object_or_404(User, email=request.data.get("user_email")) + + self._validate_permission(org, request.data.get("permission", "")) + self._set_user_permission(org, user, request.data.get("permission", "")) + + permissions = self._get_user_permissions(org, user) + serializer = self.get_serializer(permissions) + + return Response(serializer.data) + + def destroy(self, request): + org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) + user = get_object_or_404(User, email=request.data.get("user_email")) + + self._validate_permission(org, request.data.get("permission", "")) + self._remove_user_permission(org, user, request.data.get("permission", "")) + + permissions = self._get_user_permissions(org, user) + serializer = self.get_serializer(permissions) + + return Response(serializer.data) + + def _remove_user_permission(self, org: Org, user: User, permission: str): + permissions = self._get_permissions(org) + permissions.get(permission).remove(user) + + def _set_user_permission(self, org: Org, user: User, permission: str): + permissions = self._get_permissions(org) + + for perm_name, org_field in permissions.items(): + if not perm_name == permission: + org_field.remove(user) + + permissions.get(permission).add(user) + + def _validate_permission(self, org: Org, permission: str): + permissions_keys = self._get_permissions(org).keys() + + if permission not in permissions_keys: + raise ValidationError(detail=f"{permission} is not a valid permission!") + + def _get_permissions(self, org: Org) -> dict: + return { + "administrator": org.administrators, + "viewer": org.viewers, + "editor": org.editors, + "surveyor": org.surveyors, + } + + def _get_user_permissions(self, org: Org, user: User) -> dict: + permissions = {} + org_permissions = self._get_permissions(org) + + for perm_name, org_field in org_permissions.items(): + if org_field.filter(pk=user.id).exists(): + permissions[perm_name] = True + + return permissions + + +class UserEndpoint(InternalGenericViewSet, mixins.RetrieveModelMixin): + + serializer_class = UserSerializer + queryset = User.objects.all() + lookup_field = "uuid" + + + def partial_update(self, request): + instance = get_object_or_404(User, email=request.query_params.get("email")) + + if request.data.get("language") not in [language[0] for language in settings.LANGUAGES]: + raise ValidationError("Invalid argument: language") + + user_settings = instance.get_settings() + user_settings.language = request.data.get("language") + user_settings.save() + + return Response(status=status.HTTP_200_OK) + + def retrieve(self, request): + if not request.query_params.get("email"): + raise ValidationError(detail="empty email") + + user = User.objects.get_or_create( + email=request.query_params.get("email"), defaults={"username": request.query_params.get("email")} + ) + + serializer = self.get_serializer(user[0]) + return Response(serializer.data) From 4b783e965a43c0afa900dbf03d6dbfcb99cbfcaf Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:13:56 -0300 Subject: [PATCH 003/101] [Conversion gRPC-REST] Channels endpoint (#169) * feat: Add Channel endpoint * fix: auth import * fix: adjust test to new route * fix: Add org and is_active to read_only_fields * fix: add channel.release() to destroy --- weni/internal/channel/serializers.py | 152 ++++++++++++++++ weni/internal/channel/tests.py | 248 +++++++++++++++++++++++++++ weni/internal/channel/urls.py | 10 ++ weni/internal/channel/views.py | 71 ++++++++ weni/internal/urls.py | 2 + 5 files changed, 483 insertions(+) create mode 100644 weni/internal/channel/serializers.py create mode 100644 weni/internal/channel/tests.py create mode 100644 weni/internal/channel/urls.py create mode 100644 weni/internal/channel/views.py diff --git a/weni/internal/channel/serializers.py b/weni/internal/channel/serializers.py new file mode 100644 index 000000000..ee5b34638 --- /dev/null +++ b/weni/internal/channel/serializers.py @@ -0,0 +1,152 @@ +import re + +from django.http.response import HttpResponseRedirect +from django.contrib.auth.models import User +from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.messages.middleware import MessageMiddleware +from django.shortcuts import get_object_or_404 +from django.test import RequestFactory + +from rest_framework import serializers +from rest_framework import exceptions + +from weni.grpc.core import serializers as weni_serializers + +from temba.channels.models import Channel +from temba.orgs.models import Org +from temba.utils import analytics + + +class ChannelWACSerializer(serializers.Serializer): + user = weni_serializers.UserEmailRelatedField(required=True, write_only=True) + org = weni_serializers.OrgUUIDRelatedField(required=True, write_only=True) + phone_number_id = serializers.CharField(required=True, write_only=True) + uuid = serializers.CharField(read_only=True) + name = serializers.CharField(read_only=True) + address = serializers.CharField(read_only=True) + config = serializers.JSONField(required=True) + + class Meta: + model = Channel + proto_class = Channel + fields = ("user", "org", "phone_number_id", "uuid", "name", "address", "config") + + def validate_phone_number_id(self, value): + if Channel.objects.filter(is_active=True, address=value).exists(): + raise serializers.ValidationError( + { + "error": "a Channel with that 'phone_number_id' alredy exists", + "error_type": "WhatsApp.config.error.channel_already_exists", + } + ) + return value + + def validate_config(self, value): + if "wa_verified_name" not in value: + raise serializers.ValidationError({"error": "You need to define a wa_verified_name in config"}) + return value + + def create(self, validated_data): + channel_type = Channel.get_type_from_code("WAC") + schemes = channel_type.schemes + + org = validated_data.get("org") + name = validated_data.get("name") + phone_number_id = validated_data.get("phone_number_id") + config = validated_data.get("config", {}) + user = validated_data.get("user") + + channel = Channel.objects.create( + org=org, + country=None, + channel_type=channel_type.code, + name=config.get("wa_verified_name"), + address=phone_number_id, + config=config, + role=Channel.DEFAULT_ROLE, + schemes=schemes, + created_by=user, + modified_by=user, + ) + + analytics.track(user, "temba.channel_created", dict(channel_type=channel_type.code)) + + return channel + + +class CreateChannelSerializer(serializers.Serializer): + uuid = serializers.CharField(read_only=True) + name = serializers.CharField(read_only=True) + config = serializers.JSONField(read_only=True) + address = serializers.CharField(read_only=True) + + user = serializers.EmailField(required=True, write_only=True) + org = serializers.CharField(required=True, write_only=True) + data = serializers.JSONField(write_only=True) + channeltype_code = serializers.CharField(required=True, write_only=True) + + def create(self, validated_data): + data = validated_data.get("data") + + user = get_object_or_404(User, email=validated_data.get("user")) + org = get_object_or_404(Org, uuid=validated_data.get("org")) + + channel_type = Channel.get_type_from_code(validated_data.get("channeltype_code")) + + if channel_type is None: + channel_type_code = validated_data.get("channeltype_code") + raise exceptions.ValidationError(f"No channels found with '{channel_type_code}' code") + + url = self.create_channel(user, org, data, channel_type) + + if url is None: + raise exceptions.ValidationError(f"Url not created") + + if "/users/login/?next=" in url: + raise exceptions.ValidationError(f"User: {user.email} do not have permission in Org: {org.uuid}") + + regex = "[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}" + channe_uuid = re.findall(regex, url)[0] + channel = Channel.objects.get(uuid=channe_uuid) + + return channel + + def create_channel(self, user: User, org: Org, data: dict, channel_type) -> str: + factory = RequestFactory() + url = f"channels/types/{channel_type.slug}/claim" + + request = factory.post(url, data) + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + + user._org = org + request.user = user + response = MessageMiddleware(channel_type.claim_view.as_view(channel_type=channel_type))(request) + + if isinstance(response, HttpResponseRedirect): + return response.url + + +class ChannelSerializer(serializers.ModelSerializer): + config = serializers.JSONField() + + class Meta: + extra_kwargs = { + 'org': {'read_only': True}, + 'is_active': {'read_only': True}, + } + model = Channel + fields = ( + "uuid", + "name", + "config", + "address", + "org", + "is_active", + ) + + def to_representation(self, instance): + ret = super().to_representation(instance) + ret['org'] = instance.uuid + return ret diff --git a/weni/internal/channel/tests.py b/weni/internal/channel/tests.py new file mode 100644 index 000000000..2fec93c61 --- /dev/null +++ b/weni/internal/channel/tests.py @@ -0,0 +1,248 @@ +import json +from abc import ABC, abstractmethod +from uuid import uuid1 +from unittest.mock import patch + +from django.contrib.auth.models import Group +from django.urls import reverse +from django.utils import timezone as tz +from django.utils.http import urlencode +from django.contrib.auth.models import User + +from temba.api.models import APIToken + +from temba.flows.models import Flow +from temba.orgs.models import Org, OrgRole +from temba.contacts.models import Contact +from temba.channels.models import Channel + +from temba.tests import TembaTest, mock_mailroom + + +class TembaRequestMixin(ABC): + def reverse(self, viewname, kwargs=None, query_params=None): + url = reverse(viewname, kwargs=kwargs) + + if query_params: + return "%s?%s" % (url, urlencode(query_params)) + else: + return url + + def request_get(self, **query_params): + url = self.reverse(self.get_url_namespace(), query_params=query_params) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_detail(self, uuid): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_post(self, data): + url = reverse(self.get_url_namespace()) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.post( + url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + def request_delete(self, uuid): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.delete(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + @abstractmethod + def get_url_namespace(self): + ... + + +class CreateWACServiceTest(TembaTest, TembaRequestMixin): + def setUp(self): + self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") + self.org = Org.objects.create( + name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + ) + self.org.add_user(self.user, OrgRole.ADMINISTRATOR) + + self.config = { + "wa_number": "5561995743921", + "wa_verified_name": "Weni Test", + "wa_waba_id": "2433443436435435", + "wa_currency": "USD", + "wa_business_id": "3443243234254322", + "wa_message_template_namespace": "6b186dea_ds6d_44s2_b9xd_de87a12212e5", + } + + super().setUp() + + @patch("temba.channels.types.whatsapp_cloud.type.WhatsAppCloudType.activate") + def test_create_whatsapp_cloud_channel(self, mock): + mock.return_value = None + + phone_number_id = "5426423432" + + payload = { + "org": str(self.org.uuid), + "user": self.user.email, + "phone_number_id": phone_number_id, + "config": self.config, + } + + channel = self.request_post(data=payload).json() + + self.assertTrue("uuid" in channel) + self.assertEqual(channel.get("name"), self.config.get("wa_verified_name")) + self.assertEqual(channel.get("config"), self.config) + self.assertEqual(channel.get("address"), phone_number_id) + + @patch("temba.channels.types.whatsapp_cloud.type.WhatsAppCloudType.activate") + def test_create_whatsapp_cloud_channel_invalid_address(self, mock): + mock.return_value = None + + phone_number_id = "5426423432" + + payload = { + "org": str(self.org.uuid), + "user": self.user.email, + "phone_number_id": phone_number_id, + "config": self.config, + } + + self.request_post(data=payload) + + channel = self.request_post(data=payload) + + self.assertEqual(channel.status_code, 400) + self.assertEqual( + channel.json().get("phone_number_id").get("error_type"), "WhatsApp.config.error.channel_already_exists" + ) + + def get_url_namespace(self): + return "channel-create-wac" + + +class ReleaseChannelTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.org_user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + self.my_org = Org.objects.create( + name="Weni", timezone="Africa/Kigali", created_by=self.org_user, modified_by=self.org_user + ) + + super().setUp() + + self.channel_obj = Channel.create(self.my_org, self.org_user, None, "WWC", "Test WWC") + + def test_released_channel_is_active_equal_to_false(self): + self.request_delete(uuid=str(self.channel_obj.uuid)) + self.assertFalse(Channel.objects.get(id=self.channel_obj.id).is_active) + + def get_url_namespace(self): + return "channel-detail" + + +class CreateChannelTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.org_user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") + self.my_org = Org.objects.create( + name="Weni", timezone="America/Sao_Paulo", created_by=self.org_user, modified_by=self.org_user + ) + self.my_org.add_user(self.org_user, OrgRole.ADMINISTRATOR) + + super().setUp() + + def test_create_weni_web_chat_channel(self): + payload = { + "user": self.org_user.email, + "org": str(self.my_org.uuid), + "data": {"name": "test", "base_url": "https://weni.ai"}, + "channeltype_code": "WWC", + } + + response = self.request_post(data=payload).json() + + print(response) + + channel = Channel.objects.get(uuid=response.get("uuid")) + self.assertEqual(channel.address, response.get("address")) + self.assertEqual(channel.name, response.get("name")) + self.assertEqual(channel.config.get("base_url"), "https://weni.ai") + self.assertEqual(channel.org, self.my_org) + self.assertEqual(channel.created_by, self.org_user) + self.assertEqual(channel.modified_by, self.org_user) + self.assertEqual(channel.channel_type, "WWC") + + def get_url_namespace(self): + return "channel-list" + + +class RetrieveChannelTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") + self.org = Org.objects.create( + name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + ) + + super().setUp() + + self.channel_obj = Channel.create( + self.org, self.user, None, "WWC", "Test WWC", "test", {"fake_key": "fake_value"} + ) + + def test_channel_retrieve_returned_fields(self): + response = self.request_detail(uuid=str(self.channel_obj.uuid)).json() + + self.assertEqual(response.get("name"), self.channel_obj.name) + self.assertEqual(response.get("address"), self.channel_obj.address) + self.assertEqual(response.get("config"), self.channel_obj.config) + + def get_url_namespace(self): + return "channel-detail" + + +class ListChannelTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.admin = User.objects.create_user( + username="testuseradmin", password="123", email="test@weni.ai", is_superuser=True + ) + self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") + self.orgs = [ + Org.objects.create( + name=f"Org {org}", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + ) + for org in range(2) + ] + + for channel in range(6): + Channel.create( + self.orgs[0] if channel % 2 == 0 else self.orgs[1], + self.user, + None, + "WWC" if channel % 2 == 0 else "VK", + f"Test {channel}", + "test", + {}, + ) + + super().setUp() + + def test_list_all_channels(self): + response = self.request_get().json() + self.assertEqual(len(response), 7) + + def test_list_channels_filtered_by_type(self): + response = self.request_get(channel_type="WWC").json() + self.assertEqual(len(response), 3) + + def test_list_channels_filtered_by_org_uuid(self): + org_uuid = str(self.orgs[0].uuid) + response = self.request_get(org=org_uuid).json() + self.assertEqual(len(response), 3) + + channel = Channel.objects.get(uuid=response[0].get("uuid")) + self.assertEqual(channel.org, self.orgs[0]) + + def get_url_namespace(self): + return "channel-list" diff --git a/weni/internal/channel/urls.py b/weni/internal/channel/urls.py new file mode 100644 index 000000000..92a3de099 --- /dev/null +++ b/weni/internal/channel/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework import routers + +from .views import ChannelEndpoint + +router = routers.SimpleRouter() +router.register("channel", ChannelEndpoint, basename="channel") + +urlpatterns = format_suffix_patterns(router.urls, allowed=["json", "api"]) diff --git a/weni/internal/channel/views.py b/weni/internal/channel/views.py new file mode 100644 index 000000000..9b1678caf --- /dev/null +++ b/weni/internal/channel/views.py @@ -0,0 +1,71 @@ +from django.contrib.auth import get_user_model +from django.shortcuts import get_object_or_404 +from django.http import JsonResponse + +from rest_framework.response import Response +from rest_framework.decorators import action +from rest_framework import viewsets +from rest_framework import status +from rest_framework.pagination import PageNumberPagination + +from weni.internal.views import InternalGenericViewSet +from temba.channels.models import Channel + +from .serializers import ChannelSerializer, CreateChannelSerializer, ChannelWACSerializer + +User = get_user_model() + +class ChannelEndpoint(viewsets.ModelViewSet, InternalGenericViewSet): + serializer_class = ChannelSerializer + lookup_field = "uuid" + + def get_queryset(self): + channel_type = self.request.query_params.get("channel_type") + org = self.request.query_params.get("org") + + queryset = Channel.objects.all() + + if channel_type is not None: + return queryset.filter(channel_type=channel_type) + + if org is not None: + return queryset.filter(org__uuid=org) + + return queryset + + def retrieve(self, request, uuid=None): + try: + channel = Channel.objects.get(uuid=uuid) + except Channel.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + return JsonResponse(data=self.get_serializer(channel).data, status=status.HTTP_200_OK) + + def create(self, request): + serializer = CreateChannelSerializer(data=request.data) + + if not serializer.is_valid(): + return JsonResponse(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + serializer.save() + + return JsonResponse(data=serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, uuid=None): + channel = get_object_or_404(Channel, uuid=uuid) + user = get_object_or_404(User, email=request.data.get("user")) + + channel.release(user) + + return Response(status=status.HTTP_200_OK) + + @action(methods=["POST"], detail=False) + def create_wac(self, request): + serializer = ChannelWACSerializer(data=request.data) + + if not serializer.is_valid(): + return JsonResponse(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + serializer.save() + + return JsonResponse(data=serializer.data, status=status.HTTP_200_OK) diff --git a/weni/internal/urls.py b/weni/internal/urls.py index 6d02d4025..4550a9f43 100644 --- a/weni/internal/urls.py +++ b/weni/internal/urls.py @@ -14,6 +14,7 @@ from weni.internal.flows.urls import urlpatterns as flows_urls from weni.internal.users.urls import urlpatterns as users_urls from weni.internal.tickets.urls import urlpatterns as tickets_urls +from weni.internal.channel.urls import urlpatterns as channel_urls from weni.internal.statistic.urls import urlpatterns as statistics_urls @@ -22,6 +23,7 @@ internal_urlpatterns += flows_urls internal_urlpatterns += users_urls internal_urlpatterns += tickets_urls +internal_urlpatterns += channel_urls internal_urlpatterns += statistics_urls From 5b4724e1bcb278c1a51ca238fd9a0b4587bf6d5d Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:14:13 -0300 Subject: [PATCH 004/101] feat: Add org endpoint (#170) --- weni/internal/orgs/serializers.py | 67 ++++++ weni/internal/orgs/tests.py | 384 ++++++++++++++++++++++++++++++ weni/internal/orgs/urls.py | 4 +- weni/internal/orgs/views.py | 123 +++++++++- 4 files changed, 575 insertions(+), 3 deletions(-) create mode 100644 weni/internal/orgs/tests.py diff --git a/weni/internal/orgs/serializers.py b/weni/internal/orgs/serializers.py index 958a227af..596845114 100644 --- a/weni/internal/orgs/serializers.py +++ b/weni/internal/orgs/serializers.py @@ -2,6 +2,7 @@ from django.contrib.auth import get_user_model from temba.orgs.models import Org +from weni.grpc.core import serializers as weni_serializers User = get_user_model() @@ -38,3 +39,69 @@ def create(self, validated_data): org.initialize(sample_flows=False, internal_ticketer=False) return org + + +class OrgSerializer(serializers.ModelSerializer): + + users = serializers.SerializerMethodField() + timezone = serializers.CharField() + + def set_user_permission(self, user: dict, permission: str) -> dict: + user["permission_type"] = permission + return user + + def get_users(self, org: Org): + values = ["id", "email", "username", "first_name", "last_name"] + + administrators = list(org.administrators.all().values(*values)) + viewers = list(org.viewers.all().values(*values)) + editors = list(org.editors.all().values(*values)) + surveyors = list(org.surveyors.all().values(*values)) + + administrators = list(map(lambda user: self.set_user_permission(user, "administrator"), administrators)) + viewers = list(map(lambda user: self.set_user_permission(user, "viewer"), viewers)) + editors = list(map(lambda user: self.set_user_permission(user, "editor"), editors)) + surveyors = list(map(lambda user: self.set_user_permission(user, "surveyor"), surveyors)) + + users = administrators + viewers + editors + surveyors + + return users + + class Meta: + model = Org + fields = ["id", "name", "uuid", "timezone", "date_format", "users"] + + +class OrgCreateSerializer(serializers.ModelSerializer): + + user_email = serializers.EmailField() + + class Meta: + model = Org + fields = ["name", "timezone", "user_email"] + + +class OrgUpdateSerializer(serializers.ModelSerializer): + + uuid = serializers.CharField(read_only=True) + modified_by = weni_serializers.UserEmailRelatedField(required=False, write_only=True) + timezone = serializers.CharField(required=False) + name = serializers.CharField(required=False) + plan_end = serializers.DateTimeField(required=False) + + class Meta: + model = Org + fields = [ + "uuid", + "modified_by", + "name", + "timezone", + "date_format", + "plan", + "plan_end", + "brand", + "is_anon", + "is_multi_user", + "is_multi_org", + "is_suspended", + ] diff --git a/weni/internal/orgs/tests.py b/weni/internal/orgs/tests.py new file mode 100644 index 000000000..1b5c9569d --- /dev/null +++ b/weni/internal/orgs/tests.py @@ -0,0 +1,384 @@ +import json +from abc import ABC, abstractmethod +from datetime import datetime +import random +from unittest.mock import patch + +from django.contrib.auth.models import Group +from django.contrib.auth.models import User +from django.conf import settings +from django.utils.http import urlencode +from django.urls import reverse + +from temba.orgs.models import Org + +from temba.api.models import APIToken +from temba.tests import TembaTest + + +class TembaRequestMixin(ABC): + def reverse(self, viewname, kwargs=None, query_params=None): + url = reverse(viewname, kwargs=kwargs) + + if query_params: + return "%s?%s" % (url, urlencode(query_params)) + else: + return url + + def request_get(self, **query_params): + url = self.reverse(self.get_url_namespace(), query_params=query_params) + url = url.replace("channel", "channel.json") + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_detail(self, uuid): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_post(self, data): + url = reverse(self.get_url_namespace()) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.post( + url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + def request_delete(self, uuid, **query_params): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}, query_params=query_params) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.delete(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_patch(self, uuid, data): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.patch( + url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + @abstractmethod + def get_url_namespace(self): + ... + + +class OrgListTest(TembaTest, TembaRequestMixin): + + WRONG_ID = -1 + WRONG_UUID = "31313-dasda-dasdasd-23123" + WRONG_EMAIL = "wrong@email.com" + + def setUp(self): + User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + User.objects.create_user(username="weniuser", password="123", email="wene@user.com") + + user = User.objects.get(username="testuser") + + Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + + super().setUp() + + def test_list_orgs(self): + response = self.request_get() + self.assertEqual(response.status_code, 400) + + response = self.request_get(user_email="wrong@email.com") + self.assertEqual(response.status_code, 404) + + orgs = Org.objects.all() + user = User.objects.get(username="testuser") + + weni_org = orgs.get(name="Weni") + temba_org = orgs.get(name="Tembinha") + test_org = orgs.get(name="Test") + + weni_org.administrators.add(user) + weni_org.is_active = False + weni_org.save(update_fields=["is_active"]) + + response = self.request_get(user_email=user.email).json() + self.assertEquals(len(response), 0) + + weni_org.is_active = True + weni_org.save(update_fields=["is_active"]) + + response = self.request_get(user_email=user.email).json() + self.assertEquals(len(response), 1) + + temba_org.viewers.add(user) + response = self.request_get(user_email=user.email).json() + self.assertEquals(len(response), 2) + + test_org.editors.add(user) + response = self.request_get(user_email=user.email).json() + self.assertEquals(len(response), 3) + + def test_list_users_on_org(self): + org = Org.objects.get(name="Tembinha") + + testuser = User.objects.get(username="testuser") + weniuser = User.objects.get(username="weniuser") + + org.administrators.add(testuser) + response = self.request_get(user_email=testuser.email).json() + self.assertEquals(len(response[0].get("users")), 1) + + org.administrators.add(weniuser) + response = self.request_get(user_email=testuser.email).json() + self.assertEquals(len(response[0].get("users")), 2) + + def get_url_namespace(self): + return "orgs-list" + + +class OrgCreateTest(TembaTest, TembaRequestMixin): + + WRONG_ID = -1 + WRONG_UUID = "31313-dasda-dasdasd-23123" + WRONG_EMAIL = "wrong@email.com" + + def setUp(self): + + User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + User.objects.create_user(username="weniuser", password="123", email="wene@user.com") + + user = User.objects.get(username="testuser") + + Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + + super().setUp() + + @patch("temba.orgs.models.Org.create_sample_flows") + def test_create_org(self, mock): + + org_name = "TestCreateOrg" + user = User.objects.first() + + response = self.request_post(data=dict(name=org_name, timezone="Wrong/Zone", user_email=user.email)).json() + + self.assertEqual(response.get("timezone")[0], '"Wrong/Zone" is not a valid choice.') + + response = self.request_post( + data=dict(name=org_name, timezone="Africa/Kigali", user_email="newemail@email.com") + ).json() + + newuser_qs = User.objects.filter(email="newemail@email.com") + + self.assertTrue(newuser_qs.exists()) + + newuser = newuser_qs.first() + + orgs = Org.objects.filter(name=org_name) + org = orgs.first() + + self.assertEquals(orgs.count(), 1) + + created_by = org.created_by + modified_by = org.modified_by + administrators = org.administrators.all() + administrator = administrators.get(pk=newuser.pk) + + self.assertEquals(created_by, newuser) + self.assertEquals(modified_by, newuser) + self.assertFalse(org.uses_topups) + + self.assertEquals(administrators.count(), 1) + self.assertEquals(administrator, newuser) + + response = self.request_post( + data=dict(name="neworg", timezone="Africa/Kigali", user_email="newemail@email.com") + ).json() + + self.assertEqual(User.objects.filter(email="newemail@email.com").count(), 1) + + def get_url_namespace(self): + return "orgs-list" + + +class OrgRetrieveTest(TembaTest, TembaRequestMixin): + + WRONG_ID = -1 + WRONG_UUID = "31313-dasda-dasdasd-23123" + WRONG_EMAIL = "wrong@email.com" + + def setUp(self): + + User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + User.objects.create_user(username="weniuser", password="123", email="wene@user.com") + + user = User.objects.get(username="testuser") + + Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + + super().setUp() + + def test_retrieve_org(self): + org = Org.objects.last() + user = User.objects.last() + + permission_types = ("administrator", "viewer", "editor", "surveyor") + + random_permission = random.choice(permission_types) + + if random_permission == "administrator": + org.administrators.add(user) + if random_permission == "viewer": + org.viewers.add(user) + if random_permission == "editor": + org.editors.add(user) + if random_permission == "surveyor": + org.surveyors.add(user) + + org_uuid = str(org.uuid) + org_timezone = str(org.timezone) + + response = self.request_detail(uuid=org_uuid).json() + + response_user = response.get("users")[-1] + + self.assertEqual(response.get("id"), org.id) + self.assertEqual(response.get("name"), org.name) + self.assertEqual(response.get("uuid"), org_uuid) + self.assertEqual(org_timezone, response.get("timezone")) + self.assertEqual(org.date_format, response.get("date_format")) + + self.assertEqual(user.id, response_user.get("id")) + self.assertEqual(user.email, response_user.get("email")) + self.assertEqual(user.username, response_user.get("username")) + + self.assertEqual(response_user.get("permission_type"), random_permission) + + def get_url_namespace(self): + return "orgs-detail" + + +class OrgDestroyTest(TembaTest, TembaRequestMixin): + + WRONG_ID = -1 + WRONG_UUID = "31313-dasda-dasdasd-23123" + WRONG_EMAIL = "wrong@email.com" + + def setUp(self): + + User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + User.objects.create_user(username="weniuser", password="123", email="wene@user.com") + + user = User.objects.get(username="testuser") + + Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + + super().setUp() + + def test_destroy_org(self): + org = Org.objects.last() + is_active = org.is_active + modified_by = org.modified_by + + weniuser = User.objects.get(username="weniuser") + + self.request_delete(uuid=str(org.uuid), user_email=weniuser.email) + + destroyed_org = Org.objects.get(id=org.id) + + self.assertFalse(destroyed_org.is_active) + self.assertNotEquals(is_active, destroyed_org.is_active) + self.assertEquals(weniuser, destroyed_org.modified_by) + self.assertNotEquals(modified_by, destroyed_org.modified_by) + + def get_url_namespace(self): + return "orgs-detail" + + +class OrgUpdateTest(TembaTest, TembaRequestMixin): + + WRONG_ID = -1 + WRONG_UUID = "31313-dasda-dasdasd-23123" + WRONG_EMAIL = "wrong@email.com" + + def setUp(self): + + User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + User.objects.create_user(username="weniuser", password="123", email="wene@user.com") + + user = User.objects.get(username="testuser") + + Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + + super().setUp() + + def test_update_org(self): + org = Org.objects.first() + + permission_error_message = f"User: {self.user.id} has no permission to update Org: {org.uuid}" + + response = self.request_patch(uuid=str(org.uuid), data=dict(modified_by=self.user.email)).json() + + self.assertEqual(response[0], permission_error_message) + + self.user.is_superuser = True + self.user.save() + + org.administrators.add(self.user) + + update_fields = { + "name": "NewOrgName", + "timezone": "America/Maceio", + "date_format": "M", + "plan": settings.INFINITY_PLAN, + "plan_end": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "brand": "push.ia", + "is_anon": True, + "is_multi_user": True, + "is_multi_org": True, + "is_suspended": True, + "modified_by": self.user.email, + } + + response = self.request_patch(uuid=str(org.uuid), data=update_fields).json() + + updated_org = Org.objects.get(pk=org.pk) + + self.assertEquals(update_fields.get("name"), updated_org.name) + self.assertNotEquals(org.name, updated_org.name) + + self.assertEquals(update_fields.get("timezone"), str(updated_org.timezone)) + self.assertNotEquals(org.timezone, updated_org.timezone) + + self.assertEquals(update_fields.get("date_format"), updated_org.date_format) + self.assertNotEquals(org.date_format, updated_org.date_format) + + self.assertEquals(updated_org.plan, settings.INFINITY_PLAN) + self.assertNotEquals(org.plan, updated_org.plan) + self.assertFalse(updated_org.uses_topups) + self.assertEquals(updated_org.plan_end, None) + + self.assertEquals(update_fields.get("brand"), updated_org.brand) + self.assertNotEquals(org.brand, updated_org.brand) + + self.assertEquals(update_fields.get("is_anon"), updated_org.is_anon) + self.assertNotEquals(org.is_anon, updated_org.is_anon) + + self.assertEquals(update_fields.get("is_multi_user"), updated_org.is_multi_user) + self.assertNotEquals(org.is_multi_user, updated_org.is_multi_user) + + self.assertEquals(update_fields.get("is_multi_org"), updated_org.is_multi_org) + self.assertNotEquals(org.is_multi_org, updated_org.is_multi_org) + + self.assertEquals(update_fields.get("is_suspended"), updated_org.is_suspended) + self.assertNotEquals(org.is_suspended, updated_org.is_suspended) + + def get_url_namespace(self): + return "orgs-detail" diff --git a/weni/internal/orgs/urls.py b/weni/internal/orgs/urls.py index eedbefc13..d544881d1 100644 --- a/weni/internal/orgs/urls.py +++ b/weni/internal/orgs/urls.py @@ -1,10 +1,10 @@ from rest_framework import routers -from weni.internal.orgs.views import TemplateOrgViewSet +from weni.internal.orgs.views import TemplateOrgViewSet, OrgViewSet router = routers.DefaultRouter() router.register(r"template-orgs", TemplateOrgViewSet, basename="template-orgs") - +router.register(r"orgs", OrgViewSet, basename="orgs") urlpatterns = router.urls diff --git a/weni/internal/orgs/views.py b/weni/internal/orgs/views.py index cd15bbe4e..9659a9b2b 100644 --- a/weni/internal/orgs/views.py +++ b/weni/internal/orgs/views.py @@ -1,8 +1,129 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.shortcuts import get_object_or_404 + +from rest_framework.response import Response +from rest_framework import viewsets +from rest_framework import exceptions +from rest_framework import status from rest_framework.mixins import CreateModelMixin from weni.internal.views import InternalGenericViewSet -from weni.internal.orgs.serializers import TemplateOrgSerializer +from weni.internal.orgs.serializers import ( + TemplateOrgSerializer, + OrgCreateSerializer, + OrgSerializer, + OrgUpdateSerializer, +) + +from temba.orgs.models import Org class TemplateOrgViewSet(CreateModelMixin, InternalGenericViewSet): serializer_class = TemplateOrgSerializer + + +class OrgViewSet(viewsets.ModelViewSet, InternalGenericViewSet): + queryset = Org.objects + lookup_field = "uuid" + + def list(self, request): + user = self.get_user(request) + orgs = self.get_orgs(user) + + serializer = OrgSerializer(orgs, many=True) + + return Response(serializer.data) + + def create(self, request): + serializer = OrgCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user, created = User.objects.get_or_create( + email=request.data.get("user_email"), defaults={"username": request.data.get("user_email")} + ) + + org = Org.objects.create( + name=request.data.get("name"), + timezone=request.data.get("timezone"), + created_by=user, + modified_by=user, + plan="infinity", + ) + + org.administrators.add(user) + org.initialize() + + org_serializer = OrgSerializer(org) + + return Response(org_serializer.data) + + def retrieve(self, request, uuid=None): + org = get_object_or_404(Org, uuid=uuid) + serializer = OrgSerializer(org) + + return Response(serializer.data) + + def destroy(self, request, uuid=None): + org = get_object_or_404(Org, uuid=uuid) + user = get_object_or_404(User, email=request.query_params.get("user_email")) + + self.pre_destroy(org, user) + org.release(user) + + return Response(status=status.HTTP_204_NO_CONTENT) + + def partial_update(self, request, uuid=None): + org = get_object_or_404(Org, uuid=uuid) + + serializer = OrgUpdateSerializer(org, data=request.data) + serializer.is_valid(raise_exception=True) + + modified_by = serializer.validated_data.get("modified_by", None) + plan = serializer.validated_data.get("plan", None) + + if modified_by and not self._user_has_permisson(modified_by, org) and not modified_by.is_superuser: + raise exceptions.ValidationError( + f"User: {modified_by.pk} has no permission to update Org: {org.uuid}", + ) + + if plan is not None and plan == settings.INFINITY_PLAN: + org.uses_topups = False + org.plan_end = None + + serializer.save(plan_end=None) + return Response(serializer.data) + + serializer.save() + return Response(serializer.data) + + def pre_destroy(self, org: Org, user: User): + if user.id and user.id > 0 and hasattr(org, "modified_by_id"): + org.modified_by = user + + # Interim fix, remove after implementation in the model. + org.save(update_fields=["modified_by"]) + + def get_user(self, request): + user_email = request.query_params.get("user_email") + + if not user_email: + raise exceptions.ValidationError("Email cannot be null") + + return get_object_or_404(User, email=request.query_params.get("user_email")) + + def _user_has_permisson(self, user: User, org: Org) -> bool: + return ( + user.org_admins.filter(pk=org.pk) + or user.org_viewers.filter(pk=org.pk) + or user.org_editors.filter(pk=org.pk) + or user.org_surveyors.filter(pk=org.pk) + ) + + def get_orgs(self, user: User): + admins = user.org_admins.filter(is_active=True) + viewers = user.org_viewers.filter(is_active=True) + editors = user.org_editors.filter(is_active=True) + surveyors = user.org_surveyors.filter(is_active=True) + + return admins.union(viewers, editors, surveyors) From cdd106a75e12e56bb4aed13e37d83364fa863e89 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:23:31 -0300 Subject: [PATCH 005/101] Fix/Remove pagination_class from internal views (#179) * update: 1.0.31 * fix: remove pagination_class of InternalGenericViewSet --- weni/internal/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/weni/internal/views.py b/weni/internal/views.py index a24bde9ad..f006289e7 100644 --- a/weni/internal/views.py +++ b/weni/internal/views.py @@ -9,5 +9,6 @@ class InternalGenericViewSet(GenericViewSet): authentication_classes = [InternalOIDCAuthentication] permission_classes = [IsAuthenticated, CanCommunicateInternally] + pagination_class = None renderer_classes = [JSONRenderer] throttle_classes = [] From e443bc9501162629fdf5735eab8a7805b033c1e1 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:23:45 -0300 Subject: [PATCH 006/101] feat: Add Classifier endpoint (#173) --- weni/internal/classifier/serializers.py | 35 ++++ weni/internal/classifier/tests.py | 230 ++++++++++++++++++++++++ weni/internal/classifier/urls.py | 10 ++ weni/internal/classifier/views.py | 96 ++++++++++ weni/internal/urls.py | 2 + 5 files changed, 373 insertions(+) create mode 100644 weni/internal/classifier/serializers.py create mode 100644 weni/internal/classifier/tests.py create mode 100644 weni/internal/classifier/urls.py create mode 100644 weni/internal/classifier/views.py diff --git a/weni/internal/classifier/serializers.py b/weni/internal/classifier/serializers.py new file mode 100644 index 000000000..dbd92d497 --- /dev/null +++ b/weni/internal/classifier/serializers.py @@ -0,0 +1,35 @@ +from rest_framework import serializers + +from temba.classifiers.models import Classifier +from weni.protobuf.flows import classifier_pb2 +from weni.grpc.core import serializers as weni_serializers + + +class ClassifierSerializer(serializers.Serializer): + + uuid = serializers.UUIDField(read_only=True) + is_active = serializers.BooleanField(read_only=True) + classifier_type = serializers.CharField(required=True) + name = serializers.CharField(required=True) + access_token = weni_serializers.SerializerMethodCharField(required=True) + user = weni_serializers.UserEmailRelatedField(write_only=True) + org = weni_serializers.OrgUUIDRelatedField(write_only=True) + + def get_access_token(self, instance): + return instance.config.get("access_token") + + def create(self, validated_data: dict) -> Classifier: + config = dict(access_token=validated_data["access_token"]) + validated_data.pop("access_token") + + return Classifier.create(config=config, **validated_data) + + class Meta: + model = Classifier + proto_class = classifier_pb2.Classifier + fields = ["uuid", "is_active", "classifier_type", "name", "access_token", "org", "user"] + + +class ClassifierDeleteSerializer(serializers.Serializer): + uuid = serializers.UUIDField(required=True) + user = weni_serializers.UserEmailRelatedField(required=True) diff --git a/weni/internal/classifier/tests.py b/weni/internal/classifier/tests.py new file mode 100644 index 000000000..8245bb076 --- /dev/null +++ b/weni/internal/classifier/tests.py @@ -0,0 +1,230 @@ +import json +from abc import ABC, abstractmethod +from unittest.mock import patch +from django.contrib.auth.models import User + +from django.contrib.auth.models import Group +from django.urls import reverse +from django.utils.http import urlencode +from django.contrib.auth.models import User + +from temba.api.models import APIToken + +from temba.tests import TembaTest, mock_mailroom +from temba.orgs.models import Org +from temba.classifiers.models import Classifier, Intent +from temba.classifiers.types.wit import WitType +from temba.classifiers.types.luis import LuisType +from weni.protobuf.flows import classifier_pb2, classifier_pb2_grpc + + +class TembaRequestMixin(ABC): + def reverse(self, viewname, kwargs=None, query_params=None): + url = reverse(viewname, kwargs=kwargs) + + if query_params: + return "%s?%s" % (url, urlencode(query_params)) + else: + return url + + def request_get(self, **query_params): + url = self.reverse(self.get_url_namespace(), query_params=query_params) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + # self.client.force_login(self.admin) + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_detail(self, uuid): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_post(self, data): + url = reverse(self.get_url_namespace()) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.post( + url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + def request_delete(self, uuid, user_email): + url = self.reverse(self.get_url_namespace(), query_params={"user_email": user_email}, kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.delete(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + @abstractmethod + def get_url_namespace(self): + ... + + +class ClassifierTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.config = {"access_token": "hbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"} + + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + + print(self.admin.is_authenticated) + + self.org = Org.objects.create( + name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + ) + + super().setUp() + + def test_list_classifier(self): + org = Org.objects.first() + org_uuid = str(org.uuid) + + classifier = Classifier.create(org, self.admin, WitType.slug, "Booker", self.config, sync=False) + + response = self.request_get(org_uuid=org_uuid, is_active=1).json() + print(response) + + self.assertEqual(len(response), 1) + + response = response[0] + + self.assertEqual(response.get("name"), "Booker") + self.assertEqual(response.get("classifier_type"), WitType.slug) + self.assertEqual(response.get("uuid"), str(classifier.uuid)) + self.assertEqual(response.get("access_token"), self.config["access_token"]) + + classifier = Classifier.create(org, self.admin, LuisType.slug, "Test", self.config, sync=False) + + response = self.request_get(org_uuid=org_uuid, is_active=1).json() + + self.assertEqual(len(response), 2) + + response = response[1] + + self.assertEqual(response.get("name"), "Test") + self.assertEqual(response.get("classifier_type"), LuisType.slug) + self.assertEqual(response.get("uuid"), str(classifier.uuid)) + self.assertEqual(response.get("access_token"), self.config["access_token"]) + + response = self.request_get(org_uuid=org_uuid, is_active=1, classifier_type=LuisType.slug).json() + + self.assertEqual(len(response), 1) + + response = response[0] + + self.assertEqual(response.get("name"), "Test") + self.assertEqual(response.get("classifier_type"), LuisType.slug) + self.assertEqual(response.get("uuid"), str(classifier.uuid)) + self.assertEqual(response.get("access_token"), self.config["access_token"]) + + classifier = Classifier.create(org, self.admin, LuisType.slug, "Test2", self.config, sync=False) + + response = self.request_get(org_uuid=org_uuid, is_active=1, classifier_type=LuisType.slug).json() + + self.assertEqual(len(response), 2) + + response = self.request_get(org_uuid=org_uuid, is_active=1).json() + + self.assertEqual(len(response), 3) + + def get_url_namespace(self): + return "classifier-list" + + +class ClassifierCreateTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.config = {"access_token": "hbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"} + + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + + self.org = Org.objects.create( + name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + ) + + super().setUp() + + @patch("temba.classifiers.tasks.sync_classifier_intents") + def test_create_classifier(self, mock): + mock.return_value = None + + org = Org.objects.first() + user = self.admin + access_token = self.config["access_token"] + + name = "Test Name" + classifier_type = "Test Type" + + payload = { + "classifier_type": classifier_type, + "user": user.email, + "org": str(org.uuid), + "name": name, + "access_token": access_token, + } + + response = self.request_post(data=payload).json() + + self.assertEqual(response.get("name"), name) + self.assertEqual(response.get("classifier_type"), classifier_type) + self.assertEqual(response.get("access_token"), access_token) + + def get_url_namespace(self): + return "classifier-list" + + +class ClassifierRetrieveTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.config = {"access_token": "hbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"} + + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + + self.org = Org.objects.create( + name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + ) + + super().setUp() + + def test_retrieve_classifier_by_valid_uuid(self): + classifier = Classifier.create(self.org, self.admin, LuisType.slug, "Test2", self.config, sync=False) + response = self.request_detail(uuid=str(classifier.uuid)).json() + + self.assertEqual(classifier.classifier_type, response.get("classifier_type")) + self.assertEqual(classifier.name, response.get("name")) + self.assertEqual(classifier.config["access_token"], response.get("access_token")) + self.assertEqual(classifier.is_active, response.get("is_active")) + + def get_url_namespace(self): + return "classifier-detail" + + +class ClassifierDestroyTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + self.config = {"access_token": "hbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"} + + self.admin = User.objects.create_user( + username="testuser", password="123", email="test@weni.ai", is_superuser=True + ) + + self.org = Org.objects.create( + name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + ) + + super().setUp() + + def test_destroy_classifier_by_valid_uuid(self): + classifier = Classifier.create(self.org, self.admin, LuisType.slug, "Test2", self.config, sync=False) + Intent.objects.create(classifier=classifier, name="Test Intent", external_id="FakeExternal") + + self.assertEqual(classifier.intents.count(), 1) + + t = self.request_delete(uuid=str(classifier.uuid), user_email=self.admin.email) + + classifier = Classifier.objects.get(uuid=classifier.uuid) + self.assertEqual(classifier.intents.count(), 0) + self.assertFalse(classifier.is_active) + + def get_url_namespace(self): + return "classifier-detail" diff --git a/weni/internal/classifier/urls.py b/weni/internal/classifier/urls.py new file mode 100644 index 000000000..9d1ab1e12 --- /dev/null +++ b/weni/internal/classifier/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework import routers + +from .views import ClassifierEndpoint + +router = routers.SimpleRouter() +router.register("classifier", ClassifierEndpoint, basename="classifier") + +urlpatterns = format_suffix_patterns(router.urls, allowed=["json", "api"]) diff --git a/weni/internal/classifier/views.py b/weni/internal/classifier/views.py new file mode 100644 index 000000000..0fb630883 --- /dev/null +++ b/weni/internal/classifier/views.py @@ -0,0 +1,96 @@ +from rest_framework import viewsets +from rest_framework import generics +from rest_framework import mixins +from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response +from rest_framework import status +from rest_framework.exceptions import ValidationError + +from django.contrib.auth.models import User +from django.db.models import Count, Prefetch, Q +from django.urls import reverse +from django.http import JsonResponse + +from temba.api.v2.views_base import BaseAPIView, ListAPIMixin +from temba.contacts.models import Contact, ContactGroup +from temba.classifiers.models import Classifier +from temba.orgs.models import Org + +from .serializers import ClassifierSerializer, ClassifierDeleteSerializer +from weni.internal.views import InternalGenericViewSet + + +class ClassifierEndpoint(viewsets.ModelViewSet, InternalGenericViewSet): + + serializer_class = ClassifierSerializer + lookup_field = "uuid" + + def get_queryset(self): + + is_active_possibilities = { + "True": True, + "False": False, + "true": True, + "false": False, + } + + org_uuid = self.request.query_params.get("org_uuid") + is_active = is_active_possibilities.get(self.request.query_params.get("is_active")) + classifier_type = self.request.query_params.get("classifier_type") + + queryset = Classifier.objects.all() + + filters = {} + + if org_uuid is not None: + try: + org = Org.objects.get(uuid=org_uuid) + filters["org"] = org + except Org.DoesNotExist: + raise ValidationError(detail="Org does not exist") + + if is_active is not None: + try: + filters["is_active"] = is_active + except ValueError: + raise ValidationError(detail="is_active cannot be null") + + if classifier_type is not None: + filters["classifier_type"] = classifier_type + + return queryset.filter(**filters) + + def create(self, request): + serializer = ClassifierSerializer(data=request.data) + + if not serializer.is_valid(): + return JsonResponse(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + serializer.save() + + return JsonResponse(data=serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request, uuid=None): + + try: + classifier = Classifier.objects.get(uuid=uuid) + except Classifier.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + return JsonResponse(data=self.get_serializer(classifier).data, status=status.HTTP_200_OK) + + def destroy(self, request, uuid=None): + + data = { + "uuid": uuid, + "user": request.query_params.get("user_email"), + } + + serializer = ClassifierDeleteSerializer(data=data) + if not serializer.is_valid(): + return JsonResponse(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + classifier = Classifier.objects.get(uuid=uuid) + classifier.release(serializer.validated_data.get("user")) + + return Response(status=status.HTTP_200_OK) diff --git a/weni/internal/urls.py b/weni/internal/urls.py index 4550a9f43..af79febc4 100644 --- a/weni/internal/urls.py +++ b/weni/internal/urls.py @@ -14,6 +14,7 @@ from weni.internal.flows.urls import urlpatterns as flows_urls from weni.internal.users.urls import urlpatterns as users_urls from weni.internal.tickets.urls import urlpatterns as tickets_urls +from weni.internal.classifier.urls import urlpatterns as classifier_urls from weni.internal.channel.urls import urlpatterns as channel_urls from weni.internal.statistic.urls import urlpatterns as statistics_urls @@ -23,6 +24,7 @@ internal_urlpatterns += flows_urls internal_urlpatterns += users_urls internal_urlpatterns += tickets_urls +internal_urlpatterns += classifier_urls internal_urlpatterns += channel_urls internal_urlpatterns += statistics_urls From 5b3a1efdd50aa5c7d056e4bd63303be4894baa8c Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:24:09 -0300 Subject: [PATCH 007/101] [Conversion gRPC-REST] Flow endpoint (#182) * add flow endpoint * fix: tests lint * remove unnecessary code --- weni/internal/flows/serializers.py | 7 ++ weni/internal/flows/tests.py | 118 +++++++++++++++++++++++++++++ weni/internal/flows/urls.py | 3 +- weni/internal/flows/views.py | 26 ++++++- 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 weni/internal/flows/tests.py diff --git a/weni/internal/flows/serializers.py b/weni/internal/flows/serializers.py index 80d10bedb..87f8e61fd 100644 --- a/weni/internal/flows/serializers.py +++ b/weni/internal/flows/serializers.py @@ -24,3 +24,10 @@ def create(self, validated_data): org.import_app(sample_flows, org.created_by) return org.flows.order_by("created_on").last() + + +class FlowListSerializer(serializers.Serializer): + flow_name = serializers.CharField(required=True, write_only=True) + org_uuid = weni_serializers.OrgUUIDRelatedField(required=True, write_only=True) + uuid = serializers.CharField(read_only=True) + name = serializers.CharField(read_only=True) diff --git a/weni/internal/flows/tests.py b/weni/internal/flows/tests.py new file mode 100644 index 000000000..0ff5b259b --- /dev/null +++ b/weni/internal/flows/tests.py @@ -0,0 +1,118 @@ +import json +from abc import ABC, abstractmethod + +from django.contrib.auth.models import Group +from django.urls import reverse +from django.utils.http import urlencode +from django.contrib.auth.models import User + +from temba.tests import TembaTest +from temba.api.models import APIToken + +from temba.orgs.models import Org +from temba.flows.models import Flow + + +class TembaRequestMixin(ABC): + def reverse(self, viewname, kwargs=None, query_params=None): + url = reverse(viewname, kwargs=kwargs) + + if query_params: + return "%s?%s" % (url, urlencode(query_params)) + else: + return url + + def request_get(self, **query_params): + url = self.reverse(self.get_url_namespace(), query_params=query_params) + url = url.replace("channel", "channel.json") + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_detail(self, uuid): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + def request_post(self, data): + url = reverse(self.get_url_namespace()) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.post( + url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + ) + + def request_delete(self, uuid): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + + return self.client.delete(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") + + @abstractmethod + def get_url_namespace(self): + ... + + +class ListFlowTestCase(TembaTest, TembaRequestMixin): + def setUp(self): + + User.objects.create_user(username="testuser", password="123", email="test@weni.ai") + + user = User.objects.first() + + # print(Org.objects.all()) + + temba = Org.objects.create(name="Temba", timezone="America/Maceio", created_by=user, modified_by=user) + weni = Org.objects.create(name="Weni", timezone="America/Maceio", created_by=user, modified_by=user) + + Flow.create(name="Test Temba", user=user, org=temba) + Flow.create(name="Test flow name", user=user, org=weni) + Flow.create(name="Test Weni flow name", user=user, org=weni) + + super().setUp() + + def test_list_flow(self): + + temba = Org.objects.filter(name="Temba").first() + weni = Org.objects.get(name="Weni") + + response = self.request_get(flow_name="test", org_uuid="123") # {'org_uuid': ['“123” is not a valid UUID.']} + self.assertEquals(response.status_code, 400) + + response = self.request_get(flow_name="test", org_uuid="") # {'org_id': ['This field may not be blank.']} + self.assertEquals(response.status_code, 400) + + response = self.request_get(flow_name="test", org_uuid=str(temba.uuid)).json() + + flows, flows_count = self.get_flows_and_count(response) + + self.assertEquals(flows_count, 1) + self.assertEquals(flows[0].get("name"), "Test Temba") + + response = self.request_get(flow_name="test", org_uuid=str(weni.uuid)).json() + + flows, flows_count = self.get_flows_and_count(response) + + weni_flow_names = [flow.name for flow in Flow.objects.filter(org=weni.id)] + + self.assertEquals(flows_count, 2) + + for flow in flows: + self.assertIn(flow.get("name"), weni_flow_names) + + response = self.request_get(flow_name="weni", org_uuid=str(weni.uuid)).json() + + flows, flows_count = self.get_flows_and_count(response) + + self.assertEquals(flows[0].get("name"), "Test Weni flow name") + self.assertEquals(flows_count, 1) + + def get_flows_and_count(self, response) -> (list, int): + flows = [flow for flow in response] + flows_count = len(flows) + + return flows, flows_count + + def get_url_namespace(self): + return "flows-list" diff --git a/weni/internal/flows/urls.py b/weni/internal/flows/urls.py index d0f7f2349..c913632b3 100644 --- a/weni/internal/flows/urls.py +++ b/weni/internal/flows/urls.py @@ -1,10 +1,11 @@ from rest_framework import routers -from weni.internal.flows.views import FlowViewSet +from weni.internal.flows.views import FlowViewSet, ProjectFlowsViewSet router = routers.DefaultRouter() router.register(r"flows", FlowViewSet, basename="flows") +router.register(r"project-flows", ProjectFlowsViewSet, basename="project-flows") urlpatterns = router.urls diff --git a/weni/internal/flows/views.py b/weni/internal/flows/views.py index 49c37afac..8acd2d2d8 100644 --- a/weni/internal/flows/views.py +++ b/weni/internal/flows/views.py @@ -1,8 +1,30 @@ -from rest_framework.mixins import CreateModelMixin +from rest_framework.mixins import CreateModelMixin, ListModelMixin +from rest_framework.exceptions import NotFound from weni.internal.views import InternalGenericViewSet -from weni.internal.flows.serializers import FlowSerializer +from weni.internal.flows.serializers import FlowSerializer, FlowListSerializer + +from temba.flows.models import Flow class FlowViewSet(CreateModelMixin, InternalGenericViewSet): serializer_class = FlowSerializer + + +class ProjectFlowsViewSet(ListModelMixin, InternalGenericViewSet): + serializer_class = FlowListSerializer + + def get_queryset(self): + serializer = self.get_serializer(data=self.request.query_params.dict()) + serializer.is_valid(raise_exception=True) + + queryset = Flow.objects.filter( + name__icontains=serializer.validated_data.get("flow_name"), + org=serializer.validated_data.get("org_uuid"), + is_active=True, + ).exclude(is_archived=True)[:20] + + if queryset: + return queryset + + raise NotFound() From 2f17661909344ef4e3da92415cea894347d9597e Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Thu, 17 Nov 2022 22:30:56 -0300 Subject: [PATCH 008/101] Update/1.0.33 (#183) --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 755fca6ea..bb8ee499a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## [Unreleased] +## [1.0.33] - 2022-11-17 +- Fix: Remove pagination_class from internal views +- Feat: Add internal Flow endpoint +- Feat: Add internal Classifier endpoint +- Feat: Add internal Org endpoint +- Feat: Add internal Channels endpoint +- Feat: Add internal Statistic endpoint +- Feat: Add internal User and User-Permission endpoint + ## [1.0.32] - 2022-11-17 - Feat: Use internal_tickter false on TemplateOrg creation diff --git a/pyproject.toml b/pyproject.toml index 9ed2b3f73..a740374fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "1.0.32" +version = "1.0.33" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From ddc600150890ccec3e248d854f0e90e122524cbe Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Fri, 18 Nov 2022 16:12:47 -0300 Subject: [PATCH 009/101] Create endpoint that returns success orgs (#184) * feat: Create app success_orgs * feat: Create business rules for the successful orgs endpoint * feat: Create endpoint that returns success orgs * feat: Remove default authenticantion classes of SuccessOrgAPIView * feat: Add error message when invalid token is sent --- weni/success_orgs/apps.py | 11 +++++ weni/success_orgs/business.py | 56 ++++++++++++++++++++++++ weni/success_orgs/serializers.py | 17 +++++++ weni/success_orgs/tests/test_business.py | 21 +++++++++ weni/success_orgs/urls.py | 9 ++++ weni/success_orgs/views.py | 49 +++++++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 weni/success_orgs/apps.py create mode 100644 weni/success_orgs/business.py create mode 100644 weni/success_orgs/serializers.py create mode 100644 weni/success_orgs/tests/test_business.py create mode 100644 weni/success_orgs/urls.py create mode 100644 weni/success_orgs/views.py diff --git a/weni/success_orgs/apps.py b/weni/success_orgs/apps.py new file mode 100644 index 000000000..a9e444e4a --- /dev/null +++ b/weni/success_orgs/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + + +class SucessOrgsConfig(AppConfig): + name = "weni.success_orgs" + + def ready(self): + from weni.success_orgs.urls import urlpatterns + from weni.utils.app_config import update_urlpatterns + + update_urlpatterns(urlpatterns) diff --git a/weni/success_orgs/business.py b/weni/success_orgs/business.py new file mode 100644 index 000000000..4ea50acae --- /dev/null +++ b/weni/success_orgs/business.py @@ -0,0 +1,56 @@ +from django.db.models import Exists, OuterRef, Case, When, Value, BooleanField, F +from django.contrib.auth import get_user_model + +from temba.orgs.models import Org +from temba.classifiers.models import Classifier +from temba.flows.models import Flow +from temba.channels.models import Channel +from temba.msgs.models import Msg + + +User = get_user_model() + + +SUCCESS_ORG_QUERIES = dict( + has_ia=Exists(Classifier.objects.filter(org=OuterRef("pk"), classifier_type="bothub", is_active=True)), + has_flows=Exists(Flow.objects.filter(org=OuterRef("pk"), is_active=True)), + has_channel=Exists(Channel.objects.filter(org=OuterRef("pk"), is_active=True)), + has_msg=Exists(Msg.objects.filter(org=OuterRef("pk"))), +) + + +class UserDoesNotExist(User.DoesNotExist): + pass + + +def get_user_by_email(email: str) -> User: + try: + return User.objects.get(email=email) + except User.DoesNotExist as error: + raise UserDoesNotExist(error) + + +def get_user_orgs(user: User): + return Org.objects.filter(created_by=user) + + +def get_user_success_orgs(user: User): + user_orgs = get_user_orgs(user) + + return ( + user_orgs.annotate(user_last_login=F("created_by__last_login")) + .annotate(**SUCCESS_ORG_QUERIES) + .annotate( + is_success_project=Case( + When(has_ia=True, has_flows=True, has_channel=True, has_msg=True, then=Value(True)), + output_field=BooleanField(), + default=Value(False), + ) + ) + ) + + +def get_user_success_orgs_by_email(email: str): + user = get_user_by_email(email) + + return dict(email=user.email, last_login=user.last_login, orgs=get_user_success_orgs(user)) diff --git a/weni/success_orgs/serializers.py b/weni/success_orgs/serializers.py new file mode 100644 index 000000000..16b2ee082 --- /dev/null +++ b/weni/success_orgs/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + + +class SuccessOrgSerializer(serializers.Serializer): + uuid = serializers.UUIDField() + name = serializers.CharField() + has_ia = serializers.BooleanField() + has_flows = serializers.BooleanField() + has_channel = serializers.BooleanField() + has_msg = serializers.BooleanField() + is_success_project = serializers.BooleanField() + + +class UserSuccessOrgSerializer(serializers.Serializer): + email = serializers.EmailField() + last_login = serializers.CharField() + orgs = SuccessOrgSerializer(many=True) diff --git a/weni/success_orgs/tests/test_business.py b/weni/success_orgs/tests/test_business.py new file mode 100644 index 000000000..cc2a6922d --- /dev/null +++ b/weni/success_orgs/tests/test_business.py @@ -0,0 +1,21 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model + +from weni.success_orgs.business import UserDoesNotExist, get_user_by_email + + +User = get_user_model() + + +class GetUserByEmailTestCase(TestCase): + def setUp(self) -> None: + self.user_email = "fake@weni.ai" + self.user = User.objects.create(email=self.user_email) + + def test_get_user_by_email(self): + user = get_user_by_email(self.user_email) + self.assertEqual(user.email, self.user_email) + + def test_get_user_by_email_raise_does_not_exist_exception_with_wrong_email(self): + with self.assertRaises(UserDoesNotExist): + user = get_user_by_email("wrong@weni.ai") diff --git a/weni/success_orgs/urls.py b/weni/success_orgs/urls.py new file mode 100644 index 000000000..96fcfd844 --- /dev/null +++ b/weni/success_orgs/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns + +from weni.success_orgs.views import SuccessOrgAPIView + + +urlpatterns = [path("success_orgs", SuccessOrgAPIView.as_view(), name="api.v2.success_orgs")] + +urlpatterns = format_suffix_patterns(urlpatterns, allowed=["json", "api"]) diff --git a/weni/success_orgs/views.py b/weni/success_orgs/views.py new file mode 100644 index 000000000..2accf3546 --- /dev/null +++ b/weni/success_orgs/views.py @@ -0,0 +1,49 @@ +from django.conf import settings + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import exceptions +from rest_framework.authentication import get_authorization_header + +from .business import get_user_success_orgs_by_email, UserDoesNotExist +from .serializers import UserSuccessOrgSerializer + + +class SuccessOrgAPIView(APIView): + + authentication_classes = [] + permission_classes = [] + + def check_permissions(self, request): + + auth = get_authorization_header(request).split() + + if not auth: + raise exceptions.NotAuthenticated() + + if len(auth) == 1: + msg = "Invalid token header. No credentials provided." + raise exceptions.AuthenticationFailed(msg) + + elif len(auth) > 2: + msg = "Invalid token header. Token string should not contain spaces." + raise exceptions.AuthenticationFailed(msg) + + if auth[1].decode() != settings.FIXED_SUPER_ACCESS_TOKEN: + raise exceptions.PermissionDenied(detail="Invalid token!") + + def get(self, request, **kwargs): + + user_email = request.query_params.get("email") + + if user_email is None: + raise exceptions.ValidationError("The query param: user_email is required!") + + try: + user_sucess_orgs = get_user_success_orgs_by_email(user_email) + except UserDoesNotExist: + raise exceptions.ValidationError(f"User with email: {user_email} does not exist") + + serializer = UserSuccessOrgSerializer(user_sucess_orgs) + + return Response(serializer.data) From 9455fb9d6abc2a7b0b66e02254382a9a732957c7 Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Fri, 18 Nov 2022 16:19:17 -0300 Subject: [PATCH 010/101] Bump to 1.0.34 (#185) --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8ee499a..ce0442cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [1.0.34] - 2022-11-18 +- Feat: Create endpoint that returns success orgs + ## [1.0.33] - 2022-11-17 - Fix: Remove pagination_class from internal views - Feat: Add internal Flow endpoint diff --git a/pyproject.toml b/pyproject.toml index a740374fc..3cbffbbd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "1.0.33" +version = "1.0.34" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 2dc53afba5fd9da6f7040346cb56e2d27d5fd0ea Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 23 Dec 2022 05:17:57 -0300 Subject: [PATCH 011/101] Feature: Create endpoint for channel type (#164) * feature: create endpoint for channel type * feature: adds exclusion list of channels that go to integrations Co-authored-by: elitonzky --- weni/internal/channel/tests.py | 186 ++++++++++++++++++++++++++++++++- weni/internal/channel/urls.py | 4 +- weni/internal/channel/views.py | 141 +++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 4 deletions(-) diff --git a/weni/internal/channel/tests.py b/weni/internal/channel/tests.py index 2fec93c61..46791630e 100644 --- a/weni/internal/channel/tests.py +++ b/weni/internal/channel/tests.py @@ -2,23 +2,29 @@ from abc import ABC, abstractmethod from uuid import uuid1 from unittest.mock import patch +from unittest import mock +from unittest import TestCase +from rest_framework.test import APIRequestFactory, force_authenticate +from rest_framework import status from django.contrib.auth.models import Group from django.urls import reverse from django.utils import timezone as tz from django.utils.http import urlencode from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from temba.api.models import APIToken - from temba.flows.models import Flow from temba.orgs.models import Org, OrgRole from temba.contacts.models import Contact from temba.channels.models import Channel - +from temba.channels.types import TYPES from temba.tests import TembaTest, mock_mailroom - +from .views import AvailableChannels, extract_form_info, extract_type_info + + class TembaRequestMixin(ABC): def reverse(self, viewname, kwargs=None, query_params=None): url = reverse(viewname, kwargs=kwargs) @@ -246,3 +252,177 @@ def test_list_channels_filtered_by_org_uuid(self): def get_url_namespace(self): return "channel-list" + + +class ListChannelAvailableTestCase(TembaTest, TembaRequestMixin): + url ='/api/v2/flows-backend/channels/' + + def setUp(self): + super().setUp() + content_type = ContentType.objects.get_for_model(User) + self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") + self.admin.user_permissions.create(codename='can_communicate_internally', content_type=content_type) + + def test_list_all_channels(self): + factory = APIRequestFactory() + view = AvailableChannels.as_view({'get': 'list'}) + view.permission_classes = [] + + request = factory.get(self.url) + force_authenticate(request, user=self.admin) + response = view(request) + total_attrs = 0 + + channel_types = response.data.get('channel_types') + for key in channel_types.keys(): + attributes = response.data.get('channel_types').get(key) + if attributes: + if len(attributes)>0: + total_attrs += 1 + + # checks if status code is 200 - ok + self.assertEqual(response.status_code, status.HTTP_200_OK) + # checks if the amount of #types returned is equivalent to the available response types + self.assertEqual(len(TYPES), len(response.data.get('channel_types'))) + # checks if response data have existing attributes + self.assertEqual(total_attrs, len(TYPES)) + + def test_list_channels_without_authentication(self): + """ testing without authenticated user """ + factory = APIRequestFactory() + view = AvailableChannels.as_view({'get': 'list'}) + + request = factory.get(self.url) + response = view(request) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_list_channels_without_permission(self): + """ testing user without permission """ + factory = APIRequestFactory() + view = AvailableChannels.as_view({'get': 'list'}) + + request = factory.get(self.url) + force_authenticate(request, user=self.user) + response = view(request) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_retrieve_channel_with_permission(self): + """ Testing retrieve response is ok """ + have_attribute = False + have_form = False + form_ok = True + + factory = APIRequestFactory() + request = factory.get(self.url) + view = AvailableChannels.as_view({'get': 'retrieve'}) + view.permission_classes = [] + force_authenticate(request, user=self.admin) + response = view(request, 'ac') + + if response.data.get('attributes'): + have_attribute = True + + if response.data.get('form'): + have_form = True + if len(response.data.get('form'))>0: + form = response.data.get('form') + for field in form: + if not field.get('name') \ + or not field.get('type') \ + or not field.get('help_text'): + form_ok = False + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(True, have_attribute) + if have_form: + self.assertEqual(True, form_ok) + + def test_retrieve_channel_without_permission(self): + """ testing retrieve without permission """ + factory = APIRequestFactory() + view = AvailableChannels.as_view({'get': 'retrieve'}) + + request = factory.get(self.url) + force_authenticate(request, user=self.user) + response = view(request, 'ac') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_retrieve_channel_without_authentication(self): + """ testing retrieve without being authenticated """ + factory = APIRequestFactory() + view = AvailableChannels.as_view({'get': 'retrieve'}) + + request = factory.get(self.url) + response = view(request, 'ac') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_invalid_response_info_form(self): + """ test missing values """ + self.assertEqual(extract_form_info('', 'name_field'), None) + + def test_invalid_response_info_type(self): + """ test missing values """ + self.assertEqual(extract_type_info(''), None) + + def get_url_namespace(self): + return "api.v2.flows_backend.channels-list" + + +class FormatFunctionTestCase(TestCase): + types = TYPES + + def test_form_with_values(self): + """ checks if the treatment was done correctly """ + test_form = { + 'widget': to_object(**{'input_type': 'text'}), + 'help_text': 'test field' + } + + expect_form = { + 'name': 'test_form01', + 'type': 'text', + 'help_text': 'test field' + } + + result = extract_form_info(to_object(**test_form),'test_form01') + self.assertEqual(result, expect_form) + + def test_form_without_name_value(self): + """ check response without #name attribute """ + test_form = { + 'widget': to_object(**{'input_type': 'text'}), + 'help_text': 'test field' + } + result = extract_form_info(to_object(**test_form),'') + self.assertEqual(result, None) + + def test_form_without_type_value(self): + """ check response without #widget attribute """ + test_form = { + 'help_text': 'test field' + } + result = extract_form_info(to_object(**test_form),'test_form03') + self.assertEqual(result, None) + + def test_type_contains_code_and_name(self): + """ make sure that all results have code and name """ + have_code_name = True + for value in self.types: + type_in = self.types[value] + result = extract_type_info(type_in) + if not (result.get('code')) or not (result.get('name')): + have_code_name = False + + self.assertEqual(have_code_name, True) + + def test_all_types_response_contains_dict(self): + """ make sure that all results have been processed and converted to dictionaries """ + for value in self.types: + type_in = self.types[value] + result = extract_type_info(type_in) + self.assertEqual(type(result), dict) + + +class to_object: + def __init__(self, **entries): + return self.__dict__.update(entries) diff --git a/weni/internal/channel/urls.py b/weni/internal/channel/urls.py index 92a3de099..1cf109056 100644 --- a/weni/internal/channel/urls.py +++ b/weni/internal/channel/urls.py @@ -2,9 +2,11 @@ from rest_framework.urlpatterns import format_suffix_patterns from rest_framework import routers -from .views import ChannelEndpoint +from .views import ChannelEndpoint, AvailableChannels + router = routers.SimpleRouter() router.register("channel", ChannelEndpoint, basename="channel") +router.register("channels", AvailableChannels, basename="channels") urlpatterns = format_suffix_patterns(router.urls, allowed=["json", "api"]) diff --git a/weni/internal/channel/views.py b/weni/internal/channel/views.py index 9b1678caf..16f3f8898 100644 --- a/weni/internal/channel/views.py +++ b/weni/internal/channel/views.py @@ -1,3 +1,5 @@ +import inspect + from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 from django.http import JsonResponse @@ -9,7 +11,10 @@ from rest_framework.pagination import PageNumberPagination from weni.internal.views import InternalGenericViewSet +from django.conf import settings + from temba.channels.models import Channel +from temba.channels.types import TYPES from .serializers import ChannelSerializer, CreateChannelSerializer, ChannelWACSerializer @@ -69,3 +74,139 @@ def create_wac(self, request): serializer.save() return JsonResponse(data=serializer.data, status=status.HTTP_200_OK) + + +class AvailableChannels(viewsets.ViewSet, InternalGenericViewSet): + + def list(self, request): + types_available = TYPES + channel_types = {} + for value in types_available: + if value not in settings.DISABLED_CHANNELS_INTEGRATIONS: + fields_types = {} + attibutes_type = extract_type_info(types_available[value]) + if not (attibutes_type): + return Response(status=status.HTTP_404_NOT_FOUND) + + fields_types['attributes'] = attibutes_type + channel_types[value] = fields_types + + payload = { + "channel_types": channel_types, + } + return Response(payload) + + def retrieve(self, request, pk=None): + channel_type = None + fields_form = {} + code_type = pk + if code_type: + channel_type = TYPES.get(code_type.upper(), None) + + if channel_type is None: + return Response(status=status.HTTP_404_NOT_FOUND) + + fields_in_form = [] + if channel_type.claim_view: + if channel_type.claim_view.form_class: + form = channel_type.claim_view.form_class.base_fields + for field in form: + fields_in_form.append(extract_form_info(form[field], field)) + + if not (fields_in_form): + return Response(status=status.HTTP_404_NOT_FOUND) + + fields_form['form'] = fields_in_form + + fields_types = {} + attibutes_type = extract_type_info(channel_type) + if not (attibutes_type): + return Response(status=status.HTTP_404_NOT_FOUND) + + fields_types['attributes'] = attibutes_type + + payload = { + "attributes": fields_types.get('attributes'), + "form": fields_form.get('form') + } + + return Response(payload) + + +def extract_type_info(_class): + channel = {} + type_exclude = [""] + items_exclude = ["redact_response_keys", "claim_view_kwargs", + "extra_links", "redact_request_keys"] + + for i in _class.__class__.__dict__.items(): + if not i[0].startswith('_'): + if not inspect.isclass(i[1]) and str(type(i[1])) not in(type_exclude) \ + and i[0] not in items_exclude: + if str(type(i[1])) == "": + channel[i[0]] = {"name": i[1].name if i[1].name else "", + "value": i[1].value if i[1].value else ""} + + elif i[0] == "configuration_urls": + if i[1]: + if i[1][0]: + urls_list = [] + url_dict = {} + for url in i[1]: + if url.get('label'): + url_dict['label'] = str(url.get('label')) + + if i[1][0].get('url'): + url_dict['url'] = str(url.get('url')) + + if i[1][0].get('description'): + url_dict['description'] = str(url.get('description')) + + urls_list.append(url_dict) + channel[i[0]] = urls_list + + elif i[0] == "configuration_blurb": + channel[i[0]] = str(i[1]) + + elif i[0] == "claim_blurb": + channel[i[0]] = str(i[1]) + + elif (i[0]) == "ivr_protocol": + channel[i[0]] = {"name": i[1].name if i[1].name else "", + "value": i[1].value if i[1].value else ""} + else: + channel[i[0]] = (i[1]) + + if (not (channel.get('code'))) or (not (len(channel))>0) \ + or (not (channel.get('name'))): + return None + + channel['num_fields'] = len(channel) + return ((channel)) + +def extract_form_info(_form, name_form): + detail = {} + detail['name'] = name_form if name_form else None + + try: + detail['type'] = str(_form.widget.input_type) + except: + detail['type'] = None + + if _form.help_text: + detail['help_text'] = str(_form.help_text) + else: + detail['help_text'] = None + + if detail.get('type') == 'select': + detail['choices'] = _form.choices + + if _form.label: + detail['label'] = str(_form.label) + else: + detail['label'] = None + + if not (detail.get('name')) or not (detail.get('type')): + return None + + return detail From 7ff169d218d5afc9ea820d6b7401ed2b0eb7d277 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 23 Dec 2022 05:23:37 -0300 Subject: [PATCH 012/101] Update to version 2.0.0 (#192) Co-authored-by: elitonzky --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0442cbd..3545d8c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.0.0] - 2022-12-23 +- Feature: Create endpoint for channel type + ## [1.0.34] - 2022-11-18 - Feat: Create endpoint that returns success orgs diff --git a/pyproject.toml b/pyproject.toml index 3cbffbbd4..3cdd7dd1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "1.0.34" +version = "2.0.0" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From a4bbd82e344d0509e30cd0e5416e1aedb9b3de65 Mon Sep 17 00:00:00 2001 From: nataliaweni Date: Fri, 23 Dec 2022 19:11:31 -0300 Subject: [PATCH 013/101] Add workflows --- .../workflows/build-rapidpro-apps-pypi.yaml | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 .github/workflows/build-rapidpro-apps-pypi.yaml diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml new file mode 100644 index 000000000..ade2171af --- /dev/null +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -0,0 +1,143 @@ +name: Build and Publish +on: + push: + tags: + - '*.*.*' + +jobs: + # build: + # runs-on: ubuntu-latest + + # steps: + # - name: Checkout repository + # uses: actions/checkout@v3 + + # - name: Install python + # uses: actions/setup-python@v3 + # with: + # python-version: '3.9' + + # - name: Install poetry + # uses: Gr1N/setup-poetry@v7 + + # - name: Checkout poetry version + # run: poetry --version + + # - name: Upload built package + # uses: actions/upload-artifact@v3 + # with: + # name: dist + # path: dist/ + # retention-days: 1 + + # Run pytest using built package + # test: + # needs: build + # runs-on: ubuntu-latest + # strategy: + # matrix: + # python: ["3.8", "3.9", "3.10"] + + # steps: + # - name: Checkout repository + # uses: actions/checkout@v2 + + # - name: Install python + # uses: actions/setup-python@v2 + # with: + # python-version: ${{ matrix.python }} + # cache: 'pip' + # cache-dependency-path: "poetry.lock" + + # - name: Download built package + # uses: actions/download-artifact@v3 + # with: + # name: dist + + # - name: Install weni-rp-apps and pytest + # shell: bash + # run: | + # WHL_NAME=$(ls weni-rp-apps-*.whl) + # pip install ${WHL_NAME}[experiments,entmoot] pytest + + # - name: Run tests + # shell: bash + # run: weni-rp-apps-tests + + # Publish to pypi on version change + # This is based on https://github.com/coveooss/pypi-publish-with-poetry + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install poetry + uses: Gr1N/setup-poetry@v7 + + - name: Build package + run: poetry build + + # - name: Install coveo-pypi-cli + # run: pip install coveo-pypi-cli + + # - name: Determine the version for this release from the build + # id: current + # run: | + # BUILD_VER="$(ls dist/weni-rp-apps-*.tar.gz)" + # echo "Path: $BUILD_VER" + # if [[ $BUILD_VER =~ (weni-rp-apps-)([^,][0-9.]{4}) ]]; then + # echo "::set-output name=version::${BASH_REMATCH[2]}" + # echo "Version of build: ${BASH_REMATCH[2]}" + # else + # echo "No version found found" + # fi + + # - name: Get latest published version + # id: published + # run: | + # PUB_VER="$(pypi current-version weni-rp-apps)" + # echo "::set-output name=version::$PUB_VER" + # echo "Latest published version: $PUB_VER" + + + - name: Publish to pypi + # if: (steps.current.outputs.version != steps.published.outputs.version) + shell: bash + run: | + poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} + # if [[ '${{ github.ref_name }}' == 'master' ]]; then + poetry publish + # else + # echo "Dry run of publishing the package" + # poetry publish --dry-run + # fi + + # - name: Tag repository + # shell: bash + # id: get-next-tag + # if: (steps.current.outputs.version != steps.published.outputs.version) + # run: | + # TAG_NAME=${{ steps.current.outputs.version }} + # echo "::set-output name=tag-name::$TAG_NAME" + # echo "This release will be tagged as $TAG_NAME" + # git config user.name "github-actions" + # git config user.email "actions@users.noreply.github.com" + # git tag --annotate --message="Automated tagging system" $TAG_NAME ${{ github.sha }} + + # - name: Push the tag + # if: (steps.current.outputs.version != steps.published.outputs.version) + # env: + # TAG_NAME: ${{ steps.current.outputs.version }} + # run: | + # if [[ ${{ github.ref_name }} == 'master' ]]; then + # git push origin $TAG_NAME + # else + # echo "If this was the master branch, I would push a new tag named $TAG_NAME" + # fi \ No newline at end of file From 797118ebf9fe9269fa15e55884cbe8b2f363e437 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Tue, 27 Dec 2022 14:09:34 -0300 Subject: [PATCH 014/101] feat: add weni bucket callback (#193) --- weni/s3/__init__.py | 1 + weni/s3/apps.py | 11 +++++++++++ weni/s3/urls.py | 9 +++++++++ weni/s3/views.py | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 weni/s3/__init__.py create mode 100644 weni/s3/apps.py create mode 100644 weni/s3/urls.py create mode 100644 weni/s3/views.py diff --git a/weni/s3/__init__.py b/weni/s3/__init__.py new file mode 100644 index 000000000..ad27fc2ce --- /dev/null +++ b/weni/s3/__init__.py @@ -0,0 +1 @@ +default_app_config = "weni.s3.apps.S3Config" diff --git a/weni/s3/apps.py b/weni/s3/apps.py new file mode 100644 index 000000000..3be93805b --- /dev/null +++ b/weni/s3/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + + +class S3Config(AppConfig): + name = "weni.s3" + + def ready(self): + from .urls import urlpatterns + from ..utils.app_config import update_urlpatterns + + update_urlpatterns(urlpatterns) diff --git a/weni/s3/urls.py b/weni/s3/urls.py new file mode 100644 index 000000000..795b0b9ff --- /dev/null +++ b/weni/s3/urls.py @@ -0,0 +1,9 @@ +from rest_framework.urlpatterns import format_suffix_patterns +from django.conf.urls import url + +from .views import WeniFileCallbackView + + +urlpatterns = [ + url(r"^file/(?P[\w\-./]+)$", WeniFileCallbackView.as_view(), name="file_callback"), +] diff --git a/weni/s3/views.py b/weni/s3/views.py new file mode 100644 index 000000000..42ffa75af --- /dev/null +++ b/weni/s3/views.py @@ -0,0 +1,21 @@ +import requests + +import magic +from django.http import HttpResponse +from django.conf import settings + +from temba.tickets.types.zendesk.views import FileCallbackView + + +class WeniFileCallbackView(FileCallbackView): + + def post(self, request, *args, **kwargs): + path = "media/" + kwargs["path"] + assert ".." not in kwargs["path"] + + url = f"{settings.COURIER_S3_ENDPOINT}/{path}" + + file = requests.get(url).content + file_type = magic.from_buffer(file, mime=True) + + return HttpResponse(file, content_type=file_type) From e37f9306f09c5b0de1116ca49f7d2af64f8a3c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nat=C3=A1lia=20de=20Assis?= <98845410+nataliaweni@users.noreply.github.com> Date: Tue, 27 Dec 2022 16:14:42 -0300 Subject: [PATCH 015/101] Update build-rapidpro-apps-pypi.yaml --- .github/workflows/build-rapidpro-apps-pypi.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index ade2171af..afb5c83da 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -3,6 +3,7 @@ on: push: tags: - '*.*.*' + - '*.*.*a*' jobs: # build: @@ -140,4 +141,4 @@ jobs: # git push origin $TAG_NAME # else # echo "If this was the master branch, I would push a new tag named $TAG_NAME" - # fi \ No newline at end of file + # fi From ab6c957d968e4c72b0456dca97550a67e386ac13 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Tue, 27 Dec 2022 16:52:37 -0300 Subject: [PATCH 016/101] feat: Add is_active filter in this endpoint (#189) --- weni/internal/users/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index dc3e86d80..1dd9515fc 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -40,11 +40,10 @@ def api_token(self, request: "Request", **kwargs): class UserPermissionEndpoint(InternalGenericViewSet): serializer_class = UserPermissionSerializer - # lookup_field = "org_id" def retrieve(self, request): org = get_object_or_404(Org, uuid=request.query_params.get("org_uuid")) - user = get_object_or_404(User, email=request.query_params.get("user_email")) + user = get_object_or_404(User, email=request.query_params.get("user_email"), is_active=request.query_params.get("is_active", True)) permissions = self._get_user_permissions(org, user) serializer = self.get_serializer(permissions) @@ -53,7 +52,7 @@ def retrieve(self, request): def partial_update(self, request): org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) - user = get_object_or_404(User, email=request.data.get("user_email")) + user = get_object_or_404(User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True)) self._validate_permission(org, request.data.get("permission", "")) self._set_user_permission(org, user, request.data.get("permission", "")) @@ -65,7 +64,7 @@ def partial_update(self, request): def destroy(self, request): org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) - user = get_object_or_404(User, email=request.data.get("user_email")) + user = get_object_or_404(User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True)) self._validate_permission(org, request.data.get("permission", "")) self._remove_user_permission(org, user, request.data.get("permission", "")) From 80e1f80ce96c492dc564714d3e6697e26ee64e66 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Tue, 27 Dec 2022 17:08:16 -0300 Subject: [PATCH 017/101] update: 2.0.1 (#194) --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3545d8c14..0a8b97d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [2.0.1] - 2022-12-27 +- Feature: Add a new endpoint thats return file from Weni Bucket +- Feature: Add is_active filter to User-Permission endpoint + ## [2.0.0] - 2022-12-23 - Feature: Create endpoint for channel type diff --git a/pyproject.toml b/pyproject.toml index 3cdd7dd1a..bf09a0afe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.0.0" +version = "2.0.1" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 0905ec9c4d48d90bd11267c8cc036f87b1a9cfaf Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:10:42 -0300 Subject: [PATCH 018/101] feat: When creating a classifier syncs it synchronously (#191) --- weni/internal/classifier/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/weni/internal/classifier/serializers.py b/weni/internal/classifier/serializers.py index dbd92d497..30acc9681 100644 --- a/weni/internal/classifier/serializers.py +++ b/weni/internal/classifier/serializers.py @@ -22,7 +22,10 @@ def create(self, validated_data: dict) -> Classifier: config = dict(access_token=validated_data["access_token"]) validated_data.pop("access_token") - return Classifier.create(config=config, **validated_data) + classifier = Classifier.create(config=config, sync=False, **validated_data) + classifier.sync() + + return classifier class Meta: model = Classifier From ef08e432fc0fcaa87c85a35ae935a1577c698927 Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:11:38 -0300 Subject: [PATCH 019/101] Create new Django App from recent activity (#190) * feat: Adds a new recent activity app * feat: Add task that creates recent activities in connect * feat: Creates signals that notify the connect of creation and change in Flow, Channel, Trigger and Campaign models * feat: Adds feature that prevents connect from being called when releasing a model * feat: Add condition that prevents two events from being sent when creating a flow --- weni/activities/__init__.py | 0 weni/activities/apps.py | 9 +++++ weni/activities/migrations/__init__.py | 0 weni/activities/signals.py | 54 ++++++++++++++++++++++++++ weni/activities/tasks.py | 9 +++++ 5 files changed, 72 insertions(+) create mode 100644 weni/activities/__init__.py create mode 100644 weni/activities/apps.py create mode 100644 weni/activities/migrations/__init__.py create mode 100644 weni/activities/signals.py create mode 100644 weni/activities/tasks.py diff --git a/weni/activities/__init__.py b/weni/activities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/activities/apps.py b/weni/activities/apps.py new file mode 100644 index 000000000..2ee8cf5cf --- /dev/null +++ b/weni/activities/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ActivitiesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "weni.activities" + + def ready(self) -> None: + from weni.activities import signals diff --git a/weni/activities/migrations/__init__.py b/weni/activities/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/activities/signals.py b/weni/activities/signals.py new file mode 100644 index 000000000..5aa775753 --- /dev/null +++ b/weni/activities/signals.py @@ -0,0 +1,54 @@ +import celery +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver + +from temba.channels.models import Channel +from temba.flows.models import Flow +from temba.triggers.models import Trigger +from temba.campaigns.models import Campaign + + +def create_recent_activity(instance: models.Model, created: bool): + if instance.is_active: + action = "CREATE" if created else "UPDATE" + + celery.execute.send_task("create_recent_activity", kwargs=dict( + action=action, + entity=instance.__class__.__name__.upper(), + entity_name=getattr(instance, "name", None), + user=instance.modified_by.email, + flow_organization=str(instance.org.uuid), + )) + + +@receiver(post_save, sender=Channel) +def channel_recent_activity_signal(sender, instance: Channel, created: bool, **kwargs): + create_recent_activity(instance, created) + + +@receiver(post_save, sender=Flow) +def flow_recent_activity_signal(sender, instance: Flow, created: bool, **kwargs): + update_fields = kwargs.get("update_fields") + if update_fields != frozenset({ + 'version_number', + 'modified_on', + 'saved_on', + 'modified_by', + 'metadata', + 'saved_by', + 'base_language', + 'has_issues' + }): + # This condition prevents two events from being sent when creating a flow + create_recent_activity(instance, created) + + +@receiver(post_save, sender=Trigger) +def trigger_recent_activity_signal(sender, instance: Trigger, created: bool, **kwargs): + create_recent_activity(instance, created) + + +@receiver(post_save, sender=Campaign) +def campaign_recent_activity_signal(sender, instance: Campaign, created: bool, **kwargs): + create_recent_activity(instance, created) diff --git a/weni/activities/tasks.py b/weni/activities/tasks.py new file mode 100644 index 000000000..238b08c37 --- /dev/null +++ b/weni/activities/tasks.py @@ -0,0 +1,9 @@ +from celery import shared_task + +from weni.internal.clients import ConnectInternalClient + + +@shared_task(name="create_recent_activity") +def create_recent_activity(action: str, entity: str, entity_name: str, user: str, flow_organization: str): + client = ConnectInternalClient() + client.create_recent_activity(action, entity, entity_name, user, flow_organization) From 0526fc5b534ed081137470dcffff333ae5c4f3da Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:18:22 -0300 Subject: [PATCH 020/101] Create an internal client for the connect REST API (#188) * feat: Add base for internal client * feat: Create an internal client for the connect REST API --- weni/internal/clients/__init__.py | 5 +++++ weni/internal/clients/authenticators.py | 24 ++++++++++++++++++++++++ weni/internal/clients/base.py | 11 +++++++++++ weni/internal/clients/connect.py | 18 ++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 weni/internal/clients/__init__.py create mode 100644 weni/internal/clients/authenticators.py create mode 100644 weni/internal/clients/base.py create mode 100644 weni/internal/clients/connect.py diff --git a/weni/internal/clients/__init__.py b/weni/internal/clients/__init__.py new file mode 100644 index 000000000..54291c803 --- /dev/null +++ b/weni/internal/clients/__init__.py @@ -0,0 +1,5 @@ +from weni.internal.clients.connect import ConnectInternalClient + +__all__ = ( + "ConnectInternalClient" +) diff --git a/weni/internal/clients/authenticators.py b/weni/internal/clients/authenticators.py new file mode 100644 index 000000000..d565e2dec --- /dev/null +++ b/weni/internal/clients/authenticators.py @@ -0,0 +1,24 @@ +import requests +from django.conf import settings + + +class InternalAuthenticator(object): + def _get_module_token(self): + response = requests.post( + url=settings.OIDC_OP_TOKEN_ENDPOINT, + data={ + "client_id": settings.OIDC_RP_CLIENT_ID, + "client_secret": settings.OIDC_RP_CLIENT_SECRET, + "grant_type": "client_credentials", + }, + ) + + token = response.json().get("access_token") + return f"Bearer {token}" + + @property + def headers(self): + return { + "Content-Type": "application/json; charset: utf-8", + "Authorization": self._get_module_token(), + } diff --git a/weni/internal/clients/base.py b/weni/internal/clients/base.py new file mode 100644 index 000000000..1d1482130 --- /dev/null +++ b/weni/internal/clients/base.py @@ -0,0 +1,11 @@ +from django.conf import settings +from weni.internal.clients.authenticators import InternalAuthenticator + + +class BaseInternalClient(object): + def __init__(self, base_url: str = None, authenticator: InternalAuthenticator = None): + self.base_url = base_url if base_url else settings.CONNECT_BASE_URL + self.authenticator = authenticator if authenticator else InternalAuthenticator() + + def get_url(self, endpoint: str) -> str: + return f"{self.base_url}{endpoint}" diff --git a/weni/internal/clients/connect.py b/weni/internal/clients/connect.py new file mode 100644 index 000000000..0f34f68a0 --- /dev/null +++ b/weni/internal/clients/connect.py @@ -0,0 +1,18 @@ +import requests + +from weni.internal.clients.base import BaseInternalClient + + +class ConnectInternalClient(BaseInternalClient): + + def create_recent_activity(self, action: str, entity: str, entity_name: str, user: str, flow_organization: str): + body = dict( + action=action, + entity=entity, + entity_name=entity_name, + user=user, + flow_organization=flow_organization, + ) + response = requests.post(self.get_url("/v1/recent-activity"), headers=self.authenticator.headers, json=body) + + return response From a288ea310a7e585988032da466e9cd57c42a9eec Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:43:21 -0300 Subject: [PATCH 021/101] Add success org retrieve endpoint (#186) * feat: Add success org retrieve endpoint * feat: Adjust business rules to return org whose user has any permission level * feat: Separate views from retrieve and successful org listing and allow authenticating from keycloak --- weni/success_orgs/business.py | 46 ++++++++++++---- weni/success_orgs/tests/__init__.py | 0 weni/success_orgs/tests/test_business.py | 69 ++++++++++++++++++++++-- weni/success_orgs/urls.py | 10 +++- weni/success_orgs/views.py | 64 +++++++++++++++++----- 5 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 weni/success_orgs/tests/__init__.py diff --git a/weni/success_orgs/business.py b/weni/success_orgs/business.py index 4ea50acae..aac615990 100644 --- a/weni/success_orgs/business.py +++ b/weni/success_orgs/business.py @@ -1,3 +1,7 @@ +# TODO: move code to a package and separate between logic and exceptions + +from typing import TYPE_CHECKING + from django.db.models import Exists, OuterRef, Case, When, Value, BooleanField, F from django.contrib.auth import get_user_model @@ -8,6 +12,10 @@ from temba.msgs.models import Msg +if TYPE_CHECKING: + from django.db.models.query import QuerySet + + User = get_user_model() @@ -23,6 +31,20 @@ class UserDoesNotExist(User.DoesNotExist): pass +class OrgDoesNotExist(Org.DoesNotExist): + pass + + +def user_has_org_permission(user: User, org: Org) -> bool: + return ( + org.created_by == user + or user.org_admins.filter(pk=org.pk) + or user.org_viewers.filter(pk=org.pk) + or user.org_editors.filter(pk=org.pk) + or user.org_surveyors.filter(pk=org.pk) + ) + + def get_user_by_email(email: str) -> User: try: return User.objects.get(email=email) @@ -30,15 +52,9 @@ def get_user_by_email(email: str) -> User: raise UserDoesNotExist(error) -def get_user_orgs(user: User): - return Org.objects.filter(created_by=user) - - -def get_user_success_orgs(user: User): - user_orgs = get_user_orgs(user) - +def get_success_orgs() -> "QuerySet[Org]": return ( - user_orgs.annotate(user_last_login=F("created_by__last_login")) + Org.objects.annotate(user_last_login=F("created_by__last_login")) .annotate(**SUCCESS_ORG_QUERIES) .annotate( is_success_project=Case( @@ -50,7 +66,19 @@ def get_user_success_orgs(user: User): ) -def get_user_success_orgs_by_email(email: str): +def get_user_success_orgs(user: User) -> "QuerySet[Org]": + return get_success_orgs().filter(created_by=user) + + +def get_user_success_orgs_by_email(email: str) -> dict: user = get_user_by_email(email) return dict(email=user.email, last_login=user.last_login, orgs=get_user_success_orgs(user)) + + +def retrieve_success_org(org_uuid: str) -> Org: + + try: + return get_success_orgs().get(uuid=org_uuid) + except Org.DoesNotExist as error: + raise OrgDoesNotExist(error) diff --git a/weni/success_orgs/tests/__init__.py b/weni/success_orgs/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/success_orgs/tests/test_business.py b/weni/success_orgs/tests/test_business.py index cc2a6922d..a0ed1095c 100644 --- a/weni/success_orgs/tests/test_business.py +++ b/weni/success_orgs/tests/test_business.py @@ -1,21 +1,84 @@ +import uuid + from django.test import TestCase from django.contrib.auth import get_user_model -from weni.success_orgs.business import UserDoesNotExist, get_user_by_email +from weni.success_orgs.business import ( + UserDoesNotExist, + OrgDoesNotExist, + get_user_by_email, + get_user_success_orgs, + retrieve_success_org, +) +from temba.orgs.models import Org +from temba.channels.models import Channel +from temba.classifiers.models import Classifier +from temba.flows.models import Flow User = get_user_model() -class GetUserByEmailTestCase(TestCase): +class SetupMixin(object): def setUp(self) -> None: self.user_email = "fake@weni.ai" self.user = User.objects.create(email=self.user_email) + self.org = Org.objects.create(created_by=self.user, modified_by=self.user, name="fakeorg") + +class GetUserByEmailTestCase(SetupMixin, TestCase): def test_get_user_by_email(self): user = get_user_by_email(self.user_email) self.assertEqual(user.email, self.user_email) def test_get_user_by_email_raise_does_not_exist_exception_with_wrong_email(self): with self.assertRaises(UserDoesNotExist): - user = get_user_by_email("wrong@weni.ai") + get_user_by_email("wrong@weni.ai") + + +class GetUserSuccessOrgsTestCase(SetupMixin, TestCase): + def test_function_returns_extra_fields(self): + org = get_user_success_orgs(self.user).first() + self.assertTrue(hasattr(org, "has_ia")) + self.assertTrue(hasattr(org, "has_flows")) + self.assertTrue(hasattr(org, "has_channel")) + self.assertTrue(hasattr(org, "has_msg")) + + def test_when_adding_classifier_it_returns_true(self): + Classifier.objects.create( + org=self.org, created_by=self.user, modified_by=self.user, config={}, classifier_type="bothub" + ) + + org = get_user_success_orgs(self.user).first() + self.assertTrue(org.has_ia) + self.assertFalse(org.has_flows) + self.assertFalse(org.has_channel) + self.assertFalse(org.has_msg) + + def test_when_adding_flow_it_returns_true(self): + Flow.objects.create(org=self.org, created_by=self.user, modified_by=self.user, saved_by=self.user) + + org = get_user_success_orgs(self.user).first() + self.assertFalse(org.has_ia) + self.assertTrue(org.has_flows) + self.assertFalse(org.has_channel) + self.assertFalse(org.has_msg) + + def test_when_adding_channel_it_returns_true(self): + Channel.objects.create(org=self.org, created_by=self.user, modified_by=self.user) + + org = get_user_success_orgs(self.user).first() + self.assertFalse(org.has_ia) + self.assertFalse(org.has_flows) + self.assertTrue(org.has_channel) + self.assertFalse(org.has_msg) + + +class RetrieveSuccessOrgTestCase(SetupMixin, TestCase): + def test_returns_org_by_uuid(self): + org = retrieve_success_org(str(self.org.uuid)) + self.assertEqual(org, self.org) + + def test_retrieve_success_org_raise_does_not_exist_exception_with_wrong_uuid(self): + with self.assertRaises(OrgDoesNotExist): + retrieve_success_org(uuid.uuid4()) diff --git a/weni/success_orgs/urls.py b/weni/success_orgs/urls.py index 96fcfd844..1d5211eee 100644 --- a/weni/success_orgs/urls.py +++ b/weni/success_orgs/urls.py @@ -1,9 +1,15 @@ from django.urls import path from rest_framework.urlpatterns import format_suffix_patterns -from weni.success_orgs.views import SuccessOrgAPIView +from weni.success_orgs.views import ListSuccessOrgAPIView, RetrieveSuccessOrgAPIView -urlpatterns = [path("success_orgs", SuccessOrgAPIView.as_view(), name="api.v2.success_orgs")] +urlpatterns = [ + path("success_orgs", ListSuccessOrgAPIView.as_view(), name="api.v2.success_orgs"), +] urlpatterns = format_suffix_patterns(urlpatterns, allowed=["json", "api"]) + +urlpatterns.append( + path("success_orgs/", RetrieveSuccessOrgAPIView.as_view(), name="api.v2.success_orgs_retrieve"), +) diff --git a/weni/success_orgs/views.py b/weni/success_orgs/views.py index 2accf3546..d45ee239c 100644 --- a/weni/success_orgs/views.py +++ b/weni/success_orgs/views.py @@ -1,16 +1,27 @@ from django.conf import settings - +from django.http import Http404 +from django.core import exceptions as django_exceptions +from rest_framework.renderers import JSONRenderer from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework import exceptions +from rest_framework import exceptions as drf_exceptions from rest_framework.authentication import get_authorization_header -from .business import get_user_success_orgs_by_email, UserDoesNotExist -from .serializers import UserSuccessOrgSerializer + +from weni.internal.authenticators import InternalOIDCAuthentication +from .business import ( + get_user_success_orgs_by_email, + retrieve_success_org, + user_has_org_permission, + UserDoesNotExist, + OrgDoesNotExist, +) +from .serializers import UserSuccessOrgSerializer, SuccessOrgSerializer -class SuccessOrgAPIView(APIView): +class ListSuccessOrgAPIView(APIView): + renderer_classes = [JSONRenderer] authentication_classes = [] permission_classes = [] @@ -19,31 +30,58 @@ def check_permissions(self, request): auth = get_authorization_header(request).split() if not auth: - raise exceptions.NotAuthenticated() + raise drf_exceptions.NotAuthenticated() if len(auth) == 1: msg = "Invalid token header. No credentials provided." - raise exceptions.AuthenticationFailed(msg) + raise drf_exceptions.AuthenticationFailed(msg) elif len(auth) > 2: msg = "Invalid token header. Token string should not contain spaces." - raise exceptions.AuthenticationFailed(msg) + raise drf_exceptions.AuthenticationFailed(msg) if auth[1].decode() != settings.FIXED_SUPER_ACCESS_TOKEN: - raise exceptions.PermissionDenied(detail="Invalid token!") - - def get(self, request, **kwargs): + raise drf_exceptions.PermissionDenied(detail="Invalid token!") + def get_user_email(self, request) -> str: user_email = request.query_params.get("email") if user_email is None: - raise exceptions.ValidationError("The query param: user_email is required!") + raise drf_exceptions.ValidationError("The query param: user_email is required!") + + return user_email + + def get(self, request, **kwargs) -> Response: + user_email = self.get_user_email(request) try: user_sucess_orgs = get_user_success_orgs_by_email(user_email) except UserDoesNotExist: - raise exceptions.ValidationError(f"User with email: {user_email} does not exist") + raise drf_exceptions.ValidationError(f"User with email: {user_email} does not exist") serializer = UserSuccessOrgSerializer(user_sucess_orgs) return Response(serializer.data) + + +class RetrieveSuccessOrgAPIView(APIView): + + authentication_classes = [InternalOIDCAuthentication] + renderer_classes = [JSONRenderer] + throttle_classes = [] + + def get(self, request, uuid) -> Response: + + try: + org = retrieve_success_org(uuid) + except OrgDoesNotExist: + raise Http404 + except django_exceptions.ValidationError as error: + raise drf_exceptions.ValidationError(error.messages) + + if not user_has_org_permission(request.user, org): + raise Http404 + + serializer = SuccessOrgSerializer(org) + + return Response(serializer.data) From d061e6e390a483e84d0b26c98057df01225f05da Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:59:46 -0300 Subject: [PATCH 022/101] Update to 2.1.0 (#195) --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8b97d79..2a7d1cd66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## [Unreleased] +## [2.1.0] - 2022-12-27 +- Add success org retrieve endpoint +- Create an internal client for the connect REST API +- Create new Django App from recent activity +- When creating a classifier syncs it synchronously + ## [2.0.1] - 2022-12-27 - Feature: Add a new endpoint thats return file from Weni Bucket - Feature: Add is_active filter to User-Permission endpoint diff --git a/pyproject.toml b/pyproject.toml index bf09a0afe..4f918d7a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.0.1" +version = "2.1.0" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 34786d2d68792c8d33a2289ff1750d4a5d3cfd89 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Wed, 28 Dec 2022 17:09:27 -0300 Subject: [PATCH 023/101] fix: remove magic usage on s3 api (#196) --- weni/s3/views.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/weni/s3/views.py b/weni/s3/views.py index 42ffa75af..a064695c5 100644 --- a/weni/s3/views.py +++ b/weni/s3/views.py @@ -1,6 +1,5 @@ import requests -import magic from django.http import HttpResponse from django.conf import settings @@ -8,14 +7,13 @@ class WeniFileCallbackView(FileCallbackView): - def post(self, request, *args, **kwargs): path = "media/" + kwargs["path"] assert ".." not in kwargs["path"] url = f"{settings.COURIER_S3_ENDPOINT}/{path}" - file = requests.get(url).content - file_type = magic.from_buffer(file, mime=True) + response = requests.get(url) + file_type = response.headers.get("Content-Type") - return HttpResponse(file, content_type=file_type) + return HttpResponse(response.content, content_type=file_type) From c6ab54fa5fd51b26c61d0f271d132038e1382e99 Mon Sep 17 00:00:00 2001 From: Paulo Abreu Date: Wed, 28 Dec 2022 17:52:57 -0300 Subject: [PATCH 024/101] update from 2.1.0 to 2.1.1 (#197) --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a7d1cd66..4943d46e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.1.1] - 2022-12-28 +- Fix error with magic package usage + ## [2.1.0] - 2022-12-27 - Add success org retrieve endpoint - Create an internal client for the connect REST API diff --git a/pyproject.toml b/pyproject.toml index 4f918d7a2..0adcc4c71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.1.0" +version = "2.1.1" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 81ce80a408c937e7b6fedff03959b109bda46489 Mon Sep 17 00:00:00 2001 From: nataliaweni Date: Wed, 28 Dec 2022 18:51:35 -0300 Subject: [PATCH 025/101] Update workflow --- .../workflows/build-rapidpro-apps-pypi.yaml | 148 ++++-------------- 1 file changed, 34 insertions(+), 114 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index afb5c83da..7b0826d23 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -6,67 +6,6 @@ on: - '*.*.*a*' jobs: - # build: - # runs-on: ubuntu-latest - - # steps: - # - name: Checkout repository - # uses: actions/checkout@v3 - - # - name: Install python - # uses: actions/setup-python@v3 - # with: - # python-version: '3.9' - - # - name: Install poetry - # uses: Gr1N/setup-poetry@v7 - - # - name: Checkout poetry version - # run: poetry --version - - # - name: Upload built package - # uses: actions/upload-artifact@v3 - # with: - # name: dist - # path: dist/ - # retention-days: 1 - - # Run pytest using built package - # test: - # needs: build - # runs-on: ubuntu-latest - # strategy: - # matrix: - # python: ["3.8", "3.9", "3.10"] - - # steps: - # - name: Checkout repository - # uses: actions/checkout@v2 - - # - name: Install python - # uses: actions/setup-python@v2 - # with: - # python-version: ${{ matrix.python }} - # cache: 'pip' - # cache-dependency-path: "poetry.lock" - - # - name: Download built package - # uses: actions/download-artifact@v3 - # with: - # name: dist - - # - name: Install weni-rp-apps and pytest - # shell: bash - # run: | - # WHL_NAME=$(ls weni-rp-apps-*.whl) - # pip install ${WHL_NAME}[experiments,entmoot] pytest - - # - name: Run tests - # shell: bash - # run: weni-rp-apps-tests - - # Publish to pypi on version change - # This is based on https://github.com/coveooss/pypi-publish-with-poetry publish: runs-on: ubuntu-latest @@ -74,6 +13,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Set variables + shell: bash + run: | + TAG="$( echo "${GITHUB_REF}" | cut -d'/' -f3 )" + VERSION="${TAG}" + echo "VERSION=${VERSION}" | tee -a "${GITHUB_ENV}" + - name: Install python uses: actions/setup-python@v2 with: @@ -81,64 +27,38 @@ jobs: - name: Install poetry uses: Gr1N/setup-poetry@v7 + + - name: Update version in pyproject.toml + shell: bash + run: | + sed -i 's;^version *=.*$;version = "'$VERSION'";g' pyproject.toml - name: Build package run: poetry build - # - name: Install coveo-pypi-cli - # run: pip install coveo-pypi-cli - - # - name: Determine the version for this release from the build - # id: current - # run: | - # BUILD_VER="$(ls dist/weni-rp-apps-*.tar.gz)" - # echo "Path: $BUILD_VER" - # if [[ $BUILD_VER =~ (weni-rp-apps-)([^,][0-9.]{4}) ]]; then - # echo "::set-output name=version::${BASH_REMATCH[2]}" - # echo "Version of build: ${BASH_REMATCH[2]}" - # else - # echo "No version found found" - # fi - - # - name: Get latest published version - # id: published - # run: | - # PUB_VER="$(pypi current-version weni-rp-apps)" - # echo "::set-output name=version::$PUB_VER" - # echo "Latest published version: $PUB_VER" - - - name: Publish to pypi - # if: (steps.current.outputs.version != steps.published.outputs.version) shell: bash run: | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} - # if [[ '${{ github.ref_name }}' == 'master' ]]; then - poetry publish - # else - # echo "Dry run of publishing the package" - # poetry publish --dry-run - # fi - - # - name: Tag repository - # shell: bash - # id: get-next-tag - # if: (steps.current.outputs.version != steps.published.outputs.version) - # run: | - # TAG_NAME=${{ steps.current.outputs.version }} - # echo "::set-output name=tag-name::$TAG_NAME" - # echo "This release will be tagged as $TAG_NAME" - # git config user.name "github-actions" - # git config user.email "actions@users.noreply.github.com" - # git tag --annotate --message="Automated tagging system" $TAG_NAME ${{ github.sha }} - - # - name: Push the tag - # if: (steps.current.outputs.version != steps.published.outputs.version) - # env: - # TAG_NAME: ${{ steps.current.outputs.version }} - # run: | - # if [[ ${{ github.ref_name }} == 'master' ]]; then - # git push origin $TAG_NAME - # else - # echo "If this was the master branch, I would push a new tag named $TAG_NAME" - # fi + poetry publish + + - name: Check out Kubernetes Manifests + uses: actions/checkout@master + with: + ref: main + repository: Ilhasoft/rapidpro + token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" + path: ./rapidpro/ + + - name: Update version on pip-requires.txt + run: | + sed -i 's;^weni-rp-apps@==.*$;weni-rp-apps@=='$VERSION';g' ./rapidpro/docker/pip-requires.txt + + - name: Commit & Push changes + uses: actions-js/push@master + with: + github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" + repository: Ilhasoft/rapidpro + directory: ./rapidpro/ + branch: main + message: "Update version of weni-rp-apps to ${{ env.VERSION }})" From be723c5af18e525b5031dc0a85f8713aefc38efe Mon Sep 17 00:00:00 2001 From: nataliaweni Date: Wed, 28 Dec 2022 18:55:53 -0300 Subject: [PATCH 026/101] Update workflow --- .../workflows/build-rapidpro-apps-pypi.yaml | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 7b0826d23..942a6fa63 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -43,22 +43,22 @@ jobs: poetry publish - name: Check out Kubernetes Manifests - uses: actions/checkout@master - with: - ref: main - repository: Ilhasoft/rapidpro - token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" - path: ./rapidpro/ + uses: actions/checkout@master + with: + ref: main + repository: Ilhasoft/rapidpro + token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" + path: ./rapidpro/ - name: Update version on pip-requires.txt run: | sed -i 's;^weni-rp-apps@==.*$;weni-rp-apps@=='$VERSION';g' ./rapidpro/docker/pip-requires.txt - name: Commit & Push changes - uses: actions-js/push@master - with: - github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" - repository: Ilhasoft/rapidpro - directory: ./rapidpro/ - branch: main - message: "Update version of weni-rp-apps to ${{ env.VERSION }})" + uses: actions-js/push@master + with: + github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" + repository: Ilhasoft/rapidpro + directory: ./rapidpro/ + branch: main + message: "Update version of weni-rp-apps to ${{ env.VERSION }})" From 5078691a6920039219f2178bca15498582bb26e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nat=C3=A1lia=20de=20Assis?= <98845410+nataliaweni@users.noreply.github.com> Date: Mon, 2 Jan 2023 17:47:52 -0300 Subject: [PATCH 027/101] Update build-rapidpro-apps-pypi.yaml --- .github/workflows/build-rapidpro-apps-pypi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 942a6fa63..1497b31d7 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -42,7 +42,7 @@ jobs: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} poetry publish - - name: Check out Kubernetes Manifests + - name: Check out Rapidpro-apps repository uses: actions/checkout@master with: ref: main From 5b7df5eb0bb497cc64729353a1061fe57b6f0b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nat=C3=A1lia=20de=20Assis?= <98845410+nataliaweni@users.noreply.github.com> Date: Mon, 2 Jan 2023 17:48:44 -0300 Subject: [PATCH 028/101] Update build-rapidpro-apps-pypi.yaml --- .github/workflows/build-rapidpro-apps-pypi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 1497b31d7..31d810e59 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -42,7 +42,7 @@ jobs: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} poetry publish - - name: Check out Rapidpro-apps repository + - name: Check out Rapidpro repository uses: actions/checkout@master with: ref: main From cc972955d3cfb120160c880b40e70fc7fef6f6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nat=C3=A1lia=20de=20Assis?= <98845410+nataliaweni@users.noreply.github.com> Date: Tue, 3 Jan 2023 09:28:41 -0300 Subject: [PATCH 029/101] Update workflow --- .github/workflows/build-rapidpro-apps-pypi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 31d810e59..170f4774a 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -1,4 +1,4 @@ -name: Build and Publish +name: Build and Publish Rapidpro-apps With Poetry in PyPI on: push: tags: @@ -42,7 +42,7 @@ jobs: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} poetry publish - - name: Check out Rapidpro repository + - name: Checkout Rapidpro repository uses: actions/checkout@master with: ref: main From e0db0e58139ab599ac3f0f815c4a70c9bdeedbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nat=C3=A1lia=20de=20Assis?= <98845410+nataliaweni@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:30:57 -0300 Subject: [PATCH 030/101] Update build-rapidpro-apps-pypi.yaml --- .../workflows/build-rapidpro-apps-pypi.yaml | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 170f4774a..df50a3b7a 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -42,23 +42,23 @@ jobs: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} poetry publish - - name: Checkout Rapidpro repository - uses: actions/checkout@master - with: - ref: main - repository: Ilhasoft/rapidpro - token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" - path: ./rapidpro/ +# - name: Checkout Rapidpro repository +# uses: actions/checkout@master +# with: +# ref: main +# repository: Ilhasoft/rapidpro +# token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" +# path: ./rapidpro/ - - name: Update version on pip-requires.txt - run: | - sed -i 's;^weni-rp-apps@==.*$;weni-rp-apps@=='$VERSION';g' ./rapidpro/docker/pip-requires.txt +# - name: Update version on pip-requires.txt +# run: | +# sed -i 's;^weni-rp-apps@==.*$;weni-rp-apps@=='$VERSION';g' ./rapidpro/docker/pip-requires.txt - - name: Commit & Push changes - uses: actions-js/push@master - with: - github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" - repository: Ilhasoft/rapidpro - directory: ./rapidpro/ - branch: main - message: "Update version of weni-rp-apps to ${{ env.VERSION }})" +# - name: Commit & Push changes +# uses: actions-js/push@master +# with: +# github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" +# repository: Ilhasoft/rapidpro +# directory: ./rapidpro/ +# branch: main +# message: "Update version of weni-rp-apps to ${{ env.VERSION }})" From fe549a97f1efb8f4721aca6690d7705913333012 Mon Sep 17 00:00:00 2001 From: Marcello Alexandre <22059535+marcelloale@users.noreply.github.com> Date: Thu, 19 Jan 2023 15:23:19 -0300 Subject: [PATCH 031/101] Update build-rapidpro-apps-pypi.yaml (#199) --- .../workflows/build-rapidpro-apps-pypi.yaml | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index df50a3b7a..d7f69b604 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -32,6 +32,15 @@ jobs: shell: bash run: | sed -i 's;^version *=.*$;version = "'$VERSION'";g' pyproject.toml + + - name: Commit & Push changes in rapidpro-apps + uses: actions-js/push@master + with: + github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" + repository: Ilhasoft/rapidpro-apps + directory: . + branch: "update/${{ env.VERSION }}" + message: "Update version of weni-rp-apps to ${{ env.VERSION }})" - name: Build package run: poetry build @@ -42,23 +51,25 @@ jobs: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} poetry publish -# - name: Checkout Rapidpro repository -# uses: actions/checkout@master -# with: -# ref: main -# repository: Ilhasoft/rapidpro -# token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" -# path: ./rapidpro/ + - name: Checkout Rapidpro repository + uses: actions/checkout@master + with: + ref: main + repository: Ilhasoft/rapidpro + token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" + path: ./rapidpro/ -# - name: Update version on pip-requires.txt -# run: | -# sed -i 's;^weni-rp-apps@==.*$;weni-rp-apps@=='$VERSION';g' ./rapidpro/docker/pip-requires.txt + - name: Update version on pip-requires.txt + run: | + VERSION="${{ env.VERSION }}" + sed -i 's;^weni-rp-apps@==.*$;weni-rp-apps@=='$VERSION';g' ./rapidpro/docker/pip-requires.txt + + - name: Commit & Push changes in rapidpro + uses: actions-js/push@master + with: + github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" + repository: Ilhasoft/rapidpro + directory: ./rapidpro/ + branch: "update/${{ env.VERSION }}" + message: "Update version of weni-rp-apps to ${{ env.VERSION }})" -# - name: Commit & Push changes -# uses: actions-js/push@master -# with: -# github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" -# repository: Ilhasoft/rapidpro -# directory: ./rapidpro/ -# branch: main -# message: "Update version of weni-rp-apps to ${{ env.VERSION }})" From 64e1218d5d014fd7e5497580eb5b7df7ac0c4d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nat=C3=A1lia=20de=20Assis?= <98845410+nataliaweni@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:35:13 -0300 Subject: [PATCH 032/101] Update build-rapidpro-apps-pypi.yaml --- .../workflows/build-rapidpro-apps-pypi.yaml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index d7f69b604..0cace9170 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -32,15 +32,22 @@ jobs: shell: bash run: | sed -i 's;^version *=.*$;version = "'$VERSION'";g' pyproject.toml - + + # - name: Create a new branch in rapidpro-apps + # uses: peterjgrainger/action-create-branch@v2.2.0 + # env: + # GITHUB_TOKEN: ${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }} + # with: + # branch: 'update/${{ env.VERSION }}' + - name: Commit & Push changes in rapidpro-apps uses: actions-js/push@master with: github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" repository: Ilhasoft/rapidpro-apps directory: . - branch: "update/${{ env.VERSION }}" - message: "Update version of weni-rp-apps to ${{ env.VERSION }})" + branch: "refs/heads/update/${{ env.VERSION }}" + message: "Update version of weni-rp-apps to ${{ env.VERSION }}" - name: Build package run: poetry build @@ -70,6 +77,6 @@ jobs: github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" repository: Ilhasoft/rapidpro directory: ./rapidpro/ - branch: "update/${{ env.VERSION }}" - message: "Update version of weni-rp-apps to ${{ env.VERSION }})" - + branch: "refs/heads/update/${{ env.VERSION }}" + message: "Update version of weni-rp-apps to ${{ env.VERSION }}" + From 96ea1c34c219270ffcfbacf3faf506729814ec34 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 3 Feb 2023 19:21:45 -0300 Subject: [PATCH 033/101] fix: change the has_issues field to False, when create flow (#201) * fix: set has issues to false for all flows in creation --- weni/internal/flows/serializers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/weni/internal/flows/serializers.py b/weni/internal/flows/serializers.py index 87f8e61fd..f16847bae 100644 --- a/weni/internal/flows/serializers.py +++ b/weni/internal/flows/serializers.py @@ -9,7 +9,6 @@ class FlowSerializer(serializers.ModelSerializer): - org = weni_serializers.OrgUUIDRelatedField() sample_flow = serializers.JSONField(write_only=True) @@ -20,11 +19,14 @@ class Meta: def create(self, validated_data): org = validated_data.get("org") sample_flows = validated_data.get("sample_flow") - org.import_app(sample_flows, org.created_by) - + self.disable_flows_has_issues(org, sample_flows) return org.flows.order_by("created_on").last() + def disable_flows_has_issues(self, org, sample_flows): + flows_name = list(map(lambda flow: flow.get("name"), sample_flows.get("flows"))) + org.flows.filter(name__in=flows_name).update(has_issues=False) + class FlowListSerializer(serializers.Serializer): flow_name = serializers.CharField(required=True, write_only=True) From 99d1600e53b39bdfeacced24e46cdfa1fa264816 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 3 Feb 2023 19:32:12 -0300 Subject: [PATCH 034/101] Update version of weni-rp-apps to 2.1.2 (#204) Co-authored-by: github-actions[bot] --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4943d46e8..fde27cefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.1.2] - 2023-02-03 +- Fix change the has_issues field to False, when create flow + ## [2.1.1] - 2022-12-28 - Fix error with magic package usage diff --git a/pyproject.toml b/pyproject.toml index 0adcc4c71..39769d452 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.1.1" +version = "2.1.2" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 71af7850849744ef13bfc865ceccdd4bcbfe05c2 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Wed, 8 Feb 2023 10:30:46 -0300 Subject: [PATCH 035/101] feat: Add Project model to internal app --- weni/internal/migrations/0002_project.py | 36 ++++++++++++++++++++++++ weni/internal/models.py | 17 +++++++++++ 2 files changed, 53 insertions(+) create mode 100644 weni/internal/migrations/0002_project.py diff --git a/weni/internal/migrations/0002_project.py b/weni/internal/migrations/0002_project.py new file mode 100644 index 000000000..cb517ed1e --- /dev/null +++ b/weni/internal/migrations/0002_project.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.17 on 2023-02-07 18:50 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("orgs", "0090_auto_20211209_2120"), + ("internal", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Project", + fields=[ + ( + "org_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="orgs.org", + ), + ), + ("project_uuid", models.UUIDField(default=uuid.uuid4, unique=True)), + ], + options={ + "db_table": "internal_project", + }, + bases=("orgs.org",), + ), + ] diff --git a/weni/internal/models.py b/weni/internal/models.py index ffe78fc9a..f4005773e 100644 --- a/weni/internal/models.py +++ b/weni/internal/models.py @@ -1,6 +1,9 @@ +from uuid import uuid4 + from django.db import models from temba.tickets.models import Ticketer, Topic +from temba.orgs.models import Org class TicketerQueue(Topic): @@ -12,3 +15,17 @@ class Meta: def __str__(self): return f"Queue[uuid={self.uuid}, name={self.name}]" + + +class Project(Org): + project_uuid = models.UUIDField(default=uuid4, unique=True) + + class Meta: + db_table = "internal_project" + + def __str__(self): + return f"Project[uuid={self.project_uuid}, org={self.org}]" + + @property + def org(self): + return self.org_ptr From 416cffc0a7dd47432c3e9274a2654e7ead1906e6 Mon Sep 17 00:00:00 2001 From: Marcello Alexandre Date: Fri, 10 Feb 2023 13:24:58 -0300 Subject: [PATCH 036/101] Update build-rapidpro-apps-pypi.yaml (name branch rapidpro and no create branch alpha) --- .github/workflows/build-rapidpro-apps-pypi.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 0cace9170..8820db817 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -32,15 +32,9 @@ jobs: shell: bash run: | sed -i 's;^version *=.*$;version = "'$VERSION'";g' pyproject.toml - - # - name: Create a new branch in rapidpro-apps - # uses: peterjgrainger/action-create-branch@v2.2.0 - # env: - # GITHUB_TOKEN: ${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }} - # with: - # branch: 'update/${{ env.VERSION }}' - name: Commit & Push changes in rapidpro-apps + if: ${{ !contains(env.VERSION, 'a') }} uses: actions-js/push@master with: github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" @@ -77,6 +71,6 @@ jobs: github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" repository: Ilhasoft/rapidpro directory: ./rapidpro/ - branch: "refs/heads/update/${{ env.VERSION }}" + branch: "refs/heads/update/weni-rp-apps-${{ env.VERSION }}" message: "Update version of weni-rp-apps to ${{ env.VERSION }}" From 86aaf51e2590c160f9a0e5b2ff3a6b51bc20fbc8 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 17 Feb 2023 18:45:47 -0300 Subject: [PATCH 037/101] fix: does not create activities when updating the channel.config at whatsapp and whatsapp-cloud (#200) --- weni/activities/signals.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/weni/activities/signals.py b/weni/activities/signals.py index 5aa775753..28e71b577 100644 --- a/weni/activities/signals.py +++ b/weni/activities/signals.py @@ -24,7 +24,10 @@ def create_recent_activity(instance: models.Model, created: bool): @receiver(post_save, sender=Channel) def channel_recent_activity_signal(sender, instance: Channel, created: bool, **kwargs): - create_recent_activity(instance, created) + update_fields = kwargs.get("update_fields") + if instance.channel_type not in ['WA', 'WAC'] \ + or update_fields != frozenset({'config',}): + create_recent_activity(instance, created) @receiver(post_save, sender=Flow) From c5807aeb366391fbbceb29beae0155706c58b0ea Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Fri, 17 Feb 2023 18:46:44 -0300 Subject: [PATCH 038/101] fix: Adjust the org field in the channel listing return (#203) --- weni/internal/channel/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weni/internal/channel/serializers.py b/weni/internal/channel/serializers.py index ee5b34638..193c8686a 100644 --- a/weni/internal/channel/serializers.py +++ b/weni/internal/channel/serializers.py @@ -148,5 +148,5 @@ class Meta: def to_representation(self, instance): ret = super().to_representation(instance) - ret['org'] = instance.uuid + ret['org'] = instance.org.uuid return ret From def493494d3c5c20259b4f72ee61bf4d77492260 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 17 Feb 2023 19:01:52 -0300 Subject: [PATCH 039/101] Update to 2.1.3 (#210) --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fde27cefb..e09ac0d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [2.1.3] - 2023-02-17 +- Adjust the org field in the channel listing return +- Fix: does not create activities when updating the channel.config at whatsapp and whatsapp-cloud + ## [2.1.2] - 2023-02-03 - Fix change the has_issues field to False, when create flow diff --git a/pyproject.toml b/pyproject.toml index 39769d452..994455bfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.1.2" +version = "2.1.3" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From e1d6472a9130f6242400f6d49516e8697019fc83 Mon Sep 17 00:00:00 2001 From: Marcello Alexandre Date: Thu, 23 Feb 2023 13:42:03 -0300 Subject: [PATCH 040/101] Update build-rapidpro-apps-pypi.yaml to weni-ai/flows --- .github/workflows/build-rapidpro-apps-pypi.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 8820db817..48d549edb 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -38,7 +38,7 @@ jobs: uses: actions-js/push@master with: github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" - repository: Ilhasoft/rapidpro-apps + repository: weni-ai/rapidpro-apps directory: . branch: "refs/heads/update/${{ env.VERSION }}" message: "Update version of weni-rp-apps to ${{ env.VERSION }}" @@ -52,11 +52,11 @@ jobs: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} poetry publish - - name: Checkout Rapidpro repository + - name: Checkout Flows repository uses: actions/checkout@master with: ref: main - repository: Ilhasoft/rapidpro + repository: weni-ai/flows token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" path: ./rapidpro/ @@ -65,11 +65,11 @@ jobs: VERSION="${{ env.VERSION }}" sed -i 's;^weni-rp-apps@==.*$;weni-rp-apps@=='$VERSION';g' ./rapidpro/docker/pip-requires.txt - - name: Commit & Push changes in rapidpro + - name: Commit & Push changes in Flows uses: actions-js/push@master with: github_token: "${{ secrets.DEVOPS_GITHUB_PERMANENT_TOKEN }}" - repository: Ilhasoft/rapidpro + repository: weni-ai/flows directory: ./rapidpro/ branch: "refs/heads/update/weni-rp-apps-${{ env.VERSION }}" message: "Update version of weni-rp-apps to ${{ env.VERSION }}" From 7338d587346133b5c4eabcd612f28f028e34d6ba Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:06:33 -0300 Subject: [PATCH 041/101] Add endpoint that allows creating, retrieving, destroying and listing globals (#208) * feat: Create and test GlobalSerializer * feat: create GlobalViewSet with Create, Retrieve, Destroy and List endpoints * feat: Add globals URls on internal app * feat: Add insert many objects function --------- Co-authored-by: lucaslinhares --- weni/internal/globals/__init__.py | 0 weni/internal/globals/serializers.py | 63 +++++++++++++++++++ weni/internal/globals/tests/__init__.py | 0 .../globals/tests/test_serializers.py | 53 ++++++++++++++++ weni/internal/globals/urls.py | 9 +++ weni/internal/globals/views.py | 32 ++++++++++ weni/internal/urls.py | 2 + 7 files changed, 159 insertions(+) create mode 100644 weni/internal/globals/__init__.py create mode 100644 weni/internal/globals/serializers.py create mode 100644 weni/internal/globals/tests/__init__.py create mode 100644 weni/internal/globals/tests/test_serializers.py create mode 100644 weni/internal/globals/urls.py create mode 100644 weni/internal/globals/views.py diff --git a/weni/internal/globals/__init__.py b/weni/internal/globals/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/internal/globals/serializers.py b/weni/internal/globals/serializers.py new file mode 100644 index 000000000..40064e976 --- /dev/null +++ b/weni/internal/globals/serializers.py @@ -0,0 +1,63 @@ +from rest_framework import serializers +from weni import serializers as weni_serializers + +from temba.globals.models import Global +from temba.orgs.models import Org + + +class GlobalSerializer(serializers.ModelSerializer): + org = weni_serializers.OrgUUIDRelatedField(required=True) + user = weni_serializers.UserEmailRelatedField(required=True, write_only=True) + + def validate(self, attrs) -> dict: + # All the logic needs to be recreated because + # Rapidpro didn't separate the business rule from the view layer + + validated_data = super().validate(attrs) + org = validated_data.get("org") + name = validated_data.get("name") + + org_active_globals_limit = org.get_limit(Org.LIMIT_GLOBALS) + + if org.globals.filter(is_active=True).count() >= org_active_globals_limit: + raise serializers.ValidationError(f"Cannot create a new global as limit is {org_active_globals_limit}.") + + if not Global.is_valid_name(name): + message = { + "name": serializers.ErrorDetail("Can only contain letters, numbers and hypens.", code="invalid") + } + raise serializers.ValidationError(message) + + if not Global.is_valid_key(Global.make_key(name)): + message = {"name": serializers.ErrorDetail("Isn't a valid name", code="invalid")} + raise serializers.ValidationError(message) + + return validated_data + + def create(self, validated_data): + name = validated_data.get("name") + + return Global.get_or_create( + validated_data.get("org"), + validated_data.get("user"), + key=Global.make_key(name=name), + value=validated_data.get("value"), + name=name, + ) + + def create_many(self, validated_data_list): + for validated_data in validated_data_list: + name = validated_data.get("name") + + Global.get_or_create( + validated_data.get("org"), + validated_data.get("user"), + key=Global.make_key(name=name), + value=validated_data.get("value"), + name=name, + ) + + class Meta: + model = Global + fields = ("uuid", "org", "user", "name", "value") + read_only_fields = ("uuid",) diff --git a/weni/internal/globals/tests/__init__.py b/weni/internal/globals/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/internal/globals/tests/test_serializers.py b/weni/internal/globals/tests/test_serializers.py new file mode 100644 index 000000000..492cbcb47 --- /dev/null +++ b/weni/internal/globals/tests/test_serializers.py @@ -0,0 +1,53 @@ +from unittest.mock import patch, MagicMock + +from rest_framework.exceptions import ValidationError, ErrorDetail + +from temba.tests import TembaTest +from temba.orgs.models import Org +from temba.globals.models import Global +from weni.internal.globals.serializers import GlobalSerializer + + +class GlobalSerializerTestCase(TembaTest): + def setUp(self): + super().setUp() + + self.global_data = { + "org": str(self.org.uuid), + "name": "Fake Global", + "value": "Sandro", + "user": self.user.email, + } + + @patch("django.db.models.query.QuerySet.count") + def test_exceeding_globals_limit_returns_validation_error(self, mock: MagicMock): + org_active_globals_limit = self.org.get_limit(Org.LIMIT_GLOBALS) + mock.return_value = org_active_globals_limit + 1 + + message = f"Cannot create a new global as limit is {org_active_globals_limit}." + + with self.assertRaisesMessage(ValidationError, message): + GlobalSerializer(data=self.global_data).is_valid(raise_exception=True) + + @patch("temba.globals.models.Global.is_valid_name") + def test_sending_invalid_name_returns_validation_error(self, mock: MagicMock): + mock.return_value = False + + message = str({"name": [ErrorDetail("Can only contain letters, numbers and hypens.", code="invalid")]}) + with self.assertRaisesMessage(ValidationError, message): + GlobalSerializer(data=self.global_data).is_valid(raise_exception=True) + + @patch("temba.globals.models.Global.is_valid_key") + def test_sending_invalid_name_key_returns_validation_error(self, mock: MagicMock): + mock.return_value = False + + message = str({"name": [ErrorDetail("Isn't a valid name", code="invalid")]}) + with self.assertRaisesMessage(ValidationError, message): + GlobalSerializer(data=self.global_data).is_valid(raise_exception=True) + + def tests_serializer_successfully_creates_global(self): + serializer = GlobalSerializer(data=self.global_data) + serializer.is_valid(raise_exception=True) + serializer.save() + + self.assertTrue(Global.objects.filter(name="Fake Global").exists()) diff --git a/weni/internal/globals/urls.py b/weni/internal/globals/urls.py new file mode 100644 index 000000000..051e4881c --- /dev/null +++ b/weni/internal/globals/urls.py @@ -0,0 +1,9 @@ +from rest_framework_nested import routers + +from weni.internal.globals.views import GlobalViewSet + + +router = routers.SimpleRouter() +router.register(r"globals", GlobalViewSet, basename="global") + +urlpatterns = router.urls diff --git a/weni/internal/globals/views.py b/weni/internal/globals/views.py new file mode 100644 index 000000000..df15089a0 --- /dev/null +++ b/weni/internal/globals/views.py @@ -0,0 +1,32 @@ +from rest_framework import mixins +from rest_framework import status +from rest_framework.response import Response + +from temba.globals.models import Global +from weni.internal.views import InternalGenericViewSet +from weni.internal.globals.serializers import GlobalSerializer + + +class GlobalViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + InternalGenericViewSet, +): + serializer_class = GlobalSerializer + queryset = Global.objects.filter(is_active=True) + lookup_field = "uuid" + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data, many=True) + serializer.is_valid(raise_exception=True) + + self.perform_create(serializer.validated_data) + + headers = self.get_success_headers(serializer.data) + + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, validated_data_list): + self.get_serializer().create_many(validated_data_list) diff --git a/weni/internal/urls.py b/weni/internal/urls.py index af79febc4..4c93b83d3 100644 --- a/weni/internal/urls.py +++ b/weni/internal/urls.py @@ -17,6 +17,7 @@ from weni.internal.classifier.urls import urlpatterns as classifier_urls from weni.internal.channel.urls import urlpatterns as channel_urls from weni.internal.statistic.urls import urlpatterns as statistics_urls +from weni.internal.globals.urls import urlpatterns as globals_urls internal_urlpatterns = [] @@ -27,6 +28,7 @@ internal_urlpatterns += classifier_urls internal_urlpatterns += channel_urls internal_urlpatterns += statistics_urls +internal_urlpatterns += globals_urls urlpatterns = [path("internals/", include(internal_urlpatterns))] From 175863fd66fdb496a6daeef66fe6a075773b8da9 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:14:32 -0300 Subject: [PATCH 042/101] Update CHANGELOG to 2.2.0 (#211) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e09ac0d05..29c9a9750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.2.0] - 2023-03-30 +- Add endpoint that allows creating, retrieving, destroying and listing globals + ## [2.1.3] - 2023-02-17 - Adjust the org field in the channel listing return - Fix: does not create activities when updating the channel.config at whatsapp and whatsapp-cloud From 22610f38361b9dfdc0f5aff220ed838f65b8953f Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Thu, 6 Apr 2023 17:55:52 -0300 Subject: [PATCH 043/101] Add endpoint allows the generic creation of an external service (#202) * feat: Add new internal app named `externals` * feat: Add endpoint allows the generic creation of an external service * feat: Add ExternalServicesAPIView endpoint to urls.py --- weni/internal/externals/__init__.py | 0 weni/internal/externals/serializers.py | 35 ++++++++++++++++++++++++++ weni/internal/externals/urls.py | 7 ++++++ weni/internal/externals/views.py | 30 ++++++++++++++++++++++ weni/internal/urls.py | 2 ++ 5 files changed, 74 insertions(+) create mode 100644 weni/internal/externals/__init__.py create mode 100644 weni/internal/externals/serializers.py create mode 100644 weni/internal/externals/urls.py create mode 100644 weni/internal/externals/views.py diff --git a/weni/internal/externals/__init__.py b/weni/internal/externals/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/internal/externals/serializers.py b/weni/internal/externals/serializers.py new file mode 100644 index 000000000..100e82079 --- /dev/null +++ b/weni/internal/externals/serializers.py @@ -0,0 +1,35 @@ +from rest_framework import serializers + + +from weni.serializers import OrgUUIDRelatedField, UserEmailRelatedField +from temba.externals.models import ExternalService + + +class ExternalServicesSerializer(serializers.Serializer): + + uuid = serializers.UUIDField(read_only=True) + type_code = serializers.CharField(write_only=True) + type_fields = serializers.JSONField(write_only=True) + org = OrgUUIDRelatedField(write_only=True) + user = UserEmailRelatedField(write_only=True) + + external_service_type = serializers.CharField(read_only=True) + name = serializers.CharField(read_only=True) + config = serializers.JSONField(read_only=True) + + def create(self, validated_data: dict): + validated_data = validated_data + + type_code = validated_data.get("type_code") + type_fields = validated_data.get("type_fields") + user = validated_data.get("user") + org = validated_data.get("org") + + try: + type_ = ExternalService.get_type_from_code(type_code) + except KeyError as error: + raise serializers.ValidationError(error) + + type_serializer = type_.serializer_class(data=type_fields) + type_serializer.is_valid(raise_exception=True) + return type_serializer.save(type=type_, created_by=user, modified_by=user, org=org) diff --git a/weni/internal/externals/urls.py b/weni/internal/externals/urls.py new file mode 100644 index 000000000..633c569f0 --- /dev/null +++ b/weni/internal/externals/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from weni.internal.externals.views import ExternalServicesAPIView + + +urlpatterns = [ + path("externals", ExternalServicesAPIView.as_view(), name="api.v2.externals"), +] diff --git a/weni/internal/externals/views.py b/weni/internal/externals/views.py new file mode 100644 index 000000000..c2acae26e --- /dev/null +++ b/weni/internal/externals/views.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer +from rest_framework.permissions import IsAuthenticated +from rest_framework import status + +from weni.internal.authenticators import InternalOIDCAuthentication +from weni.internal.permissions import CanCommunicateInternally +from weni.internal.externals.serializers import ExternalServicesSerializer + + +if TYPE_CHECKING: + from rest_framework.request import Request + + +class ExternalServicesAPIView(APIView): + authentication_classes = [InternalOIDCAuthentication] + permission_classes = [IsAuthenticated, CanCommunicateInternally] + pagination_class = None + renderer_classes = [JSONRenderer] + throttle_classes = [] + + def post(self, request: "Request") -> Response: + serializer = ExternalServicesSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/weni/internal/urls.py b/weni/internal/urls.py index 4c93b83d3..72e555343 100644 --- a/weni/internal/urls.py +++ b/weni/internal/urls.py @@ -18,6 +18,7 @@ from weni.internal.channel.urls import urlpatterns as channel_urls from weni.internal.statistic.urls import urlpatterns as statistics_urls from weni.internal.globals.urls import urlpatterns as globals_urls +from weni.internal.externals.urls import urlpatterns as externals_urls internal_urlpatterns = [] @@ -29,6 +30,7 @@ internal_urlpatterns += channel_urls internal_urlpatterns += statistics_urls internal_urlpatterns += globals_urls +internal_urlpatterns += externals_urls urlpatterns = [path("internals/", include(internal_urlpatterns))] From 785a0dd20b41d9041ba9400f1a67a58063d64613 Mon Sep 17 00:00:00 2001 From: Sandro Meireles <49874732+Sandro-Meireles@users.noreply.github.com> Date: Thu, 6 Apr 2023 18:27:38 -0300 Subject: [PATCH 044/101] Update version of weni-rp-apps to 2.3.0 (#213) --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c9a9750..fde5d3670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.3.0] - 2023-04-06 +- Add endpoint allows the generic creation of an external service + ## [2.2.0] - 2023-03-30 - Add endpoint that allows creating, retrieving, destroying and listing globals diff --git a/pyproject.toml b/pyproject.toml index 994455bfb..d46be63dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.1.3" +version = "2.3.0" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 5d098a2c066b5c70dfbc733a73308532acfc7c55 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Tue, 11 Apr 2023 18:32:06 -0300 Subject: [PATCH 045/101] feat: Adjust Channel app to new Project model. --- weni/internal/channel/serializers.py | 19 ++-- weni/internal/channel/tests.py | 137 ++++++++++++++------------- weni/internal/channel/views.py | 82 ++++++++-------- 3 files changed, 117 insertions(+), 121 deletions(-) diff --git a/weni/internal/channel/serializers.py b/weni/internal/channel/serializers.py index ee5b34638..e1385aae0 100644 --- a/weni/internal/channel/serializers.py +++ b/weni/internal/channel/serializers.py @@ -10,16 +10,17 @@ from rest_framework import serializers from rest_framework import exceptions -from weni.grpc.core import serializers as weni_serializers +from weni.serializers import fields as weni_serializers from temba.channels.models import Channel from temba.orgs.models import Org from temba.utils import analytics +from weni.internal.models import Project class ChannelWACSerializer(serializers.Serializer): user = weni_serializers.UserEmailRelatedField(required=True, write_only=True) - org = weni_serializers.OrgUUIDRelatedField(required=True, write_only=True) + org = weni_serializers.ProjectUUIDRelatedField(required=True, write_only=True) phone_number_id = serializers.CharField(required=True, write_only=True) uuid = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True) @@ -50,7 +51,7 @@ def create(self, validated_data): channel_type = Channel.get_type_from_code("WAC") schemes = channel_type.schemes - org = validated_data.get("org") + org = validated_data["org"].org name = validated_data.get("name") phone_number_id = validated_data.get("phone_number_id") config = validated_data.get("config", {}) @@ -89,7 +90,7 @@ def create(self, validated_data): data = validated_data.get("data") user = get_object_or_404(User, email=validated_data.get("user")) - org = get_object_or_404(Org, uuid=validated_data.get("org")) + org = get_object_or_404(Project, project_uuid=validated_data.get("org")) channel_type = Channel.get_type_from_code(validated_data.get("channeltype_code")) @@ -97,13 +98,13 @@ def create(self, validated_data): channel_type_code = validated_data.get("channeltype_code") raise exceptions.ValidationError(f"No channels found with '{channel_type_code}' code") - url = self.create_channel(user, org, data, channel_type) + url = self.create_channel(user, org.org, data, channel_type) if url is None: raise exceptions.ValidationError(f"Url not created") if "/users/login/?next=" in url: - raise exceptions.ValidationError(f"User: {user.email} do not have permission in Org: {org.uuid}") + raise exceptions.ValidationError(f"User: {user.email} do not have permission in Org: {org.org.uuid}") regex = "[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}" channe_uuid = re.findall(regex, url)[0] @@ -111,7 +112,7 @@ def create(self, validated_data): return channel - def create_channel(self, user: User, org: Org, data: dict, channel_type) -> str: + def create_channel(self, user: User, org: Project, data: dict, channel_type) -> str: factory = RequestFactory() url = f"channels/types/{channel_type.slug}/claim" @@ -148,5 +149,5 @@ class Meta: def to_representation(self, instance): ret = super().to_representation(instance) - ret['org'] = instance.uuid - return ret + ret['org'] = instance.org.project.project_uuid + return ret \ No newline at end of file diff --git a/weni/internal/channel/tests.py b/weni/internal/channel/tests.py index 46791630e..16cb997a8 100644 --- a/weni/internal/channel/tests.py +++ b/weni/internal/channel/tests.py @@ -21,10 +21,11 @@ from temba.channels.models import Channel from temba.channels.types import TYPES from temba.tests import TembaTest, mock_mailroom +from weni.internal.models import Project from .views import AvailableChannels, extract_form_info, extract_type_info - + class TembaRequestMixin(ABC): def reverse(self, viewname, kwargs=None, query_params=None): url = reverse(viewname, kwargs=kwargs) @@ -48,14 +49,14 @@ def request_detail(self, uuid): def request_post(self, data): url = reverse(self.get_url_namespace()) - token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + token = APIToken.get_or_create(self.project, self.admin, Group.objects.get(name="Administrators")) return self.client.post( url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" ) - def request_delete(self, uuid): - url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + def request_delete(self, uuid, **query_params): + url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}, query_params=query_params) token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.delete(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") @@ -68,10 +69,10 @@ def get_url_namespace(self): class CreateWACServiceTest(TembaTest, TembaRequestMixin): def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") - self.org = Org.objects.create( + self.project = Project.objects.create( name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user ) - self.org.add_user(self.user, OrgRole.ADMINISTRATOR) + self.project.add_user(self.user, OrgRole.ADMINISTRATOR) self.config = { "wa_number": "5561995743921", @@ -91,7 +92,7 @@ def test_create_whatsapp_cloud_channel(self, mock): phone_number_id = "5426423432" payload = { - "org": str(self.org.uuid), + "org": str(self.project.project_uuid), "user": self.user.email, "phone_number_id": phone_number_id, "config": self.config, @@ -111,7 +112,7 @@ def test_create_whatsapp_cloud_channel_invalid_address(self, mock): phone_number_id = "5426423432" payload = { - "org": str(self.org.uuid), + "org": str(self.project.project_uuid), "user": self.user.email, "phone_number_id": phone_number_id, "config": self.config, @@ -133,16 +134,16 @@ def get_url_namespace(self): class ReleaseChannelTestCase(TembaTest, TembaRequestMixin): def setUp(self): self.org_user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") - self.my_org = Org.objects.create( + self.my_org = Project.objects.create( name="Weni", timezone="Africa/Kigali", created_by=self.org_user, modified_by=self.org_user ) super().setUp() - - self.channel_obj = Channel.create(self.my_org, self.org_user, None, "WWC", "Test WWC") + self.channel_obj = Channel.create(self.my_org.org, self.org_user, None, "WWC", "Test WWC") def test_released_channel_is_active_equal_to_false(self): - self.request_delete(uuid=str(self.channel_obj.uuid)) + response = self.request_delete(uuid=str(self.channel_obj.uuid), user=self.org_user.email) + self.assertEqual(response.status_code, 200) self.assertFalse(Channel.objects.get(id=self.channel_obj.id).is_active) def get_url_namespace(self): @@ -152,30 +153,28 @@ def get_url_namespace(self): class CreateChannelTestCase(TembaTest, TembaRequestMixin): def setUp(self): self.org_user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") - self.my_org = Org.objects.create( + self.project = Project.objects.create( name="Weni", timezone="America/Sao_Paulo", created_by=self.org_user, modified_by=self.org_user ) - self.my_org.add_user(self.org_user, OrgRole.ADMINISTRATOR) + self.project.add_user(self.org_user, OrgRole.ADMINISTRATOR) super().setUp() def test_create_weni_web_chat_channel(self): payload = { "user": self.org_user.email, - "org": str(self.my_org.uuid), + "org": str(self.project.project_uuid), "data": {"name": "test", "base_url": "https://weni.ai"}, "channeltype_code": "WWC", } response = self.request_post(data=payload).json() - print(response) - channel = Channel.objects.get(uuid=response.get("uuid")) self.assertEqual(channel.address, response.get("address")) self.assertEqual(channel.name, response.get("name")) self.assertEqual(channel.config.get("base_url"), "https://weni.ai") - self.assertEqual(channel.org, self.my_org) + self.assertEqual(channel.org, self.project.org) self.assertEqual(channel.created_by, self.org_user) self.assertEqual(channel.modified_by, self.org_user) self.assertEqual(channel.channel_type, "WWC") @@ -187,14 +186,14 @@ def get_url_namespace(self): class RetrieveChannelTestCase(TembaTest, TembaRequestMixin): def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") - self.org = Org.objects.create( + self.project = Project.objects.create( name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user ) super().setUp() self.channel_obj = Channel.create( - self.org, self.user, None, "WWC", "Test WWC", "test", {"fake_key": "fake_value"} + self.project.org, self.user, None, "WWC", "Test WWC", "test", {"fake_key": "fake_value"} ) def test_channel_retrieve_returned_fields(self): @@ -214,16 +213,16 @@ def setUp(self): username="testuseradmin", password="123", email="test@weni.ai", is_superuser=True ) self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") - self.orgs = [ - Org.objects.create( - name=f"Org {org}", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + self.projects = [ + Project.objects.create( + name=f"Org {project}", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user ) - for org in range(2) + for project in range(2) ] for channel in range(6): Channel.create( - self.orgs[0] if channel % 2 == 0 else self.orgs[1], + self.projects[0].org if channel % 2 == 0 else self.projects[1].org, self.user, None, "WWC" if channel % 2 == 0 else "VK", @@ -243,29 +242,31 @@ def test_list_channels_filtered_by_type(self): self.assertEqual(len(response), 3) def test_list_channels_filtered_by_org_uuid(self): - org_uuid = str(self.orgs[0].uuid) - response = self.request_get(org=org_uuid).json() + org_uuid = str(self.projects[0].project_uuid) + response = self.request_get(org_uuid=org_uuid).json() + self.assertEqual(len(response), 3) channel = Channel.objects.get(uuid=response[0].get("uuid")) - self.assertEqual(channel.org, self.orgs[0]) + + self.assertEqual(channel.org, self.projects[0].org) def get_url_namespace(self): return "channel-list" class ListChannelAvailableTestCase(TembaTest, TembaRequestMixin): - url ='/api/v2/flows-backend/channels/' - + url = "/api/v2/flows-backend/channels/" + def setUp(self): super().setUp() content_type = ContentType.objects.get_for_model(User) self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") - self.admin.user_permissions.create(codename='can_communicate_internally', content_type=content_type) + self.admin.user_permissions.create(codename="can_communicate_internally", content_type=content_type) def test_list_all_channels(self): factory = APIRequestFactory() - view = AvailableChannels.as_view({'get': 'list'}) + view = AvailableChannels.as_view({"get": "list"}) view.permission_classes = [] request = factory.get(self.url) @@ -273,33 +274,33 @@ def test_list_all_channels(self): response = view(request) total_attrs = 0 - channel_types = response.data.get('channel_types') + channel_types = response.data.get("channel_types") for key in channel_types.keys(): - attributes = response.data.get('channel_types').get(key) + attributes = response.data.get("channel_types").get(key) if attributes: - if len(attributes)>0: + if len(attributes) > 0: total_attrs += 1 # checks if status code is 200 - ok self.assertEqual(response.status_code, status.HTTP_200_OK) # checks if the amount of #types returned is equivalent to the available response types - self.assertEqual(len(TYPES), len(response.data.get('channel_types'))) + self.assertEqual(len(TYPES), len(response.data.get("channel_types"))) # checks if response data have existing attributes self.assertEqual(total_attrs, len(TYPES)) def test_list_channels_without_authentication(self): - """ testing without authenticated user """ + """testing without authenticated user""" factory = APIRequestFactory() - view = AvailableChannels.as_view({'get': 'list'}) + view = AvailableChannels.as_view({"get": "list"}) request = factory.get(self.url) response = view(request) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_list_channels_without_permission(self): - """ testing user without permission """ + """testing user without permission""" factory = APIRequestFactory() - view = AvailableChannels.as_view({'get': 'list'}) + view = AvailableChannels.as_view({"get": "list"}) request = factory.get(self.url) force_authenticate(request, user=self.user) @@ -307,75 +308,74 @@ def test_list_channels_without_permission(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_retrieve_channel_with_permission(self): - """ Testing retrieve response is ok """ + """Testing retrieve response is ok""" have_attribute = False have_form = False form_ok = True - + factory = APIRequestFactory() request = factory.get(self.url) - view = AvailableChannels.as_view({'get': 'retrieve'}) + view = AvailableChannels.as_view({"get": "retrieve"}) view.permission_classes = [] force_authenticate(request, user=self.admin) - response = view(request, 'ac') + response = view(request, "ac") - if response.data.get('attributes'): + if response.data.get("attributes"): have_attribute = True - if response.data.get('form'): + if response.data.get("form"): have_form = True - if len(response.data.get('form'))>0: - form = response.data.get('form') + if len(response.data.get("form")) > 0: + form = response.data.get("form") for field in form: - if not field.get('name') \ - or not field.get('type') \ - or not field.get('help_text'): + if not field.get("name") or not field.get("type") or not field.get("help_text"): form_ok = False - + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(True, have_attribute) if have_form: self.assertEqual(True, form_ok) def test_retrieve_channel_without_permission(self): - """ testing retrieve without permission """ + """testing retrieve without permission""" factory = APIRequestFactory() - view = AvailableChannels.as_view({'get': 'retrieve'}) + view = AvailableChannels.as_view({"get": "retrieve"}) request = factory.get(self.url) force_authenticate(request, user=self.user) - response = view(request, 'ac') + response = view(request, "ac") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + def test_retrieve_channel_without_authentication(self): - """ testing retrieve without being authenticated """ + """testing retrieve without being authenticated""" factory = APIRequestFactory() - view = AvailableChannels.as_view({'get': 'retrieve'}) + view = AvailableChannels.as_view({"get": "retrieve"}) request = factory.get(self.url) - response = view(request, 'ac') + response = view(request, "ac") self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_invalid_response_info_form(self): - """ test missing values """ - self.assertEqual(extract_form_info('', 'name_field'), None) + """test missing values""" + self.assertEqual(extract_form_info("", "name_field"), None) def test_invalid_response_info_type(self): - """ test missing values """ - self.assertEqual(extract_type_info(''), None) + """test missing values""" + self.assertEqual(extract_type_info(""), None) def get_url_namespace(self): return "api.v2.flows_backend.channels-list" -class FormatFunctionTestCase(TestCase): +'''class FormatFunctionTestCase(TestCase): types = TYPES def test_form_with_values(self): """ checks if the treatment was done correctly """ test_form = { 'widget': to_object(**{'input_type': 'text'}), - 'help_text': 'test field' + 'help_text': 'test field', + 'label': 'test_label' } expect_form = { @@ -391,7 +391,8 @@ def test_form_without_name_value(self): """ check response without #name attribute """ test_form = { 'widget': to_object(**{'input_type': 'text'}), - 'help_text': 'test field' + 'help_text': 'test field', + 'label': 'test_label' } result = extract_form_info(to_object(**test_form),'') self.assertEqual(result, None) @@ -399,7 +400,8 @@ def test_form_without_name_value(self): def test_form_without_type_value(self): """ check response without #widget attribute """ test_form = { - 'help_text': 'test field' + 'help_text': 'test field', + 'label': 'test_label' } result = extract_form_info(to_object(**test_form),'test_form03') self.assertEqual(result, None) @@ -426,3 +428,4 @@ def test_all_types_response_contains_dict(self): class to_object: def __init__(self, **entries): return self.__dict__.update(entries) +''' \ No newline at end of file diff --git a/weni/internal/channel/views.py b/weni/internal/channel/views.py index 16f3f8898..a58cb0d4b 100644 --- a/weni/internal/channel/views.py +++ b/weni/internal/channel/views.py @@ -20,6 +20,7 @@ User = get_user_model() + class ChannelEndpoint(viewsets.ModelViewSet, InternalGenericViewSet): serializer_class = ChannelSerializer lookup_field = "uuid" @@ -27,14 +28,13 @@ class ChannelEndpoint(viewsets.ModelViewSet, InternalGenericViewSet): def get_queryset(self): channel_type = self.request.query_params.get("channel_type") org = self.request.query_params.get("org") - queryset = Channel.objects.all() if channel_type is not None: return queryset.filter(channel_type=channel_type) if org is not None: - return queryset.filter(org__uuid=org) + return queryset.filter(org__project__project_uuid=org) return queryset @@ -58,7 +58,7 @@ def create(self, request): def destroy(self, request, uuid=None): channel = get_object_or_404(Channel, uuid=uuid) - user = get_object_or_404(User, email=request.data.get("user")) + user = get_object_or_404(User, email=request.query_params.get("user")) channel.release(user) @@ -77,18 +77,17 @@ def create_wac(self, request): class AvailableChannels(viewsets.ViewSet, InternalGenericViewSet): - def list(self, request): types_available = TYPES channel_types = {} for value in types_available: if value not in settings.DISABLED_CHANNELS_INTEGRATIONS: fields_types = {} - attibutes_type = extract_type_info(types_available[value]) + attibutes_type = extract_type_info(types_available[value]) if not (attibutes_type): return Response(status=status.HTTP_404_NOT_FOUND) - fields_types['attributes'] = attibutes_type + fields_types["attributes"] = attibutes_type channel_types[value] = fields_types payload = { @@ -99,7 +98,7 @@ def list(self, request): def retrieve(self, request, pk=None): channel_type = None fields_form = {} - code_type = pk + code_type = pk if code_type: channel_type = TYPES.get(code_type.upper(), None) @@ -116,19 +115,16 @@ def retrieve(self, request, pk=None): if not (fields_in_form): return Response(status=status.HTTP_404_NOT_FOUND) - fields_form['form'] = fields_in_form + fields_form["form"] = fields_in_form fields_types = {} - attibutes_type = extract_type_info(channel_type) + attibutes_type = extract_type_info(channel_type) if not (attibutes_type): return Response(status=status.HTTP_404_NOT_FOUND) - fields_types['attributes'] = attibutes_type + fields_types["attributes"] = attibutes_type - payload = { - "attributes": fields_types.get('attributes'), - "form": fields_form.get('form') - } + payload = {"attributes": fields_types.get("attributes"), "form": fields_form.get("form")} return Response(payload) @@ -136,16 +132,13 @@ def retrieve(self, request, pk=None): def extract_type_info(_class): channel = {} type_exclude = [""] - items_exclude = ["redact_response_keys", "claim_view_kwargs", - "extra_links", "redact_request_keys"] + items_exclude = ["redact_response_keys", "claim_view_kwargs", "extra_links", "redact_request_keys"] for i in _class.__class__.__dict__.items(): - if not i[0].startswith('_'): - if not inspect.isclass(i[1]) and str(type(i[1])) not in(type_exclude) \ - and i[0] not in items_exclude: + if not i[0].startswith("_"): + if not inspect.isclass(i[1]) and str(type(i[1])) not in (type_exclude) and i[0] not in items_exclude: if str(type(i[1])) == "": - channel[i[0]] = {"name": i[1].name if i[1].name else "", - "value": i[1].value if i[1].value else ""} + channel[i[0]] = {"name": i[1].name if i[1].name else "", "value": i[1].value if i[1].value else ""} elif i[0] == "configuration_urls": if i[1]: @@ -153,14 +146,14 @@ def extract_type_info(_class): urls_list = [] url_dict = {} for url in i[1]: - if url.get('label'): - url_dict['label'] = str(url.get('label')) + if url.get("label"): + url_dict["label"] = str(url.get("label")) - if i[1][0].get('url'): - url_dict['url'] = str(url.get('url')) + if i[1][0].get("url"): + url_dict["url"] = str(url.get("url")) - if i[1][0].get('description'): - url_dict['description'] = str(url.get('description')) + if i[1][0].get("description"): + url_dict["description"] = str(url.get("description")) urls_list.append(url_dict) channel[i[0]] = urls_list @@ -172,41 +165,40 @@ def extract_type_info(_class): channel[i[0]] = str(i[1]) elif (i[0]) == "ivr_protocol": - channel[i[0]] = {"name": i[1].name if i[1].name else "", - "value": i[1].value if i[1].value else ""} + channel[i[0]] = {"name": i[1].name if i[1].name else "", "value": i[1].value if i[1].value else ""} else: - channel[i[0]] = (i[1]) + channel[i[0]] = i[1] - if (not (channel.get('code'))) or (not (len(channel))>0) \ - or (not (channel.get('name'))): + if (not (channel.get("code"))) or (not (len(channel)) > 0) or (not (channel.get("name"))): return None - channel['num_fields'] = len(channel) - return ((channel)) + channel["num_fields"] = len(channel) + return channel + def extract_form_info(_form, name_form): detail = {} - detail['name'] = name_form if name_form else None + detail["name"] = name_form if name_form else None try: - detail['type'] = str(_form.widget.input_type) + detail["type"] = str(_form.widget.input_type) except: - detail['type'] = None + detail["type"] = None if _form.help_text: - detail['help_text'] = str(_form.help_text) + detail["help_text"] = str(_form.help_text) else: - detail['help_text'] = None + detail["help_text"] = None - if detail.get('type') == 'select': - detail['choices'] = _form.choices + if detail.get("type") == "select": + detail["choices"] = _form.choices if _form.label: - detail['label'] = str(_form.label) + detail["label"] = str(_form.label) else: - detail['label'] = None + detail["label"] = None - if not (detail.get('name')) or not (detail.get('type')): + if not (detail.get("name")) or not (detail.get("type")): return None - return detail + return detail \ No newline at end of file From 92b97bea433dfe2a021261668b22ec54a5a7d90e Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Tue, 21 Mar 2023 18:59:05 -0300 Subject: [PATCH 046/101] fix: Adjust serializers and test in internal/channel --- weni/internal/channel/serializers.py | 5 +++-- weni/internal/channel/tests.py | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/weni/internal/channel/serializers.py b/weni/internal/channel/serializers.py index e1385aae0..0306338ac 100644 --- a/weni/internal/channel/serializers.py +++ b/weni/internal/channel/serializers.py @@ -149,5 +149,6 @@ class Meta: def to_representation(self, instance): ret = super().to_representation(instance) - ret['org'] = instance.org.project.project_uuid - return ret \ No newline at end of file + ret["org"] = instance.org.project.project_uuid if hasattr(instance.org, "project") else None + + return ret diff --git a/weni/internal/channel/tests.py b/weni/internal/channel/tests.py index 16cb997a8..5d903e8ee 100644 --- a/weni/internal/channel/tests.py +++ b/weni/internal/channel/tests.py @@ -23,7 +23,10 @@ from temba.tests import TembaTest, mock_mailroom from weni.internal.models import Project -from .views import AvailableChannels, extract_form_info, extract_type_info +from .views import AvailableChannels, ChannelEndpoint, extract_form_info, extract_type_info + +view_class = ChannelEndpoint +view_class.permission_classes = [] class TembaRequestMixin(ABC): @@ -222,7 +225,7 @@ def setUp(self): for channel in range(6): Channel.create( - self.projects[0].org if channel % 2 == 0 else self.projects[1].org, + self.projects[0] if channel % 2 == 0 else self.projects[1], self.user, None, "WWC" if channel % 2 == 0 else "VK", @@ -243,7 +246,8 @@ def test_list_channels_filtered_by_type(self): def test_list_channels_filtered_by_org_uuid(self): org_uuid = str(self.projects[0].project_uuid) - response = self.request_get(org_uuid=org_uuid).json() + + response = self.request_get(org=org_uuid).json() self.assertEqual(len(response), 3) @@ -355,16 +359,16 @@ def test_retrieve_channel_without_authentication(self): response = view(request, "ac") self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_invalid_response_info_form(self): + def get_url_namespace(self): + return "api.v2.flows_backend.channels-list" + + '''def test_invalid_response_info_form(self): """test missing values""" self.assertEqual(extract_form_info("", "name_field"), None) def test_invalid_response_info_type(self): """test missing values""" - self.assertEqual(extract_type_info(""), None) - - def get_url_namespace(self): - return "api.v2.flows_backend.channels-list" + self.assertEqual(extract_type_info(""), None)''' '''class FormatFunctionTestCase(TestCase): From 9ae44384f7c1c93839204db746b147968aea3178 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Tue, 11 Apr 2023 18:40:47 -0300 Subject: [PATCH 047/101] Add ProjectUUIDRelatedField in serializers field --- weni/serializers/fields.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/weni/serializers/fields.py b/weni/serializers/fields.py index a1c5e1960..b2b013c11 100644 --- a/weni/serializers/fields.py +++ b/weni/serializers/fields.py @@ -2,6 +2,7 @@ from rest_framework import relations from temba.orgs.models import Org +from weni.internal.models import Project User = get_user_model() @@ -15,3 +16,8 @@ def __init__(self, **kwargs): class OrgUUIDRelatedField(relations.SlugRelatedField): def __init__(self, **kwargs): super().__init__(slug_field="uuid", queryset=Org.objects.all(), **kwargs) + + +class ProjectUUIDRelatedField(relations.SlugRelatedField): + def __init__(self, **kwargs): + super().__init__(slug_field="project_uuid", queryset=Project.objects.all(), **kwargs) From 2bde7206398cfc055fcb56be0082561286ed49da Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Mon, 13 Feb 2023 17:18:50 -0300 Subject: [PATCH 048/101] feat: Adjust Users app to new Project model. --- weni/internal/users/serializers.py | 4 +- weni/internal/users/tests.py | 102 +++++++++++++++++------------ weni/internal/users/views.py | 74 +++++++++++---------- 3 files changed, 102 insertions(+), 78 deletions(-) diff --git a/weni/internal/users/serializers.py b/weni/internal/users/serializers.py index 3b342c19a..7809b9f3c 100644 --- a/weni/internal/users/serializers.py +++ b/weni/internal/users/serializers.py @@ -2,14 +2,14 @@ from rest_framework import serializers -from weni.grpc.core import serializers as weni_serializers +from weni.serializers import fields as weni_serializers User = get_user_model() class UserAPITokenSerializer(serializers.Serializer): user = weni_serializers.UserEmailRelatedField(required=True) - org = weni_serializers.OrgUUIDRelatedField(required=True) + project = weni_serializers.ProjectUUIDRelatedField(required=True) class UserPermissionSerializer(serializers.Serializer): diff --git a/weni/internal/users/tests.py b/weni/internal/users/tests.py index 74460410d..34c681df0 100644 --- a/weni/internal/users/tests.py +++ b/weni/internal/users/tests.py @@ -11,6 +11,17 @@ from temba.tests import TembaTest from temba.orgs.models import Org +from weni.internal.models import Project +from weni.internal.users.views import UserEndpoint, UserPermissionEndpoint, UserViewSet + +view_set = UserViewSet +view_set.permission_classes = [] + +view_permissions = UserPermissionEndpoint +view_permissions.permission_classes = [] + +view_endpoint = UserEndpoint +view_endpoint.permission_classes = [] class TembaRequestMixin(ABC): @@ -52,7 +63,7 @@ def request_post(self, data): def request_delete(self, data, **kwargs): url = self.reverse(self.get_url_namespace(), query_params=kwargs) - token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + token = APIToken.get_or_create(self.project, self.admin, Group.objects.get(name="Administrators")) return self.client.delete( f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" @@ -68,83 +79,87 @@ def setUp(self): self.admin = User.objects.create_user( username="testuser", password="123", email="test@weni.ai", is_superuser=True ) + + self.project = Project.objects.create( + name="Test", timezone="Africa/Kigali", created_by=self.admin, modified_by=self.admin + ) super().setUp() def test_user_permission_destroy(self): - org = Org.objects.first() + project = Project.objects.first() user = User.objects.first() destroy_wrong_permission = self.request_delete( - data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="adm") + data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="adm") ) self.assertEqual(destroy_wrong_permission.status_code, 400) self.assertEqual(destroy_wrong_permission.json()[0], "adm is not a valid permission!") - self.request_patch(data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="viewer")) - user_permissions = self._get_user_permissions(org=org, user=user) + self.request_patch(data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="viewer")) + user_permissions = self._get_user_permissions(project=project, user=user) - self.request_delete(data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="viewer")) - user_permissions_removed = self._get_user_permissions(org=org, user=user) + self.request_delete(data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="viewer")) + user_permissions_removed = self._get_user_permissions(project=project, user=user) self.assertFalse(user_permissions_removed.get("viewer", False)) self.assertNotEquals(user_permissions, user_permissions_removed) def test_user_permission_update(self): - org = Org.objects.first() + project = Project.objects.first() user = User.objects.first() update_wrong_permission_response = self.request_patch( - data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="adm") + data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="adm") ) self.assertEqual(update_wrong_permission_response.status_code, 400) self.assertEqual(update_wrong_permission_response.json()[0], "adm is not a valid permission!") update_response = self.request_patch( - data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="administrator") + data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="administrator") ).json() - user_permissions = self._get_user_permissions(org, user) + user_permissions = self._get_user_permissions(project, user) self.assertTrue(user_permissions.get("administrator")) self.assertTrue(self._permission_is_unique_true(update_response, "administrator")) update_response = self.request_patch( - data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="viewer") + data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="viewer") ).json() - user_permissions = self._get_user_permissions(org, user) + user_permissions = self._get_user_permissions(project, user) self.assertTrue(user_permissions.get("viewer")) self.assertTrue(self._permission_is_unique_true(update_response, "viewer")) update_response = self.request_patch( - data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="editor") + data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="editor") ).json() - user_permissions = self._get_user_permissions(org, user) + user_permissions = self._get_user_permissions(project, user) self.assertTrue(user_permissions.get("editor")) self.assertTrue(self._permission_is_unique_true(update_response, "editor")) update_response = self.request_patch( - data=dict(org_uuid=str(org.uuid), user_email=user.email, permission="surveyor") + data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="surveyor") ).json() - user_permissions = self._get_user_permissions(org, user) + user_permissions = self._get_user_permissions(project, user) self.assertTrue(user_permissions.get("surveyor")) self.assertTrue(self._permission_is_unique_true(update_response, "surveyor")) - def _get_permissions(self, org: Org) -> dict: + def _get_permissions(self, project: Project) -> dict: return { - "administrator": org.administrators, - "viewer": org.viewers, - "editor": org.editors, - "surveyor": org.surveyors, + "administrator": project.administrators, + "viewer": project.viewers, + "editor": project.editors, + "surveyor": project.surveyors, } - def _get_user_permissions(self, org: Org, user: User) -> dict: + def _get_user_permissions(self, project: Project, user: User) -> dict: permissions = {} - org_permissions = self._get_permissions(org) + project_permissions = self._get_permissions(project) - for perm_name, org_field in org_permissions.items(): - if org_field.filter(pk=user.id).exists(): + for perm_name, project_field in project_permissions.items(): + if project_field.filter(pk=user.id).exists(): permissions[perm_name] = True return permissions @@ -169,51 +184,54 @@ def setUp(self): self.admin = User.objects.create_user( username="testuser", password="123", email="test@weni.ai", is_superuser=True ) + self.project = Project.objects.create( + name="Test", timezone="Africa/Kigali", created_by=self.admin, modified_by=self.admin + ) super().setUp() def test_user_permission_retrieve(self): - org = Org.objects.first() + project = Project.objects.first() user = User.objects.first() - response_wrong_org = self.request_detail( + response_wrong_project = self.request_detail( org_uuid="f7e70145-6d17-4384-a1f2-d6981397866a", user_email="wrong@weni.ai" ) - self.assertEqual(response_wrong_org.status_code, 404) - self.assertEqual(response_wrong_org.json().get("detail"), "Not found.") + self.assertEqual(response_wrong_project.status_code, 404) + self.assertEqual(response_wrong_project.json().get("detail"), "Not found.") - org.administrators.add(user) + project.administrators.add(user) - response_wrong_user = self.request_detail(org_uuid=org.uuid, user_email=0) + response_wrong_user = self.request_detail(org_uuid=project.project_uuid, user_email=0) self.assertEqual(response_wrong_user.status_code, 404) self.assertEqual(response_wrong_user.json().get("detail"), "Not found.") - response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + response = self.request_detail(org_uuid=project.project_uuid, user_email=user.email).json() self.assertTrue(response.get("administrator")) self.assertTrue(self.permission_is_unique_true(response, "administrator")) - org.administrators.remove(user) - org.viewers.add(user) + project.administrators.remove(user) + project.viewers.add(user) - response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + response = self.request_detail(org_uuid=project.project_uuid, user_email=user.email).json() self.assertTrue(response.get("viewer")) self.assertTrue(self.permission_is_unique_true(response, "viewer")) - org.viewers.remove(user) - org.editors.add(user) + project.viewers.remove(user) + project.editors.add(user) - response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + response = self.request_detail(org_uuid=project.project_uuid, user_email=user.email).json() self.assertTrue(response.get("editor")) self.assertTrue(self.permission_is_unique_true(response, "editor")) - org.editors.remove(user) - org.surveyors.add(user) + project.editors.remove(user) + project.surveyors.add(user) - response = self.request_detail(org_uuid=org.uuid, user_email=user.email).json() + response = self.request_detail(org_uuid=project.project_uuid, user_email=user.email).json() self.assertTrue(response.get("surveyor")) self.assertTrue(self.permission_is_unique_true(response, "surveyor")) diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index 1dd9515fc..d793272a4 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -15,6 +15,7 @@ from weni.internal.users.serializers import UserAPITokenSerializer, UserSerializer, UserPermissionSerializer from temba.api.models import APIToken from temba.orgs.models import Org +from weni.internal.models import Project if TYPE_CHECKING: @@ -26,7 +27,6 @@ class UserViewSet(InternalGenericViewSet): @action(detail=False, methods=["GET"], url_path="api-token", serializer_class=UserAPITokenSerializer) def api_token(self, request: "Request", **kwargs): - serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) @@ -35,90 +35,96 @@ def api_token(self, request: "Request", **kwargs): except APIToken.DoesNotExist: raise exceptions.PermissionDenied() - return Response(dict(user=api_token.user.email, org=api_token.org.uuid, api_token=api_token.key)) + return Response( + dict(user=api_token.user.email, project=api_token.project.project_uuid, api_token=api_token.key) + ) class UserPermissionEndpoint(InternalGenericViewSet): serializer_class = UserPermissionSerializer def retrieve(self, request): - org = get_object_or_404(Org, uuid=request.query_params.get("org_uuid")) - user = get_object_or_404(User, email=request.query_params.get("user_email"), is_active=request.query_params.get("is_active", True)) + project = get_object_or_404(Project, project_uuid=request.query_params.get("org_uuid")) + user = get_object_or_404( + User, email=request.query_params.get("user_email"), is_active=request.query_params.get("is_active", True) + ) - permissions = self._get_user_permissions(org, user) + permissions = self._get_user_permissions(project, user) serializer = self.get_serializer(permissions) return Response(serializer.data) def partial_update(self, request): - org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) - user = get_object_or_404(User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True)) + project = get_object_or_404(Project, project_uuid=request.data.get("org_uuid")) + user = get_object_or_404( + User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True) + ) - self._validate_permission(org, request.data.get("permission", "")) - self._set_user_permission(org, user, request.data.get("permission", "")) + self._validate_permission(project, request.data.get("permission", "")) + self._set_user_permission(project, user, request.data.get("permission", "")) - permissions = self._get_user_permissions(org, user) + permissions = self._get_user_permissions(project, user) serializer = self.get_serializer(permissions) return Response(serializer.data) def destroy(self, request): - org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) - user = get_object_or_404(User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True)) + project = get_object_or_404(Project, project_uuid=request.data.get("org_uuid")) + user = get_object_or_404( + User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True) + ) - self._validate_permission(org, request.data.get("permission", "")) - self._remove_user_permission(org, user, request.data.get("permission", "")) + self._validate_permission(project, request.data.get("permission", "")) + self._remove_user_permission(project, user, request.data.get("permission", "")) - permissions = self._get_user_permissions(org, user) + permissions = self._get_user_permissions(project, user) serializer = self.get_serializer(permissions) return Response(serializer.data) - def _remove_user_permission(self, org: Org, user: User, permission: str): - permissions = self._get_permissions(org) + def _remove_user_permission(self, project: Project, user: User, permission: str): + permissions = self._get_permissions(project) permissions.get(permission).remove(user) - def _set_user_permission(self, org: Org, user: User, permission: str): - permissions = self._get_permissions(org) + def _set_user_permission(self, project: Project, user: User, permission: str): + permissions = self._get_permissions(project) - for perm_name, org_field in permissions.items(): + for perm_name, project_field in permissions.items(): if not perm_name == permission: - org_field.remove(user) + project_field.remove(user) permissions.get(permission).add(user) - def _validate_permission(self, org: Org, permission: str): - permissions_keys = self._get_permissions(org).keys() + def _validate_permission(self, project: Project, permission: str): + permissions_keys = self._get_permissions(project).keys() if permission not in permissions_keys: raise ValidationError(detail=f"{permission} is not a valid permission!") - def _get_permissions(self, org: Org) -> dict: + def _get_permissions(self, project: Project) -> dict: return { - "administrator": org.administrators, - "viewer": org.viewers, - "editor": org.editors, - "surveyor": org.surveyors, + "administrator": project.administrators, + "viewer": project.viewers, + "editor": project.editors, + "surveyor": project.surveyors, } - def _get_user_permissions(self, org: Org, user: User) -> dict: + def _get_user_permissions(self, project: Project, user: User) -> dict: permissions = {} - org_permissions = self._get_permissions(org) + project_permissions = self._get_permissions(project) - for perm_name, org_field in org_permissions.items(): - if org_field.filter(pk=user.id).exists(): + for perm_name, project_field in project_permissions.items(): + if project_field.filter(pk=user.id).exists(): permissions[perm_name] = True return permissions class UserEndpoint(InternalGenericViewSet, mixins.RetrieveModelMixin): - serializer_class = UserSerializer queryset = User.objects.all() lookup_field = "uuid" - def partial_update(self, request): instance = get_object_or_404(User, email=request.query_params.get("email")) From f061ae7ea525445842cc2db19b942c1fa6dc2207 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:42:47 -0300 Subject: [PATCH 049/101] Update get function to filter by org (#212) * fix: Update get function to filter by org * Update validation error detail to dict --- weni/internal/globals/views.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/weni/internal/globals/views.py b/weni/internal/globals/views.py index df15089a0..1990a47a8 100644 --- a/weni/internal/globals/views.py +++ b/weni/internal/globals/views.py @@ -1,8 +1,10 @@ from rest_framework import mixins from rest_framework import status from rest_framework.response import Response +from rest_framework.exceptions import ValidationError from temba.globals.models import Global +from temba.orgs.models import Org from weni.internal.views import InternalGenericViewSet from weni.internal.globals.serializers import GlobalSerializer @@ -15,9 +17,21 @@ class GlobalViewSet( InternalGenericViewSet, ): serializer_class = GlobalSerializer - queryset = Global.objects.filter(is_active=True) lookup_field = "uuid" + def get_queryset(self): + queryset = Global.objects.filter(is_active=True) + org = self.request.query_params.get("org") + + try: + org_object = Org.objects.get(uuid=org) + queryset = queryset.filter(org=org_object) + return queryset + + except Org.DoesNotExist as error: + raise ValidationError(detail={"message": str(error)}) + + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) From 10ee72a75facf2f6a5a0171e9c0fd9ca19c433f2 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:50:45 -0300 Subject: [PATCH 050/101] Update version of weni-rp-apps to 2.3.2 (#219) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fde5d3670..d8cf4338f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.3.2] - 2023-04-14 +- Update globals endpoint to filter by org + ## [2.3.0] - 2023-04-06 - Add endpoint allows the generic creation of an external service From 73e0fe08542068f516b9772d7e12bc4b791f76f5 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Thu, 20 Apr 2023 10:48:38 -0300 Subject: [PATCH 051/101] Update api-token action to recieve project --- weni/internal/users/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index d793272a4..dda6d2a3a 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -29,14 +29,16 @@ class UserViewSet(InternalGenericViewSet): def api_token(self, request: "Request", **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) + user = serializer.validated_data.get("user") + project = serializer.validated_data.get("project") try: - api_token = APIToken.objects.get(**serializer.validated_data) + api_token = APIToken.objects.get(user=user, org=project.org) except APIToken.DoesNotExist: raise exceptions.PermissionDenied() return Response( - dict(user=api_token.user.email, project=api_token.project.project_uuid, api_token=api_token.key) + dict(user=api_token.user.email, project=project.project_uuid, api_token=api_token.key) ) From 778ac2e660e7df9ee77254569911f5f93549c7e2 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:11:44 -0300 Subject: [PATCH 052/101] fix: add agent permission in get_permissions method (#224) --- weni/internal/users/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index 1dd9515fc..6b2510769 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -99,6 +99,7 @@ def _get_permissions(self, org: Org) -> dict: "viewer": org.viewers, "editor": org.editors, "surveyor": org.surveyors, + "agent": org.agents, } def _get_user_permissions(self, org: Org, user: User) -> dict: From 20be5c0a5a6f813fc0b6e1747cba22f8cfa909c5 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:12:11 -0300 Subject: [PATCH 053/101] feat: add delete endpoint in external-services (#215) --- weni/internal/externals/urls.py | 1 + weni/internal/externals/views.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/weni/internal/externals/urls.py b/weni/internal/externals/urls.py index 633c569f0..7a3b7b993 100644 --- a/weni/internal/externals/urls.py +++ b/weni/internal/externals/urls.py @@ -4,4 +4,5 @@ urlpatterns = [ path("externals", ExternalServicesAPIView.as_view(), name="api.v2.externals"), + path(r"externals//", ExternalServicesAPIView.as_view(), name="api.v2.externals"), ] diff --git a/weni/internal/externals/views.py b/weni/internal/externals/views.py index c2acae26e..4db52fe18 100644 --- a/weni/internal/externals/views.py +++ b/weni/internal/externals/views.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING +from django.contrib.auth.models import User +from django.shortcuts import get_object_or_404 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.renderers import JSONRenderer @@ -9,6 +11,7 @@ from weni.internal.authenticators import InternalOIDCAuthentication from weni.internal.permissions import CanCommunicateInternally from weni.internal.externals.serializers import ExternalServicesSerializer +from temba.externals.models import ExternalService if TYPE_CHECKING: @@ -28,3 +31,11 @@ def post(self, request: "Request") -> Response: serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) + + def delete(self, request, uuid=None): + external_service = get_object_or_404(ExternalService, uuid=uuid) + user = get_object_or_404(User, email=request.query_params.get("user")) + + external_service.release(user) + + return Response(status=status.HTTP_204_NO_CONTENT) From c2ca7d0df1db28398e663847d7faee7502293086 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:15:23 -0300 Subject: [PATCH 054/101] fix: removing the internal ticketer from org serializer template it will no longer be used (#220) --- weni/internal/orgs/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weni/internal/orgs/serializers.py b/weni/internal/orgs/serializers.py index 596845114..0d586b863 100644 --- a/weni/internal/orgs/serializers.py +++ b/weni/internal/orgs/serializers.py @@ -36,7 +36,7 @@ def create(self, validated_data): org = super().create(validated_data) org.administrators.add(validated_data.get("created_by")) - org.initialize(sample_flows=False, internal_ticketer=False) + org.initialize(sample_flows=False) return org From 10b19a5a973ed944b3f361a97507a9e793ac0f24 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:30:39 -0300 Subject: [PATCH 055/101] Update version of weni-rp-apps to 2.3.3 (#225) --- CHANGELOG.md | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8cf4338f..63b1c806f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## [Unreleased] +## [2.3.3] - 2023-04-20 +- Removing the internal ticketer from org serializer template +- Add agent permission in get_permissions method +- Add delete endpoint in external-services + ## [2.3.2] - 2023-04-14 - Update globals endpoint to filter by org diff --git a/pyproject.toml b/pyproject.toml index d46be63dc..e72bed46f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.3.0" +version = "2.3.3" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From d76a5c4ad8a1f494a24186de0b358184ebc7b80f Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Tue, 25 Apr 2023 12:43:14 -0300 Subject: [PATCH 056/101] Change class UserPermission to receive org --- weni/internal/users/views.py | 69 +++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index dda6d2a3a..989988241 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -46,77 +46,82 @@ class UserPermissionEndpoint(InternalGenericViewSet): serializer_class = UserPermissionSerializer def retrieve(self, request): - project = get_object_or_404(Project, project_uuid=request.query_params.get("org_uuid")) + org = get_object_or_404(Org, uuid=request.query_params.get("org_uuid")) user = get_object_or_404( - User, email=request.query_params.get("user_email"), is_active=request.query_params.get("is_active", True) + User, + email=request.query_params.get("user_email"), + is_active=request.query_params.get("is_active", True), ) - permissions = self._get_user_permissions(project, user) + permissions = self._get_user_permissions(org, user) serializer = self.get_serializer(permissions) return Response(serializer.data) def partial_update(self, request): - project = get_object_or_404(Project, project_uuid=request.data.get("org_uuid")) - user = get_object_or_404( - User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True) + org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) + user, created = User.objects.get_or_create( + email=request.data.get("user_email"), + defaults={"username": request.data.get("user_email")}, + is_active=request.query_params.get("is_active", True), ) - self._validate_permission(project, request.data.get("permission", "")) - self._set_user_permission(project, user, request.data.get("permission", "")) + self._validate_permission(org, request.data.get("permission", "")) + self._set_user_permission(org, user, request.data.get("permission", "")) - permissions = self._get_user_permissions(project, user) + permissions = self._get_user_permissions(org, user) serializer = self.get_serializer(permissions) return Response(serializer.data) def destroy(self, request): - project = get_object_or_404(Project, project_uuid=request.data.get("org_uuid")) + org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) user = get_object_or_404( - User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True) + User, + email=request.data.get("user_email"), + is_active=request.query_params.get("is_active", True), ) - self._validate_permission(project, request.data.get("permission", "")) - self._remove_user_permission(project, user, request.data.get("permission", "")) + self._validate_permission(org, request.data.get("permission", "")) + self._remove_user_permission(org, user, request.data.get("permission", "")) - permissions = self._get_user_permissions(project, user) + permissions = self._get_user_permissions(org, user) serializer = self.get_serializer(permissions) return Response(serializer.data) - def _remove_user_permission(self, project: Project, user: User, permission: str): - permissions = self._get_permissions(project) + def _remove_user_permission(self, org: Org, user: User, permission: str): + permissions = self._get_permissions(org) permissions.get(permission).remove(user) - def _set_user_permission(self, project: Project, user: User, permission: str): - permissions = self._get_permissions(project) + def _set_user_permission(self, org: Org, user: User, permission: str): + permissions = self._get_permissions(org) - for perm_name, project_field in permissions.items(): + for perm_name, org_field in permissions.items(): if not perm_name == permission: - project_field.remove(user) + org_field.remove(user) permissions.get(permission).add(user) - def _validate_permission(self, project: Project, permission: str): - permissions_keys = self._get_permissions(project).keys() - + def _validate_permission(self, org: Org, permission: str): + permissions_keys = self._get_permissions(org).keys() if permission not in permissions_keys: raise ValidationError(detail=f"{permission} is not a valid permission!") - def _get_permissions(self, project: Project) -> dict: + def _get_permissions(self, org: Org) -> dict: return { - "administrator": project.administrators, - "viewer": project.viewers, - "editor": project.editors, - "surveyor": project.surveyors, + "administrator": org.administrators, + "viewer": org.viewers, + "editor": org.editors, + "surveyor": org.surveyors, } - def _get_user_permissions(self, project: Project, user: User) -> dict: + def _get_user_permissions(self, org: Org, user: User) -> dict: permissions = {} - project_permissions = self._get_permissions(project) + org_permissions = self._get_permissions(org) - for perm_name, project_field in project_permissions.items(): - if project_field.filter(pk=user.id).exists(): + for perm_name, org_field in org_permissions.items(): + if org_field.filter(pk=user.id).exists(): permissions[perm_name] = True return permissions From a6e486b740c2ceb147e96e1ca1fddf28163fb521 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Wed, 26 Apr 2023 18:11:06 -0300 Subject: [PATCH 057/101] Update pre-commit config (#221) * fix: Update pre-commit config * create a flake8 config file --- .flake8 | 2 ++ .pre-commit-config.yaml | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..0055fc322 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore=E501,F405,T003,E203,W503 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ebba9125..31d3fbc0a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,17 +2,17 @@ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v4.4.0 hooks: - id: check-merge-conflict - id: end-of-file-fixer - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 + rev: 6.0.0 hooks: - id: flake8 From 0dda1f15ed830e87d6f17800d590cc4407796231 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Mon, 24 Apr 2023 17:44:50 -0300 Subject: [PATCH 058/101] remove Tema New create --- weni/auth/backends.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/weni/auth/backends.py b/weni/auth/backends.py index fd8efd8c1..6a0df0a44 100644 --- a/weni/auth/backends.py +++ b/weni/auth/backends.py @@ -1,10 +1,6 @@ import logging -import pytz -from django.conf import settings - from mozilla_django_oidc.auth import OIDCAuthenticationBackend -from temba.orgs.models import Org LOGGER = logging.getLogger("weni_django_oidc") @@ -33,18 +29,6 @@ def create_user(self, claims): user.save() - org = Org.objects.create( - name="Temba New", - timezone=pytz.timezone("America/Sao_Paulo"), - brand=settings.DEFAULT_BRAND, - created_by=user, - modified_by=user, - ) - org.administrators.add(user) - - # initialize our org, but without any credits - org.initialize(branding=org.get_branding(), topup_size=0) - return user def update_user(self, user, claims): From c15b1f59da2995001640a66013c0fcf6fc502da1 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Wed, 26 Apr 2023 18:40:35 -0300 Subject: [PATCH 059/101] Change get_object_or_404 to get_or_create in partial_update --- weni/internal/users/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index 6b2510769..77a11ee50 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -52,7 +52,11 @@ def retrieve(self, request): def partial_update(self, request): org = get_object_or_404(Org, uuid=request.data.get("org_uuid")) - user = get_object_or_404(User, email=request.data.get("user_email"), is_active=request.query_params.get("is_active", True)) + user, created = User.objects.get_or_create( + email=request.data.get("user_email"), + defaults={"username": request.data.get("user_email")}, + is_active=request.query_params.get("is_active", True), + ) self._validate_permission(org, request.data.get("permission", "")) self._set_user_permission(org, user, request.data.get("permission", "")) From 08396c7baefb6e6abbc32f9c98ad431b046ace4b Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Thu, 27 Apr 2023 22:14:25 -0300 Subject: [PATCH 060/101] Update version of weni-rp-apps to 2.3.4 --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b1c806f..5fe41b401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [2.3.4] - 2023-04-27 +- Update permission endpoint from User permission +- Update pre-commit config + ## [2.3.3] - 2023-04-20 - Removing the internal ticketer from org serializer template - Add agent permission in get_permissions method diff --git a/pyproject.toml b/pyproject.toml index e72bed46f..04ce681a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.3.3" +version = "2.3.4" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From b16d1cb0aa1fc9f3c0f065a9c035b9b65dcfccb1 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Wed, 8 Feb 2023 10:30:46 -0300 Subject: [PATCH 061/101] feat: Add Project model to internal app --- weni/internal/migrations/0002_project.py | 36 ++++++++++++++++++++++++ weni/internal/models.py | 17 +++++++++++ 2 files changed, 53 insertions(+) create mode 100644 weni/internal/migrations/0002_project.py diff --git a/weni/internal/migrations/0002_project.py b/weni/internal/migrations/0002_project.py new file mode 100644 index 000000000..cb517ed1e --- /dev/null +++ b/weni/internal/migrations/0002_project.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.17 on 2023-02-07 18:50 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("orgs", "0090_auto_20211209_2120"), + ("internal", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Project", + fields=[ + ( + "org_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="orgs.org", + ), + ), + ("project_uuid", models.UUIDField(default=uuid.uuid4, unique=True)), + ], + options={ + "db_table": "internal_project", + }, + bases=("orgs.org",), + ), + ] diff --git a/weni/internal/models.py b/weni/internal/models.py index ffe78fc9a..f4005773e 100644 --- a/weni/internal/models.py +++ b/weni/internal/models.py @@ -1,6 +1,9 @@ +from uuid import uuid4 + from django.db import models from temba.tickets.models import Ticketer, Topic +from temba.orgs.models import Org class TicketerQueue(Topic): @@ -12,3 +15,17 @@ class Meta: def __str__(self): return f"Queue[uuid={self.uuid}, name={self.name}]" + + +class Project(Org): + project_uuid = models.UUIDField(default=uuid4, unique=True) + + class Meta: + db_table = "internal_project" + + def __str__(self): + return f"Project[uuid={self.project_uuid}, org={self.org}]" + + @property + def org(self): + return self.org_ptr From 5764a7b836ebe40a053d6d65970603c302f520d5 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Tue, 14 Feb 2023 17:42:23 -0300 Subject: [PATCH 062/101] feat: Adjust Orgs app to new Project model. --- weni/internal/orgs/serializers.py | 37 +++++---- weni/internal/orgs/tests.py | 120 +++++++++++++++--------------- weni/internal/orgs/views.py | 60 +++++++-------- 3 files changed, 106 insertions(+), 111 deletions(-) diff --git a/weni/internal/orgs/serializers.py b/weni/internal/orgs/serializers.py index 0d586b863..aa33ce1a1 100644 --- a/weni/internal/orgs/serializers.py +++ b/weni/internal/orgs/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from django.contrib.auth import get_user_model -from temba.orgs.models import Org +from weni.internal.models import Project from weni.grpc.core import serializers as weni_serializers @@ -9,12 +9,11 @@ class TemplateOrgSerializer(serializers.ModelSerializer): - user_email = serializers.EmailField(write_only=True) timezone = serializers.CharField() class Meta: - model = Org + model = Project fields = ("user_email", "name", "timezone", "uuid") read_only_fields = ("uuid",) @@ -33,30 +32,30 @@ def validate(self, attrs): def create(self, validated_data): validated_data["plan"] = "infinity" - org = super().create(validated_data) + project = super().create(validated_data) - org.administrators.add(validated_data.get("created_by")) - org.initialize(sample_flows=False) + project.administrators.add(validated_data.get("created_by")) + project.initialize(sample_flows=False) - return org + return project class OrgSerializer(serializers.ModelSerializer): - users = serializers.SerializerMethodField() timezone = serializers.CharField() + uuid = serializers.UUIDField(source="project_uuid") def set_user_permission(self, user: dict, permission: str) -> dict: user["permission_type"] = permission return user - def get_users(self, org: Org): + def get_users(self, project: Project): values = ["id", "email", "username", "first_name", "last_name"] - administrators = list(org.administrators.all().values(*values)) - viewers = list(org.viewers.all().values(*values)) - editors = list(org.editors.all().values(*values)) - surveyors = list(org.surveyors.all().values(*values)) + administrators = list(project.administrators.all().values(*values)) + viewers = list(project.viewers.all().values(*values)) + editors = list(project.editors.all().values(*values)) + surveyors = list(project.surveyors.all().values(*values)) administrators = list(map(lambda user: self.set_user_permission(user, "administrator"), administrators)) viewers = list(map(lambda user: self.set_user_permission(user, "viewer"), viewers)) @@ -68,31 +67,29 @@ def get_users(self, org: Org): return users class Meta: - model = Org + model = Project fields = ["id", "name", "uuid", "timezone", "date_format", "users"] class OrgCreateSerializer(serializers.ModelSerializer): - user_email = serializers.EmailField() class Meta: - model = Org + model = Project fields = ["name", "timezone", "user_email"] class OrgUpdateSerializer(serializers.ModelSerializer): - - uuid = serializers.CharField(read_only=True) + project_uuid = serializers.CharField(read_only=True) modified_by = weni_serializers.UserEmailRelatedField(required=False, write_only=True) timezone = serializers.CharField(required=False) name = serializers.CharField(required=False) plan_end = serializers.DateTimeField(required=False) class Meta: - model = Org + model = Project fields = [ - "uuid", + "project_uuid", "modified_by", "name", "timezone", diff --git a/weni/internal/orgs/tests.py b/weni/internal/orgs/tests.py index 1b5c9569d..0b3dde907 100644 --- a/weni/internal/orgs/tests.py +++ b/weni/internal/orgs/tests.py @@ -6,14 +6,17 @@ from django.contrib.auth.models import Group from django.contrib.auth.models import User -from django.conf import settings from django.utils.http import urlencode from django.urls import reverse -from temba.orgs.models import Org - +from weni.internal.models import Project from temba.api.models import APIToken from temba.tests import TembaTest +from weni.internal.orgs.views import OrgViewSet + + +view_class = OrgViewSet +view_class.permission_classes = [] class TembaRequestMixin(ABC): @@ -33,7 +36,7 @@ def request_get(self, **query_params): return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") def request_detail(self, uuid): - url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + url = self.reverse(self.get_url_namespace(), kwargs={"project_uuid": uuid}) token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") @@ -47,13 +50,13 @@ def request_post(self, data): ) def request_delete(self, uuid, **query_params): - url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}, query_params=query_params) + url = self.reverse(self.get_url_namespace(), kwargs={"project_uuid": uuid}, query_params=query_params) token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.delete(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") def request_patch(self, uuid, data): - url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + url = self.reverse(self.get_url_namespace(), kwargs={"project_uuid": uuid}) token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.patch( @@ -66,7 +69,6 @@ def get_url_namespace(self): class OrgListTest(TembaTest, TembaRequestMixin): - WRONG_ID = -1 WRONG_UUID = "31313-dasda-dasdasd-23123" WRONG_EMAIL = "wrong@email.com" @@ -77,9 +79,9 @@ def setUp(self): user = User.objects.get(username="testuser") - Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) super().setUp() @@ -90,7 +92,7 @@ def test_list_orgs(self): response = self.request_get(user_email="wrong@email.com") self.assertEqual(response.status_code, 404) - orgs = Org.objects.all() + orgs = Project.objects.all() user = User.objects.get(username="testuser") weni_org = orgs.get(name="Weni") @@ -119,13 +121,14 @@ def test_list_orgs(self): self.assertEquals(len(response), 3) def test_list_users_on_org(self): - org = Org.objects.get(name="Tembinha") + org = Project.objects.get(name="Tembinha") testuser = User.objects.get(username="testuser") weniuser = User.objects.get(username="weniuser") org.administrators.add(testuser) response = self.request_get(user_email=testuser.email).json() + self.assertEquals(len(response[0].get("users")), 1) org.administrators.add(weniuser) @@ -137,30 +140,32 @@ def get_url_namespace(self): class OrgCreateTest(TembaTest, TembaRequestMixin): - WRONG_ID = -1 WRONG_UUID = "31313-dasda-dasdasd-23123" WRONG_EMAIL = "wrong@email.com" def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") User.objects.create_user(username="weniuser", password="123", email="wene@user.com") user = User.objects.get(username="testuser") - Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create( + name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user, plan="infinity" + ) + Project.objects.create( + name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user, plan="infinity" + ) + Project.objects.create( + name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user, plan="infinity" + ) super().setUp() @patch("temba.orgs.models.Org.create_sample_flows") def test_create_org(self, mock): - org_name = "TestCreateOrg" - user = User.objects.first() - + user = User.objects.get(username="weniuser") response = self.request_post(data=dict(name=org_name, timezone="Wrong/Zone", user_email=user.email)).json() self.assertEqual(response.get("timezone")[0], '"Wrong/Zone" is not a valid choice.') @@ -175,7 +180,7 @@ def test_create_org(self, mock): newuser = newuser_qs.first() - orgs = Org.objects.filter(name=org_name) + orgs = Project.objects.filter(name=org_name) org = orgs.first() self.assertEquals(orgs.count(), 1) @@ -203,26 +208,24 @@ def get_url_namespace(self): class OrgRetrieveTest(TembaTest, TembaRequestMixin): - WRONG_ID = -1 WRONG_UUID = "31313-dasda-dasdasd-23123" WRONG_EMAIL = "wrong@email.com" def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") User.objects.create_user(username="weniuser", password="123", email="wene@user.com") user = User.objects.get(username="testuser") - Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) super().setUp() def test_retrieve_org(self): - org = Org.objects.last() + org = Project.objects.first() user = User.objects.last() permission_types = ("administrator", "viewer", "editor", "surveyor") @@ -238,11 +241,10 @@ def test_retrieve_org(self): if random_permission == "surveyor": org.surveyors.add(user) - org_uuid = str(org.uuid) + org_uuid = str(org.project_uuid) org_timezone = str(org.timezone) response = self.request_detail(uuid=org_uuid).json() - response_user = response.get("users")[-1] self.assertEqual(response.get("id"), org.id) @@ -262,34 +264,32 @@ def get_url_namespace(self): class OrgDestroyTest(TembaTest, TembaRequestMixin): - WRONG_ID = -1 WRONG_UUID = "31313-dasda-dasdasd-23123" WRONG_EMAIL = "wrong@email.com" def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") User.objects.create_user(username="weniuser", password="123", email="wene@user.com") user = User.objects.get(username="testuser") - Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) super().setUp() def test_destroy_org(self): - org = Org.objects.last() - is_active = org.is_active - modified_by = org.modified_by + project = Project.objects.last() + is_active = project.is_active + modified_by = project.modified_by weniuser = User.objects.get(username="weniuser") - self.request_delete(uuid=str(org.uuid), user_email=weniuser.email) + self.request_delete(uuid=str(project.uuid), user_email=weniuser.email) - destroyed_org = Org.objects.get(id=org.id) + destroyed_org = Project.objects.get(id=project.id) self.assertFalse(destroyed_org.is_active) self.assertNotEquals(is_active, destroyed_org.is_active) @@ -301,43 +301,41 @@ def get_url_namespace(self): class OrgUpdateTest(TembaTest, TembaRequestMixin): - WRONG_ID = -1 WRONG_UUID = "31313-dasda-dasdasd-23123" WRONG_EMAIL = "wrong@email.com" def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") User.objects.create_user(username="weniuser", password="123", email="wene@user.com") user = User.objects.get(username="testuser") - Org.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) - Org.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Tembinha", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Weni", timezone="Africa/Kigali", created_by=user, modified_by=user) + Project.objects.create(name="Test", timezone="Africa/Kigali", created_by=user, modified_by=user) super().setUp() def test_update_org(self): - org = Org.objects.first() + project = Project.objects.first() - permission_error_message = f"User: {self.user.id} has no permission to update Org: {org.uuid}" + permission_error_message = f"User: {self.user.id} has no permission to update Org: {project.project_uuid}" - response = self.request_patch(uuid=str(org.uuid), data=dict(modified_by=self.user.email)).json() + response = self.request_patch(uuid=str(project.project_uuid), data=dict(modified_by=self.user.email)).json() self.assertEqual(response[0], permission_error_message) self.user.is_superuser = True self.user.save() - org.administrators.add(self.user) + project.administrators.add(self.user) update_fields = { "name": "NewOrgName", "timezone": "America/Maceio", "date_format": "M", - "plan": settings.INFINITY_PLAN, + "plan": "infinity", "plan_end": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "brand": "push.ia", "is_anon": True, @@ -347,38 +345,38 @@ def test_update_org(self): "modified_by": self.user.email, } - response = self.request_patch(uuid=str(org.uuid), data=update_fields).json() + response = self.request_patch(uuid=str(project.project_uuid), data=update_fields).json() - updated_org = Org.objects.get(pk=org.pk) + updated_org = Project.objects.get(pk=project.pk) self.assertEquals(update_fields.get("name"), updated_org.name) - self.assertNotEquals(org.name, updated_org.name) + self.assertNotEquals(project.name, updated_org.name) self.assertEquals(update_fields.get("timezone"), str(updated_org.timezone)) - self.assertNotEquals(org.timezone, updated_org.timezone) + self.assertNotEquals(project.timezone, updated_org.timezone) self.assertEquals(update_fields.get("date_format"), updated_org.date_format) - self.assertNotEquals(org.date_format, updated_org.date_format) + self.assertNotEquals(project.date_format, updated_org.date_format) - self.assertEquals(updated_org.plan, settings.INFINITY_PLAN) - self.assertNotEquals(org.plan, updated_org.plan) + self.assertEquals(updated_org.plan, "infinity") + self.assertNotEquals(project.plan, updated_org.plan) self.assertFalse(updated_org.uses_topups) self.assertEquals(updated_org.plan_end, None) self.assertEquals(update_fields.get("brand"), updated_org.brand) - self.assertNotEquals(org.brand, updated_org.brand) + self.assertNotEquals(project.brand, updated_org.brand) self.assertEquals(update_fields.get("is_anon"), updated_org.is_anon) - self.assertNotEquals(org.is_anon, updated_org.is_anon) + self.assertNotEquals(project.is_anon, updated_org.is_anon) self.assertEquals(update_fields.get("is_multi_user"), updated_org.is_multi_user) - self.assertNotEquals(org.is_multi_user, updated_org.is_multi_user) + self.assertNotEquals(project.is_multi_user, updated_org.is_multi_user) self.assertEquals(update_fields.get("is_multi_org"), updated_org.is_multi_org) - self.assertNotEquals(org.is_multi_org, updated_org.is_multi_org) + self.assertNotEquals(project.is_multi_org, updated_org.is_multi_org) self.assertEquals(update_fields.get("is_suspended"), updated_org.is_suspended) - self.assertNotEquals(org.is_suspended, updated_org.is_suspended) + self.assertNotEquals(project.is_suspended, updated_org.is_suspended) def get_url_namespace(self): return "orgs-detail" diff --git a/weni/internal/orgs/views.py b/weni/internal/orgs/views.py index 9659a9b2b..60f0d3e9e 100644 --- a/weni/internal/orgs/views.py +++ b/weni/internal/orgs/views.py @@ -16,7 +16,7 @@ OrgUpdateSerializer, ) -from temba.orgs.models import Org +from weni.internal.models import Project class TemplateOrgViewSet(CreateModelMixin, InternalGenericViewSet): @@ -24,14 +24,14 @@ class TemplateOrgViewSet(CreateModelMixin, InternalGenericViewSet): class OrgViewSet(viewsets.ModelViewSet, InternalGenericViewSet): - queryset = Org.objects - lookup_field = "uuid" + queryset = Project.objects + lookup_field = "project_uuid" def list(self, request): user = self.get_user(request) - orgs = self.get_orgs(user) - - serializer = OrgSerializer(orgs, many=True) + orgs_ids = self.get_orgs(user).values_list("id", flat=True) + projects = Project.objects.filter(id__in=orgs_ids) + serializer = OrgSerializer(projects, many=True) return Response(serializer.data) @@ -43,7 +43,7 @@ def create(self, request): email=request.data.get("user_email"), defaults={"username": request.data.get("user_email")} ) - org = Org.objects.create( + project = Project.objects.create( name=request.data.get("name"), timezone=request.data.get("timezone"), created_by=user, @@ -51,45 +51,45 @@ def create(self, request): plan="infinity", ) - org.administrators.add(user) - org.initialize() + project.administrators.add(user) + project.initialize() - org_serializer = OrgSerializer(org) + org_serializer = OrgSerializer(project) return Response(org_serializer.data) - def retrieve(self, request, uuid=None): - org = get_object_or_404(Org, uuid=uuid) - serializer = OrgSerializer(org) + def retrieve(self, request, project_uuid=None): + project = get_object_or_404(Project, project_uuid=project_uuid) + serializer = OrgSerializer(project) return Response(serializer.data) - def destroy(self, request, uuid=None): - org = get_object_or_404(Org, uuid=uuid) + def destroy(self, request, project_uuid=None): + project = get_object_or_404(Project, uuid=project_uuid) user = get_object_or_404(User, email=request.query_params.get("user_email")) - self.pre_destroy(org, user) - org.release(user) + self.pre_destroy(project, user) + project.release(user) return Response(status=status.HTTP_204_NO_CONTENT) - def partial_update(self, request, uuid=None): - org = get_object_or_404(Org, uuid=uuid) + def partial_update(self, request, project_uuid=None): + project = get_object_or_404(Project, project_uuid=project_uuid) - serializer = OrgUpdateSerializer(org, data=request.data) + serializer = OrgUpdateSerializer(project, data=request.data) serializer.is_valid(raise_exception=True) modified_by = serializer.validated_data.get("modified_by", None) plan = serializer.validated_data.get("plan", None) - if modified_by and not self._user_has_permisson(modified_by, org) and not modified_by.is_superuser: + if modified_by and not self._user_has_permisson(modified_by, project) and not modified_by.is_superuser: raise exceptions.ValidationError( - f"User: {modified_by.pk} has no permission to update Org: {org.uuid}", + f"User: {modified_by.pk} has no permission to update Org: {project.project_uuid}", ) if plan is not None and plan == settings.INFINITY_PLAN: - org.uses_topups = False - org.plan_end = None + project.uses_topups = False + project.plan_end = None serializer.save(plan_end=None) return Response(serializer.data) @@ -97,7 +97,7 @@ def partial_update(self, request, uuid=None): serializer.save() return Response(serializer.data) - def pre_destroy(self, org: Org, user: User): + def pre_destroy(self, org: Project, user: User): if user.id and user.id > 0 and hasattr(org, "modified_by_id"): org.modified_by = user @@ -112,12 +112,12 @@ def get_user(self, request): return get_object_or_404(User, email=request.query_params.get("user_email")) - def _user_has_permisson(self, user: User, org: Org) -> bool: + def _user_has_permisson(self, user: User, project: Project) -> bool: return ( - user.org_admins.filter(pk=org.pk) - or user.org_viewers.filter(pk=org.pk) - or user.org_editors.filter(pk=org.pk) - or user.org_surveyors.filter(pk=org.pk) + user.org_admins.filter(pk=project.pk) + or user.org_viewers.filter(pk=project.pk) + or user.org_editors.filter(pk=project.pk) + or user.org_surveyors.filter(pk=project.pk) ) def get_orgs(self, user: User): From d74631b6d552f5eac7082e73b11453218697ccc7 Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Wed, 19 Apr 2023 10:43:45 -0300 Subject: [PATCH 063/101] update view and serializers from org to add uuid in create --- weni/internal/orgs/serializers.py | 5 +++-- weni/internal/orgs/views.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/weni/internal/orgs/serializers.py b/weni/internal/orgs/serializers.py index aa33ce1a1..6e67074a9 100644 --- a/weni/internal/orgs/serializers.py +++ b/weni/internal/orgs/serializers.py @@ -44,6 +44,7 @@ class OrgSerializer(serializers.ModelSerializer): users = serializers.SerializerMethodField() timezone = serializers.CharField() uuid = serializers.UUIDField(source="project_uuid") + flow_organization = serializers.UUIDField(source="uuid") def set_user_permission(self, user: dict, permission: str) -> dict: user["permission_type"] = permission @@ -68,7 +69,7 @@ def get_users(self, project: Project): class Meta: model = Project - fields = ["id", "name", "uuid", "timezone", "date_format", "users"] + fields = ["id", "name", "uuid", "timezone", "date_format", "users", "flow_organization"] class OrgCreateSerializer(serializers.ModelSerializer): @@ -76,7 +77,7 @@ class OrgCreateSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = ["name", "timezone", "user_email"] + fields = ["name", "timezone", "user_email", "uuid"] class OrgUpdateSerializer(serializers.ModelSerializer): diff --git a/weni/internal/orgs/views.py b/weni/internal/orgs/views.py index 60f0d3e9e..ac77567b3 100644 --- a/weni/internal/orgs/views.py +++ b/weni/internal/orgs/views.py @@ -49,6 +49,8 @@ def create(self, request): created_by=user, modified_by=user, plan="infinity", + project_uuid=request.data.get("uuid") + ) project.administrators.add(user) From c8b9726932ac92d40534699965aa1977675082ca Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Wed, 19 Apr 2023 18:01:59 -0300 Subject: [PATCH 064/101] update destroy endpoint --- weni/internal/orgs/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weni/internal/orgs/views.py b/weni/internal/orgs/views.py index ac77567b3..972c37daf 100644 --- a/weni/internal/orgs/views.py +++ b/weni/internal/orgs/views.py @@ -67,7 +67,7 @@ def retrieve(self, request, project_uuid=None): return Response(serializer.data) def destroy(self, request, project_uuid=None): - project = get_object_or_404(Project, uuid=project_uuid) + project = get_object_or_404(Project, project_uuid=project_uuid) user = get_object_or_404(User, email=request.query_params.get("user_email")) self.pre_destroy(project, user) From 8f91b42f09904bf37885b12e81821b1ce91c1b4a Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Wed, 19 Apr 2023 18:36:44 -0300 Subject: [PATCH 065/101] Update serializers to create template org --- weni/internal/orgs/serializers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/weni/internal/orgs/serializers.py b/weni/internal/orgs/serializers.py index 6e67074a9..273af7774 100644 --- a/weni/internal/orgs/serializers.py +++ b/weni/internal/orgs/serializers.py @@ -11,11 +11,12 @@ class TemplateOrgSerializer(serializers.ModelSerializer): user_email = serializers.EmailField(write_only=True) timezone = serializers.CharField() + uuid = serializers.UUIDField(read_only=False, required=True) + flow_organization = serializers.UUIDField(source="uuid", read_only=True) class Meta: model = Project - fields = ("user_email", "name", "timezone", "uuid") - read_only_fields = ("uuid",) + fields = ("user_email", "name", "timezone", "uuid", "flow_organization") def validate(self, attrs): attrs = dict(attrs) @@ -28,9 +29,16 @@ def validate(self, attrs): attrs.pop("user_email") return super().validate(attrs) + + def to_representation(self, instance): + ret = super().to_representation(instance) + ret["uuid"] = instance.project_uuid + + return ret def create(self, validated_data): validated_data["plan"] = "infinity" + validated_data["project_uuid"] = validated_data.pop("uuid") project = super().create(validated_data) From 47422d9ada2cbc9ee97f06a21a1de7be1399cf9a Mon Sep 17 00:00:00 2001 From: lucaslinhares Date: Wed, 3 May 2023 23:13:39 -0300 Subject: [PATCH 066/101] Update version of weni-rp-apps to 2.4.0 --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe41b401..a1ab639ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## [Unreleased] +## [2.4.0] - 2023-05-03 +- Add Project model to internal app +- Adjust Channel app to new project model +- Adjust Org app to new project model +- Adjust Users app to new project model + ## [2.3.4] - 2023-04-27 - Update permission endpoint from User permission - Update pre-commit config diff --git a/pyproject.toml b/pyproject.toml index 04ce681a0..14d292b6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.3.4" +version = "2.4.0" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 5bb8dfc3a1bd55c66346abb6e09e311d7fc49643 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Wed, 17 May 2023 21:32:12 -0300 Subject: [PATCH 067/101] fix: add external_api channel form in channel retrieve (#198) --- weni/internal/channel/views.py | 75 ++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/weni/internal/channel/views.py b/weni/internal/channel/views.py index a58cb0d4b..eec480d53 100644 --- a/weni/internal/channel/views.py +++ b/weni/internal/channel/views.py @@ -8,7 +8,6 @@ from rest_framework.decorators import action from rest_framework import viewsets from rest_framework import status -from rest_framework.pagination import PageNumberPagination from weni.internal.views import InternalGenericViewSet from django.conf import settings @@ -16,7 +15,11 @@ from temba.channels.models import Channel from temba.channels.types import TYPES -from .serializers import ChannelSerializer, CreateChannelSerializer, ChannelWACSerializer +from .serializers import ( + ChannelSerializer, + CreateChannelSerializer, + ChannelWACSerializer, +) User = get_user_model() @@ -44,13 +47,17 @@ def retrieve(self, request, uuid=None): except Channel.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) - return JsonResponse(data=self.get_serializer(channel).data, status=status.HTTP_200_OK) + return JsonResponse( + data=self.get_serializer(channel).data, status=status.HTTP_200_OK + ) def create(self, request): serializer = CreateChannelSerializer(data=request.data) if not serializer.is_valid(): - return JsonResponse(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return JsonResponse( + data=serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) serializer.save() @@ -69,7 +76,9 @@ def create_wac(self, request): serializer = ChannelWACSerializer(data=request.data) if not serializer.is_valid(): - return JsonResponse(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return JsonResponse( + data=serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) serializer.save() @@ -99,6 +108,7 @@ def retrieve(self, request, pk=None): channel_type = None fields_form = {} code_type = pk + current_form = None if code_type: channel_type = TYPES.get(code_type.upper(), None) @@ -108,7 +118,12 @@ def retrieve(self, request, pk=None): fields_in_form = [] if channel_type.claim_view: if channel_type.claim_view.form_class: - form = channel_type.claim_view.form_class.base_fields + current_form = channel_type.claim_view.form_class + elif channel_type.claim_view.ClaimForm: + current_form = channel_type.claim_view.ClaimForm + + if current_form: + form = current_form.base_fields for field in form: fields_in_form.append(extract_form_info(form[field], field)) @@ -124,7 +139,10 @@ def retrieve(self, request, pk=None): fields_types["attributes"] = attibutes_type - payload = {"attributes": fields_types.get("attributes"), "form": fields_form.get("form")} + payload = { + "attributes": fields_types.get("attributes"), + "form": fields_form.get("form"), + } return Response(payload) @@ -132,13 +150,25 @@ def retrieve(self, request, pk=None): def extract_type_info(_class): channel = {} type_exclude = [""] - items_exclude = ["redact_response_keys", "claim_view_kwargs", "extra_links", "redact_request_keys"] + items_exclude = [ + "redact_response_keys", + "claim_view_kwargs", + "extra_links", + "redact_request_keys", + ] for i in _class.__class__.__dict__.items(): if not i[0].startswith("_"): - if not inspect.isclass(i[1]) and str(type(i[1])) not in (type_exclude) and i[0] not in items_exclude: + if ( + not inspect.isclass(i[1]) + and str(type(i[1])) not in (type_exclude) + and i[0] not in items_exclude + ): if str(type(i[1])) == "": - channel[i[0]] = {"name": i[1].name if i[1].name else "", "value": i[1].value if i[1].value else ""} + channel[i[0]] = { + "name": i[1].name if i[1].name else "", + "value": i[1].value if i[1].value else "", + } elif i[0] == "configuration_urls": if i[1]: @@ -153,7 +183,9 @@ def extract_type_info(_class): url_dict["url"] = str(url.get("url")) if i[1][0].get("description"): - url_dict["description"] = str(url.get("description")) + url_dict["description"] = str( + url.get("description") + ) urls_list.append(url_dict) channel[i[0]] = urls_list @@ -165,11 +197,18 @@ def extract_type_info(_class): channel[i[0]] = str(i[1]) elif (i[0]) == "ivr_protocol": - channel[i[0]] = {"name": i[1].name if i[1].name else "", "value": i[1].value if i[1].value else ""} + channel[i[0]] = { + "name": i[1].name if i[1].name else "", + "value": i[1].value if i[1].value else "", + } else: channel[i[0]] = i[1] - if (not (channel.get("code"))) or (not (len(channel)) > 0) or (not (channel.get("name"))): + if ( + (not (channel.get("code"))) + or (not (len(channel)) > 0) + or (not (channel.get("name"))) + ): return None channel["num_fields"] = len(channel) @@ -182,13 +221,13 @@ def extract_form_info(_form, name_form): try: detail["type"] = str(_form.widget.input_type) - except: - detail["type"] = None + except AttributeError: + detail["type"] = str(_form.widget.__class__.__name__).lower() if _form.help_text: detail["help_text"] = str(_form.help_text) else: - detail["help_text"] = None + detail["help_text"] = "" if detail.get("type") == "select": detail["choices"] = _form.choices @@ -196,9 +235,9 @@ def extract_form_info(_form, name_form): if _form.label: detail["label"] = str(_form.label) else: - detail["label"] = None + detail["label"] = "" if not (detail.get("name")) or not (detail.get("type")): return None - return detail \ No newline at end of file + return detail From 0dd7eaa70727cb7dd68fd61f65105fc607d11b3d Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Wed, 17 May 2023 22:19:55 -0300 Subject: [PATCH 068/101] feature: release ticketer instance before delete queue (#232) --- weni/internal/models.py | 13 +++++++++++-- weni/internal/tickets/views.py | 24 +++++++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/weni/internal/models.py b/weni/internal/models.py index f4005773e..47db00b42 100644 --- a/weni/internal/models.py +++ b/weni/internal/models.py @@ -7,8 +7,12 @@ class TicketerQueue(Topic): - topic = models.OneToOneField(Topic, on_delete=models.CASCADE, parent_link=True, related_name="queue") - ticketer = models.ForeignKey(Ticketer, on_delete=models.CASCADE, related_name="queues") + topic = models.OneToOneField( + Topic, on_delete=models.CASCADE, parent_link=True, related_name="queue" + ) + ticketer = models.ForeignKey( + Ticketer, on_delete=models.CASCADE, related_name="queues" + ) class Meta: db_table = "internal_tickets_ticketerqueue" @@ -16,6 +20,11 @@ class Meta: def __str__(self): return f"Queue[uuid={self.uuid}, name={self.name}]" + def release(self, user): + self.ticketer.release(user=user) + self.is_active = False + self.save() + class Project(Org): project_uuid = models.UUIDField(default=uuid4, unique=True) diff --git a/weni/internal/tickets/views.py b/weni/internal/tickets/views.py index f33fc707c..b31667ddb 100644 --- a/weni/internal/tickets/views.py +++ b/weni/internal/tickets/views.py @@ -4,11 +4,17 @@ from temba.tickets.models import Ticketer from weni.internal.views import InternalGenericViewSet from weni.internal.models import TicketerQueue -from weni.internal.tickets.serializers import TicketerSerializer, TicketerQueueSerializer +from weni.internal.tickets.serializers import ( + TicketerSerializer, + TicketerQueueSerializer, +) class TicketerViewSet( - mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, InternalGenericViewSet + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + InternalGenericViewSet, ): serializer_class = TicketerSerializer queryset = Ticketer.objects.filter(is_active=True) @@ -19,7 +25,10 @@ def perform_destroy(self, instance): class TicketerQueueViewSet( - mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, InternalGenericViewSet + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + InternalGenericViewSet, ): serializer_class = TicketerQueueSerializer queryset = TicketerQueue.objects @@ -28,10 +37,12 @@ class TicketerQueueViewSet( @property def _ticketer(self): sector_uuid = self.kwargs.get("ticketer_uuid") - return get_object_or_404(Ticketer, config__sector_uuid=sector_uuid) + return get_object_or_404( + Ticketer, is_active=True, config__sector_uuid=sector_uuid + ) def get_queryset(self): - return super().get_queryset().filter(ticketer=self._ticketer) + return super().get_queryset().filter(is_active=True, ticketer=self._ticketer) def perform_create(self, serializer): ticketer = self._ticketer @@ -49,3 +60,6 @@ def update(self, request, *args, **kwargs): self.http_method_not_allowed(request, *args, **kwargs) return super().update(request, *args, **kwargs) + + def perform_destroy(self, instance): + instance.release(self.request.user) From 399d430151aa8a458f26ba1307565c8799b794c1 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Wed, 17 May 2023 22:23:11 -0300 Subject: [PATCH 069/101] fix: optimize search filter for active contacts (#227) --- weni/internal/statistic/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/weni/internal/statistic/views.py b/weni/internal/statistic/views.py index 97ae78a2d..fce39f9df 100644 --- a/weni/internal/statistic/views.py +++ b/weni/internal/statistic/views.py @@ -5,6 +5,7 @@ from rest_framework import status from temba.orgs.models import Org +from temba.contacts.models import ContactGroup from weni.internal.views import InternalGenericViewSet @@ -14,11 +15,12 @@ class StatisticEndpoint(RetrieveModelMixin, InternalGenericViewSet): def retrieve(self, request, uuid=None): org = get_object_or_404(Org, uuid=uuid) + group = ContactGroup.all_groups.get(org=org, group_type='A') response = { "active_flows": org.flows.filter(is_active=True, is_archived=False).exclude(is_system=True).count(), "active_classifiers": org.classifiers.filter(is_active=True).count(), - "active_contacts": org.contacts.filter(is_active=True).count(), + "active_contacts": group.get_member_count(), } return Response(data=response, status=status.HTTP_200_OK) From a76b222da01a938c5894274d5857fe1852b301da Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Wed, 17 May 2023 22:30:29 -0300 Subject: [PATCH 070/101] Update version of weni-rp-apps to 2.4.1 (#233) --- CHANGELOG.md | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ab639ec..c1f53fb97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## [Unreleased] +## [2.4.1] - 2023-05-17 +- Add external_api channel form in channel retrieve +- Release ticketer instance before delete queue +- Optimize search filter for active contacts + ## [2.4.0] - 2023-05-03 - Add Project model to internal app - Adjust Channel app to new project model diff --git a/pyproject.toml b/pyproject.toml index 14d292b6d..8d26930b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.4.0" +version = "2.4.1" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 7cb124c8004322a142081b84ea09db3236f04034 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Thu, 18 May 2023 00:33:01 -0300 Subject: [PATCH 071/101] hotfix: filter active orgs to return statistics (#234) --- weni/internal/statistic/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weni/internal/statistic/views.py b/weni/internal/statistic/views.py index fce39f9df..1d2ac95da 100644 --- a/weni/internal/statistic/views.py +++ b/weni/internal/statistic/views.py @@ -14,7 +14,7 @@ class StatisticEndpoint(RetrieveModelMixin, InternalGenericViewSet): lookup_field = "uuid" def retrieve(self, request, uuid=None): - org = get_object_or_404(Org, uuid=uuid) + org = get_object_or_404(Org, uuid=uuid, is_active=True) group = ContactGroup.all_groups.get(org=org, group_type='A') response = { From efc4322c3c0d1fdf47f308863a96aec3a586a608 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Thu, 18 May 2023 00:38:06 -0300 Subject: [PATCH 072/101] Update version of weni-rp-apps to 2.4.2 (#235) --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1f53fb97..5e628f718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.4.2] - 2023-05-18 +- Filter active orgs to return statistics + ## [2.4.1] - 2023-05-17 - Add external_api channel form in channel retrieve - Release ticketer instance before delete queue diff --git a/pyproject.toml b/pyproject.toml index 8d26930b3..20c5879d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.4.1" +version = "2.4.2" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 0414290607d73be72b0edc6ddc44d7efdaf368a0 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 26 May 2023 17:12:38 -0300 Subject: [PATCH 073/101] fix: removes the ticketer object from the queue.release() method (#237) --- weni/internal/models.py | 3 +-- weni/internal/tickets/views.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/weni/internal/models.py b/weni/internal/models.py index 47db00b42..d9885c563 100644 --- a/weni/internal/models.py +++ b/weni/internal/models.py @@ -20,8 +20,7 @@ class Meta: def __str__(self): return f"Queue[uuid={self.uuid}, name={self.name}]" - def release(self, user): - self.ticketer.release(user=user) + def release(self): self.is_active = False self.save() diff --git a/weni/internal/tickets/views.py b/weni/internal/tickets/views.py index b31667ddb..0ca0ae5a4 100644 --- a/weni/internal/tickets/views.py +++ b/weni/internal/tickets/views.py @@ -21,7 +21,7 @@ class TicketerViewSet( lookup_field = "uuid" def perform_destroy(self, instance): - instance.release(self.request.user) + instance.release() class TicketerQueueViewSet( From 2c0e62f76f655726f051d66494e0c29481752629 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Fri, 26 May 2023 18:58:13 -0300 Subject: [PATCH 074/101] Run black and flake8 in rp-apps (#231) * run black and flake8 in rp-apps * ignoring flake8 in activities app * run black according to --line-lenght=119 validation * ignoring flake8 to init.py file on serializers app * include # noqa in clients init --- .pre-commit-config.yaml | 1 + weni/activities/apps.py | 2 +- weni/activities/signals.py | 46 +++++++------ weni/analytics_api/urls.py | 12 +++- weni/analytics_api/views.py | 15 +++-- weni/auth/urls.py | 16 ++++- weni/channel_stats/urls.py | 6 +- weni/grpc/billing/services.py | 3 +- weni/grpc/billing/tests.py | 26 +++++--- weni/grpc/channel/serializers.py | 3 - weni/grpc/channel/services.py | 23 +++++-- weni/grpc/channel/tests.py | 40 ++++++++--- weni/grpc/classifier/serializers.py | 11 ++- weni/grpc/classifier/services.py | 1 - weni/grpc/classifier/tests.py | 25 +++---- weni/grpc/core/apps.py | 2 +- weni/grpc/core/management/commands/grpc.py | 8 ++- weni/grpc/core/serializers.py | 2 +- weni/grpc/core/services.py | 3 +- weni/grpc/flow/services.py | 4 +- weni/grpc/flow/tests.py | 2 - weni/grpc/org/serializers.py | 10 +-- weni/grpc/org/services.py | 15 +++-- weni/grpc/org/tests.py | 3 - weni/grpc/statistic/tests.py | 2 - weni/grpc/user/services.py | 9 +-- weni/grpc/user/tests.py | 2 - weni/internal/channel/serializers.py | 10 ++- weni/internal/channel/tests.py | 66 ++++++++++++------ weni/internal/channel/urls.py | 1 - weni/internal/classifier/serializers.py | 11 ++- weni/internal/classifier/tests.py | 37 +++++++--- weni/internal/classifier/urls.py | 1 - weni/internal/classifier/views.py | 12 ---- weni/internal/clients/__init__.py | 6 +- weni/internal/clients/authenticators.py | 2 +- weni/internal/clients/connect.py | 16 ++++- weni/internal/externals/serializers.py | 1 - weni/internal/flows/tests.py | 7 +- weni/internal/globals/views.py | 1 - weni/internal/migrations/0001_initial.py | 30 +++++++-- weni/internal/orgs/serializers.py | 19 +++++- weni/internal/orgs/views.py | 6 +- weni/internal/tickets/serializers.py | 2 - weni/internal/tickets/tests/test_views.py | 5 -- weni/internal/users/tests.py | 78 ++++++++++++++++++---- weni/internal/users/views.py | 23 +++++-- weni/s3/urls.py | 7 +- weni/serializers/__init__.py | 2 +- weni/success_orgs/business.py | 9 ++- weni/success_orgs/views.py | 4 -- weni/template_message/serializers.py | 1 - weni/template_message/tests.py | 3 - weni/templates/context_processors.py | 5 +- weni/utils/app_config.py | 2 +- 55 files changed, 429 insertions(+), 230 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31d3fbc0a..8773c08d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,7 @@ repos: rev: 23.3.0 hooks: - id: black + args: [--line-length=119] - repo: https://github.com/pycqa/flake8 rev: 6.0.0 diff --git a/weni/activities/apps.py b/weni/activities/apps.py index 2ee8cf5cf..13773447e 100644 --- a/weni/activities/apps.py +++ b/weni/activities/apps.py @@ -6,4 +6,4 @@ class ActivitiesConfig(AppConfig): name = "weni.activities" def ready(self) -> None: - from weni.activities import signals + from weni.activities import signals # noqa: F401 diff --git a/weni/activities/signals.py b/weni/activities/signals.py index 28e71b577..badc3754f 100644 --- a/weni/activities/signals.py +++ b/weni/activities/signals.py @@ -13,36 +13,44 @@ def create_recent_activity(instance: models.Model, created: bool): if instance.is_active: action = "CREATE" if created else "UPDATE" - celery.execute.send_task("create_recent_activity", kwargs=dict( - action=action, - entity=instance.__class__.__name__.upper(), - entity_name=getattr(instance, "name", None), - user=instance.modified_by.email, - flow_organization=str(instance.org.uuid), - )) + celery.execute.send_task( + "create_recent_activity", + kwargs=dict( + action=action, + entity=instance.__class__.__name__.upper(), + entity_name=getattr(instance, "name", None), + user=instance.modified_by.email, + flow_organization=str(instance.org.uuid), + ), + ) @receiver(post_save, sender=Channel) def channel_recent_activity_signal(sender, instance: Channel, created: bool, **kwargs): update_fields = kwargs.get("update_fields") - if instance.channel_type not in ['WA', 'WAC'] \ - or update_fields != frozenset({'config',}): + if instance.channel_type not in ["WA", "WAC"] or update_fields != frozenset( + { + "config", + } + ): create_recent_activity(instance, created) @receiver(post_save, sender=Flow) def flow_recent_activity_signal(sender, instance: Flow, created: bool, **kwargs): update_fields = kwargs.get("update_fields") - if update_fields != frozenset({ - 'version_number', - 'modified_on', - 'saved_on', - 'modified_by', - 'metadata', - 'saved_by', - 'base_language', - 'has_issues' - }): + if update_fields != frozenset( + { + "version_number", + "modified_on", + "saved_on", + "modified_by", + "metadata", + "saved_by", + "base_language", + "has_issues", + } + ): # This condition prevents two events from being sent when creating a flow create_recent_activity(instance, created) diff --git a/weni/analytics_api/urls.py b/weni/analytics_api/urls.py index 6b24ffa81..c40c5b331 100644 --- a/weni/analytics_api/urls.py +++ b/weni/analytics_api/urls.py @@ -4,8 +4,16 @@ from .views import ContactAnalyticsEndpoint, FlowRunAnalyticsEndpoint urlpatterns = [ - url(r"^analytics/contacts/$", ContactAnalyticsEndpoint.as_view(), name="api.v2.analytics.contacts",), - url(r"^analytics/flow-runs/$", FlowRunAnalyticsEndpoint.as_view(), name="api.v2.analytics.flow_runs",), + url( + r"^analytics/contacts/$", + ContactAnalyticsEndpoint.as_view(), + name="api.v2.analytics.contacts", + ), + url( + r"^analytics/flow-runs/$", + FlowRunAnalyticsEndpoint.as_view(), + name="api.v2.analytics.flow_runs", + ), ] urlpatterns = format_suffix_patterns(urlpatterns, allowed=["json", "api"]) diff --git a/weni/analytics_api/views.py b/weni/analytics_api/views.py index 5caee5ae6..238deec72 100644 --- a/weni/analytics_api/views.py +++ b/weni/analytics_api/views.py @@ -1,6 +1,5 @@ from django.db.models import Count, Prefetch, Q from django.urls import reverse -from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from temba.api.v2.views_base import BaseAPIView, ListAPIMixin @@ -111,8 +110,16 @@ def get_read_explorer(cls): "url": reverse("api.v2.analytics.contacts"), "slug": "contacts-analytics", "params": [ - {"name": "group", "required": False, "help": "A group name or UUID to filter by. ex: Customers"}, - {"name": "deleted", "required": False, "help": "Whether to return only deleted contacts. ex: false"}, + { + "name": "group", + "required": False, + "help": "A group name or UUID to filter by. ex: Customers", + }, + { + "name": "deleted", + "required": False, + "help": "Whether to return only deleted contacts. ex: false", + }, { "name": "before", "required": False, @@ -133,7 +140,7 @@ class FlowRunAnalyticsEndpoint(BaseAPIView, ListAPIMixin): ## List Analytics Flow Runs data - A **GET** returns analytical data related to flows, containing information about the type + A **GET** returns analytical data related to flows, containing information about the type of runs and being able to segment by date * **flow_uuid** - A flow UUID to filter by, ex: f5901b62-ba76-4003-9c62-72fdacc1b7b7. diff --git a/weni/auth/urls.py b/weni/auth/urls.py index ebc364976..2ede39bcf 100644 --- a/weni/auth/urls.py +++ b/weni/auth/urls.py @@ -11,9 +11,19 @@ urlpatterns = [ url(r"^oidc/", include("mozilla_django_oidc.urls")), path("check-user-legacy//", check_user_legacy, name="check-user-legacy"), - path("weni//authenticate", WeniAuthenticationRequestView.as_view(), name="weni-authenticate",), path( - "weni//flow//editor", FlowEditorRedirectView.as_view(), name="weni-flow-editor", + "weni//authenticate", + WeniAuthenticationRequestView.as_view(), + name="weni-authenticate", + ), + path( + "weni//flow//editor", + FlowEditorRedirectView.as_view(), + name="weni-flow-editor", + ), + path( + "weni//config", + OrgHomeRedirectView.as_view(), + name="weni-org-home", ), - path("weni//config", OrgHomeRedirectView.as_view(), name="weni-org-home"), ] diff --git a/weni/channel_stats/urls.py b/weni/channel_stats/urls.py index c515e5e97..16c65ed92 100644 --- a/weni/channel_stats/urls.py +++ b/weni/channel_stats/urls.py @@ -4,7 +4,11 @@ from . import views urlpatterns = [ - url(r"^channel_stats$", views.ChannelStatsEndpoint.as_view(), name="api.v2.channel_stats.channels",), + url( + r"^channel_stats$", + views.ChannelStatsEndpoint.as_view(), + name="api.v2.channel_stats.channels", + ), ] urlpatterns = format_suffix_patterns(urlpatterns, allowed=["json", "api"]) diff --git a/weni/grpc/billing/services.py b/weni/grpc/billing/services.py index 5272d1d27..e1a39bf8f 100644 --- a/weni/grpc/billing/services.py +++ b/weni/grpc/billing/services.py @@ -12,8 +12,8 @@ from google.protobuf import empty_pb2 -class BillingService(generics.GenericService): +class BillingService(generics.GenericService): serializer_class = BillingRequestSerializer def Total(self, request, context): @@ -55,4 +55,3 @@ def MessageDetail(self, request, context): msg_serializer = MsgDetailSerializer(msg) return msg_serializer.message - diff --git a/weni/grpc/billing/tests.py b/weni/grpc/billing/tests.py index 3e9182162..be4dbd733 100644 --- a/weni/grpc/billing/tests.py +++ b/weni/grpc/billing/tests.py @@ -6,14 +6,15 @@ from google.protobuf.timestamp_pb2 import Timestamp as TimestampMessage from rest_framework.exceptions import ErrorDetail from temba.orgs.models import Org -from temba.msgs.models import Msg from django.contrib.auth.models import User -from temba.contacts.models import Contact from temba.tests import TembaTest from weni.protobuf.flows import billing_pb2 as pb2, billing_pb2_grpc as stubs from weni.grpc.billing.queries import ActiveContactsQuery -from weni.grpc.billing.serializers import BillingRequestSerializer, ActiveContactDetailSerializer -from django_grpc_framework.test import FakeRpcError, RPCTransactionTestCase +from weni.grpc.billing.serializers import ( + BillingRequestSerializer, + ActiveContactDetailSerializer, +) +from django_grpc_framework.test import RPCTransactionTestCase from google.protobuf import empty_pb2 @@ -232,20 +233,22 @@ def test_message_detail(self): user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") org = Org.objects.create(name="Temba", timezone="Africa/Kigali", created_by=user, modified_by=user) - contact = self.create_contact(f"Contact 1", phone=f"+553124826922") + contact = self.create_contact(f'{"Contact 1"}', phone=f'{"+553124826922"}') contact.org = org contact.save(update_fields=["org"]) channel = self.create_channel(channel_type="WA", name="channel_test", address="address_test", org=org) msg = self.create_incoming_msg(contact=contact, text="incoming message test", channel=channel) - msg1 = self.create_outgoing_msg(contact=contact, text="incoming message test", channel=channel) before = tz.now() after = tz.now() - tz.timedelta(minutes=1) result = self.billing_detail_msg( - org_uuid=str(org.uuid), contact_uuid=str(contact.uuid), before=str(before), after=str(after) + org_uuid=str(org.uuid), + contact_uuid=str(contact.uuid), + before=str(before), + after=str(after), ) self.assertEqual(str(msg.uuid), result.uuid) @@ -258,19 +261,22 @@ def test_message_detail_fail(self): user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") org = Org.objects.create(name="Temba", timezone="Africa/Kigali", created_by=user, modified_by=user) - contact = self.create_contact(f"Contact 1", phone=f"+553124826922") + contact = self.create_contact("Contact 1", phone="+553124826922") contact.org = org contact.save(update_fields=["org"]) channel = self.create_channel(channel_type="WA", name="channel_test", address="address_test", org=org) - msg = self.create_outgoing_msg(contact=contact, text="incoming message test", channel=channel, status="F") + self.create_outgoing_msg(contact=contact, text="incoming message test", channel=channel, status="F") before = tz.now() after = tz.now() - tz.timedelta(minutes=1) result = self.billing_detail_msg( - org_uuid=str(org.uuid), contact_uuid=str(contact.uuid), before=str(before), after=str(after) + org_uuid=str(org.uuid), + contact_uuid=str(contact.uuid), + before=str(before), + after=str(after), ) self.assertEqual(type(result), empty_pb2.Empty) diff --git a/weni/grpc/channel/serializers.py b/weni/grpc/channel/serializers.py index 05c200fd3..6b86c6101 100644 --- a/weni/grpc/channel/serializers.py +++ b/weni/grpc/channel/serializers.py @@ -14,7 +14,6 @@ class WeniWebChatProtoSerializer(proto_serializers.ProtoSerializer): - org = weni_serializers.OrgUUIDRelatedField(write_only=True) user = weni_serializers.UserEmailRelatedField(write_only=True) name = serializers.CharField() @@ -22,7 +21,6 @@ class WeniWebChatProtoSerializer(proto_serializers.ProtoSerializer): uuid = serializers.UUIDField(read_only=True) def create(self, validated_data): - user = validated_data["user"] name = validated_data["name"] config = {CONFIG_BASE_URL: validated_data["base_url"]} @@ -46,7 +44,6 @@ class Meta: class ChannelProtoSerializer(proto_serializers.ModelProtoSerializer): - user = weni_serializers.UserEmailRelatedField(write_only=True, required=True) config = serializers.SerializerMethodField() org = serializers.SerializerMethodField() diff --git a/weni/grpc/channel/services.py b/weni/grpc/channel/services.py index 243a60f10..d399286d4 100644 --- a/weni/grpc/channel/services.py +++ b/weni/grpc/channel/services.py @@ -1,7 +1,7 @@ import json import re -from django.http import Http404, HttpResponseBadRequest +from django.http import Http404 from django.http.response import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.contrib.sessions.middleware import SessionMiddleware @@ -16,21 +16,26 @@ from temba.orgs.models import Org from temba.channels.types import TYPES -from weni.grpc.channel.serializers import WeniWebChatProtoSerializer, ChannelProtoSerializer, ChannelWACSerializer +from weni.grpc.channel.serializers import ( + WeniWebChatProtoSerializer, + ChannelProtoSerializer, + ChannelWACSerializer, +) from weni.protobuf.flows import channel_pb2 # this class will be deprecated class WeniWebChatService(mixins.CreateModelMixin, mixins.DestroyModelMixin, generics.GenericService): - channel_type = WeniWebChatType serializer_class = WeniWebChatProtoSerializer class ChannelService( - mixins.RetrieveModelMixin, mixins.ListModelMixin, mixins.DestroyModelMixin, generics.GenericService + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.DestroyModelMixin, + generics.GenericService, ): - queryset = Channel.objects lookup_field = "uuid" serializer_class = ChannelProtoSerializer @@ -86,7 +91,8 @@ def Create(self, request, context): if "/users/login/?next=" in url: self.context.abort( - grpc.StatusCode.INVALID_ARGUMENT, f"User: {user.email} do not have permission in Org: {org.uuid}" + grpc.StatusCode.INVALID_ARGUMENT, + f"User: {user.email} do not have permission in Org: {org.uuid}", ) regex = "[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}" @@ -94,7 +100,10 @@ def Create(self, request, context): channel = Channel.objects.get(uuid=channe_uuid) return channel_pb2.Channel( - uuid=channe_uuid, name=channel.name, address=channel.address, config=json.dumps(channel.config) + uuid=channe_uuid, + name=channel.name, + address=channel.address, + config=json.dumps(channel.config), ) def create_channel(self, user: User, org: Org, data: dict, channel_type) -> str: diff --git a/weni/grpc/channel/tests.py b/weni/grpc/channel/tests.py index c2d5d24d6..10b6e3972 100644 --- a/weni/grpc/channel/tests.py +++ b/weni/grpc/channel/tests.py @@ -7,7 +7,6 @@ from temba.channels.models import Channel from temba.orgs.models import Org, OrgRole -from temba.channels.types.weniwebchat.type import CONFIG_BASE_URL from weni.protobuf.flows import channel_pb2, channel_pb2_grpc @@ -40,7 +39,10 @@ class ReleaseChannelServiceTest(gRPCClient, RPCTransactionTestCase): def setUp(self): self.user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") self.org = Org.objects.create( - name="Weni", timezone="Africa/Kigali", created_by=self.user, modified_by=self.user + name="Weni", + timezone="Africa/Kigali", + created_by=self.user, + modified_by=self.user, ) super().setUp() @@ -56,7 +58,10 @@ class CreateWACServiceTest(gRPCClient, RPCTransactionTestCase): def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.org = Org.objects.create( - name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + name="Weni", + timezone="America/Sao_Paulo", + created_by=self.user, + modified_by=self.user, ) self.org.add_user(self.user, OrgRole.ADMINISTRATOR) @@ -115,18 +120,23 @@ class CreateChannelServiceTest(gRPCClient, RPCTransactionTestCase): def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.org = Org.objects.create( - name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + name="Weni", + timezone="America/Sao_Paulo", + created_by=self.user, + modified_by=self.user, ) self.org.add_user(self.user, OrgRole.ADMINISTRATOR) super().setUp() def test_create_weni_web_chat_channel(self): - data = json.dumps({"name": "test", "base_url": "https://weni.ai"}) response = self.channel_create_request( - user=self.user.email, org=str(self.org.uuid), data=data, channeltype_code="WWC" + user=self.user.email, + org=str(self.org.uuid), + data=data, + channeltype_code="WWC", ) channel = Channel.objects.get(uuid=response.uuid) self.assertEqual(channel.address, response.address) @@ -142,13 +152,22 @@ class RetrieveChannelServiceTest(gRPCClient, RPCTransactionTestCase): def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.org = Org.objects.create( - name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + name="Weni", + timezone="America/Sao_Paulo", + created_by=self.user, + modified_by=self.user, ) super().setUp() self.channel_obj = Channel.create( - self.org, self.user, None, "WWC", "Test WWC", "test", {"fake_key": "fake_value"} + self.org, + self.user, + None, + "WWC", + "Test WWC", + "test", + {"fake_key": "fake_value"}, ) def test_channel_retrieve_returned_fields(self): @@ -163,7 +182,10 @@ def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.orgs = [ Org.objects.create( - name=f"Org {org}", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + name=f"Org {org}", + timezone="America/Sao_Paulo", + created_by=self.user, + modified_by=self.user, ) for org in range(2) ] diff --git a/weni/grpc/classifier/serializers.py b/weni/grpc/classifier/serializers.py index 9428c52f3..d92c585b7 100644 --- a/weni/grpc/classifier/serializers.py +++ b/weni/grpc/classifier/serializers.py @@ -7,7 +7,6 @@ class ClassifierProtoSerializer(proto_serializers.ModelProtoSerializer): - uuid = serializers.UUIDField(read_only=True) is_active = serializers.BooleanField(read_only=True) classifier_type = serializers.CharField(required=True) @@ -28,4 +27,12 @@ def create(self, validated_data: dict) -> Classifier: class Meta: model = Classifier proto_class = classifier_pb2.Classifier - fields = ["uuid", "is_active", "classifier_type", "name", "access_token", "org", "user"] + fields = [ + "uuid", + "is_active", + "classifier_type", + "name", + "access_token", + "org", + "user", + ] diff --git a/weni/grpc/classifier/services.py b/weni/grpc/classifier/services.py index 46b0fde97..06f627e2a 100644 --- a/weni/grpc/classifier/services.py +++ b/weni/grpc/classifier/services.py @@ -13,7 +13,6 @@ class ClassifierService( generics.GenericService, AbstractService, ): - serializer_class = ClassifierProtoSerializer queryset = Classifier.objects.all() lookup_field = "uuid" diff --git a/weni/grpc/classifier/tests.py b/weni/grpc/classifier/tests.py index 7660764e5..5c79f2797 100644 --- a/weni/grpc/classifier/tests.py +++ b/weni/grpc/classifier/tests.py @@ -14,12 +14,12 @@ def get_test_classifier(test: grpc_test.RPCTransactionTestCase) -> Classifier: creates a new classifier object containing an intention and returns it. """ response = test.classifier_create_request( - classifier_type="Test Type", - user=test.admin.email, - org=str(test.org.uuid), - name="Test Name", - access_token=test.config["access_token"] - ) + classifier_type="Test Type", + user=test.admin.email, + org=str(test.org.uuid), + name="Test Name", + access_token=test.config["access_token"], + ) classifier = Classifier.objects.get(uuid=response.uuid) @@ -29,7 +29,6 @@ def get_test_classifier(test: grpc_test.RPCTransactionTestCase) -> Classifier: class BaseClassifierServiceTest(grpc_test.RPCTransactionTestCase): - def setUp(self): self.config = {"access_token": "hbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"} @@ -37,7 +36,12 @@ def setUp(self): username="testuser", password="123", email="test@weni.ai", is_superuser=True ) - self.org = Org.objects.create(name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin) + self.org = Org.objects.create( + name="Weni", + timezone="America/Maceio", + created_by=self.admin, + modified_by=self.admin, + ) super().setUp() @@ -57,7 +61,6 @@ def classifier_destroy_request(self, **kwargs): class ClassifierServiceTest(BaseClassifierServiceTest): - def test_list_classifier(self): org = Org.objects.first() org_uuid = str(org.uuid) @@ -127,7 +130,7 @@ def test_create_classifier(self): user=user.email, org=str(org.uuid), name=name, - access_token=access_token + access_token=access_token, ) self.assertEqual(response.name, name) @@ -136,7 +139,6 @@ def test_create_classifier(self): class ClassifierServiceRetrieveTest(BaseClassifierServiceTest): - def test_retrieve_classifier_by_valid_uuid(self): classifier = get_test_classifier(self) response = self.classifier_retrieve_request(uuid=str(classifier.uuid)) @@ -154,7 +156,6 @@ def test_retrieve_classifier_by_invalid_uuid(self): class ClassifierServiceDestroyTest(BaseClassifierServiceTest): - def test_destroy_classifier_by_valid_uuid(self): classifier = get_test_classifier(self) self.assertEqual(classifier.intents.count(), 1) diff --git a/weni/grpc/core/apps.py b/weni/grpc/core/apps.py index 7cb531c3a..0e5a3f318 100644 --- a/weni/grpc/core/apps.py +++ b/weni/grpc/core/apps.py @@ -2,4 +2,4 @@ class GrpcCentralConfig(AppConfig): - name = 'weni.grpc.core' + name = "weni.grpc.core" diff --git a/weni/grpc/core/management/commands/grpc.py b/weni/grpc/core/management/commands/grpc.py index 56f259790..762fd7bd6 100644 --- a/weni/grpc/core/management/commands/grpc.py +++ b/weni/grpc/core/management/commands/grpc.py @@ -1,8 +1,9 @@ from concurrent import futures import grpc -from django_grpc_framework.management.commands.grpcrunserver import \ - Command as BaseCommand +from django_grpc_framework.management.commands.grpcrunserver import ( + Command as BaseCommand, +) from django_grpc_framework.settings import grpc_settings @@ -21,7 +22,8 @@ def handle(self, *args, **options): def _serve(self): server = grpc.server( - futures.ThreadPoolExecutor(max_workers=self.max_workers), interceptors=grpc_settings.SERVER_INTERCEPTORS, + futures.ThreadPoolExecutor(max_workers=self.max_workers), + interceptors=grpc_settings.SERVER_INTERCEPTORS, ) grpc_settings.ROOT_HANDLERS_HOOK(server) diff --git a/weni/grpc/core/serializers.py b/weni/grpc/core/serializers.py index acc413849..1eac24041 100644 --- a/weni/grpc/core/serializers.py +++ b/weni/grpc/core/serializers.py @@ -28,7 +28,7 @@ def __init__(self, method_name=None, **kwargs): def bind(self, field_name, parent): # The method name defaults to `get_{field_name}`. if self.method_name is None: - self.method_name = 'get_{field_name}'.format(field_name=field_name) + self.method_name = "get_{field_name}".format(field_name=field_name) super().bind(field_name, parent) diff --git a/weni/grpc/core/services.py b/weni/grpc/core/services.py index 3f88fafe7..e15bbb466 100644 --- a/weni/grpc/core/services.py +++ b/weni/grpc/core/services.py @@ -14,7 +14,6 @@ def get_org_object(self, value, query_parameter: str = "pk") -> Org: return self._get_object(Org, value, query_parameter) def _get_object(self, model, value, query_parameter: str): - query = {query_parameter: value} try: @@ -27,4 +26,4 @@ def _get_object(self, model, value, query_parameter: str): def raises_not_fount(self, model_name, value): if not value: value = "None" - self.context.abort(grpc.StatusCode.NOT_FOUND, f"{model_name}: {value} not found!") \ No newline at end of file + self.context.abort(grpc.StatusCode.NOT_FOUND, f"{model_name}: {value} not found!") diff --git a/weni/grpc/flow/services.py b/weni/grpc/flow/services.py index 8ec865161..629d4692f 100644 --- a/weni/grpc/flow/services.py +++ b/weni/grpc/flow/services.py @@ -12,7 +12,9 @@ def List(self, request, _): name__icontains=request.flow_name, org=org.id, is_active=True, - ).exclude(is_archived=True)[:20] + ).exclude( + is_archived=True + )[:20] serializer = FlowProtoSerializer(queryset, many=True) for message in serializer.message: diff --git a/weni/grpc/flow/tests.py b/weni/grpc/flow/tests.py index e97a1b24a..b598a9104 100644 --- a/weni/grpc/flow/tests.py +++ b/weni/grpc/flow/tests.py @@ -10,7 +10,6 @@ class FlowServiceTest(RPCTransactionTestCase): def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") user = User.objects.first() @@ -27,7 +26,6 @@ def setUp(self): self.stub = flow_pb2_grpc.FlowControllerStub(self.channel) def test_list_flow(self): - temba = Org.objects.get(name="Temba") weni = Org.objects.get(name="Weni") diff --git a/weni/grpc/org/serializers.py b/weni/grpc/org/serializers.py index 312e1deff..28bc06ecd 100644 --- a/weni/grpc/org/serializers.py +++ b/weni/grpc/org/serializers.py @@ -16,7 +16,6 @@ def get_object(cls, model, pk: int): class OrgProtoSerializer(proto_serializers.ModelProtoSerializer): - users = serializers.SerializerMethodField() timezone = serializers.CharField() @@ -32,7 +31,12 @@ def get_users(self, org: Org): editors = list(org.editors.all().values(*values)) surveyors = list(org.surveyors.all().values(*values)) - administrators = list(map(lambda user: self.set_user_permission(user, "administrator"), administrators)) + administrators = list( + map( + lambda user: self.set_user_permission(user, "administrator"), + administrators, + ) + ) viewers = list(map(lambda user: self.set_user_permission(user, "viewer"), viewers)) editors = list(map(lambda user: self.set_user_permission(user, "editor"), editors)) surveyors = list(map(lambda user: self.set_user_permission(user, "surveyor"), surveyors)) @@ -48,7 +52,6 @@ class Meta: class OrgCreateProtoSerializer(proto_serializers.ModelProtoSerializer): - user_email = serializers.EmailField() class Meta: @@ -58,7 +61,6 @@ class Meta: class OrgUpdateProtoSerializer(proto_serializers.ModelProtoSerializer): - uuid = serializers.CharField() modified_by = weni_serializers.UserEmailRelatedField(required=False, write_only=True) timezone = serializers.CharField(required=False) diff --git a/weni/grpc/org/services.py b/weni/grpc/org/services.py index 49e827845..a7c0452ce 100644 --- a/weni/grpc/org/services.py +++ b/weni/grpc/org/services.py @@ -5,17 +5,19 @@ from google.protobuf import empty_pb2 from temba.orgs.models import Org -from weni.grpc.org.serializers import OrgCreateProtoSerializer, OrgProtoSerializer, OrgUpdateProtoSerializer +from weni.grpc.org.serializers import ( + OrgCreateProtoSerializer, + OrgProtoSerializer, + OrgUpdateProtoSerializer, +) from weni.grpc.core.services import AbstractService class OrgService(AbstractService, generics.GenericService, mixins.ListModelMixin): - queryset = Org.objects lookup_field = "uuid" def List(self, request, context): - user = self.get_user(request) orgs = self.get_orgs(user) @@ -25,14 +27,17 @@ def List(self, request, context): yield msg def Create(self, request, context): - serializer = OrgCreateProtoSerializer(message=request) serializer.is_valid(raise_exception=True) user, created = User.objects.get_or_create(email=request.user_email, defaults={"username": request.user_email}) org = Org.objects.create( - name=request.name, timezone=request.timezone, created_by=user, modified_by=user, plan="infinity" + name=request.name, + timezone=request.timezone, + created_by=user, + modified_by=user, + plan="infinity", ) org.administrators.add(user) diff --git a/weni/grpc/org/tests.py b/weni/grpc/org/tests.py index 98ed9e228..04c3d8554 100644 --- a/weni/grpc/org/tests.py +++ b/weni/grpc/org/tests.py @@ -12,13 +12,11 @@ class OrgServiceTest(RPCTransactionTestCase): - WRONG_ID = -1 WRONG_UUID = "31313-dasda-dasdasd-23123" WRONG_EMAIL = "wrong@email.com" def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") User.objects.create_user(username="weniuser", password="123", email="wene@user.com") @@ -41,7 +39,6 @@ def test_serializer_utils(self): self.assertEquals(user, SerializerUtils.get_object(User, user.pk)) def test_list_orgs(self): - with self.assertRaises(FakeRpcError): for org in self.stub_org_list_request(): ... diff --git a/weni/grpc/statistic/tests.py b/weni/grpc/statistic/tests.py index 33b006917..083fd9f4f 100644 --- a/weni/grpc/statistic/tests.py +++ b/weni/grpc/statistic/tests.py @@ -14,7 +14,6 @@ class OrgStatisticServiceTest(test_grpc.RPCTransactionTestCase): def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") user = User.objects.get(username="testuser") @@ -41,7 +40,6 @@ def test_retrieve_statistic(self, mr_mocks): self.org_statistic_list_request(org_uuid="123") with DisableTriggersOn(Contact): - test_contact = Contact.objects.create(name="Test Contact", org=org) Contact.objects.create(name="Weni Contact", org=org) diff --git a/weni/grpc/user/services.py b/weni/grpc/user/services.py index caca321c2..3a8ebf7c5 100644 --- a/weni/grpc/user/services.py +++ b/weni/grpc/user/services.py @@ -22,7 +22,10 @@ def get_user(user_email: str) -> User: class UserPermissionService( - AbstractService, generics.GenericService, mixins.RetrieveModelMixin, mixins.UpdateModelMixin + AbstractService, + generics.GenericService, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, ): def Retrieve(self, request, context): org = self.get_org_object(request.org_uuid, "uuid") @@ -98,13 +101,11 @@ def get_user_permissions(self, org: Org, user: User) -> dict: class UserService(generics.GenericService, AbstractService, mixins.RetrieveModelMixin): - serializer_class = UserProtoSerializer def Update(self, request, context): - if request.language not in [language[0] for language in settings.LANGUAGES]: - self.context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"Invalid argument: language") + self.context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid argument: language") user = get_user(request.email) user_settings = user.get_settings() diff --git a/weni/grpc/user/tests.py b/weni/grpc/user/tests.py index 56aa481a8..daa5fa312 100644 --- a/weni/grpc/user/tests.py +++ b/weni/grpc/user/tests.py @@ -10,7 +10,6 @@ class UserServiceTest(RPCTransactionTestCase): - WRONG_EMAIL = "wrong@wrong.wrong" WRONG_UUID = "wrong-wrong-wrong-wrong-wrong." @@ -214,7 +213,6 @@ def permission_is_unique_true(self, response, permission: str) -> bool: return len(false_valeues) == len(permissions.items()) - 1 and permission not in false_valeues def validate_response_user(self, response, user: User): - self.assertEquals(response.id, user.id) self.assertEquals(response.username, user.username) self.assertEquals(response.email, user.email) diff --git a/weni/internal/channel/serializers.py b/weni/internal/channel/serializers.py index 0306338ac..7570140b3 100644 --- a/weni/internal/channel/serializers.py +++ b/weni/internal/channel/serializers.py @@ -13,7 +13,6 @@ from weni.serializers import fields as weni_serializers from temba.channels.models import Channel -from temba.orgs.models import Org from temba.utils import analytics from weni.internal.models import Project @@ -52,7 +51,6 @@ def create(self, validated_data): schemes = channel_type.schemes org = validated_data["org"].org - name = validated_data.get("name") phone_number_id = validated_data.get("phone_number_id") config = validated_data.get("config", {}) user = validated_data.get("user") @@ -101,7 +99,7 @@ def create(self, validated_data): url = self.create_channel(user, org.org, data, channel_type) if url is None: - raise exceptions.ValidationError(f"Url not created") + raise exceptions.ValidationError("Url not created") if "/users/login/?next=" in url: raise exceptions.ValidationError(f"User: {user.email} do not have permission in Org: {org.org.uuid}") @@ -134,8 +132,8 @@ class ChannelSerializer(serializers.ModelSerializer): class Meta: extra_kwargs = { - 'org': {'read_only': True}, - 'is_active': {'read_only': True}, + "org": {"read_only": True}, + "is_active": {"read_only": True}, } model = Channel fields = ( @@ -145,7 +143,7 @@ class Meta: "address", "org", "is_active", - ) + ) def to_representation(self, instance): ret = super().to_representation(instance) diff --git a/weni/internal/channel/tests.py b/weni/internal/channel/tests.py index 5d903e8ee..6d8351763 100644 --- a/weni/internal/channel/tests.py +++ b/weni/internal/channel/tests.py @@ -1,29 +1,23 @@ import json from abc import ABC, abstractmethod -from uuid import uuid1 from unittest.mock import patch -from unittest import mock -from unittest import TestCase from rest_framework.test import APIRequestFactory, force_authenticate from rest_framework import status from django.contrib.auth.models import Group from django.urls import reverse -from django.utils import timezone as tz from django.utils.http import urlencode from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from temba.api.models import APIToken -from temba.flows.models import Flow -from temba.orgs.models import Org, OrgRole -from temba.contacts.models import Contact +from temba.orgs.models import OrgRole from temba.channels.models import Channel from temba.channels.types import TYPES -from temba.tests import TembaTest, mock_mailroom +from temba.tests import TembaTest from weni.internal.models import Project -from .views import AvailableChannels, ChannelEndpoint, extract_form_info, extract_type_info +from .views import AvailableChannels, ChannelEndpoint view_class = ChannelEndpoint view_class.permission_classes = [] @@ -55,7 +49,10 @@ def request_post(self, data): token = APIToken.get_or_create(self.project, self.admin, Group.objects.get(name="Administrators")) return self.client.post( - url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + url, + HTTP_AUTHORIZATION=f"Token {token.key}", + data=json.dumps(data), + content_type="application/json", ) def request_delete(self, uuid, **query_params): @@ -73,7 +70,10 @@ class CreateWACServiceTest(TembaTest, TembaRequestMixin): def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.project = Project.objects.create( - name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + name="Weni", + timezone="America/Sao_Paulo", + created_by=self.user, + modified_by=self.user, ) self.project.add_user(self.user, OrgRole.ADMINISTRATOR) @@ -127,7 +127,8 @@ def test_create_whatsapp_cloud_channel_invalid_address(self, mock): self.assertEqual(channel.status_code, 400) self.assertEqual( - channel.json().get("phone_number_id").get("error_type"), "WhatsApp.config.error.channel_already_exists" + channel.json().get("phone_number_id").get("error_type"), + "WhatsApp.config.error.channel_already_exists", ) def get_url_namespace(self): @@ -138,7 +139,10 @@ class ReleaseChannelTestCase(TembaTest, TembaRequestMixin): def setUp(self): self.org_user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") self.my_org = Project.objects.create( - name="Weni", timezone="Africa/Kigali", created_by=self.org_user, modified_by=self.org_user + name="Weni", + timezone="Africa/Kigali", + created_by=self.org_user, + modified_by=self.org_user, ) super().setUp() @@ -157,7 +161,10 @@ class CreateChannelTestCase(TembaTest, TembaRequestMixin): def setUp(self): self.org_user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.project = Project.objects.create( - name="Weni", timezone="America/Sao_Paulo", created_by=self.org_user, modified_by=self.org_user + name="Weni", + timezone="America/Sao_Paulo", + created_by=self.org_user, + modified_by=self.org_user, ) self.project.add_user(self.org_user, OrgRole.ADMINISTRATOR) @@ -190,13 +197,22 @@ class RetrieveChannelTestCase(TembaTest, TembaRequestMixin): def setUp(self): self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.project = Project.objects.create( - name="Weni", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + name="Weni", + timezone="America/Sao_Paulo", + created_by=self.user, + modified_by=self.user, ) super().setUp() self.channel_obj = Channel.create( - self.project.org, self.user, None, "WWC", "Test WWC", "test", {"fake_key": "fake_value"} + self.project.org, + self.user, + None, + "WWC", + "Test WWC", + "test", + {"fake_key": "fake_value"}, ) def test_channel_retrieve_returned_fields(self): @@ -213,12 +229,18 @@ def get_url_namespace(self): class ListChannelTestCase(TembaTest, TembaRequestMixin): def setUp(self): self.admin = User.objects.create_user( - username="testuseradmin", password="123", email="test@weni.ai", is_superuser=True + username="testuseradmin", + password="123", + email="test@weni.ai", + is_superuser=True, ) self.user = User.objects.create_user(username="fake@weni.ai", password="123", email="fake@weni.ai") self.projects = [ Project.objects.create( - name=f"Org {project}", timezone="America/Sao_Paulo", created_by=self.user, modified_by=self.user + name=f"Org {project}", + timezone="America/Sao_Paulo", + created_by=self.user, + modified_by=self.user, ) for project in range(2) ] @@ -400,7 +422,7 @@ def test_form_without_name_value(self): } result = extract_form_info(to_object(**test_form),'') self.assertEqual(result, None) - + def test_form_without_type_value(self): """ check response without #widget attribute """ test_form = { @@ -418,9 +440,9 @@ def test_type_contains_code_and_name(self): result = extract_type_info(type_in) if not (result.get('code')) or not (result.get('name')): have_code_name = False - + self.assertEqual(have_code_name, True) - + def test_all_types_response_contains_dict(self): """ make sure that all results have been processed and converted to dictionaries """ for value in self.types: @@ -432,4 +454,4 @@ def test_all_types_response_contains_dict(self): class to_object: def __init__(self, **entries): return self.__dict__.update(entries) -''' \ No newline at end of file +''' diff --git a/weni/internal/channel/urls.py b/weni/internal/channel/urls.py index 1cf109056..fdf77baf8 100644 --- a/weni/internal/channel/urls.py +++ b/weni/internal/channel/urls.py @@ -1,4 +1,3 @@ -from django.conf.urls import url from rest_framework.urlpatterns import format_suffix_patterns from rest_framework import routers diff --git a/weni/internal/classifier/serializers.py b/weni/internal/classifier/serializers.py index 30acc9681..4509788f7 100644 --- a/weni/internal/classifier/serializers.py +++ b/weni/internal/classifier/serializers.py @@ -6,7 +6,6 @@ class ClassifierSerializer(serializers.Serializer): - uuid = serializers.UUIDField(read_only=True) is_active = serializers.BooleanField(read_only=True) classifier_type = serializers.CharField(required=True) @@ -30,7 +29,15 @@ def create(self, validated_data: dict) -> Classifier: class Meta: model = Classifier proto_class = classifier_pb2.Classifier - fields = ["uuid", "is_active", "classifier_type", "name", "access_token", "org", "user"] + fields = [ + "uuid", + "is_active", + "classifier_type", + "name", + "access_token", + "org", + "user", + ] class ClassifierDeleteSerializer(serializers.Serializer): diff --git a/weni/internal/classifier/tests.py b/weni/internal/classifier/tests.py index 8245bb076..78b226be1 100644 --- a/weni/internal/classifier/tests.py +++ b/weni/internal/classifier/tests.py @@ -6,16 +6,14 @@ from django.contrib.auth.models import Group from django.urls import reverse from django.utils.http import urlencode -from django.contrib.auth.models import User from temba.api.models import APIToken -from temba.tests import TembaTest, mock_mailroom +from temba.tests import TembaTest from temba.orgs.models import Org from temba.classifiers.models import Classifier, Intent from temba.classifiers.types.wit import WitType from temba.classifiers.types.luis import LuisType -from weni.protobuf.flows import classifier_pb2, classifier_pb2_grpc class TembaRequestMixin(ABC): @@ -44,11 +42,18 @@ def request_post(self, data): token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.post( - url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + url, + HTTP_AUTHORIZATION=f"Token {token.key}", + data=json.dumps(data), + content_type="application/json", ) def request_delete(self, uuid, user_email): - url = self.reverse(self.get_url_namespace(), query_params={"user_email": user_email}, kwargs={"uuid": uuid}) + url = self.reverse( + self.get_url_namespace(), + query_params={"user_email": user_email}, + kwargs={"uuid": uuid}, + ) token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.delete(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") @@ -69,7 +74,10 @@ def setUp(self): print(self.admin.is_authenticated) self.org = Org.objects.create( - name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + name="Weni", + timezone="America/Maceio", + created_by=self.admin, + modified_by=self.admin, ) super().setUp() @@ -139,7 +147,10 @@ def setUp(self): ) self.org = Org.objects.create( - name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + name="Weni", + timezone="America/Maceio", + created_by=self.admin, + modified_by=self.admin, ) super().setUp() @@ -182,7 +193,10 @@ def setUp(self): ) self.org = Org.objects.create( - name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + name="Weni", + timezone="America/Maceio", + created_by=self.admin, + modified_by=self.admin, ) super().setUp() @@ -209,7 +223,10 @@ def setUp(self): ) self.org = Org.objects.create( - name="Weni", timezone="America/Maceio", created_by=self.admin, modified_by=self.admin + name="Weni", + timezone="America/Maceio", + created_by=self.admin, + modified_by=self.admin, ) super().setUp() @@ -220,7 +237,7 @@ def test_destroy_classifier_by_valid_uuid(self): self.assertEqual(classifier.intents.count(), 1) - t = self.request_delete(uuid=str(classifier.uuid), user_email=self.admin.email) + self.request_delete(uuid=str(classifier.uuid), user_email=self.admin.email) classifier = Classifier.objects.get(uuid=classifier.uuid) self.assertEqual(classifier.intents.count(), 0) diff --git a/weni/internal/classifier/urls.py b/weni/internal/classifier/urls.py index 9d1ab1e12..5c8227971 100644 --- a/weni/internal/classifier/urls.py +++ b/weni/internal/classifier/urls.py @@ -1,4 +1,3 @@ -from django.conf.urls import url from rest_framework.urlpatterns import format_suffix_patterns from rest_framework import routers diff --git a/weni/internal/classifier/views.py b/weni/internal/classifier/views.py index 0fb630883..c3be1311c 100644 --- a/weni/internal/classifier/views.py +++ b/weni/internal/classifier/views.py @@ -1,18 +1,10 @@ from rest_framework import viewsets -from rest_framework import generics -from rest_framework import mixins -from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework import status from rest_framework.exceptions import ValidationError -from django.contrib.auth.models import User -from django.db.models import Count, Prefetch, Q -from django.urls import reverse from django.http import JsonResponse -from temba.api.v2.views_base import BaseAPIView, ListAPIMixin -from temba.contacts.models import Contact, ContactGroup from temba.classifiers.models import Classifier from temba.orgs.models import Org @@ -21,12 +13,10 @@ class ClassifierEndpoint(viewsets.ModelViewSet, InternalGenericViewSet): - serializer_class = ClassifierSerializer lookup_field = "uuid" def get_queryset(self): - is_active_possibilities = { "True": True, "False": False, @@ -71,7 +61,6 @@ def create(self, request): return JsonResponse(data=serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, uuid=None): - try: classifier = Classifier.objects.get(uuid=uuid) except Classifier.DoesNotExist: @@ -80,7 +69,6 @@ def retrieve(self, request, uuid=None): return JsonResponse(data=self.get_serializer(classifier).data, status=status.HTTP_200_OK) def destroy(self, request, uuid=None): - data = { "uuid": uuid, "user": request.query_params.get("user_email"), diff --git a/weni/internal/clients/__init__.py b/weni/internal/clients/__init__.py index 54291c803..4fec75d4d 100644 --- a/weni/internal/clients/__init__.py +++ b/weni/internal/clients/__init__.py @@ -1,5 +1,3 @@ -from weni.internal.clients.connect import ConnectInternalClient +from weni.internal.clients.connect import ConnectInternalClient # noqa: F401 -__all__ = ( - "ConnectInternalClient" -) +__all__ = "ConnectInternalClient" diff --git a/weni/internal/clients/authenticators.py b/weni/internal/clients/authenticators.py index d565e2dec..4fdf05a2a 100644 --- a/weni/internal/clients/authenticators.py +++ b/weni/internal/clients/authenticators.py @@ -15,7 +15,7 @@ def _get_module_token(self): token = response.json().get("access_token") return f"Bearer {token}" - + @property def headers(self): return { diff --git a/weni/internal/clients/connect.py b/weni/internal/clients/connect.py index 0f34f68a0..3a3a67862 100644 --- a/weni/internal/clients/connect.py +++ b/weni/internal/clients/connect.py @@ -4,8 +4,14 @@ class ConnectInternalClient(BaseInternalClient): - - def create_recent_activity(self, action: str, entity: str, entity_name: str, user: str, flow_organization: str): + def create_recent_activity( + self, + action: str, + entity: str, + entity_name: str, + user: str, + flow_organization: str, + ): body = dict( action=action, entity=entity, @@ -13,6 +19,10 @@ def create_recent_activity(self, action: str, entity: str, entity_name: str, use user=user, flow_organization=flow_organization, ) - response = requests.post(self.get_url("/v1/recent-activity"), headers=self.authenticator.headers, json=body) + response = requests.post( + self.get_url("/v1/recent-activity"), + headers=self.authenticator.headers, + json=body, + ) return response diff --git a/weni/internal/externals/serializers.py b/weni/internal/externals/serializers.py index 100e82079..7220430a3 100644 --- a/weni/internal/externals/serializers.py +++ b/weni/internal/externals/serializers.py @@ -6,7 +6,6 @@ class ExternalServicesSerializer(serializers.Serializer): - uuid = serializers.UUIDField(read_only=True) type_code = serializers.CharField(write_only=True) type_fields = serializers.JSONField(write_only=True) diff --git a/weni/internal/flows/tests.py b/weni/internal/flows/tests.py index 0ff5b259b..f20b1d8e1 100644 --- a/weni/internal/flows/tests.py +++ b/weni/internal/flows/tests.py @@ -40,7 +40,10 @@ def request_post(self, data): token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.post( - url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + url, + HTTP_AUTHORIZATION=f"Token {token.key}", + data=json.dumps(data), + content_type="application/json", ) def request_delete(self, uuid): @@ -56,7 +59,6 @@ def get_url_namespace(self): class ListFlowTestCase(TembaTest, TembaRequestMixin): def setUp(self): - User.objects.create_user(username="testuser", password="123", email="test@weni.ai") user = User.objects.first() @@ -73,7 +75,6 @@ def setUp(self): super().setUp() def test_list_flow(self): - temba = Org.objects.filter(name="Temba").first() weni = Org.objects.get(name="Weni") diff --git a/weni/internal/globals/views.py b/weni/internal/globals/views.py index 1990a47a8..19a7d7376 100644 --- a/weni/internal/globals/views.py +++ b/weni/internal/globals/views.py @@ -31,7 +31,6 @@ def get_queryset(self): except Org.DoesNotExist as error: raise ValidationError(detail={"message": str(error)}) - def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) diff --git a/weni/internal/migrations/0001_initial.py b/weni/internal/migrations/0001_initial.py index a03a148e5..d764f95a3 100644 --- a/weni/internal/migrations/0001_initial.py +++ b/weni/internal/migrations/0001_initial.py @@ -5,23 +5,39 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('tickets', '0025_remove_ticket_subject'), + ("tickets", "0025_remove_ticket_subject"), ] operations = [ migrations.CreateModel( - name='TicketerQueue', + name="TicketerQueue", fields=[ - ('topic', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='queue', serialize=False, to='tickets.topic')), - ('ticketer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='queues', to='tickets.ticketer')), + ( + "topic", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="queue", + serialize=False, + to="tickets.topic", + ), + ), + ( + "ticketer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="queues", + to="tickets.ticketer", + ), + ), ], options={ - 'db_table': 'internal_tickets_ticketerqueue', + "db_table": "internal_tickets_ticketerqueue", }, - bases=('tickets.topic',), + bases=("tickets.topic",), ), ] diff --git a/weni/internal/orgs/serializers.py b/weni/internal/orgs/serializers.py index 273af7774..7a0b44c03 100644 --- a/weni/internal/orgs/serializers.py +++ b/weni/internal/orgs/serializers.py @@ -29,7 +29,7 @@ def validate(self, attrs): attrs.pop("user_email") return super().validate(attrs) - + def to_representation(self, instance): ret = super().to_representation(instance) ret["uuid"] = instance.project_uuid @@ -66,7 +66,12 @@ def get_users(self, project: Project): editors = list(project.editors.all().values(*values)) surveyors = list(project.surveyors.all().values(*values)) - administrators = list(map(lambda user: self.set_user_permission(user, "administrator"), administrators)) + administrators = list( + map( + lambda user: self.set_user_permission(user, "administrator"), + administrators, + ) + ) viewers = list(map(lambda user: self.set_user_permission(user, "viewer"), viewers)) editors = list(map(lambda user: self.set_user_permission(user, "editor"), editors)) surveyors = list(map(lambda user: self.set_user_permission(user, "surveyor"), surveyors)) @@ -77,7 +82,15 @@ def get_users(self, project: Project): class Meta: model = Project - fields = ["id", "name", "uuid", "timezone", "date_format", "users", "flow_organization"] + fields = [ + "id", + "name", + "uuid", + "timezone", + "date_format", + "users", + "flow_organization", + ] class OrgCreateSerializer(serializers.ModelSerializer): diff --git a/weni/internal/orgs/views.py b/weni/internal/orgs/views.py index 972c37daf..c2fdc3a11 100644 --- a/weni/internal/orgs/views.py +++ b/weni/internal/orgs/views.py @@ -40,7 +40,8 @@ def create(self, request): serializer.is_valid(raise_exception=True) user, created = User.objects.get_or_create( - email=request.data.get("user_email"), defaults={"username": request.data.get("user_email")} + email=request.data.get("user_email"), + defaults={"username": request.data.get("user_email")}, ) project = Project.objects.create( @@ -49,8 +50,7 @@ def create(self, request): created_by=user, modified_by=user, plan="infinity", - project_uuid=request.data.get("uuid") - + project_uuid=request.data.get("uuid"), ) project.administrators.add(user) diff --git a/weni/internal/tickets/serializers.py b/weni/internal/tickets/serializers.py index 5b0093c9f..325224453 100644 --- a/weni/internal/tickets/serializers.py +++ b/weni/internal/tickets/serializers.py @@ -15,7 +15,6 @@ class TicketerConfigSerializer(serializers.Serializer): class TicketerSerializer(serializers.ModelSerializer): - org = weni_serializers.OrgUUIDRelatedField(required=True) config = TicketerConfigSerializer(required=True) @@ -34,7 +33,6 @@ def create(self, validated_data): class TicketerQueueSerializer(serializers.ModelSerializer): - uuid = serializers.UUIDField(required=True) class Meta: diff --git a/weni/internal/tickets/tests/test_views.py b/weni/internal/tickets/tests/test_views.py index 94c7d9245..1d4693c57 100644 --- a/weni/internal/tickets/tests/test_views.py +++ b/weni/internal/tickets/tests/test_views.py @@ -2,7 +2,6 @@ from uuid import uuid4 from django.urls import reverse -from django.db import transaction from rest_framework.test import APIRequestFactory, force_authenticate from rest_framework.permissions import AllowAny from rest_framework import status @@ -15,7 +14,6 @@ class TicketerQueueViewTestMixin(object): - action: dict def setUp(self): @@ -49,7 +47,6 @@ def request(self, method: str, *args, data: dict = None, **kwargs): class CreateTicketerQueueViewTestCase(TicketerQueueViewTestMixin, TembaTest): - action = dict(post="create") def test_create_queue(self): @@ -62,7 +59,6 @@ def test_create_queue(self): class UpdateTicketerQueueViewTestCase(TicketerQueueViewTestMixin, TembaTest): - action = dict(patch="partial_update") def test_update_queue(self): @@ -80,7 +76,6 @@ def test_update_queue(self): class DestroyTicketerQueueViewTestCase(TicketerQueueViewTestMixin, TembaTest): - action = dict(delete="destroy") def test_destroy_queue(self): diff --git a/weni/internal/users/tests.py b/weni/internal/users/tests.py index 34c681df0..5dd8fe4ea 100644 --- a/weni/internal/users/tests.py +++ b/weni/internal/users/tests.py @@ -10,7 +10,6 @@ from temba.api.models import APIToken from temba.tests import TembaTest -from temba.orgs.models import Org from weni.internal.models import Project from weni.internal.users.views import UserEndpoint, UserPermissionEndpoint, UserViewSet @@ -50,7 +49,10 @@ def request_patch(self, data, **kwargs): token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.patch( - f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + f"{url}", + HTTP_AUTHORIZATION=f"Token {token.key}", + data=json.dumps(data), + content_type="application/json", ) def request_post(self, data): @@ -58,7 +60,10 @@ def request_post(self, data): token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.post( - url, HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + url, + HTTP_AUTHORIZATION=f"Token {token.key}", + data=json.dumps(data), + content_type="application/json", ) def request_delete(self, data, **kwargs): @@ -66,7 +71,10 @@ def request_delete(self, data, **kwargs): token = APIToken.get_or_create(self.project, self.admin, Group.objects.get(name="Administrators")) return self.client.delete( - f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}", data=json.dumps(data), content_type="application/json" + f"{url}", + HTTP_AUTHORIZATION=f"Token {token.key}", + data=json.dumps(data), + content_type="application/json", ) @abstractmethod @@ -81,7 +89,10 @@ def setUp(self): ) self.project = Project.objects.create( - name="Test", timezone="Africa/Kigali", created_by=self.admin, modified_by=self.admin + name="Test", + timezone="Africa/Kigali", + created_by=self.admin, + modified_by=self.admin, ) super().setUp() @@ -90,15 +101,31 @@ def test_user_permission_destroy(self): user = User.objects.first() destroy_wrong_permission = self.request_delete( - data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="adm") + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="adm", + ) ) self.assertEqual(destroy_wrong_permission.status_code, 400) self.assertEqual(destroy_wrong_permission.json()[0], "adm is not a valid permission!") - self.request_patch(data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="viewer")) + self.request_patch( + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="viewer", + ) + ) user_permissions = self._get_user_permissions(project=project, user=user) - self.request_delete(data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="viewer")) + self.request_delete( + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="viewer", + ) + ) user_permissions_removed = self._get_user_permissions(project=project, user=user) self.assertFalse(user_permissions_removed.get("viewer", False)) @@ -109,13 +136,21 @@ def test_user_permission_update(self): user = User.objects.first() update_wrong_permission_response = self.request_patch( - data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="adm") + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="adm", + ) ) self.assertEqual(update_wrong_permission_response.status_code, 400) self.assertEqual(update_wrong_permission_response.json()[0], "adm is not a valid permission!") update_response = self.request_patch( - data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="administrator") + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="administrator", + ) ).json() user_permissions = self._get_user_permissions(project, user) @@ -123,7 +158,11 @@ def test_user_permission_update(self): self.assertTrue(self._permission_is_unique_true(update_response, "administrator")) update_response = self.request_patch( - data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="viewer") + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="viewer", + ) ).json() user_permissions = self._get_user_permissions(project, user) @@ -131,7 +170,11 @@ def test_user_permission_update(self): self.assertTrue(self._permission_is_unique_true(update_response, "viewer")) update_response = self.request_patch( - data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="editor") + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="editor", + ) ).json() user_permissions = self._get_user_permissions(project, user) @@ -139,7 +182,11 @@ def test_user_permission_update(self): self.assertTrue(self._permission_is_unique_true(update_response, "editor")) update_response = self.request_patch( - data=dict(org_uuid=str(project.project_uuid), user_email=user.email, permission="surveyor") + data=dict( + org_uuid=str(project.project_uuid), + user_email=user.email, + permission="surveyor", + ) ).json() user_permissions = self._get_user_permissions(project, user) @@ -185,7 +232,10 @@ def setUp(self): username="testuser", password="123", email="test@weni.ai", is_superuser=True ) self.project = Project.objects.create( - name="Test", timezone="Africa/Kigali", created_by=self.admin, modified_by=self.admin + name="Test", + timezone="Africa/Kigali", + created_by=self.admin, + modified_by=self.admin, ) super().setUp() diff --git a/weni/internal/users/views.py b/weni/internal/users/views.py index c76e5e5fa..76a271b34 100644 --- a/weni/internal/users/views.py +++ b/weni/internal/users/views.py @@ -12,10 +12,13 @@ from rest_framework.exceptions import ValidationError from weni.internal.views import InternalGenericViewSet -from weni.internal.users.serializers import UserAPITokenSerializer, UserSerializer, UserPermissionSerializer +from weni.internal.users.serializers import ( + UserAPITokenSerializer, + UserSerializer, + UserPermissionSerializer, +) from temba.api.models import APIToken from temba.orgs.models import Org -from weni.internal.models import Project if TYPE_CHECKING: @@ -25,7 +28,12 @@ class UserViewSet(InternalGenericViewSet): - @action(detail=False, methods=["GET"], url_path="api-token", serializer_class=UserAPITokenSerializer) + @action( + detail=False, + methods=["GET"], + url_path="api-token", + serializer_class=UserAPITokenSerializer, + ) def api_token(self, request: "Request", **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) @@ -38,7 +46,11 @@ def api_token(self, request: "Request", **kwargs): raise exceptions.PermissionDenied() return Response( - dict(user=api_token.user.email, project=project.project_uuid, api_token=api_token.key) + dict( + user=api_token.user.email, + project=project.project_uuid, + api_token=api_token.key, + ) ) @@ -150,7 +162,8 @@ def retrieve(self, request): raise ValidationError(detail="empty email") user = User.objects.get_or_create( - email=request.query_params.get("email"), defaults={"username": request.query_params.get("email")} + email=request.query_params.get("email"), + defaults={"username": request.query_params.get("email")}, ) serializer = self.get_serializer(user[0]) diff --git a/weni/s3/urls.py b/weni/s3/urls.py index 795b0b9ff..a26ce464c 100644 --- a/weni/s3/urls.py +++ b/weni/s3/urls.py @@ -1,9 +1,12 @@ -from rest_framework.urlpatterns import format_suffix_patterns from django.conf.urls import url from .views import WeniFileCallbackView urlpatterns = [ - url(r"^file/(?P[\w\-./]+)$", WeniFileCallbackView.as_view(), name="file_callback"), + url( + r"^file/(?P[\w\-./]+)$", + WeniFileCallbackView.as_view(), + name="file_callback", + ), ] diff --git a/weni/serializers/__init__.py b/weni/serializers/__init__.py index 44874c738..a53c70a53 100644 --- a/weni/serializers/__init__.py +++ b/weni/serializers/__init__.py @@ -1 +1 @@ -from weni.serializers.fields import UserEmailRelatedField, OrgUUIDRelatedField +from weni.serializers.fields import UserEmailRelatedField, OrgUUIDRelatedField # noqa: F401 diff --git a/weni/success_orgs/business.py b/weni/success_orgs/business.py index aac615990..3e871c561 100644 --- a/weni/success_orgs/business.py +++ b/weni/success_orgs/business.py @@ -58,7 +58,13 @@ def get_success_orgs() -> "QuerySet[Org]": .annotate(**SUCCESS_ORG_QUERIES) .annotate( is_success_project=Case( - When(has_ia=True, has_flows=True, has_channel=True, has_msg=True, then=Value(True)), + When( + has_ia=True, + has_flows=True, + has_channel=True, + has_msg=True, + then=Value(True), + ), output_field=BooleanField(), default=Value(False), ) @@ -77,7 +83,6 @@ def get_user_success_orgs_by_email(email: str) -> dict: def retrieve_success_org(org_uuid: str) -> Org: - try: return get_success_orgs().get(uuid=org_uuid) except Org.DoesNotExist as error: diff --git a/weni/success_orgs/views.py b/weni/success_orgs/views.py index d45ee239c..a910a9961 100644 --- a/weni/success_orgs/views.py +++ b/weni/success_orgs/views.py @@ -20,13 +20,11 @@ class ListSuccessOrgAPIView(APIView): - renderer_classes = [JSONRenderer] authentication_classes = [] permission_classes = [] def check_permissions(self, request): - auth = get_authorization_header(request).split() if not auth: @@ -65,13 +63,11 @@ def get(self, request, **kwargs) -> Response: class RetrieveSuccessOrgAPIView(APIView): - authentication_classes = [InternalOIDCAuthentication] renderer_classes = [JSONRenderer] throttle_classes = [] def get(self, request, uuid) -> Response: - try: org = retrieve_success_org(uuid) except OrgDoesNotExist: diff --git a/weni/template_message/serializers.py b/weni/template_message/serializers.py index 6642bff84..bd3b011c1 100644 --- a/weni/template_message/serializers.py +++ b/weni/template_message/serializers.py @@ -8,7 +8,6 @@ class TemplateMessageSerializers(WriteSerializer): - channel = fields.ChannelField() content = serializers.CharField() name = serializers.CharField(write_only=True) diff --git a/weni/template_message/tests.py b/weni/template_message/tests.py index 214504a68..4026240a4 100644 --- a/weni/template_message/tests.py +++ b/weni/template_message/tests.py @@ -14,7 +14,6 @@ class TembaPostRequestMixin: - url_namespace = None def request(self, data=None, user=None): @@ -25,7 +24,6 @@ def request(self, data=None, user=None): class CreateTemplateMessageTest(TembaPostRequestMixin, TembaTest): - url_namespace = "api.v2.template_messages" def setUp(self): @@ -59,7 +57,6 @@ def setUp(self): ) def test_admin_create_template(self): - data = self.request_data response = self.request(data) diff --git a/weni/templates/context_processors.py b/weni/templates/context_processors.py index ca0e5268c..57f9d6695 100644 --- a/weni/templates/context_processors.py +++ b/weni/templates/context_processors.py @@ -2,7 +2,6 @@ def enable_weni_layout(request): - host = request.get_host().split(":")[0] return {"use_weni_layout": host.endswith(settings.WENI_DOMAINS["weni"])} @@ -18,6 +17,4 @@ def weni_announcement(request): def hotjar(request): - return { - "hotjar_id": settings.HOTJAR_ID - } \ No newline at end of file + return {"hotjar_id": settings.HOTJAR_ID} diff --git a/weni/utils/app_config.py b/weni/utils/app_config.py index f0348ec73..1bb1bbb5b 100644 --- a/weni/utils/app_config.py +++ b/weni/utils/app_config.py @@ -9,7 +9,7 @@ def context_data(cls, **kwargs): added = [] for url in app_urls: if isinstance(url, URLPattern): - if hasattr(url.callback, 'view_class'): + if hasattr(url.callback, "view_class"): view = url.callback.view_class else: view = url.callback From 4ffc5f7461e7c513a5e4179e9f2db2394318c1d6 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Tue, 30 May 2023 18:49:25 -0300 Subject: [PATCH 075/101] Update version of weni-rp-apps to 2.4.3 (#241) --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e628f718..87df6190a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [2.4.3] - 2023-05-30 +- Removes the ticketer object from the queue.release() method +- Run black and flake8 in rp-apps + ## [2.4.2] - 2023-05-18 - Filter active orgs to return statistics diff --git a/pyproject.toml b/pyproject.toml index 20c5879d6..05a6f08b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "weni-rp-apps" -version = "2.4.2" +version = "2.4.3" description = "Weni apps for Rapidpro Platform" authors = ["jcbalmeida"] license = "AGPL-3.0" From 87c6042a26db086483dbf5175bcd8021ea2b70cd Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Tue, 30 May 2023 19:21:51 -0300 Subject: [PATCH 076/101] fix: removes the ticketer object from the queue.release() method (#242) --- weni/internal/tickets/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weni/internal/tickets/views.py b/weni/internal/tickets/views.py index 0ca0ae5a4..8f0799322 100644 --- a/weni/internal/tickets/views.py +++ b/weni/internal/tickets/views.py @@ -62,4 +62,4 @@ def update(self, request, *args, **kwargs): return super().update(request, *args, **kwargs) def perform_destroy(self, instance): - instance.release(self.request.user) + instance.release() From e55cf29ddaa6be868ece1c961782ddc14e0a0de6 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Tue, 30 May 2023 19:27:13 -0300 Subject: [PATCH 077/101] fix: add user to release ticketer (#243) --- weni/internal/tickets/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weni/internal/tickets/views.py b/weni/internal/tickets/views.py index 8f0799322..3e902ec31 100644 --- a/weni/internal/tickets/views.py +++ b/weni/internal/tickets/views.py @@ -21,7 +21,7 @@ class TicketerViewSet( lookup_field = "uuid" def perform_destroy(self, instance): - instance.release() + instance.release(self.request.user) class TicketerQueueViewSet( From 5ca6314f56613bb914c01338241526f3dab2df20 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Tue, 30 May 2023 19:35:39 -0300 Subject: [PATCH 078/101] Update version of to 2..3 (#244) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87df6190a..fa664a5e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [2.4.3] - 2023-05-30 - Removes the ticketer object from the queue.release() method - Run black and flake8 in rp-apps +- Add user to release ticketer ## [2.4.2] - 2023-05-18 - Filter active orgs to return statistics From bb5375b8af2ba8939cb839548f97052d5e2d8a56 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:46:03 -0300 Subject: [PATCH 079/101] change create external service to use project over org (#238) --- weni/internal/externals/serializers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/weni/internal/externals/serializers.py b/weni/internal/externals/serializers.py index 7220430a3..cbd2ececa 100644 --- a/weni/internal/externals/serializers.py +++ b/weni/internal/externals/serializers.py @@ -1,15 +1,16 @@ from rest_framework import serializers -from weni.serializers import OrgUUIDRelatedField, UserEmailRelatedField +from weni.serializers import UserEmailRelatedField from temba.externals.models import ExternalService +from weni.serializers.fields import ProjectUUIDRelatedField class ExternalServicesSerializer(serializers.Serializer): uuid = serializers.UUIDField(read_only=True) type_code = serializers.CharField(write_only=True) type_fields = serializers.JSONField(write_only=True) - org = OrgUUIDRelatedField(write_only=True) + project = ProjectUUIDRelatedField(write_only=True) user = UserEmailRelatedField(write_only=True) external_service_type = serializers.CharField(read_only=True) @@ -22,7 +23,7 @@ def create(self, validated_data: dict): type_code = validated_data.get("type_code") type_fields = validated_data.get("type_fields") user = validated_data.get("user") - org = validated_data.get("org") + project = validated_data.get("project") try: type_ = ExternalService.get_type_from_code(type_code) @@ -31,4 +32,6 @@ def create(self, validated_data: dict): type_serializer = type_.serializer_class(data=type_fields) type_serializer.is_valid(raise_exception=True) - return type_serializer.save(type=type_, created_by=user, modified_by=user, org=org) + return type_serializer.save( + type=type_, created_by=user, modified_by=user, org=project + ) From 2990fd903cab5a09983e21a3489fdf91ea623c04 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:49:10 -0300 Subject: [PATCH 080/101] add has_channel_production in success orgs (#236) --- weni/success_orgs/business.py | 16 +++++++++++++-- weni/success_orgs/serializers.py | 1 + weni/success_orgs/tests/test_business.py | 25 ++++++++++++++++++++---- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/weni/success_orgs/business.py b/weni/success_orgs/business.py index 3e871c561..808978a50 100644 --- a/weni/success_orgs/business.py +++ b/weni/success_orgs/business.py @@ -20,10 +20,19 @@ SUCCESS_ORG_QUERIES = dict( - has_ia=Exists(Classifier.objects.filter(org=OuterRef("pk"), classifier_type="bothub", is_active=True)), + has_ia=Exists( + Classifier.objects.filter( + org=OuterRef("pk"), classifier_type="bothub", is_active=True + ) + ), has_flows=Exists(Flow.objects.filter(org=OuterRef("pk"), is_active=True)), has_channel=Exists(Channel.objects.filter(org=OuterRef("pk"), is_active=True)), has_msg=Exists(Msg.objects.filter(org=OuterRef("pk"))), + has_channel_production=Exists( + Channel.objects.filter(org=OuterRef("pk"), is_active=True).exclude( + name="WhatsApp: +558231420933" + ) + ), ) @@ -63,6 +72,7 @@ def get_success_orgs() -> "QuerySet[Org]": has_flows=True, has_channel=True, has_msg=True, + has_channel_production=True, then=Value(True), ), output_field=BooleanField(), @@ -79,7 +89,9 @@ def get_user_success_orgs(user: User) -> "QuerySet[Org]": def get_user_success_orgs_by_email(email: str) -> dict: user = get_user_by_email(email) - return dict(email=user.email, last_login=user.last_login, orgs=get_user_success_orgs(user)) + return dict( + email=user.email, last_login=user.last_login, orgs=get_user_success_orgs(user) + ) def retrieve_success_org(org_uuid: str) -> Org: diff --git a/weni/success_orgs/serializers.py b/weni/success_orgs/serializers.py index 16b2ee082..3d223c8ee 100644 --- a/weni/success_orgs/serializers.py +++ b/weni/success_orgs/serializers.py @@ -8,6 +8,7 @@ class SuccessOrgSerializer(serializers.Serializer): has_flows = serializers.BooleanField() has_channel = serializers.BooleanField() has_msg = serializers.BooleanField() + has_channel_production = serializers.BooleanField() is_success_project = serializers.BooleanField() diff --git a/weni/success_orgs/tests/test_business.py b/weni/success_orgs/tests/test_business.py index a0ed1095c..dabbd7066 100644 --- a/weni/success_orgs/tests/test_business.py +++ b/weni/success_orgs/tests/test_business.py @@ -23,7 +23,9 @@ class SetupMixin(object): def setUp(self) -> None: self.user_email = "fake@weni.ai" self.user = User.objects.create(email=self.user_email) - self.org = Org.objects.create(created_by=self.user, modified_by=self.user, name="fakeorg") + self.org = Org.objects.create( + created_by=self.user, modified_by=self.user, name="fakeorg" + ) class GetUserByEmailTestCase(SetupMixin, TestCase): @@ -43,10 +45,15 @@ def test_function_returns_extra_fields(self): self.assertTrue(hasattr(org, "has_flows")) self.assertTrue(hasattr(org, "has_channel")) self.assertTrue(hasattr(org, "has_msg")) + self.assertTrue(hasattr(org, "has_channel_production")) def test_when_adding_classifier_it_returns_true(self): Classifier.objects.create( - org=self.org, created_by=self.user, modified_by=self.user, config={}, classifier_type="bothub" + org=self.org, + created_by=self.user, + modified_by=self.user, + config={}, + classifier_type="bothub", ) org = get_user_success_orgs(self.user).first() @@ -54,24 +61,34 @@ def test_when_adding_classifier_it_returns_true(self): self.assertFalse(org.has_flows) self.assertFalse(org.has_channel) self.assertFalse(org.has_msg) + self.assertFalse(org.has_channel_production) def test_when_adding_flow_it_returns_true(self): - Flow.objects.create(org=self.org, created_by=self.user, modified_by=self.user, saved_by=self.user) + Flow.objects.create( + org=self.org, + created_by=self.user, + modified_by=self.user, + saved_by=self.user, + ) org = get_user_success_orgs(self.user).first() self.assertFalse(org.has_ia) self.assertTrue(org.has_flows) self.assertFalse(org.has_channel) self.assertFalse(org.has_msg) + self.assertFalse(org.has_channel_production) def test_when_adding_channel_it_returns_true(self): - Channel.objects.create(org=self.org, created_by=self.user, modified_by=self.user) + Channel.objects.create( + org=self.org, created_by=self.user, modified_by=self.user + ) org = get_user_success_orgs(self.user).first() self.assertFalse(org.has_ia) self.assertFalse(org.has_flows) self.assertTrue(org.has_channel) self.assertFalse(org.has_msg) + self.assertTrue(org.has_channel_production) class RetrieveSuccessOrgTestCase(SetupMixin, TestCase): From e2fa228b573da0571e91328f4feda745a7719c3b Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:58:24 -0300 Subject: [PATCH 081/101] update version to 2.4.4 (#245) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa664a5e0..62d43ff48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [2.4.4] - 2023-06-02 +- Change create external service to use project over org +- Add has_channel_production flag in success orgs + ## [2.4.3] - 2023-05-30 - Removes the ticketer object from the queue.release() method - Run black and flake8 in rp-apps From fc9f8a2ac854e02869edcff47f0145ade0de4bca Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:13:16 -0300 Subject: [PATCH 082/101] hotfix: fix error in c.i (#246) --- .github/workflows/build-rapidpro-apps-pypi.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rapidpro-apps-pypi.yaml b/.github/workflows/build-rapidpro-apps-pypi.yaml index 48d549edb..c5e4c4044 100644 --- a/.github/workflows/build-rapidpro-apps-pypi.yaml +++ b/.github/workflows/build-rapidpro-apps-pypi.yaml @@ -25,8 +25,13 @@ jobs: with: python-version: '3.9' - - name: Install poetry - uses: Gr1N/setup-poetry@v7 + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - + poetry self update 1.5.1 + + - name: Verify Poetry installation + run: poetry --version - name: Update version in pyproject.toml shell: bash From 78fd41e128bb0ec2ce9e0c52afc01355a1a985e2 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Wed, 7 Jun 2023 18:05:10 -0300 Subject: [PATCH 083/101] feat: Adjust Statistic app to new Project model. (#240) --- weni/internal/statistic/tests.py | 21 +++++++++++++-------- weni/internal/statistic/views.py | 14 +++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/weni/internal/statistic/tests.py b/weni/internal/statistic/tests.py index 555f234d6..ec1bbb7b6 100644 --- a/weni/internal/statistic/tests.py +++ b/weni/internal/statistic/tests.py @@ -7,8 +7,13 @@ from temba.api.models import APIToken from temba.flows.models import Flow -from temba.orgs.models import Org from temba.tests import TembaTest +from weni.internal.models import Project +from weni.internal.statistic.views import StatisticEndpoint + + +view = StatisticEndpoint +view.permission_classes = [] class TembaRequestMixin(ABC): @@ -21,7 +26,7 @@ def reverse(self, viewname, kwargs=None, query_params=None): return url def request_detail(self, uuid): - url = self.reverse(self.get_url_namespace(), kwargs={"uuid": uuid}) + url = self.reverse(self.get_url_namespace(), kwargs={"project_uuid": uuid}) token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") @@ -37,15 +42,15 @@ def setUp(self): def test_retrieve(self): user = User.objects.create_user(username="testuser", password="123", email="test@weni.ai") - org = Org.objects.create(name="Temba", timezone="Africa/Kigali", created_by=user, modified_by=user) + project = Project.objects.create(name="Temba", timezone="Africa/Kigali", created_by=user, modified_by=user) - Flow.create(org=org, name="Flow test", user=user) + Flow.create(org=project.org, name="Flow test", user=user) - active_flows = org.flows.filter(is_active=True, is_archived=False).exclude(is_system=True).count() - active_classifiers = org.classifiers.filter(is_active=True).count() - active_contacts = org.contacts.filter(is_active=True).count() + active_flows = project.flows.filter(is_active=True, is_archived=False).exclude(is_system=True).count() + active_classifiers = project.classifiers.filter(is_active=True).count() + active_contacts = project.contacts.filter(is_active=True).count() - statistic_request = self.request_detail(uuid=str(org.uuid)).json() + statistic_request = self.request_detail(uuid=str(project.project_uuid)).json() self.assertEqual(statistic_request["active_flows"], active_flows) self.assertEqual(statistic_request["active_classifiers"], active_classifiers) diff --git a/weni/internal/statistic/views.py b/weni/internal/statistic/views.py index 1d2ac95da..a9e77b778 100644 --- a/weni/internal/statistic/views.py +++ b/weni/internal/statistic/views.py @@ -4,22 +4,22 @@ from rest_framework.mixins import RetrieveModelMixin from rest_framework import status -from temba.orgs.models import Org from temba.contacts.models import ContactGroup +from weni.internal.models import Project from weni.internal.views import InternalGenericViewSet class StatisticEndpoint(RetrieveModelMixin, InternalGenericViewSet): - lookup_field = "uuid" + lookup_field = "project_uuid" - def retrieve(self, request, uuid=None): - org = get_object_or_404(Org, uuid=uuid, is_active=True) - group = ContactGroup.all_groups.get(org=org, group_type='A') + def retrieve(self, request, project_uuid=None): + project = get_object_or_404(Project, project_uuid=project_uuid, is_active=True) + group = ContactGroup.all_groups.get(org=project.org, group_type="A") response = { - "active_flows": org.flows.filter(is_active=True, is_archived=False).exclude(is_system=True).count(), - "active_classifiers": org.classifiers.filter(is_active=True).count(), + "active_flows": project.flows.filter(is_active=True, is_archived=False).exclude(is_system=True).count(), + "active_classifiers": project.classifiers.filter(is_active=True).count(), "active_contacts": group.get_member_count(), } From 84d0d79d57c8da86348b38dbecd1f526e43d08c5 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Wed, 7 Jun 2023 18:16:23 -0300 Subject: [PATCH 084/101] Update version to 2.5.0 (#248) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d43ff48..183e7ea09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [2.5.0] - 2023-06-07 +- Adjust Statistic app to new Project model +- Fix error in CI + ## [2.4.4] - 2023-06-02 - Change create external service to use project over org - Add has_channel_production flag in success orgs From 2148b8ce7149161271d132169801d37a89b4aa22 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Wed, 21 Jun 2023 00:10:19 -0300 Subject: [PATCH 085/101] feature: adding list and update methods to external service viewset (#249) --- weni/internal/externals/serializers.py | 27 ++++++++++++++++++++++-- weni/internal/externals/views.py | 29 +++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/weni/internal/externals/serializers.py b/weni/internal/externals/serializers.py index cbd2ececa..3566e33dc 100644 --- a/weni/internal/externals/serializers.py +++ b/weni/internal/externals/serializers.py @@ -6,6 +6,13 @@ from weni.serializers.fields import ProjectUUIDRelatedField +AI_MODELS = [ + ("gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k"), + ("gpt-3.5-turbo", "gpt-3.5-turbo"), + ("gpt-4", "gpt-4"), +] + + class ExternalServicesSerializer(serializers.Serializer): uuid = serializers.UUIDField(read_only=True) type_code = serializers.CharField(write_only=True) @@ -18,8 +25,6 @@ class ExternalServicesSerializer(serializers.Serializer): config = serializers.JSONField(read_only=True) def create(self, validated_data: dict): - validated_data = validated_data - type_code = validated_data.get("type_code") type_fields = validated_data.get("type_fields") user = validated_data.get("user") @@ -35,3 +40,21 @@ def create(self, validated_data: dict): return type_serializer.save( type=type_, created_by=user, modified_by=user, org=project ) + + +class UpdateExternalServicesSerializer(serializers.Serializer): + config = serializers.JSONField() + + def validate(self, attrs): + config = attrs.get("config") + if config: + ai_model = config.get("ai_model") + if ai_model and ai_model not in [choice[0] for choice in AI_MODELS]: + raise serializers.ValidationError(f"{ai_model} is a invalid A.I Model") + + return attrs + + def update(self, instance, validated_data: dict): + instance.config = validated_data.get("config", instance.config) + instance.save() + return instance diff --git a/weni/internal/externals/views.py b/weni/internal/externals/views.py index 4db52fe18..f5508188b 100644 --- a/weni/internal/externals/views.py +++ b/weni/internal/externals/views.py @@ -10,7 +10,10 @@ from weni.internal.authenticators import InternalOIDCAuthentication from weni.internal.permissions import CanCommunicateInternally -from weni.internal.externals.serializers import ExternalServicesSerializer +from weni.internal.externals.serializers import ( + ExternalServicesSerializer, + UpdateExternalServicesSerializer, +) from temba.externals.models import ExternalService @@ -25,6 +28,10 @@ class ExternalServicesAPIView(APIView): renderer_classes = [JSONRenderer] throttle_classes = [] + def get_object(self): + uuid = self.kwargs.get("uuid") + return get_object_or_404(ExternalService, uuid=uuid) + def post(self, request: "Request") -> Response: serializer = ExternalServicesSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -39,3 +46,23 @@ def delete(self, request, uuid=None): external_service.release(user) return Response(status=status.HTTP_204_NO_CONTENT) + + def patch(self, request, uuid=None): + return self.update(request, uuid) + + def update(self, request, *args, **kwargs): + external_service = self.get_object() + serializer = UpdateExternalServicesSerializer( + external_service, data=request.data, partial=True + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def get(self, request, uuid=None): + return self.retrieve(request, uuid) + + def retrieve(self, request, uuid=None): + external_service = get_object_or_404(ExternalService, uuid=uuid) + serializer = ExternalServicesSerializer(external_service) + return Response(serializer.data) From e1801d16d4a4e46f819dff86b55fbdd585ae6c09 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Wed, 21 Jun 2023 00:35:33 -0300 Subject: [PATCH 086/101] feature: create viewset for prompts (#250) --- weni/internal/externals/serializers.py | 12 ++++++- weni/internal/externals/urls.py | 8 +++++ weni/internal/externals/views.py | 44 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/weni/internal/externals/serializers.py b/weni/internal/externals/serializers.py index 3566e33dc..b71f7a2a4 100644 --- a/weni/internal/externals/serializers.py +++ b/weni/internal/externals/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers - +from temba.externals.models import Prompt from weni.serializers import UserEmailRelatedField from temba.externals.models import ExternalService @@ -58,3 +58,13 @@ def update(self, instance, validated_data: dict): instance.config = validated_data.get("config", instance.config) instance.save() return instance + + +class PromptSerializer(serializers.Serializer): + uuid = serializers.UUIDField(read_only=True) + user = UserEmailRelatedField(write_only=True) + text = serializers.CharField() + + def create(self, validated_data): + user = validated_data.pop("user") + return Prompt.objects.create(created_by=user, modified_by=user, **validated_data) diff --git a/weni/internal/externals/urls.py b/weni/internal/externals/urls.py index 7a3b7b993..88a2a81f1 100644 --- a/weni/internal/externals/urls.py +++ b/weni/internal/externals/urls.py @@ -1,8 +1,16 @@ from django.urls import path + from weni.internal.externals.views import ExternalServicesAPIView +from weni.internal.externals.views import PromptViewSet + +from rest_framework import routers urlpatterns = [ path("externals", ExternalServicesAPIView.as_view(), name="api.v2.externals"), path(r"externals//", ExternalServicesAPIView.as_view(), name="api.v2.externals"), ] + +router = routers.SimpleRouter() +router.register(r'externals/(?P[^/.]+)/prompts', PromptViewSet, basename='prompts') +urlpatterns += router.urls diff --git a/weni/internal/externals/views.py b/weni/internal/externals/views.py index f5508188b..12954012a 100644 --- a/weni/internal/externals/views.py +++ b/weni/internal/externals/views.py @@ -7,14 +7,17 @@ from rest_framework.renderers import JSONRenderer from rest_framework.permissions import IsAuthenticated from rest_framework import status +from rest_framework import viewsets from weni.internal.authenticators import InternalOIDCAuthentication from weni.internal.permissions import CanCommunicateInternally from weni.internal.externals.serializers import ( ExternalServicesSerializer, UpdateExternalServicesSerializer, + PromptSerializer ) from temba.externals.models import ExternalService +from temba.externals.models import Prompt if TYPE_CHECKING: @@ -66,3 +69,44 @@ def retrieve(self, request, uuid=None): external_service = get_object_or_404(ExternalService, uuid=uuid) serializer = ExternalServicesSerializer(external_service) return Response(serializer.data) + + +class PromptViewSet(viewsets.ViewSet): + authentication_classes = [InternalOIDCAuthentication] + permission_classes = [IsAuthenticated, CanCommunicateInternally] + pagination_class = None + renderer_classes = [JSONRenderer] + throttle_classes = [] + lookup_field = "uuid" + + def create(self, request, external_uuid=None): + external_service = get_object_or_404(ExternalService, uuid=external_uuid) + serializer = PromptSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(chat_gpt_service=external_service) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, external_uuid=None, uuid=None): + queryset = Prompt.objects.filter(chat_gpt_service__uuid=external_uuid) + prompt = get_object_or_404(queryset, uuid=uuid) + serializer = PromptSerializer(prompt) + return Response(serializer.data) + + def list(self, request, external_uuid=None): + queryset = Prompt.objects.filter(chat_gpt_service__uuid=external_uuid) + serializer = PromptSerializer(queryset, many=True) + return Response(serializer.data) + + def destroy(self, request, external_uuid=None, uuid=None): + queryset = Prompt.objects.filter(chat_gpt_service__uuid=external_uuid) + + try: + prompt = queryset.get(uuid=uuid) + prompt.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except Prompt.DoesNotExist: + return Response( + data={"detail": f"Prompt {uuid} not found on flows"}, + status=status.HTTP_404_NOT_FOUND, + ) From b5cf51c76ce199d36d6c07fb4d51124bd343f6e8 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Wed, 21 Jun 2023 00:51:49 -0300 Subject: [PATCH 087/101] Update version to 2.6.0 (#251) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 183e7ea09..3220c7b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +## [2.6.0] - 2023-06-21 +- Adding list and update methods to external service viewset +- Create viewset for prompts + ## [2.5.0] - 2023-06-07 - Adjust Statistic app to new Project model - Fix error in CI From 75ce922858be8213aa880359086ccb65ec3c5a04 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Thu, 22 Jun 2023 18:53:31 -0300 Subject: [PATCH 088/101] Adjust Flows app to new Project model. (#239) * feat: Adjust Flows app to new Project model. * change modelSerializer to Serializer --- weni/internal/flows/serializers.py | 26 ++++++++++++------------- weni/internal/flows/tests.py | 31 +++++++++++++++--------------- weni/internal/flows/views.py | 2 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/weni/internal/flows/serializers.py b/weni/internal/flows/serializers.py index f16847bae..7baf07b70 100644 --- a/weni/internal/flows/serializers.py +++ b/weni/internal/flows/serializers.py @@ -1,35 +1,33 @@ from rest_framework import serializers from django.contrib.auth import get_user_model -from weni.grpc.core import serializers as weni_serializers -from temba.flows.models import Flow - +from weni.serializers import fields as weni_fields User = get_user_model() -class FlowSerializer(serializers.ModelSerializer): - org = weni_serializers.OrgUUIDRelatedField() +class FlowSerializer(serializers.Serializer): + project = weni_fields.ProjectUUIDRelatedField(required=True, write_only=True) sample_flow = serializers.JSONField(write_only=True) + uuid = serializers.UUIDField(read_only=True) class Meta: - model = Flow - fields = ("org", "uuid", "sample_flow") + fields = ("project", "uuid", "sample_flow") def create(self, validated_data): - org = validated_data.get("org") + project = validated_data.get("project") sample_flows = validated_data.get("sample_flow") - org.import_app(sample_flows, org.created_by) - self.disable_flows_has_issues(org, sample_flows) - return org.flows.order_by("created_on").last() + project.import_app(sample_flows, project.created_by) + self.disable_flows_has_issues(project, sample_flows) + return project.flows.order_by("created_on").last() - def disable_flows_has_issues(self, org, sample_flows): + def disable_flows_has_issues(self, project, sample_flows): flows_name = list(map(lambda flow: flow.get("name"), sample_flows.get("flows"))) - org.flows.filter(name__in=flows_name).update(has_issues=False) + project.flows.filter(name__in=flows_name).update(has_issues=False) class FlowListSerializer(serializers.Serializer): flow_name = serializers.CharField(required=True, write_only=True) - org_uuid = weni_serializers.OrgUUIDRelatedField(required=True, write_only=True) + project = weni_fields.ProjectUUIDRelatedField(required=True, write_only=True) uuid = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True) diff --git a/weni/internal/flows/tests.py b/weni/internal/flows/tests.py index f20b1d8e1..0cf9e8977 100644 --- a/weni/internal/flows/tests.py +++ b/weni/internal/flows/tests.py @@ -9,8 +9,13 @@ from temba.tests import TembaTest from temba.api.models import APIToken -from temba.orgs.models import Org from temba.flows.models import Flow +from weni.internal.models import Project +from weni.internal.flows.views import ProjectFlowsViewSet + + +view = ProjectFlowsViewSet +view.permission_classes = [] class TembaRequestMixin(ABC): @@ -25,8 +30,8 @@ def reverse(self, viewname, kwargs=None, query_params=None): def request_get(self, **query_params): url = self.reverse(self.get_url_namespace(), query_params=query_params) url = url.replace("channel", "channel.json") - token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) + token = APIToken.get_or_create(self.org, self.admin, Group.objects.get(name="Administrators")) return self.client.get(f"{url}", HTTP_AUTHORIZATION=f"Token {token.key}") def request_detail(self, uuid): @@ -63,10 +68,8 @@ def setUp(self): user = User.objects.first() - # print(Org.objects.all()) - - temba = Org.objects.create(name="Temba", timezone="America/Maceio", created_by=user, modified_by=user) - weni = Org.objects.create(name="Weni", timezone="America/Maceio", created_by=user, modified_by=user) + temba = Project.objects.create(name="Temba", timezone="America/Maceio", created_by=user, modified_by=user) + weni = Project.objects.create(name="Weni", timezone="America/Maceio", created_by=user, modified_by=user) Flow.create(name="Test Temba", user=user, org=temba) Flow.create(name="Test flow name", user=user, org=weni) @@ -75,8 +78,8 @@ def setUp(self): super().setUp() def test_list_flow(self): - temba = Org.objects.filter(name="Temba").first() - weni = Org.objects.get(name="Weni") + temba = Project.objects.filter(name="Temba").first() + weni = Project.objects.get(name="Weni") response = self.request_get(flow_name="test", org_uuid="123") # {'org_uuid': ['“123” is not a valid UUID.']} self.assertEquals(response.status_code, 400) @@ -84,17 +87,15 @@ def test_list_flow(self): response = self.request_get(flow_name="test", org_uuid="") # {'org_id': ['This field may not be blank.']} self.assertEquals(response.status_code, 400) - response = self.request_get(flow_name="test", org_uuid=str(temba.uuid)).json() - + response = self.request_get(flow_name="test", org_uuid=str(temba.project_uuid)).json() flows, flows_count = self.get_flows_and_count(response) self.assertEquals(flows_count, 1) self.assertEquals(flows[0].get("name"), "Test Temba") - response = self.request_get(flow_name="test", org_uuid=str(weni.uuid)).json() + response = self.request_get(flow_name="test", org_uuid=str(weni.project_uuid)).json() flows, flows_count = self.get_flows_and_count(response) - weni_flow_names = [flow.name for flow in Flow.objects.filter(org=weni.id)] self.assertEquals(flows_count, 2) @@ -102,18 +103,18 @@ def test_list_flow(self): for flow in flows: self.assertIn(flow.get("name"), weni_flow_names) - response = self.request_get(flow_name="weni", org_uuid=str(weni.uuid)).json() + response = self.request_get(flow_name="weni", org_uuid=str(weni.project_uuid)).json() flows, flows_count = self.get_flows_and_count(response) self.assertEquals(flows[0].get("name"), "Test Weni flow name") self.assertEquals(flows_count, 1) - def get_flows_and_count(self, response) -> (list, int): + def get_flows_and_count(self, response): flows = [flow for flow in response] flows_count = len(flows) return flows, flows_count def get_url_namespace(self): - return "flows-list" + return "project-flows-list" diff --git a/weni/internal/flows/views.py b/weni/internal/flows/views.py index 8acd2d2d8..03b723bf1 100644 --- a/weni/internal/flows/views.py +++ b/weni/internal/flows/views.py @@ -20,7 +20,7 @@ def get_queryset(self): queryset = Flow.objects.filter( name__icontains=serializer.validated_data.get("flow_name"), - org=serializer.validated_data.get("org_uuid"), + org=serializer.validated_data.get("project").org, is_active=True, ).exclude(is_archived=True)[:20] From afd1a204131d6667565ae1e5c57225e935af2289 Mon Sep 17 00:00:00 2001 From: Lucas <44686243+lucaslinhares@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:00:55 -0300 Subject: [PATCH 089/101] Update version to 2.7.0 (#253) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3220c7b56..dc5ae557b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.7.0] - 2023-06-22 +- Adjust Flows app to new Project model + ## [2.6.0] - 2023-06-21 - Adding list and update methods to external service viewset - Create viewset for prompts From d5a486e1e8bf5d51564b36738a1b472806fbcbd7 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:29:43 -0300 Subject: [PATCH 090/101] feature: report of sent messages (#252) --- weni/internal/msgs/__init__.py | 0 weni/internal/msgs/tasks.py | 177 ++++++++++++++++++++++++++++++ weni/internal/msgs/tests/tests.py | 0 weni/internal/msgs/urls.py | 11 ++ weni/internal/msgs/views.py | 74 +++++++++++++ weni/internal/urls.py | 3 +- 6 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 weni/internal/msgs/__init__.py create mode 100644 weni/internal/msgs/tasks.py create mode 100644 weni/internal/msgs/tests/tests.py create mode 100644 weni/internal/msgs/urls.py create mode 100644 weni/internal/msgs/views.py diff --git a/weni/internal/msgs/__init__.py b/weni/internal/msgs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/internal/msgs/tasks.py b/weni/internal/msgs/tasks.py new file mode 100644 index 000000000..871b5eb6a --- /dev/null +++ b/weni/internal/msgs/tasks.py @@ -0,0 +1,177 @@ +import smtplib +import logging + +from celery import shared_task + +from django_redis import get_redis_connection +from django.conf import settings +from django.db import connection + +from openpyxl import Workbook + +from io import BytesIO + +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email import encoders + +logger = logging.getLogger(__name__) + + +@shared_task(name="generate_sent_report_messages") +def generate_sent_report_messages(**kwargs): + org_id = kwargs.get("org_id") + start_date = kwargs.get("start_date") + end_date = kwargs.get("end_date") + user = kwargs.get("user") + email_body = kwargs.get("email_body") + email_title = kwargs.get("email_title") + + redis_client = get_redis_connection() + + query = f""" + SELECT + template.name AS "Template", + flow.name AS "Fluxos Utilizados", + COUNT(msg.id) AS "Total por Template" + FROM + public.msgs_msg AS msg + INNER JOIN public.templates_template AS template + ON CAST(template.uuid AS text) = msg.metadata::json -> 'templating' -> 'template' ->> 'uuid' + INNER JOIN public.flows_flow_template_dependencies AS depent + ON depent.template_id = template.id + INNER JOIN public.flows_flow AS flow + ON flow.id = depent.flow_id + WHERE + msg.sent_on BETWEEN '{start_date}' AND '{end_date}' + AND msg.metadata::jsonb -> 'templating' IS NOT NULL + AND msg.org_id = {org_id} + GROUP BY + template.name, flow.name + ORDER BY + COUNT(msg.id) DESC; + """ + filename = "Relatorio de Messagens.xlsx" + + try: + data = fetch_query_results(query) + processed_data = process_query_results(data) + file = export_data_to_excel(processed_data) + send_report_file( + file_stream=file, + file_name=filename, + user=user, + title=email_title, + body=email_body, + ) + except Exception as e: + logger.info(f"Fail to generate report: ORG {org_id}: {e}") + finally: + redis_client.delete(f"template-messages-lock:{org_id}") + + +def fetch_query_results(query): + with connection.cursor() as cursor: + cursor.execute(query) + data = cursor.fetchall() + return data + + +def process_query_results(data): + processed_data = [] + template_dict = {} + + for row in data: + template_name, flow_name, total = row + + if template_name not in template_dict: + template_dict[template_name] = flow_name + processed_data.append((template_name, flow_name, total)) + else: + processed_data.append((template_name, flow_name)) + + return processed_data + + +def export_data_to_excel(data): + workbook = Workbook() + sheet = workbook.active + + header = ["Template", "Fluxos Utilizados", "Total por Template"] + sheet.append(header) + + for row in data: + sheet.append(row) + + file_stream = BytesIO() + workbook.save(file_stream) + file_stream.seek(0) + + return file_stream + + +def export_query_to_excel(query): + with connection.cursor() as cursor: + cursor.execute(query) + + workbook = Workbook() + sheet = workbook.active + + header = [desc[0] for desc in cursor.description] + sheet.append(header) + + for row in cursor.fetchall(): + sheet.append(row) + + file_stream = BytesIO() + workbook.save(file_stream) + file_stream.seek(0) + + return file_stream + + +def send_report_file(file_stream, file_name, user, title, body): + email_subject = title + email_body = body + + email_host = settings.EMAIL_HOST + email_port = settings.EMAIL_PORT + email_username = settings.EMAIL_HOST_USER + email_password = settings.EMAIL_HOST_PASSWORD + email_use_tls = settings.EMAIL_USE_TLS + + try: + message = MIMEMultipart() + message["Subject"] = email_subject + message["From"] = "Weni" + message["To"] = user + + body = MIMEText(email_body, "plain", "utf-8") + message.attach(body) + + attachment = MIMEBase( + "application", "vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + attachment.set_payload(file_stream.getvalue()) + encoders.encode_base64(attachment) + attachment.add_header( + "Content-Disposition", f"attachment; filename={file_name}" + ) + message.attach(attachment) + + smtp_connection = smtplib.SMTP(host=email_host, port=email_port) + smtp_connection.ehlo() + + if email_use_tls: + smtp_connection.starttls() + + smtp_connection.login(email_username, email_password) + result = smtp_connection.sendmail(email_username, user, message.as_string()) + smtp_connection.quit() + + if result: + for recipient, error_message in result.items(): + logger.info(f"Fail send message to {recipient}, error: {error_message}") + except Exception as e: + logger.warning(f"Fail to send messages report error: {e}") diff --git a/weni/internal/msgs/tests/tests.py b/weni/internal/msgs/tests/tests.py new file mode 100644 index 000000000..e69de29bb diff --git a/weni/internal/msgs/urls.py b/weni/internal/msgs/urls.py new file mode 100644 index 000000000..4e6b222ce --- /dev/null +++ b/weni/internal/msgs/urls.py @@ -0,0 +1,11 @@ +from rest_framework import routers + +from weni.internal.msgs.views import TemplateMessagesListView + + +router = routers.SimpleRouter() +router.register( + r"template-messages", TemplateMessagesListView, basename="template-messages" +) + +urlpatterns = router.urls diff --git a/weni/internal/msgs/views.py b/weni/internal/msgs/views.py new file mode 100644 index 000000000..f53fbf70e --- /dev/null +++ b/weni/internal/msgs/views.py @@ -0,0 +1,74 @@ +import celery + +from datetime import datetime + +from django.shortcuts import get_object_or_404 +from django_redis import get_redis_connection + +from rest_framework.response import Response +from rest_framework import viewsets +from rest_framework.renderers import JSONRenderer +from rest_framework.permissions import IsAuthenticated + +from weni.internal.authenticators import InternalOIDCAuthentication +from weni.internal.permissions import CanCommunicateInternally +from weni.internal.models import Project + + +class TemplateMessagesListView(viewsets.ViewSet): + authentication_classes = [InternalOIDCAuthentication] + permission_classes = [IsAuthenticated, CanCommunicateInternally] + pagination_class = None + renderer_classes = [JSONRenderer] + throttle_classes = [] + + def list(self, request): + project_uuid = request.query_params.get("project_uuid") + start_date = request.query_params.get("start_date") + end_date = request.query_params.get("end_date") + user = request.query_params.get("user") + + org = get_object_or_404(Project, project_uuid=project_uuid) + + # Convert string to datatime object + start_date_obj = datetime.strptime(start_date, "%m-%d-%Y") + end_date_obj = datetime.strptime(end_date, "%m-%d-%Y") + + # Convert from datatime object to string + start_date_str = start_date_obj.strftime("%Y-%m-%d") + end_date_str = end_date_obj.strftime("%Y-%m-%d") + + email_start_date = start_date_obj.strftime("%d/%m/%Y") + email_end_date = end_date_obj.strftime("%d/%m/%Y") + + email_body = ( + f"Relátorio em Excel relacionado ao projeto {org.name.title()} " + f"entre {email_start_date} e {email_end_date}." + ) + + email_title = f"Relátorio de Mensagens Enviadas entre {email_start_date} e {email_end_date}" + + lock_key = f"template-messages-lock:{org.id}" + redis_client = get_redis_connection() + is_locked = redis_client.get(lock_key) + kwargs = dict( + org_id=org.id, + start_date=start_date_str, + end_date=end_date_str, + user=user, + email_body=email_body, + email_title=email_title, + ) + if is_locked: + return Response(data={"detail": "Request already in process"}, status=409) + + try: + redis_client.set(lock_key, "locked") + celery.execute.send_task( + "generate_sent_report_messages", + kwargs=kwargs, + ) + except Exception as e: + return Response(data=e, status=500) + + return Response(status=200) diff --git a/weni/internal/urls.py b/weni/internal/urls.py index 72e555343..fdd212288 100644 --- a/weni/internal/urls.py +++ b/weni/internal/urls.py @@ -19,6 +19,7 @@ from weni.internal.statistic.urls import urlpatterns as statistics_urls from weni.internal.globals.urls import urlpatterns as globals_urls from weni.internal.externals.urls import urlpatterns as externals_urls +from weni.internal.msgs.urls import urlpatterns as messages_urls internal_urlpatterns = [] @@ -31,6 +32,6 @@ internal_urlpatterns += statistics_urls internal_urlpatterns += globals_urls internal_urlpatterns += externals_urls - +internal_urlpatterns += messages_urls urlpatterns = [path("internals/", include(internal_urlpatterns))] From 699753dc99719c0b4ec06b063e89e3c7eb329a2b Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:35:50 -0300 Subject: [PATCH 091/101] Update version to 2.7.1 (#254) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5ae557b..e483536da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.7.1] - 2023-06-23 +- Report of sent messages + ## [2.7.0] - 2023-06-22 - Adjust Flows app to new Project model From cd6d76e292ed6894d27da15498f2b84be6a6c484 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:10:28 -0300 Subject: [PATCH 092/101] fix: import generate_sent_report_messages, for the flows auto discovery work (#255) --- weni/internal/msgs/views.py | 1 + weni/internal/tasks.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 weni/internal/tasks.py diff --git a/weni/internal/msgs/views.py b/weni/internal/msgs/views.py index f53fbf70e..27a918665 100644 --- a/weni/internal/msgs/views.py +++ b/weni/internal/msgs/views.py @@ -69,6 +69,7 @@ def list(self, request): kwargs=kwargs, ) except Exception as e: + redis_client.delete(f"template-messages-lock:{org.id}") return Response(data=e, status=500) return Response(status=200) diff --git a/weni/internal/tasks.py b/weni/internal/tasks.py new file mode 100644 index 000000000..2381934c6 --- /dev/null +++ b/weni/internal/tasks.py @@ -0,0 +1 @@ +from weni.internal.msgs.tasks import generate_sent_report_messages From 0c5087da24fd465648cb8fb074fc1dc5c2b08556 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:14:26 -0300 Subject: [PATCH 093/101] Update version to 2.7.2 (#256) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e483536da..844e1e14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.7.2] - 2023-06-23 +- Import generate_sent_report_messages + ## [2.7.1] - 2023-06-23 - Report of sent messages From eac7979fc6384cf40e212b6e67b3eb8d793e7aa7 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:29:30 -0300 Subject: [PATCH 094/101] fix: changes in the view and task generate_sent_report_messages (#257) --- weni/internal/msgs/tasks.py | 78 ++++++++++++++----------------------- weni/internal/msgs/views.py | 36 ++++++++++------- 2 files changed, 52 insertions(+), 62 deletions(-) diff --git a/weni/internal/msgs/tasks.py b/weni/internal/msgs/tasks.py index 871b5eb6a..eaa5f6543 100644 --- a/weni/internal/msgs/tasks.py +++ b/weni/internal/msgs/tasks.py @@ -4,31 +4,30 @@ from celery import shared_task from django_redis import get_redis_connection +from django.template.loader import render_to_string from django.conf import settings from django.db import connection -from openpyxl import Workbook - -from io import BytesIO - from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email import encoders +from openpyxl import Workbook + +from io import BytesIO + + logger = logging.getLogger(__name__) @shared_task(name="generate_sent_report_messages") def generate_sent_report_messages(**kwargs): org_id = kwargs.get("org_id") - start_date = kwargs.get("start_date") - end_date = kwargs.get("end_date") - user = kwargs.get("user") - email_body = kwargs.get("email_body") - email_title = kwargs.get("email_title") - redis_client = get_redis_connection() + data = kwargs.get("data") + start_date = data["start_date"] + end_date = data["end_date"] query = f""" SELECT @@ -52,19 +51,14 @@ def generate_sent_report_messages(**kwargs): ORDER BY COUNT(msg.id) DESC; """ - filename = "Relatorio de Messagens.xlsx" + filename = "Mensagens Enviadas.xlsx" try: - data = fetch_query_results(query) - processed_data = process_query_results(data) - file = export_data_to_excel(processed_data) - send_report_file( - file_stream=file, - file_name=filename, - user=user, - title=email_title, - body=email_body, - ) + redis_client = get_redis_connection() + query_data = fetch_query_results(query) + processed_query_data = process_query_results(query_data) + file = export_data_to_excel(processed_query_data) + send_report_file(file_stream=file, file_name=filename, data=data) except Exception as e: logger.info(f"Fail to generate report: ORG {org_id}: {e}") finally: @@ -111,43 +105,28 @@ def export_data_to_excel(data): return file_stream -def export_query_to_excel(query): - with connection.cursor() as cursor: - cursor.execute(query) - - workbook = Workbook() - sheet = workbook.active - - header = [desc[0] for desc in cursor.description] - sheet.append(header) - - for row in cursor.fetchall(): - sheet.append(row) - - file_stream = BytesIO() - workbook.save(file_stream) - file_stream.seek(0) - - return file_stream - - -def send_report_file(file_stream, file_name, user, title, body): - email_subject = title - email_body = body +def send_report_file(file_stream, file_name, data): + email_subject = data["title"] + user_email = data["user_email"] email_host = settings.EMAIL_HOST email_port = settings.EMAIL_PORT email_username = settings.EMAIL_HOST_USER email_password = settings.EMAIL_HOST_PASSWORD email_use_tls = settings.EMAIL_USE_TLS + from_email = settings.DEFAULT_FROM_EMAIL + email_body = render_to_string( + "msgs/msg_mail_body.haml", + {"project_name": data["project_name"]}, + ) try: message = MIMEMultipart() message["Subject"] = email_subject - message["From"] = "Weni" - message["To"] = user + message["From"] = from_email + message["To"] = data["user_email"] - body = MIMEText(email_body, "plain", "utf-8") + body = MIMEText(email_body, "html", "utf-8") message.attach(body) attachment = MIMEBase( @@ -167,11 +146,12 @@ def send_report_file(file_stream, file_name, user, title, body): smtp_connection.starttls() smtp_connection.login(email_username, email_password) - result = smtp_connection.sendmail(email_username, user, message.as_string()) + result = smtp_connection.sendmail(from_email, user_email, message.as_string()) smtp_connection.quit() if result: for recipient, error_message in result.items(): logger.info(f"Fail send message to {recipient}, error: {error_message}") + except Exception as e: - logger.warning(f"Fail to send messages report error: {e}") + logger.exception(f"Fail to send messages report: {e}") diff --git a/weni/internal/msgs/views.py b/weni/internal/msgs/views.py index 27a918665..14f718e91 100644 --- a/weni/internal/msgs/views.py +++ b/weni/internal/msgs/views.py @@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404 from django_redis import get_redis_connection +from django.contrib.auth.models import User from rest_framework.response import Response from rest_framework import viewsets @@ -26,10 +27,21 @@ def list(self, request): project_uuid = request.query_params.get("project_uuid") start_date = request.query_params.get("start_date") end_date = request.query_params.get("end_date") - user = request.query_params.get("user") + user_email = request.query_params.get("user") org = get_object_or_404(Project, project_uuid=project_uuid) + # Check if email exists in flows + try: + User.objects.get(email=user_email) + except User.DoesNotExist: + return Response( + { + "detail": f"Error generating report. User: {user_email}, not found in flow" + }, + status=403, + ) + # Convert string to datatime object start_date_obj = datetime.strptime(start_date, "%m-%d-%Y") end_date_obj = datetime.strptime(end_date, "%m-%d-%Y") @@ -41,24 +53,22 @@ def list(self, request): email_start_date = start_date_obj.strftime("%d/%m/%Y") email_end_date = end_date_obj.strftime("%d/%m/%Y") - email_body = ( - f"Relátorio em Excel relacionado ao projeto {org.name.title()} " - f"entre {email_start_date} e {email_end_date}." - ) - - email_title = f"Relátorio de Mensagens Enviadas entre {email_start_date} e {email_end_date}" - lock_key = f"template-messages-lock:{org.id}" redis_client = get_redis_connection() is_locked = redis_client.get(lock_key) + data = { + "project_name": org.name.title(), + "user_email": user_email, + "title": f"Relatório de Mensagens Enviadas entre {email_start_date} e {email_end_date}", + "start_date": start_date_str, + "end_date": end_date_str, + } kwargs = dict( org_id=org.id, - start_date=start_date_str, - end_date=end_date_str, - user=user, - email_body=email_body, - email_title=email_title, + user=user_email, + data=data, ) + if is_locked: return Response(data={"detail": "Request already in process"}, status=409) From bb883cd34ee984ed752ebed6241b2bbd379d4853 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:45:26 -0300 Subject: [PATCH 095/101] Update version to 2.7.3 (#258) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844e1e14e..b954926ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.7.3] - 2023-06-29 +- Changes in the view and task generate_sent_report_messages + ## [2.7.2] - 2023-06-23 - Import generate_sent_report_messages From de7f4a46fa1f00d5a3b12484e296cc84c2f35e0d Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:40:16 -0300 Subject: [PATCH 096/101] fix: change date filter to created on (#260) --- weni/internal/msgs/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weni/internal/msgs/tasks.py b/weni/internal/msgs/tasks.py index eaa5f6543..574f6b477 100644 --- a/weni/internal/msgs/tasks.py +++ b/weni/internal/msgs/tasks.py @@ -43,7 +43,7 @@ def generate_sent_report_messages(**kwargs): INNER JOIN public.flows_flow AS flow ON flow.id = depent.flow_id WHERE - msg.sent_on BETWEEN '{start_date}' AND '{end_date}' + msg.created_on BETWEEN '{start_date}' AND '{end_date}' AND msg.metadata::jsonb -> 'templating' IS NOT NULL AND msg.org_id = {org_id} GROUP BY From 33fdc2f7f595e8b7cd98a3a5f151dbecccde0c97 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:46:18 -0300 Subject: [PATCH 097/101] Update version to 2.7.4 (#261) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b954926ae..ecc1dc64a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.7.4] - 2023-07-17 +- Report send msgs change date filter to created on + ## [2.7.3] - 2023-06-29 - Changes in the view and task generate_sent_report_messages From 74f140f383c77ffbda963a7894b7093ab690c858 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:10:35 -0300 Subject: [PATCH 098/101] fix: add filter by status in template report (#262) --- weni/internal/msgs/tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/weni/internal/msgs/tasks.py b/weni/internal/msgs/tasks.py index 574f6b477..cdb4a0bad 100644 --- a/weni/internal/msgs/tasks.py +++ b/weni/internal/msgs/tasks.py @@ -46,6 +46,7 @@ def generate_sent_report_messages(**kwargs): msg.created_on BETWEEN '{start_date}' AND '{end_date}' AND msg.metadata::jsonb -> 'templating' IS NOT NULL AND msg.org_id = {org_id} + AND msg.status IN ('S', 'D', 'V') GROUP BY template.name, flow.name ORDER BY From d1801b95a3de6356014a33c61c368ff101b6cff7 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Tue, 18 Jul 2023 18:00:40 -0300 Subject: [PATCH 099/101] Update version to 2.7.5 (#263) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc1dc64a..d4f98113b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.7.5] - 2023-07-18 +- Add filter by status in template report + ## [2.7.4] - 2023-07-17 - Report send msgs change date filter to created on From 55c57bb6de70dd9159c1c8f7821837411e38e7e8 Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:03:23 -0300 Subject: [PATCH 100/101] feat: add parameter to remove wpp-demo objects from the queryset list (#264) --- weni/internal/channel/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/weni/internal/channel/views.py b/weni/internal/channel/views.py index eec480d53..60ed2531e 100644 --- a/weni/internal/channel/views.py +++ b/weni/internal/channel/views.py @@ -31,13 +31,17 @@ class ChannelEndpoint(viewsets.ModelViewSet, InternalGenericViewSet): def get_queryset(self): channel_type = self.request.query_params.get("channel_type") org = self.request.query_params.get("org") + exclude_wpp_demo = self.request.query_params.get("exclude_wpp_demo") == "true" queryset = Channel.objects.all() if channel_type is not None: - return queryset.filter(channel_type=channel_type) + queryset = queryset.filter(channel_type=channel_type) if org is not None: - return queryset.filter(org__project__project_uuid=org) + queryset = queryset.filter(org__project__project_uuid=org) + + if exclude_wpp_demo: + queryset = queryset.exclude(address="+558231420933") return queryset From 9ca0dab7e4a58d4fb501f3b6a3e43280a93570cb Mon Sep 17 00:00:00 2001 From: Eliton <53982066+elitonzky@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:48:27 -0300 Subject: [PATCH 101/101] Update version to 2.7.6 (#265) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f98113b..979a00ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +## [2.7.6] - 2023-08-11 +- Add parameter to remove wpp-demo objects from the queryset list + ## [2.7.5] - 2023-07-18 - Add filter by status in template report