From 8a4f2b95d0283871be0c6adb90011a26d488ffef Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Wed, 29 May 2024 16:37:46 -0300 Subject: [PATCH 1/9] feature: creating dashboards, widgets and reports when a project its created --- .../dashboards/usecases/dashboard_creation.py | 253 ++++++++++++++++++ insights/dashboards/usecases/exceptions.py | 10 + insights/dashboards/utils.py | 0 insights/projects/usecases/create.py | 8 +- .../0002_remove_widget_report_report.py | 66 +++++ insights/widgets/models.py | 29 +- 6 files changed, 358 insertions(+), 8 deletions(-) create mode 100644 insights/dashboards/usecases/dashboard_creation.py create mode 100644 insights/dashboards/usecases/exceptions.py create mode 100644 insights/dashboards/utils.py create mode 100644 insights/widgets/migrations/0002_remove_widget_report_report.py diff --git a/insights/dashboards/usecases/dashboard_creation.py b/insights/dashboards/usecases/dashboard_creation.py new file mode 100644 index 0000000..660cb2b --- /dev/null +++ b/insights/dashboards/usecases/dashboard_creation.py @@ -0,0 +1,253 @@ +from insights.dashboards.models import Dashboard +from insights.widgets.models import Widget, Report +from django.db import transaction +from insights.dashboards.usecases.exceptions import ( + InvalidDashboardObject, + InvalidWidgetsObject, + InvalidReportsObject, +) + + +class create_atendimento_humano: + def create_dashboard(self, project): + try: + with transaction.atomic(): + atendimento_humano = Dashboard.objects.create( + project=project, + name="Atendimento Humano", + description="Dashboard de atendimento humano", + is_default=False, + ) + self.create_widgets(atendimento_humano) + + except Exception as exception: + raise InvalidDashboardObject(f"Error creating dashboard: {exception}") + + def create_widgets(self, dashboard_atendimento_humano): + try: + with transaction.atomic(): + pico_de_atendimento = Widget.objects.create( + name="Picos de atendimentos abertos", + w_type="graph_column", + source="chats", + config={ + "end_time": "18:00", + "interval": "60", + "start_time": "07:00", + }, + dashboard=dashboard_atendimento_humano, + position={"rows": [1, 1], "columns": [1, 12]}, + ) + em_andamento = Widget.objects.create( + name="Em andamento", + w_type="card", + source="chats", + config={"operation": "count", "type_result": "executions"}, + dash=dashboard_atendimento_humano, + position={"rows": [2, 2], "columns": [1, 4]}, + ) + Widget.objects.create( + name="Tempo de espera", + w_type="card", + source="chats", + config={"operation": "AVG", "type_result": "executions"}, + dash=dashboard_atendimento_humano, + position={"rows": [2, 2], "columns": [5, 8]}, + ) + encerrados = Widget.objects.create( + name="Encerrados", + w_type="card", + source="chats", + config={"operation": "AVG", "type_result": "executions"}, + dash=dashboard_atendimento_humano, + position={"rows": [2, 2], "columns": [9, 12]}, + ) + Widget.objects.create( + name="Tempo de resposta", + w_type="card", + source="chats", + config={"operation": "count", "type_result": "executions"}, + dash=dashboard_atendimento_humano, + position={"rows": [3, 3], "columns": [1, 4]}, + ) + aguardando_atendimento = Widget.objects.create( + name="Aguardando atendimento", + w_type="card", + source="chats", + config={"operation": "count", "type_result": "executions"}, + dash=dashboard_atendimento_humano, + position={"rows": [3, 3], "columns": [5, 8]}, + ) + Widget.objects.create( + name="Tempo de interação", + w_type="card", + source="chats", + config={"operation": "count", "type_result": "executions"}, + dash=dashboard_atendimento_humano, + position={"rows": [3, 3], "columns": [9, 12]}, + ) + Widget.objects.create( + name="Chats por agente", + w_type="table_dynamic_by_filter", + source="chats", + config={ + "default": { + "icon": "forum:weni-600", + "fields": [ + { + "name": "Agente", + "value": "agent", + "display": True, + "hidden_name": False, + }, + { + "name": "Em andamento", + "value": "open", + "display": True, + "hidden_name": False, + }, + { + "name": "Encerrados", + "value": "close", + "display": True, + "hidden_name": False, + }, + { + "name": "Status", + "value": "status", + "display": True, + "hidden_name": False, + }, + ], + "name_overwrite": "Agentes online", + } + }, + dash=dashboard_atendimento_humano, + position={"rows": [1, 3], "columns": [13, 18]}, + ) + + self.create_reports( + pico_de_atendimento, + em_andamento, + encerrados, + aguardando_atendimento, + ) + except Exception as exception: + raise InvalidWidgetsObject(f"Error creating widgets: {exception}") + + def create_reports( + self, pico_de_atendimento, em_andamento, encerrados, aguardando_atendimento + ): + try: + with transaction.atomic(): + Report.objects.create( + name="Pico de chats abertos por hora", + w_type="graph_column", + source="chats", + config={}, + widget=pico_de_atendimento, + ) + Report.objects.create( + name="Em andamento", + w_type="table_group", + source="chats", + config={}, + widget=em_andamento, + ) + Report.objects.create( + name="Encerrados", + w_type="table_group", + source="chats", + config={}, + widget=encerrados, + ) + Report.objects.create( + name="Aguardando atendimento", + w_type="table_group", + source="chats", + config={}, + widget=aguardando_atendimento, + ) + except Exception as exception: + raise InvalidReportsObject(f"Error creating dashboard: {exception}") + + +class create_resultado_de_fluxo: + def create_dashboard(self, project): + try: + with transaction.atomic(): + dashboard_resultado_de_fluxo = Dashboard.objects.create( + project=project, + name="Resultado de fluxo", + description="Dashboard de resultado de fluxo", + is_default=False, + ) + self.create_widgets(dashboard_resultado_de_fluxo) + + except Exception as exception: + raise InvalidDashboardObject(f"Error creating dashboard: {exception}") + + def create_widgets(self, dashboard_resultado_de_fluxo): + try: + with transaction.atomic(): + Widget.objects.create( + name="Métrica vazia", + w_type="card", + source="", + config={}, + dash=dashboard_resultado_de_fluxo, + position={"rows": [1, 1], "columns": [1, 4]}, + ) + Widget.objects.create( + name="Métrica vazia", + w_type="card", + source="", + config={}, + dash=dashboard_resultado_de_fluxo, + position={"rows": [2, 2], "columns": [1, 4]}, + ) + Widget.objects.create( + name="Métrica vazia", + w_type="card", + source="", + config={}, + dash=dashboard_resultado_de_fluxo, + position={"rows": [3, 3], "columns": [1, 4]}, + ) + Widget.objects.create( + name="Métrica vazia", + w_type="card", + source="", + config={}, + dash=dashboard_resultado_de_fluxo, + position={"rows": [1, 1], "columns": [5, 8]}, + ) + Widget.objects.create( + name="Métrica vazia", + w_type="card", + source="", + config={}, + dash=dashboard_resultado_de_fluxo, + position={"rows": [2, 2], "columns": [5, 8]}, + ) + Widget.objects.create( + name="Métrica vazia", + w_type="card", + source="", + config={}, + dash=dashboard_resultado_de_fluxo, + position={"rows": [3, 3], "columns": [5, 8]}, + ) + Widget.objects.create( + name="Métrica vazia", + w_type="graph_funnel", + source="", + config={}, + dash=dashboard_resultado_de_fluxo, + position={"rows": [1, 3], "columns": [9, 12]}, + ) + except Exception as exception: + raise InvalidWidgetsObject(f"Error creating widgets: {exception}") + + def create_reports(): + pass diff --git a/insights/dashboards/usecases/exceptions.py b/insights/dashboards/usecases/exceptions.py new file mode 100644 index 0000000..cd9ff5e --- /dev/null +++ b/insights/dashboards/usecases/exceptions.py @@ -0,0 +1,10 @@ +class InvalidDashboardObject(Exception): + pass + + +class InvalidWidgetsObject(Exception): + pass + + +class InvalidReportsObject(Exception): + pass diff --git a/insights/dashboards/utils.py b/insights/dashboards/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/insights/projects/usecases/create.py b/insights/projects/usecases/create.py index 8441160..8022424 100644 --- a/insights/projects/usecases/create.py +++ b/insights/projects/usecases/create.py @@ -2,6 +2,11 @@ from .project_dto import ProjectCreationDTO +from insights.dashboards.usecases.dashboard_creation import ( + create_atendimento_humano, + create_resultado_de_fluxo, +) + class ProjectsUseCase: @@ -23,5 +28,6 @@ def create_project(self, project_dto: ProjectCreationDTO) -> Project: timezone=project_dto.timezone, date_format=project_dto.date_format, ) - + create_atendimento_humano.create_dashboard(project) + create_resultado_de_fluxo.create_dashboard(project) return project diff --git a/insights/widgets/migrations/0002_remove_widget_report_report.py b/insights/widgets/migrations/0002_remove_widget_report_report.py new file mode 100644 index 0000000..55d7b54 --- /dev/null +++ b/insights/widgets/migrations/0002_remove_widget_report_report.py @@ -0,0 +1,66 @@ +# Generated by Django 5.0.4 on 2024-05-28 18:55 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("widgets", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="widget", + name="report", + ), + migrations.CreateModel( + name="Report", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + ( + "created_on", + models.DateTimeField(auto_now_add=True, verbose_name="Created on"), + ), + ( + "modified_on", + models.DateTimeField(auto_now=True, verbose_name="Modified on"), + ), + ( + "name", + models.CharField(default=None, max_length=255, verbose_name="Name"), + ), + ( + "w_type", + models.CharField( + default=None, max_length=50, verbose_name="Widget Type" + ), + ), + ( + "source", + models.CharField( + default=None, max_length=50, verbose_name="Data Source" + ), + ), + ("config", models.JSONField(verbose_name="Widget Configuration")), + ( + "widget", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="report", + to="widgets.widget", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/insights/widgets/models.py b/insights/widgets/models.py index c220f79..af1e0fb 100644 --- a/insights/widgets/models.py +++ b/insights/widgets/models.py @@ -3,10 +3,7 @@ from insights.shared.models import BaseModel, ConfigurableModel -class Widget(BaseModel, ConfigurableModel): - dashboard = models.ForeignKey( - "dashboards.Dashboard", related_name="widgets", on_delete=models.CASCADE - ) +class BaseWidget(BaseModel, ConfigurableModel): name = models.CharField( "Name", max_length=255, null=False, blank=False, default=None ) @@ -16,9 +13,27 @@ class Widget(BaseModel, ConfigurableModel): source = models.CharField( "Data Source", max_length=50, null=False, blank=False, default=None ) - position = models.JSONField("Widget position") + # config needs to be required in widget config = models.JSONField("Widget Configuration") - report = models.JSONField("Widget Report") + + class Meta: + abstract = True + + +class Widget(BaseWidget): + dashboard = models.ForeignKey( + "dashboards.Dashboard", related_name="widgets", on_delete=models.CASCADE + ) + position = models.JSONField("Widget position") + + def __str__(self): + return self.name + + +class Report(BaseWidget): + widget = models.OneToOneField( + Widget, related_name="report", on_delete=models.CASCADE + ) def __str__(self): - return self.description + return self.name From 29355e1a1e67a35b1688f946c695606389967e5b Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Mon, 3 Jun 2024 14:32:51 -0300 Subject: [PATCH 2/9] feature: endpoint list dashboard --- insights/authentication/permissions.py | 14 +++++++++++ .../migrations/0002_dashboard_grid.py | 18 ++++++++++++++ insights/dashboards/models.py | 1 + insights/dashboards/serializers.py | 9 +++++++ .../dashboards/usecases/dashboard_creation.py | 2 ++ insights/dashboards/utils.py | 7 ++++++ insights/dashboards/viewsets.py | 24 +++++++++++++++++++ insights/settings.py | 9 ++++++- insights/urls.py | 7 ++++++ insights/widgets/models.py | 7 ++++++ insights/widgets/serializers.py | 9 +++++++ insights/widgets/viewsets.py | 14 +++++++++++ 12 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 insights/authentication/permissions.py create mode 100644 insights/dashboards/migrations/0002_dashboard_grid.py create mode 100644 insights/dashboards/serializers.py create mode 100644 insights/dashboards/viewsets.py create mode 100644 insights/widgets/serializers.py create mode 100644 insights/widgets/viewsets.py diff --git a/insights/authentication/permissions.py b/insights/authentication/permissions.py new file mode 100644 index 0000000..c2e9b02 --- /dev/null +++ b/insights/authentication/permissions.py @@ -0,0 +1,14 @@ +from rest_framework import permissions +from rest_framework.exceptions import PermissionDenied + +from insights.projects.models import ProjectAuth + + +class ProjectAuthPermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + project = obj.dashboard.project_id + user = request.user + auth = ProjectAuth.objects.filter(project=project, user=user, role=1).first() + if not auth: + raise PermissionDenied("User does not have permission for this project") + return True diff --git a/insights/dashboards/migrations/0002_dashboard_grid.py b/insights/dashboards/migrations/0002_dashboard_grid.py new file mode 100644 index 0000000..0369281 --- /dev/null +++ b/insights/dashboards/migrations/0002_dashboard_grid.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-29 20:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dashboards", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="dashboard", + name="grid", + field=models.JSONField(default=list, verbose_name="Grid"), + ), + ] diff --git a/insights/dashboards/models.py b/insights/dashboards/models.py index 629d45d..e625ed9 100644 --- a/insights/dashboards/models.py +++ b/insights/dashboards/models.py @@ -33,6 +33,7 @@ class Dashboard(BaseModel, ConfigurableModel): null=True, blank=True, ) + grid = models.JSONField("Grid", default=list) def __str__(self): return f"{self.project.name} - {self.name}" diff --git a/insights/dashboards/serializers.py b/insights/dashboards/serializers.py new file mode 100644 index 0000000..08891de --- /dev/null +++ b/insights/dashboards/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from insights.dashboards.models import Dashboard + + +class DashboardSerializer(serializers.ModelSerializer): + class Meta: + model = Dashboard + fields = ["uuid", "name", "is_default", "grid"] diff --git a/insights/dashboards/usecases/dashboard_creation.py b/insights/dashboards/usecases/dashboard_creation.py index 660cb2b..de996c9 100644 --- a/insights/dashboards/usecases/dashboard_creation.py +++ b/insights/dashboards/usecases/dashboard_creation.py @@ -17,6 +17,7 @@ def create_dashboard(self, project): name="Atendimento Humano", description="Dashboard de atendimento humano", is_default=False, + grid=[18, 3], ) self.create_widgets(atendimento_humano) @@ -181,6 +182,7 @@ def create_dashboard(self, project): name="Resultado de fluxo", description="Dashboard de resultado de fluxo", is_default=False, + grid=[12, 3], ) self.create_widgets(dashboard_resultado_de_fluxo) diff --git a/insights/dashboards/utils.py b/insights/dashboards/utils.py index e69de29..fde11b2 100644 --- a/insights/dashboards/utils.py +++ b/insights/dashboards/utils.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination + + +class DefaultPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 100 diff --git a/insights/dashboards/viewsets.py b/insights/dashboards/viewsets.py new file mode 100644 index 0000000..5a62a7e --- /dev/null +++ b/insights/dashboards/viewsets.py @@ -0,0 +1,24 @@ +from rest_framework import mixins, viewsets + +from insights.authentication.permissions import ProjectAuthPermission + +from insights.dashboards.models import Dashboard +from .serializers import DashboardSerializer + +from insights.dashboards.utils import DefaultPagination + + +class DashboardViewSet( + mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet +): + permission_classes = [ProjectAuthPermission] + serializer_class = DashboardSerializer + pagination_class = DefaultPagination + + def get_queryset(self): + queryset = Dashboard.objects + project_id = self.request.query_params.get("project", None) + if project_id is not None: + queryset = queryset.filter(project_id=project_id) + return queryset + return {} diff --git a/insights/settings.py b/insights/settings.py index f8c23db..15bac2e 100644 --- a/insights/settings.py +++ b/insights/settings.py @@ -33,8 +33,9 @@ AUTH_USER_MODEL = "users.User" -ADMIN_ENABLED = env.bool("ADMIN_ENABLED", default=False) +ADMIN_ENABLED = env.bool("ADMIN_ENABLED", default=True) +INSIGHTS_DOMAIN = env.str(("INSIGHTS_DOMAIN")) # Application definition INSTALLED_APPS = [ @@ -50,6 +51,7 @@ "insights.sources", "insights.users", "insights.widgets", + "rest_framework", ] if ADMIN_ENABLED is True: @@ -91,6 +93,11 @@ DATABASES = {"default": env.db(var="DEFAULT_DATABASE", default="sqlite:///db.sqlite3")} +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, +} + # Password validation # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators diff --git a/insights/urls.py b/insights/urls.py index 3991411..84016ff 100644 --- a/insights/urls.py +++ b/insights/urls.py @@ -17,6 +17,8 @@ from django.conf import settings from django.urls import path +from insights.widgets.viewsets import WidgetListUpdateViewSet +from insights.dashboards.viewsets import DashboardViewSet urlpatterns = [] @@ -26,3 +28,8 @@ urlpatterns += [ path("admin/", admin.site.urls), ] + +urlpatterns += [ + path("widgets/", WidgetListUpdateViewSet.as_view({"get": "list", "put": "update"})), + path("dashboards/", DashboardViewSet.as_view({"get": "list", "put": "update"})), +] diff --git a/insights/widgets/models.py b/insights/widgets/models.py index af1e0fb..441c417 100644 --- a/insights/widgets/models.py +++ b/insights/widgets/models.py @@ -1,6 +1,7 @@ from django.db import models from insights.shared.models import BaseModel, ConfigurableModel +from settings import INSIGHTS_DOMAIN class BaseWidget(BaseModel, ConfigurableModel): @@ -29,6 +30,12 @@ class Widget(BaseWidget): def __str__(self): return self.name + @property + def url(self): + if self.config["external_url"]: + return self.config["external_url"] + return f"{INSIGHTS_DOMAIN}/dashboards/self.widget.dashboard/widgets/self.widget/report" + class Report(BaseWidget): widget = models.OneToOneField( diff --git a/insights/widgets/serializers.py b/insights/widgets/serializers.py new file mode 100644 index 0000000..823252f --- /dev/null +++ b/insights/widgets/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from .models import Widget + + +class WidgetSerializer(serializers.ModelSerializer): + class Meta: + model = Widget + fields = "__all__" diff --git a/insights/widgets/viewsets.py b/insights/widgets/viewsets.py new file mode 100644 index 0000000..e52076e --- /dev/null +++ b/insights/widgets/viewsets.py @@ -0,0 +1,14 @@ +from rest_framework import mixins, viewsets + +from insights.authentication.permissions import ProjectAuthPermission + +from .models import Widget +from .serializers import WidgetSerializer + + +class WidgetListUpdateViewSet( + mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet +): + permission_classes = [ProjectAuthPermission] + queryset = Widget.objects.all() + serializer_class = WidgetSerializer From 3295061b3e30cd0f5383b8627a0553233d01b269 Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Mon, 3 Jun 2024 17:20:03 -0300 Subject: [PATCH 3/9] feature: pin dashboard and list widgets in dashboard --- insights/authentication/permissions.py | 8 +++-- insights/dashboards/serializers.py | 29 ++++++++++++++++++ insights/dashboards/viewsets.py | 42 +++++++++++++++++++++++--- insights/urls.py | 12 +++++--- insights/widgets/models.py | 8 +---- 5 files changed, 81 insertions(+), 18 deletions(-) diff --git a/insights/authentication/permissions.py b/insights/authentication/permissions.py index c2e9b02..73d14a5 100644 --- a/insights/authentication/permissions.py +++ b/insights/authentication/permissions.py @@ -6,9 +6,13 @@ class ProjectAuthPermission(permissions.BasePermission): def has_object_permission(self, request, view, obj): - project = obj.dashboard.project_id + if hasattr(obj, "dashboard") and obj.dashboard: + project_id = obj.dashboard.project_id + else: + project_id = obj.project_id + user = request.user - auth = ProjectAuth.objects.filter(project=project, user=user, role=1).first() + auth = ProjectAuth.objects.filter(project=project_id, user=user, role=1).first() if not auth: raise PermissionDenied("User does not have permission for this project") return True diff --git a/insights/dashboards/serializers.py b/insights/dashboards/serializers.py index 08891de..ea9c059 100644 --- a/insights/dashboards/serializers.py +++ b/insights/dashboards/serializers.py @@ -1,9 +1,38 @@ from rest_framework import serializers from insights.dashboards.models import Dashboard +from insights.widgets.models import Widget, Report +from django.conf import settings class DashboardSerializer(serializers.ModelSerializer): class Meta: model = Dashboard fields = ["uuid", "name", "is_default", "grid"] + + +class DashboardIsDefaultSerializer(serializers.ModelSerializer): + class Meta: + model = Dashboard + fields = ["is_default"] + + +class ReportSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField() + + def get_url(self, obj): + if obj.config.get("external_url"): + return obj.config["external_url"] + return f"{settings.INSIGHTS_DOMAIN}/dashboards/{obj.widget.dashboard.uuid}/widgets/{obj.widget.uuid}/report" + + class Meta: + model = Report + fields = "__all__" + + +class DashboardWidgetsSerializer(serializers.ModelSerializer): + report = ReportSerializer() + + class Meta: + model = Widget + fields = "__all__" diff --git a/insights/dashboards/viewsets.py b/insights/dashboards/viewsets.py index 5a62a7e..d8597be 100644 --- a/insights/dashboards/viewsets.py +++ b/insights/dashboards/viewsets.py @@ -1,9 +1,18 @@ from rest_framework import mixins, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework import status from insights.authentication.permissions import ProjectAuthPermission from insights.dashboards.models import Dashboard -from .serializers import DashboardSerializer +from insights.widgets.models import Widget + +from .serializers import ( + DashboardSerializer, + DashboardIsDefaultSerializer, + DashboardWidgetsSerializer, +) from insights.dashboards.utils import DefaultPagination @@ -16,9 +25,32 @@ class DashboardViewSet( pagination_class = DefaultPagination def get_queryset(self): - queryset = Dashboard.objects project_id = self.request.query_params.get("project", None) if project_id is not None: - queryset = queryset.filter(project_id=project_id) - return queryset - return {} + return Dashboard.objects.filter(project_id=project_id) + return Dashboard.objects.none() + + @action(detail=True, methods=["patch"]) + def is_default(self, request, pk=None): + dashboard = self.get_object() + serializer = DashboardIsDefaultSerializer( + dashboard, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=["get"]) + def list_widgets(self, request, pk=None): + dashboard = self.get_object() + + widgets = Widget.objects.filter(dashboard=dashboard) + + paginator = DefaultPagination() + result_page = paginator.paginate_queryset(widgets, request) + + serializer = DashboardWidgetsSerializer(result_page, many=True) + + return paginator.get_paginated_response(serializer.data) diff --git a/insights/urls.py b/insights/urls.py index 84016ff..5feeef2 100644 --- a/insights/urls.py +++ b/insights/urls.py @@ -16,9 +16,10 @@ """ from django.conf import settings -from django.urls import path +from django.urls import path, include from insights.widgets.viewsets import WidgetListUpdateViewSet from insights.dashboards.viewsets import DashboardViewSet +from rest_framework.routers import DefaultRouter urlpatterns = [] @@ -29,7 +30,10 @@ path("admin/", admin.site.urls), ] -urlpatterns += [ - path("widgets/", WidgetListUpdateViewSet.as_view({"get": "list", "put": "update"})), - path("dashboards/", DashboardViewSet.as_view({"get": "list", "put": "update"})), +router = DefaultRouter() +router.register(r"widgets", WidgetListUpdateViewSet, basename="widget") +router.register(r"dashboards", DashboardViewSet, basename="dashboard") + +urlpatterns = [ + path("", include(router.urls)), ] diff --git a/insights/widgets/models.py b/insights/widgets/models.py index 441c417..708288f 100644 --- a/insights/widgets/models.py +++ b/insights/widgets/models.py @@ -1,7 +1,7 @@ from django.db import models from insights.shared.models import BaseModel, ConfigurableModel -from settings import INSIGHTS_DOMAIN +from django.conf import settings class BaseWidget(BaseModel, ConfigurableModel): @@ -30,12 +30,6 @@ class Widget(BaseWidget): def __str__(self): return self.name - @property - def url(self): - if self.config["external_url"]: - return self.config["external_url"] - return f"{INSIGHTS_DOMAIN}/dashboards/self.widget.dashboard/widgets/self.widget/report" - class Report(BaseWidget): widget = models.OneToOneField( From db2636d101dafb0791ccbe7c85236036b205a0b8 Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Tue, 4 Jun 2024 16:00:50 -0300 Subject: [PATCH 4/9] feature: pin dashboard --- insights/dashboards/serializers.py | 4 ++-- insights/dashboards/viewsets.py | 9 +++------ insights/urls.py | 7 ++++--- insights/widgets/models.py | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/insights/dashboards/serializers.py b/insights/dashboards/serializers.py index ea9c059..303ecf2 100644 --- a/insights/dashboards/serializers.py +++ b/insights/dashboards/serializers.py @@ -1,8 +1,8 @@ +from django.conf import settings from rest_framework import serializers from insights.dashboards.models import Dashboard -from insights.widgets.models import Widget, Report -from django.conf import settings +from insights.widgets.models import Report, Widget class DashboardSerializer(serializers.ModelSerializer): diff --git a/insights/dashboards/viewsets.py b/insights/dashboards/viewsets.py index d8597be..5558127 100644 --- a/insights/dashboards/viewsets.py +++ b/insights/dashboards/viewsets.py @@ -1,21 +1,18 @@ -from rest_framework import mixins, viewsets +from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework import status from insights.authentication.permissions import ProjectAuthPermission - from insights.dashboards.models import Dashboard +from insights.dashboards.utils import DefaultPagination from insights.widgets.models import Widget from .serializers import ( - DashboardSerializer, DashboardIsDefaultSerializer, + DashboardSerializer, DashboardWidgetsSerializer, ) -from insights.dashboards.utils import DefaultPagination - class DashboardViewSet( mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet diff --git a/insights/urls.py b/insights/urls.py index 5feeef2..d757b89 100644 --- a/insights/urls.py +++ b/insights/urls.py @@ -16,11 +16,12 @@ """ from django.conf import settings -from django.urls import path, include -from insights.widgets.viewsets import WidgetListUpdateViewSet -from insights.dashboards.viewsets import DashboardViewSet +from django.urls import include, path from rest_framework.routers import DefaultRouter +from insights.dashboards.viewsets import DashboardViewSet +from insights.widgets.viewsets import WidgetListUpdateViewSet + urlpatterns = [] if settings.ADMIN_ENABLED is True: diff --git a/insights/widgets/models.py b/insights/widgets/models.py index 708288f..8fe3c05 100644 --- a/insights/widgets/models.py +++ b/insights/widgets/models.py @@ -1,7 +1,7 @@ +from django.conf import settings from django.db import models from insights.shared.models import BaseModel, ConfigurableModel -from django.conf import settings class BaseWidget(BaseModel, ConfigurableModel): From 60a20a88bcec5fb82432f86ceb9b7692728f8462 Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Tue, 4 Jun 2024 18:00:50 -0300 Subject: [PATCH 5/9] feature: dashboard filters and get widget report --- insights/authentication/permissions.py | 2 +- insights/dashboards/serializers.py | 2 +- .../dashboards/usecases/dashboard_filters.py | 57 +++++++++++++++++++ insights/dashboards/viewsets.py | 29 +++++++++- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 insights/dashboards/usecases/dashboard_filters.py diff --git a/insights/authentication/permissions.py b/insights/authentication/permissions.py index 73d14a5..07cd171 100644 --- a/insights/authentication/permissions.py +++ b/insights/authentication/permissions.py @@ -11,7 +11,7 @@ def has_object_permission(self, request, view, obj): else: project_id = obj.project_id - user = request.user + user = "alan.dovale@weni.ai" auth = ProjectAuth.objects.filter(project=project_id, user=user, role=1).first() if not auth: raise PermissionDenied("User does not have permission for this project") diff --git a/insights/dashboards/serializers.py b/insights/dashboards/serializers.py index 303ecf2..f73cb63 100644 --- a/insights/dashboards/serializers.py +++ b/insights/dashboards/serializers.py @@ -23,7 +23,7 @@ class ReportSerializer(serializers.ModelSerializer): def get_url(self, obj): if obj.config.get("external_url"): return obj.config["external_url"] - return f"{settings.INSIGHTS_DOMAIN}/dashboards/{obj.widget.dashboard.uuid}/widgets/{obj.widget.uuid}/report" + return f"{settings.INSIGHTS_DOMAIN}/dashboards/{obj.widget.dashboard.uuid}/widgets/{obj.widget.uuid}/report/" class Meta: model = Report diff --git a/insights/dashboards/usecases/dashboard_filters.py b/insights/dashboards/usecases/dashboard_filters.py new file mode 100644 index 0000000..9445604 --- /dev/null +++ b/insights/dashboards/usecases/dashboard_filters.py @@ -0,0 +1,57 @@ +from insights.dashboards.models import Dashboard + + +def get_dash_filters(dash: Dashboard): + if dash.name == "Atendimento humano": + data = { + "tags": { + "type": "select", + "label": "Tags", + "source": "chats_tags", + "depends_on": {"filter": "sectors", "search_param": "sector"}, + "placeholder": "Selecione tags", + }, + "agents": { + "type": "select", + "label": "Agente", + "source": "chats_agents", + "depends_on": {"filter": "sectors", "search_param": None}, + "placeholder": "Selecione agente", + }, + "queues": { + "type": "select", + "label": "Fila", + "source": "chats_queues", + "depends_on": {"filter": "sectors", "search_param": "sector"}, + "placeholder": "Selecione fila", + }, + "contact": { + "type": "input_text", + "label": "Pesquisa por contato", + "placeholder": "Nome ou URN do contato", + }, + "sectors": { + "type": "select", + "label": "Setor", + "source": "chats_sectors", + "placeholder": "Selecione setor", + }, + "ended_at": { + "type": "date_range", + "label": "Data", + "end_sufix": "_before", + "placeholder": None, + "start_sufix": "_after", + }, + } + return data + else: + data = { + "ended_at": { + "type": "date_range", + "end_sufix": "_before", + "placeholder": None, + "start_sufix": "_after", + } + } + return data diff --git a/insights/dashboards/viewsets.py b/insights/dashboards/viewsets.py index 5558127..53f5ef1 100644 --- a/insights/dashboards/viewsets.py +++ b/insights/dashboards/viewsets.py @@ -5,13 +5,15 @@ from insights.authentication.permissions import ProjectAuthPermission from insights.dashboards.models import Dashboard from insights.dashboards.utils import DefaultPagination -from insights.widgets.models import Widget +from insights.widgets.models import Widget, Report from .serializers import ( DashboardIsDefaultSerializer, DashboardSerializer, DashboardWidgetsSerializer, + ReportSerializer, ) +from .usecases import dashboard_filters class DashboardViewSet( @@ -51,3 +53,28 @@ def list_widgets(self, request, pk=None): serializer = DashboardWidgetsSerializer(result_page, many=True) return paginator.get_paginated_response(serializer.data) + + @action(detail=True, methods=["get"]) + def filters(self, request, pk=None): + dashboard = self.get_object() + filters = dashboard_filters.get_dash_filters(dashboard) + + return Response(filters) + + @action( + detail=True, methods=["get"], url_path="widgets/(?P[^/.]+)/report" + ) + def get_widget_report(self, request, pk=None, widget_uuid=None): + try: + widget = Widget.objects.get(uuid=widget_uuid, dashboard_id=pk) + report = widget.report + serializer = ReportSerializer(report) + return Response(serializer.data, status=status.HTTP_200_OK) + except Widget.DoesNotExist: + return Response( + {"detail": "Widget not found."}, status=status.HTTP_404_NOT_FOUND + ) + except Report.DoesNotExist: + return Response( + {"detail": "Report not found."}, status=status.HTTP_404_NOT_FOUND + ) From cfcf8a74e3a66dacc0d01a31b813e418fd4cc9f4 Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Tue, 4 Jun 2024 18:01:37 -0300 Subject: [PATCH 6/9] feature: dashboard filters and get widget report --- insights/authentication/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/insights/authentication/permissions.py b/insights/authentication/permissions.py index 07cd171..73d14a5 100644 --- a/insights/authentication/permissions.py +++ b/insights/authentication/permissions.py @@ -11,7 +11,7 @@ def has_object_permission(self, request, view, obj): else: project_id = obj.project_id - user = "alan.dovale@weni.ai" + user = request.user auth = ProjectAuth.objects.filter(project=project_id, user=user, role=1).first() if not auth: raise PermissionDenied("User does not have permission for this project") From a9a03ae2553372c8aefc592b8d1bfb1e52623e89 Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Wed, 5 Jun 2024 16:16:21 -0300 Subject: [PATCH 7/9] feature: list sources from dashboard --- insights/dashboards/viewsets.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/insights/dashboards/viewsets.py b/insights/dashboards/viewsets.py index 53f5ef1..0c68f8b 100644 --- a/insights/dashboards/viewsets.py +++ b/insights/dashboards/viewsets.py @@ -78,3 +78,15 @@ def get_widget_report(self, request, pk=None, widget_uuid=None): return Response( {"detail": "Report not found."}, status=status.HTTP_404_NOT_FOUND ) + + @action(detail=True, methods=["get"]) + def list_sources(self, request, pk=None): + dashboard = self.get_object() + widgets = dashboard.widgets.all() + + sources = [{"source": widget.source} for widget in widgets] + + paginator = DefaultPagination() + paginated_sources = paginator.paginate_queryset(sources, request) + + return paginator.get_paginated_response(paginated_sources) From 1c6677bfe00c727ddf5987b4f0eee096a91690a3 Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Thu, 6 Jun 2024 14:38:10 -0300 Subject: [PATCH 8/9] formating report serializer --- insights/dashboards/serializers.py | 16 ++++++++++++++-- insights/dashboards/viewsets.py | 1 + insights/widgets/viewsets.py | 5 ++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/insights/dashboards/serializers.py b/insights/dashboards/serializers.py index f73cb63..55492db 100644 --- a/insights/dashboards/serializers.py +++ b/insights/dashboards/serializers.py @@ -17,21 +17,33 @@ class Meta: fields = ["is_default"] -class ReportSerializer(serializers.ModelSerializer): +class DashboardReportSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField() + type = serializers.SerializerMethodField() def get_url(self, obj): if obj.config.get("external_url"): return obj.config["external_url"] return f"{settings.INSIGHTS_DOMAIN}/dashboards/{obj.widget.dashboard.uuid}/widgets/{obj.widget.uuid}/report/" + def get_type(self, obj): + if obj.config.get("external_url"): + return "external" + return "internal" + + class Meta: + model = Report + fields = ["url", "type"] + + +class ReportSerializer(serializers.ModelSerializer): class Meta: model = Report fields = "__all__" class DashboardWidgetsSerializer(serializers.ModelSerializer): - report = ReportSerializer() + report = DashboardReportSerializer() class Meta: model = Widget diff --git a/insights/dashboards/viewsets.py b/insights/dashboards/viewsets.py index 0c68f8b..a51add6 100644 --- a/insights/dashboards/viewsets.py +++ b/insights/dashboards/viewsets.py @@ -11,6 +11,7 @@ DashboardIsDefaultSerializer, DashboardSerializer, DashboardWidgetsSerializer, + DashboardReportSerializer, ReportSerializer, ) from .usecases import dashboard_filters diff --git a/insights/widgets/viewsets.py b/insights/widgets/viewsets.py index e52076e..4026707 100644 --- a/insights/widgets/viewsets.py +++ b/insights/widgets/viewsets.py @@ -7,7 +7,10 @@ class WidgetListUpdateViewSet( - mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet + mixins.ListModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, + mixins.RetrieveModelMixin, ): permission_classes = [ProjectAuthPermission] queryset = Widget.objects.all() From e66ce8637293a5e2f96b1951251e2a3e9dec050f Mon Sep 17 00:00:00 2001 From: AlanJaeger Date: Thu, 6 Jun 2024 14:44:20 -0300 Subject: [PATCH 9/9] feature: changing base widget name type --- ...e_report_type_rename_w_type_widget_type.py | 23 +++++++++++++++++++ insights/widgets/models.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 insights/widgets/migrations/0003_rename_w_type_report_type_rename_w_type_widget_type.py diff --git a/insights/widgets/migrations/0003_rename_w_type_report_type_rename_w_type_widget_type.py b/insights/widgets/migrations/0003_rename_w_type_report_type_rename_w_type_widget_type.py new file mode 100644 index 0000000..9cbe6c8 --- /dev/null +++ b/insights/widgets/migrations/0003_rename_w_type_report_type_rename_w_type_widget_type.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.4 on 2024-06-06 17:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("widgets", "0002_remove_widget_report_report"), + ] + + operations = [ + migrations.RenameField( + model_name="report", + old_name="w_type", + new_name="type", + ), + migrations.RenameField( + model_name="widget", + old_name="w_type", + new_name="type", + ), + ] diff --git a/insights/widgets/models.py b/insights/widgets/models.py index 8fe3c05..c0f7072 100644 --- a/insights/widgets/models.py +++ b/insights/widgets/models.py @@ -8,7 +8,7 @@ class BaseWidget(BaseModel, ConfigurableModel): name = models.CharField( "Name", max_length=255, null=False, blank=False, default=None ) - w_type = models.CharField( + type = models.CharField( "Widget Type", max_length=50, null=False, blank=False, default=None ) source = models.CharField(