Browse Source

feat(dashboards): Add Dashboard Widget Endpoint. (#11652)

* Added widget endpoint.

* Added put method.

* organization dashboard widgets implemented with tests not passing.

* got tests working for widgets post method.

* Added docstrings and added a widget tests case.

* Updated put tests to pass.

* Fixed typo.

* Tested delete widget.

* Added 404 deletion test

* Moved widget serializers into its own file. Using new get_respond method in tests.

* Added get_response to dashboard widget test fixed typos in docstring.

* Update src/sentry/api/endpoints/organization_dashboard_widget_details.py

Co-Authored-By: lauryndbrown <lauryndbrown@gmail.com>

* Update src/sentry/api/serializers/rest_framework/widget.py

Co-Authored-By: lauryndbrown <lauryndbrown@gmail.com>

* Changed widget details test to dan's format. Added empty array to serializer tests

* Added several changes as suggested from reviews.

* removed typo in tests
Lauryn Brown 6 years ago
parent
commit
b4401e59a0

+ 31 - 1
src/sentry/api/bases/dashboard.py

@@ -1,10 +1,20 @@
 from __future__ import absolute_import
 
+from django.db.models import Max
+
 from sentry.api.bases.organization import (
     OrganizationEndpoint
 )
 from sentry.api.exceptions import ResourceDoesNotExist
-from sentry.models import Dashboard
+from sentry.models import Dashboard, Widget
+
+
+def get_next_dashboard_order(dashboard_id):
+    max_order = Widget.objects.filter(
+        dashboard_id=dashboard_id,
+    ).aggregate(Max('order'))['order__max']
+
+    return max_order + 1 if max_order else 1
 
 
 class OrganizationDashboardEndpoint(OrganizationEndpoint):
@@ -24,3 +34,23 @@ class OrganizationDashboardEndpoint(OrganizationEndpoint):
             id=dashboard_id,
             organization_id=organization.id
         )
+
+
+class OrganizationDashboardWidgetEndpoint(OrganizationDashboardEndpoint):
+    def convert_args(self, request, organization_slug, dashboard_id, widget_id, *args, **kwargs):
+        args, kwargs = super(OrganizationDashboardWidgetEndpoint,
+                             self).convert_args(request, organization_slug, dashboard_id, widget_id, *args, **kwargs)
+
+        try:
+            kwargs['widget'] = self._get_widget(
+                request, kwargs['organization'], dashboard_id, widget_id)
+        except Widget.DoesNotExist:
+            raise ResourceDoesNotExist
+
+        return (args, kwargs)
+
+    def _get_widget(self, request, organization, dashboard_id, widget_id):
+        return Widget.objects.get(
+            id=widget_id,
+            dashboard_id=dashboard_id,
+        )

+ 83 - 0
src/sentry/api/endpoints/organization_dashboard_widget_details.py

@@ -0,0 +1,83 @@
+from __future__ import absolute_import
+
+from django.db import transaction
+from rest_framework.response import Response
+
+from sentry.api.base import DocSection
+from sentry.api.bases.dashboard import (
+    OrganizationDashboardWidgetEndpoint
+)
+from sentry.api.serializers import serialize
+from sentry.api.serializers.rest_framework import WidgetSerializer
+from sentry.models import WidgetDataSource
+
+
+class OrganizationDashboardWidgetDetailsEndpoint(OrganizationDashboardWidgetEndpoint):
+
+    doc_section = DocSection.ORGANIZATIONS
+
+    def delete(self, request, organization, dashboard, widget):
+        """
+        Delete a Widget on an Organization's Dashboard
+        ``````````````````````````````````````````````
+
+        Delete a widget on an organization's dashboard.
+
+        :pparam string organization_slug: the slug of the organization the
+                                          dashboard belongs to.
+        :pparam int dashboard_id: the id of the dashboard.
+        :pparam int widget_id: the id of the widget.
+        :auth: required
+        """
+
+        widget.delete()
+        return self.respond(status=204)
+
+    def put(self, request, organization, dashboard, widget):
+        """
+        Edit a Widget on an Organization's Dashboard
+        ````````````````````````````````````````````
+
+        Edit a widget on an organization's dashboard.
+
+        :pparam string organization_slug: the slug of the organization the
+                                          dashboard belongs to.
+        :pparam int dashboard_id: the id of the dashboard.
+        :pparam int widget_id: the id of the widget.
+        :param string title: the title of the widget.
+        :param string displayType: the widget display type (i.e. line or table).
+        :param array displayOptions: the widget display options are special
+                                    variables necessary to displaying the widget correctly.
+        :param array dataSources: the sources of data for the widget to display.
+                                If supplied the entire set of data sources will be deleted
+                                and replaced with the input provided.
+        :auth: required
+        """
+        # TODO(lb): better document displayType, displayOptions, and dataSources.
+        serializer = WidgetSerializer(data=request.DATA, context={'organization': organization})
+        if not serializer.is_valid():
+            return Response(serializer.errors, status=400)
+
+        data = serializer.object
+
+        with transaction.atomic():
+            widget.update(
+                title=data.get('title', widget.title),
+                display_type=data.get('displayType', widget.display_type),
+                display_options=data.get('displayOptions', widget.display_options)
+            )
+
+            if 'dataSources' in data:
+                WidgetDataSource.objects.filter(
+                    widget_id=widget.id
+                ).delete()
+            for widget_data in data.get('dataSources', []):
+                WidgetDataSource.objects.create(
+                    name=widget_data['name'],
+                    data=widget_data['data'],
+                    type=widget_data['type'],
+                    order=widget_data['order'],
+                    widget_id=widget.id,
+                )
+
+        return Response(serialize(widget, request.user))

+ 62 - 0
src/sentry/api/endpoints/organization_dashboard_widgets.py

@@ -0,0 +1,62 @@
+from __future__ import absolute_import
+
+from django.db import IntegrityError, transaction
+from rest_framework.response import Response
+
+from sentry.api.base import DocSection
+from sentry.api.bases.dashboard import (
+    OrganizationDashboardEndpoint, get_next_dashboard_order
+)
+from sentry.api.serializers import serialize
+from sentry.api.serializers.rest_framework import WidgetSerializer
+from sentry.models import Widget, WidgetDataSource
+
+
+class OrganizationDashboardWidgetsEndpoint(OrganizationDashboardEndpoint):
+
+    doc_section = DocSection.ORGANIZATIONS
+
+    def post(self, request, organization, dashboard):
+        """
+        Create a New Widget for an Organization's Dashboard
+        ```````````````````````````````````````````````````
+        Create a new widget on the dashboard for the given Organization
+        :pparam string organization_slug: the slug of the organization the
+                                          dashboards belongs to.
+        :pparam int dashboard_id: the id of the dashboard.
+        :param string title: the title of the widget.
+        :param string displayType: the widget display type (i.e. line or table).
+        :param array displayOptions: the widget display options are special
+                                    variables necessary to displaying the widget correctly.
+        :param array dataSources: the sources of data for the widget to display.
+        :auth: required
+        """
+
+        serializer = WidgetSerializer(data=request.DATA, context={'organization': organization})
+
+        if not serializer.is_valid():
+            return Response(serializer.errors, status=400)
+
+        result = serializer.object
+
+        try:
+            with transaction.atomic():
+                widget = Widget.objects.create(
+                    display_type=result['displayType'],
+                    display_options=result.get('displayOptions', {}),
+                    title=result['title'],
+                    order=get_next_dashboard_order(dashboard.id),
+                    dashboard_id=dashboard.id,
+                )
+                for widget_data in result.get('dataSources', []):
+                    WidgetDataSource.objects.create(
+                        name=widget_data['name'],
+                        data=widget_data['data'],
+                        type=widget_data['type'],
+                        order=widget_data['order'],
+                        widget_id=widget.id,
+                    )
+        except IntegrityError:
+            return Response('This widget already exists', status=409)
+
+        return Response(serialize(widget, request.user), status=201)

+ 50 - 0
src/sentry/api/serializers/rest_framework/widget.py

@@ -0,0 +1,50 @@
+from __future__ import absolute_import
+
+from rest_framework import serializers
+
+from sentry.api.bases.discoversavedquery import DiscoverSavedQuerySerializer
+from sentry.api.serializers.rest_framework import JSONField, ListField, ValidationError
+from sentry.models import WidgetDisplayTypes, WidgetDataSourceTypes
+
+
+class WidgetDataSourceSerializer(serializers.Serializer):
+    name = serializers.CharField(required=True)
+    data = JSONField(required=True)
+    type = serializers.CharField(required=True)
+    order = serializers.IntegerField(required=True)
+
+    def validate_type(self, attrs, source):
+        type = attrs[source]
+        if type not in WidgetDataSourceTypes.TYPE_NAMES:
+            raise ValidationError('Widget data source type %s not recognized.' % type)
+        attrs[source] = WidgetDataSourceTypes.get_id_for_type_name(type)
+        return attrs
+
+    def validate(self, data):
+        super(WidgetDataSourceSerializer, self).validate(data)
+        if data['type'] == WidgetDataSourceTypes.DISCOVER_SAVED_SEARCH:
+            serializer = DiscoverSavedQuerySerializer(data=data['data'], context=self.context)
+            if not serializer.is_valid():
+                raise ValidationError('Error validating DiscoverSavedQuery: %s' % serializer.errors)
+        else:
+            raise ValidationError('Widget data source type %s not recognized.' % data['type'])
+        return data
+
+
+class WidgetSerializer(serializers.Serializer):
+    displayType = serializers.CharField(required=True)
+    displayOptions = JSONField(required=False)
+    title = serializers.CharField(required=True)
+    dataSources = ListField(
+        child=WidgetDataSourceSerializer(required=False),
+        required=False,
+        allow_null=True,
+    )
+
+    def validate_displayType(self, attrs, source):
+        display_type = attrs[source]
+        if display_type not in WidgetDisplayTypes.TYPE_NAMES:
+            raise ValidationError('Widget displayType %s not recognized.' % display_type)
+
+        attrs[source] = WidgetDisplayTypes.get_id_for_type_name(display_type)
+        return attrs

+ 12 - 0
src/sentry/api/urls.py

@@ -67,6 +67,8 @@ from .endpoints.organization_discover_saved_query_detail import OrganizationDisc
 from .endpoints.organization_events import OrganizationEventsEndpoint, OrganizationEventsMetaEndpoint, OrganizationEventsStatsEndpoint
 from .endpoints.organization_group_index import OrganizationGroupIndexEndpoint
 from .endpoints.organization_dashboard_details import OrganizationDashboardDetailsEndpoint
+from .endpoints.organization_dashboard_widget_details import OrganizationDashboardWidgetDetailsEndpoint
+from .endpoints.organization_dashboard_widgets import OrganizationDashboardWidgetsEndpoint
 from .endpoints.organization_health import OrganizationHealthTopEndpoint, OrganizationHealthGraphEndpoint
 from .endpoints.organization_shortid import ShortIdLookupEndpoint
 from .endpoints.organization_environments import OrganizationEnvironmentsEndpoint
@@ -432,6 +434,16 @@ urlpatterns = patterns(
         OrganizationDashboardsEndpoint.as_view(),
         name='sentry-api-0-organization-dashboards'
     ),
+    url(
+        r'^organizations/(?P<organization_slug>[^\/]+)/dashboards/(?P<dashboard_id>[^\/]+)/widgets/$',
+        OrganizationDashboardWidgetsEndpoint.as_view(),
+        name='sentry-api-0-organization-dashboard-widgets',
+    ),
+    url(
+        r'^organizations/(?P<organization_slug>[^\/]+)/dashboards/(?P<dashboard_id>[^\/]+)/widgets/(?P<widget_id>[^\/]+)$',
+        OrganizationDashboardWidgetDetailsEndpoint.as_view(),
+        name='sentry-api-0-organization-dashboard-widget-details',
+    ),
     url(
         r'^organizations/(?P<organization_slug>[^\/]+)/health/top/$',
         OrganizationHealthTopEndpoint.as_view(),

+ 6 - 0
src/sentry/models/widget.py

@@ -21,6 +21,12 @@ class TypesClass(object):
             if id == num:
                 return name
 
+    @classmethod
+    def get_id_for_type_name(cls, type_name):
+        for id, name in cls.TYPES:
+            if type_name == name:
+                return id
+
 
 class WidgetDisplayTypes(TypesClass):
     LINE_CHART = 0

+ 83 - 1
src/sentry/testutils/cases.py

@@ -12,7 +12,7 @@ __all__ = (
     'TestCase', 'TransactionTestCase', 'APITestCase', 'TwoFactorAPITestCase', 'AuthProviderTestCase', 'RuleTestCase',
     'PermissionTestCase', 'PluginTestCase', 'CliTestCase', 'AcceptanceTestCase',
     'IntegrationTestCase', 'UserReportEnvironmentTestCase', 'SnubaTestCase', 'IntegrationRepositoryTestCase',
-    'ReleaseCommitPatchTest', 'SetRefsTestCase'
+    'ReleaseCommitPatchTest', 'SetRefsTestCase', 'OrganizationDashboardWidgetTestCase'
 )
 
 import base64
@@ -57,6 +57,7 @@ from sentry.constants import MODULE_ROOT
 from sentry.models import (
     GroupEnvironment, GroupHash, GroupMeta, ProjectOption, Repository, DeletedOrganization,
     Environment, GroupStatus, Organization, TotpInterface, UserReport,
+    Dashboard, ObjectStatus, WidgetDataSource, WidgetDataSourceTypes
 )
 from sentry.plugins import plugins
 from sentry.rules import EventState
@@ -1013,3 +1014,84 @@ class SetRefsTestCase(APITestCase):
         assert self.org.id == commit.organization_id
         assert self.repo.id == commit.repository_id
         assert commit.key == key
+
+
+class OrganizationDashboardWidgetTestCase(APITestCase):
+    def setUp(self):
+        super(OrganizationDashboardWidgetTestCase, self).setUp()
+        self.login_as(self.user)
+        self.dashboard = Dashboard.objects.create(
+            title='Dashboard 1',
+            created_by=self.user,
+            organization=self.organization,
+        )
+        self.anon_users_query = {
+            'name': 'anonymousUsersAffectedQuery',
+            'fields': [],
+            'conditions': [['user.email', 'IS NULL', None]],
+            'aggregations': [['count()', None, 'Anonymous Users']],
+            'limit': 1000,
+            'orderby': '-time',
+            'groupby': ['time'],
+            'rollup': 86400,
+        }
+        self.known_users_query = {
+            'name': 'knownUsersAffectedQuery',
+            'fields': [],
+            'conditions': [['user.email', 'IS NOT NULL', None]],
+            'aggregations': [['uniq', 'user.email', 'Known Users']],
+            'limit': 1000,
+            'orderby': '-time',
+            'groupby': ['time'],
+            'rollup': 86400,
+        }
+        self.geo_erorrs_query = {
+            'name': 'errorsByGeo',
+            'fields': ['geo.country_code'],
+            'conditions': [['geo.country_code', 'IS NOT NULL', None]],
+            'aggregations': [['count()', None, 'count']],
+            'limit': 10,
+            'orderby': '-count',
+            'groupby': ['geo.country_code'],
+        }
+
+    def assert_widget_data_sources(self, widget_id, data):
+        result_data_sources = sorted(
+            WidgetDataSource.objects.filter(
+                widget_id=widget_id,
+                status=ObjectStatus.VISIBLE
+            ),
+            key=lambda x: x.order
+        )
+        data.sort(key=lambda x: x['order'])
+        for ds, expected_ds in zip(result_data_sources, data):
+            assert ds.name == expected_ds['name']
+            assert ds.type == WidgetDataSourceTypes.get_id_for_type_name(expected_ds['type'])
+            assert ds.order == expected_ds['order']
+            assert ds.data == expected_ds['data']
+
+    def assert_widget(self, widget, order, title, display_type,
+                      display_options=None, data_sources=None):
+        assert widget.order == order
+        assert widget.display_type == display_type
+        if display_options:
+            assert widget.display_options == display_options
+        assert widget.title == title
+
+        if not data_sources:
+            return
+
+        self.assert_widget_data_sources(widget.id, data_sources)
+
+    def assert_widget_data(self, data, order, title, display_type,
+                           display_options=None, data_sources=None):
+        assert data['order'] == order
+        assert data['displayType'] == display_type
+        if display_options:
+            assert data['displayOptions'] == display_options
+        assert data['title'] == title
+
+        if not data_sources:
+            return
+
+        self.assert_widget_data_sources(data['id'], data_sources)

+ 284 - 0
tests/sentry/api/endpoints/test_organization_dashboard_widget_details.py

@@ -0,0 +1,284 @@
+from __future__ import absolute_import
+
+from sentry.models import Dashboard, Widget, WidgetDataSource, WidgetDataSourceTypes, WidgetDisplayTypes
+from sentry.testutils import OrganizationDashboardWidgetTestCase
+
+
+class OrganizationDashboardWidgetDetailsTestCase(OrganizationDashboardWidgetTestCase):
+    endpoint = 'sentry-api-0-organization-dashboard-widget-details'
+
+    def setUp(self):
+        super(OrganizationDashboardWidgetDetailsTestCase, self).setUp()
+        self.widget = Widget.objects.create(
+            dashboard_id=self.dashboard.id,
+            order=1,
+            title='Widget 1',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+            display_options={},
+        )
+
+    def tearDown(self):
+        super(OrganizationDashboardWidgetDetailsTestCase, self).tearDown()
+        Widget.objects.all().delete()
+        WidgetDataSource.objects.all().delete()
+
+
+class OrganizationDashboardWidgetDetailsPutTestCase(OrganizationDashboardWidgetDetailsTestCase):
+    method = 'put'
+
+    def test_simple(self):
+        data_sources = [
+            {
+                'name': 'knownUsersAffectedQuery_2',
+                'data': self.known_users_query,
+                'type': 'discover_saved_search',
+                'order': 1,
+            },
+            {
+                'name': 'anonymousUsersAffectedQuery_2',
+                'data': self.anon_users_query,
+                'type': 'discover_saved_search',
+                'order': 2
+            },
+        ]
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            self.widget.id,
+            displayType='line',
+            title='User Happiness',
+            dataSources=data_sources,
+        )
+
+        assert response.status_code == 200
+
+        self.assert_widget_data(
+            response.data,
+            order='1',
+            title='User Happiness',
+            display_type='line',
+            data_sources=data_sources,
+        )
+
+        widgets = Widget.objects.filter(
+            dashboard_id=self.dashboard.id
+        )
+        assert len(widgets) == 1
+
+        self.assert_widget(
+            widgets[0],
+            order=1,
+            title='User Happiness',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+            data_sources=data_sources,
+        )
+
+    def test_widget_no_data_souces(self):
+        WidgetDataSource.objects.create(
+            name='knownUsersAffectedQuery_2',
+            data=self.known_users_query,
+            type=WidgetDataSourceTypes.DISCOVER_SAVED_SEARCH,
+            order=1,
+            widget_id=self.widget.id,
+        )
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            self.widget.id,
+            displayType='line',
+            title='User Happiness',
+            dataSources=[],
+        )
+        assert response.status_code == 200
+        self.assert_widget_data(
+            response.data,
+            order='1',
+            title='User Happiness',
+            display_type='line',
+        )
+
+        widgets = Widget.objects.filter(
+            dashboard_id=self.dashboard.id
+        )
+        assert len(widgets) == 1
+
+        self.assert_widget(
+            widgets[0],
+            order=1,
+            title='User Happiness',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+        )
+        assert not WidgetDataSource.objects.filter(
+            widget_id=widgets[0],
+        ).exists()
+
+    def test_unrecognized_display_type(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            self.widget.id,
+            displayType='happy-face',
+            title='User Happiness',
+        )
+        assert response.status_code == 400
+        assert response.data == {'displayType': [u'Widget displayType happy-face not recognized.']}
+
+    def test_unrecognized_data_source_type(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            self.widget.id,
+            title='User Happiness',
+            displayType='line',
+            dataSources=[{
+                'name': 'knownUsersAffectedQuery_3',
+                'data': self.known_users_query,
+                'type': 'not-real-type',
+                'order': 1,
+            }],
+        )
+        assert response.status_code == 400
+        assert response.data == {'dataSources': [
+            u'type: Widget data source type not-real-type not recognized.']}
+
+    def test_does_not_exists(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            1234567890,
+            displayType='line',
+            title='User Happiness',
+        )
+        assert response.status_code == 404
+
+    def test_widget_does_not_belong_to_dashboard(self):
+        dashboard = Dashboard.objects.create(
+            title='Dashboard 2',
+            created_by=self.user,
+            organization=self.organization,
+        )
+        widget = Widget.objects.create(
+            dashboard_id=dashboard.id,
+            order=1,
+            title='Widget 2',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+        )
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            widget.id,
+            displayType='line',
+            title='Happy Widget 2',
+        )
+        assert response.status_code == 404
+
+    def test_widget_does_not_belong_to_organization(self):
+        dashboard = Dashboard.objects.create(
+            title='Dashboard 2',
+            created_by=self.user,
+            organization=self.create_organization(),
+        )
+        widget = Widget.objects.create(
+            dashboard_id=dashboard.id,
+            order=1,
+            title='Widget 2',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+        )
+        response = self.get_response(
+            self.organization.slug,
+            dashboard.id,
+            widget.id,
+            displayType='line',
+            title='Happy Widget 2',
+        )
+        assert response.status_code == 404
+
+
+class OrganizationDashboardWidgetsDeleteTestCase(OrganizationDashboardWidgetDetailsTestCase):
+    method = 'delete'
+
+    def assert_deleted_widget(self, widget_id):
+        assert not Widget.objects.filter(id=widget_id).exists()
+        assert not WidgetDataSource.objects.filter(widget_id=widget_id).exists()
+
+    def test_simple(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            self.widget.id,
+        )
+        assert response.status_code == 204
+        self.assert_deleted_widget(self.widget.id)
+
+    def test_with_data_sources(self):
+        WidgetDataSource.objects.create(
+            widget_id=self.widget.id,
+            name='Data source 1',
+            data=self.known_users_query,
+            type=WidgetDataSourceTypes.DISCOVER_SAVED_SEARCH,
+            order=1,
+        )
+        WidgetDataSource.objects.create(
+            widget_id=self.widget.id,
+            name='Data source 2',
+            data=self.known_users_query,
+            type=WidgetDataSourceTypes.DISCOVER_SAVED_SEARCH,
+            order=2,
+        )
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            self.widget.id,
+        )
+        assert response.status_code == 204
+        self.assert_deleted_widget(self.widget.id)
+
+    def test_does_not_exists(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            1234567890,
+        )
+        assert response.status_code == 404
+
+    def test_widget_does_not_belong_to_dashboard(self):
+        dashboard = Dashboard.objects.create(
+            title='Dashboard 2',
+            created_by=self.user,
+            organization=self.organization,
+        )
+        widget = Widget.objects.create(
+            dashboard_id=dashboard.id,
+            order=1,
+            title='Widget 2',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+        )
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            widget.id,
+            displayType='line',
+            title='Happy Widget 2',
+        )
+        assert response.status_code == 404
+
+    def test_widget_does_not_belong_to_organization(self):
+        dashboard = Dashboard.objects.create(
+            title='Dashboard 2',
+            created_by=self.user,
+            organization=self.create_organization(),
+        )
+        widget = Widget.objects.create(
+            dashboard_id=dashboard.id,
+            order=1,
+            title='Widget 2',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+        )
+        response = self.get_response(
+            self.organization.slug,
+            dashboard.id,
+            widget.id,
+            displayType='line',
+            title='Happy Widget 2',
+        )
+        assert response.status_code == 404

+ 154 - 0
tests/sentry/api/endpoints/test_organization_dashboard_widgets.py

@@ -0,0 +1,154 @@
+from __future__ import absolute_import
+
+
+from sentry.models import Widget, WidgetDataSource, WidgetDisplayTypes
+from sentry.testutils import OrganizationDashboardWidgetTestCase
+
+
+class OrganizationDashboardWidgetsPostTestCase(OrganizationDashboardWidgetTestCase):
+    endpoint = 'sentry-api-0-organization-dashboard-widgets'
+    method = 'post'
+
+    def test_simple(self):
+        data_sources = [
+            {
+                'name': 'knownUsersAffectedQuery_2',
+                'data': self.known_users_query,
+                'type': 'discover_saved_search',
+                'order': 1,
+            },
+            {
+                'name': 'anonymousUsersAffectedQuery_2',
+                'data': self.anon_users_query,
+                'type': 'discover_saved_search',
+                'order': 2
+            },
+        ]
+
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            displayType='line',
+            title='User Happiness',
+            dataSources=data_sources,
+        )
+
+        assert response.status_code == 201
+
+        self.assert_widget_data(
+            response.data,
+            order='1',
+            title='User Happiness',
+            display_type='line',
+            data_sources=data_sources,
+        )
+
+        widgets = Widget.objects.filter(
+            dashboard_id=self.dashboard.id
+        )
+        assert len(widgets) == 1
+
+        self.assert_widget(
+            widgets[0],
+            order=1,
+            title='User Happiness',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+            data_sources=data_sources,
+        )
+
+    def test_widget_no_data_souces(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            displayType='line',
+            title='User Happiness',
+            dataSources=[],
+        )
+        assert response.status_code == 201
+        self.assert_widget_data(
+            response.data,
+            order='1',
+            title='User Happiness',
+            display_type='line',
+        )
+
+        widgets = Widget.objects.filter(
+            dashboard_id=self.dashboard.id
+        )
+        assert len(widgets) == 1
+
+        self.assert_widget(
+            widgets[0],
+            order=1,
+            title='User Happiness',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+        )
+        assert not WidgetDataSource.objects.filter(
+            widget_id=widgets[0],
+        ).exists()
+
+    def test_new_widgets_added_to_end_of_dashboard_order(self):
+        widget_1 = Widget.objects.create(
+            order=1,
+            title='Like a room without a roof',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+            dashboard_id=self.dashboard.id,
+        )
+        widget_2 = Widget.objects.create(
+            order=2,
+            title='Hello World',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+            dashboard_id=self.dashboard.id,
+        )
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            displayType='line',
+            title='User Happiness',
+        )
+        assert response.status_code == 201
+        self.assert_widget_data(
+            response.data,
+            order='3',
+            title='User Happiness',
+            display_type='line',
+        )
+        widgets = Widget.objects.filter(
+            dashboard_id=self.dashboard.id
+        )
+        assert len(widgets) == 3
+
+        self.assert_widget(
+            widgets.exclude(id__in=[widget_1.id, widget_2.id])[0],
+            order=3,
+            title='User Happiness',
+            display_type=WidgetDisplayTypes.LINE_CHART,
+            data_sources=None,
+        )
+
+    def test_unrecognized_display_type(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            displayType='happy-face',
+            title='User Happiness',
+        )
+        assert response.status_code == 400
+        assert response.data == {'displayType': [u'Widget displayType happy-face not recognized.']}
+
+    def test_unrecognized_data_source_type(self):
+        response = self.get_response(
+            self.organization.slug,
+            self.dashboard.id,
+            displayType='line',
+            title='User Happiness',
+            dataSources=[{
+                'name': 'knownUsersAffectedQuery_2',
+                'data': self.known_users_query,
+                'type': 'not-real-type',
+                'order': 1,
+            }],
+        )
+        assert response.status_code == 400
+        assert response.data == {'dataSources': [
+            u'type: Widget data source type not-real-type not recognized.']}