From a0a0e5b0854dfc43a5fbbe73f0a80fe8cd773e49 Mon Sep 17 00:00:00 2001 From: esensar Date: Mon, 22 Oct 2018 23:54:31 +0200 Subject: [PATCH] Add dashboard widgets --- app/api/blueprint.py | 9 +- app/api/resources/dashboard.py | 117 +++++++++++++++++ .../swagger/create_dashboard_widget_spec.yaml | 20 +++ .../swagger/delete_dashboard_spec.yaml | 7 +- .../swagger/delete_dashboard_widget_spec.yaml | 13 ++ .../swagger/get_dashboard_widget_spec.yaml | 26 ++++ .../swagger/get_dashboard_widgets_spec.yaml | 22 ++++ .../swagger/update_dashboard_spec.yaml | 6 +- .../swagger/update_dashboard_widget_spec.yaml | 24 ++++ app/dashboards/api.py | 79 +++++++++++- app/dashboards/models.py | 118 ++++++++++++++++++ app/swagger/template.yaml | 74 +++++++++++ migrations/versions/b5eb4a04c77e_.py | 43 +++++++ 13 files changed, 550 insertions(+), 8 deletions(-) create mode 100644 app/api/resources/swagger/create_dashboard_widget_spec.yaml create mode 100644 app/api/resources/swagger/delete_dashboard_widget_spec.yaml create mode 100644 app/api/resources/swagger/get_dashboard_widget_spec.yaml create mode 100644 app/api/resources/swagger/get_dashboard_widgets_spec.yaml create mode 100644 app/api/resources/swagger/update_dashboard_widget_spec.yaml create mode 100644 migrations/versions/b5eb4a04c77e_.py diff --git a/app/api/blueprint.py b/app/api/blueprint.py index c429367..3038b47 100644 --- a/app/api/blueprint.py +++ b/app/api/blueprint.py @@ -23,7 +23,9 @@ def add_resources(): DeviceTypeListResource, DeviceConfigurationResource) from .resources.dashboard import (DashboardResource, - DashboardListResource) + DashboardListResource, + DashboardWidgetResource, + DashboardWidgetListResource) api.add_resource(AccountResource, '/v1/accounts/') api.add_resource(AccountListResource, '/v1/accounts') @@ -48,6 +50,11 @@ def add_resources(): api.add_resource(DashboardResource, '/v1/dashboards/') api.add_resource(DashboardListResource, '/v1/dashboards') + api.add_resource( + DashboardWidgetResource, + '/v1/dashboards//widgets/') + api.add_resource(DashboardWidgetListResource, + '/v1/dashboards//widgets') add_resources() diff --git a/app/api/resources/dashboard.py b/app/api/resources/dashboard.py index 7672bde..34eb5d9 100644 --- a/app/api/resources/dashboard.py +++ b/app/api/resources/dashboard.py @@ -4,6 +4,7 @@ from marshmallow import fields from webargs.flaskparser import use_args from flasgger import swag_from import app.dashboards.api as dashboard +import app.devices.api as device from app.api.auth_protection import ProtectedResource from app.api.schemas import BaseResourceSchema @@ -15,6 +16,17 @@ class DashboardSchema(BaseResourceSchema): name = fields.String() +class DashboardWidgetSchema(BaseResourceSchema): + id = fields.Integer(dump_only=True) + device_id = fields.Integer() + height = fields.Integer() + width = fields.Integer() + x = fields.Integer() + y = fields.Integer() + chart_type = fields.String() + filters = fields.Raw() + + class DashboardResource(ProtectedResource): @swag_from('swagger/get_dashboard_spec.yaml') def get(self, dashboard_id): @@ -84,3 +96,108 @@ class DashboardListResource(ProtectedResource): dashboard.get_dashboards( g.current_account.id, request_args.get('active')), many=True), 200 + + +class DashboardWidgetListResource(ProtectedResource): + @use_args(DashboardWidgetSchema(), locations=('json',)) + @swag_from('swagger/create_dashboard_widget_spec.yaml') + def post(self, args, dashboard_id): + requested_dashboard = dashboard.get_dashboard(dashboard_id) + if requested_dashboard.account_id != g.current_account.id: + abort(403, message='You are not allowed to access this dashboard', + status='error') + if not device.can_user_access_device(g.current_account.id, + args['device_id']): + abort(403, message='You are not allowed to access this device', + status='error') + success = dashboard.create_widget( + dashboard_id, + args['device_id'], + args['height'], + args['width'], + args['x'], + args['y'], + args['chart_type'], + args['filters']) + if success: + return '', 201 + + @swag_from('swagger/get_dashboard_widgets_spec.yaml') + def get(self, dashboard_id): + requested_dashboard = dashboard.get_dashboard(dashboard_id) + if requested_dashboard.account_id != g.current_account.id: + abort(403, message='You are not allowed to access this dashboard', + status='error') + return DashboardWidgetSchema().dump( + dashboard.get_widgets(dashboard_id), many=True), 200 + + +class DashboardWidgetResource(ProtectedResource): + @swag_from('swagger/get_dashboard_widget_spec.yaml') + def get(self, dashboard_id, widget_id): + requested_dashboard = dashboard.get_dashboard(dashboard_id) + if requested_dashboard.account_id != g.current_account.id: + abort(403, message='You are not allowed to access this widget', + status='error') + requested_widget = dashboard.get_widget(widget_id) + return DashboardWidgetSchema().dump(requested_widget), 200 + + @use_args(DashboardWidgetSchema(), locations=('json',)) + @swag_from('swagger/update_dashboard_widget_spec.yaml') + def put(self, args, dashboard_id, widget_id): + print("Received stuff!") + print("Args: " + str(args)) + print("Dashboard_id: " + str(dashboard_id)) + print("Widget_id: " + str(widget_id)) + requested_dashboard = dashboard.get_dashboard(dashboard_id) + if requested_dashboard.account_id != g.current_account.id: + abort(403, message='You are not allowed to access this dashboard', + status='error') + if not device.can_user_access_device(g.current_account.id, + args['device_id']): + abort(403, message='You are not allowed to access this device', + status='error') + success = dashboard.patch_widget( + widget_id, + args['device_id'], + args['height'], + args['width'], + args['x'], + args['y'], + args['chart_type'], + args['filters']) + if success: + return '', 204 + + @use_args(DashboardWidgetSchema(partial=True), locations=('json',)) + @swag_from('swagger/update_dashboard_widget_spec.yaml') + def patch(self, args, dashboard_id, widget_id): + requested_dashboard = dashboard.get_dashboard(dashboard_id) + if requested_dashboard.account_id != g.current_account.id: + abort(403, message='You are not allowed to access this dashboard', + status='error') + if args.get('device_id') is not None: + if not device.can_user_access_device(g.current_account.id, + args['device_id']): + abort(403, message='You are not allowed to access this device', + status='error') + success = dashboard.patch_widget( + widget_id, + args.get('device_id'), + args.get('height'), + args.get('width'), + args.get('x'), + args.get('y'), + args.get('chart_type'), + args.get('filters')) + if success: + return '', 204 + + @swag_from('swagger/delete_dashboard_widget_spec.yaml') + def delete(self, dashboard_id, widget_id): + requested_dashboard = dashboard.get_dashboard(dashboard_id) + if requested_dashboard.account_id != g.current_account.id: + abort(403, message='You are not allowed to access this dashboard', + status='error') + dashboard.delete_widget(widget_id) + return '', 204 diff --git a/app/api/resources/swagger/create_dashboard_widget_spec.yaml b/app/api/resources/swagger/create_dashboard_widget_spec.yaml new file mode 100644 index 0000000..a51390c --- /dev/null +++ b/app/api/resources/swagger/create_dashboard_widget_spec.yaml @@ -0,0 +1,20 @@ +Creates new dashboard widget +Requires Widget object and creates dashboard +--- +tags: + - Dashboard +parameters: + - in: path + name: dashboard_id + required: true + type: integer + description: Id of the owning dashboard + - in: body + name: body + required: true + schema: + type: object + $ref: '#/definitions/WidgetCreation' +responses: + 201: + description: Successful creation diff --git a/app/api/resources/swagger/delete_dashboard_spec.yaml b/app/api/resources/swagger/delete_dashboard_spec.yaml index cf877a0..bad6213 100644 --- a/app/api/resources/swagger/delete_dashboard_spec.yaml +++ b/app/api/resources/swagger/delete_dashboard_spec.yaml @@ -1,4 +1,4 @@ -Deletes a dashboard +Deletes a widget --- tags: - Dashboard @@ -8,6 +8,11 @@ parameters: required: true type: integer description: Id of the dashboard + - in: path + name: widget_id + required: true + type: integer + description: Id of the widget responses: 204: description: Success diff --git a/app/api/resources/swagger/delete_dashboard_widget_spec.yaml b/app/api/resources/swagger/delete_dashboard_widget_spec.yaml new file mode 100644 index 0000000..cf877a0 --- /dev/null +++ b/app/api/resources/swagger/delete_dashboard_widget_spec.yaml @@ -0,0 +1,13 @@ +Deletes a dashboard +--- +tags: + - Dashboard +parameters: + - in: path + name: dashboard_id + required: true + type: integer + description: Id of the dashboard +responses: + 204: + description: Success diff --git a/app/api/resources/swagger/get_dashboard_widget_spec.yaml b/app/api/resources/swagger/get_dashboard_widget_spec.yaml new file mode 100644 index 0000000..006d4f9 --- /dev/null +++ b/app/api/resources/swagger/get_dashboard_widget_spec.yaml @@ -0,0 +1,26 @@ +Gets a widget +--- +tags: + - Dashboard +parameters: + - in: path + name: dashboard_id + required: true + type: integer + description: Id of the dashboard + - in: path + name: widget_id + required: true + type: integer + description: Id of the widget +responses: + 200: + description: Success + schema: + type: object + required: + - content + properties: + content: + $ref: '#/definitions/Widget' + diff --git a/app/api/resources/swagger/get_dashboard_widgets_spec.yaml b/app/api/resources/swagger/get_dashboard_widgets_spec.yaml new file mode 100644 index 0000000..7e4511e --- /dev/null +++ b/app/api/resources/swagger/get_dashboard_widgets_spec.yaml @@ -0,0 +1,22 @@ +Gets all widgets of a dashboard +--- +tags: + - Dashboard +parameters: + - in: path + name: dashboard_id + required: true + type: integer + description: Id of the dashboard +responses: + 200: + description: Success + schema: + type: object + required: + - content + properties: + content: + type: array + items: + $ref: '#/definitions/Widget' diff --git a/app/api/resources/swagger/update_dashboard_spec.yaml b/app/api/resources/swagger/update_dashboard_spec.yaml index f702179..c3e1996 100644 --- a/app/api/resources/swagger/update_dashboard_spec.yaml +++ b/app/api/resources/swagger/update_dashboard_spec.yaml @@ -14,11 +14,7 @@ parameters: required: true schema: type: object - required: - - dashboard - properties: - device: - $ref: '#/definitions/DashboardCreation' + $ref: '#/definitions/DashboardCreation' responses: 204: description: Success diff --git a/app/api/resources/swagger/update_dashboard_widget_spec.yaml b/app/api/resources/swagger/update_dashboard_widget_spec.yaml new file mode 100644 index 0000000..452946a --- /dev/null +++ b/app/api/resources/swagger/update_dashboard_widget_spec.yaml @@ -0,0 +1,24 @@ +Updates a widget +--- +tags: + - Dashboard +parameters: + - in: path + name: dashboard_id + required: true + type: integer + description: Id of the dashboard + - in: path + name: widget_id + required: true + type: integer + description: Id of the widget + - in: body + name: body + required: true + schema: + type: object + $ref: '#/definitions/WidgetCreation' +responses: + 204: + description: Success diff --git a/app/dashboards/api.py b/app/dashboards/api.py index 69a2492..6f03b69 100644 --- a/app/dashboards/api.py +++ b/app/dashboards/api.py @@ -1,4 +1,4 @@ -from .models import Dashboard +from .models import Dashboard, DashboardWidget # Public interface @@ -101,3 +101,80 @@ def get_dashboards(account_id, active): :rtype: List of Dashboard """ return Dashboard.get_many_filtered(account_id=account_id, active=active) + + +def create_widget(dashboard_id, device_id, height, width, x, y, + chart_type, filters): + """ + Tries to create a dashboard widget + """ + widget = DashboardWidget(dashboard_id, device_id, height, width, x, y, + chart_type, filters) + widget.save() + + +def delete_widget(widget_id): + """ + Tries to delete widget with given id + + :param widget_id: Id of requested widget + :type name: int + """ + widget = DashboardWidget.get(id=widget_id) + widget.delete() + + +def get_widgets(dashboard_id): + """ + Tries to fetch widgets of a dashboard with dashboard_id + + :param dashboard_id: Id of owner dashboard + :type name: int + :returns: Widget list + :rtype: List of Widgets + """ + return DashboardWidget.get_many_for_dashboard(dashboard_id) + + +def get_widget(widget_id): + """ + Tries to fetch widget with given id + + :param widget_id: Id of requested dashboard + :type name: int + :returns: Widget object + :rtype: Widget + """ + return DashboardWidget.get(id=widget_id) + + +def patch_widget(widget_id, device_id=None, height=None, width=None, + x=None, y=None, chart_type=None, filters=None): + """ + Tries to update widget with given parameters + """ + widget = DashboardWidget.get(id=widget_id) + + if device_id is not None: + widget.device_id = device_id + + if height is not None: + widget.height = height + + if width is not None: + widget.width = width + + if x is not None: + widget.x = x + + if y is not None: + widget.y = y + + if chart_type is not None: + widget.chart_type = chart_type + + if filters is not None: + widget.filters = filters + + widget.save() + print("Saved widget") diff --git a/app/dashboards/models.py b/app/dashboards/models.py index 847580c..001c945 100644 --- a/app/dashboards/models.py +++ b/app/dashboards/models.py @@ -113,3 +113,121 @@ class Dashboard(db.Model): def __repr__(self): return '' % ( self.dashboard_data, self.account_id) + + +class DashboardWidget(db.Model): + __tablename__ = 'dashboard_widgets' + + id = db.Column(db.Integer, primary_key=True) + dashboard_id = db.Column(db.Integer, db.ForeignKey('dashboards.id')) + device_id = db.Column(db.Integer, db.ForeignKey('devices.id')) + height = db.Column(db.Integer, nullable=False) + width = db.Column(db.Integer, nullable=False) + x = db.Column(db.Integer, nullable=False) + y = db.Column(db.Integer, nullable=False) + chart_type = db.Column(db.String, nullable=False) + filters = db.Column(JSON, nullable=False) + created_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp()) + modified_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp(), + onupdate=db.func.current_timestamp()) + + def __init__(self, dashboard_id, device_id, height, width, x, y, + chart_type, filters): + self.dashboard_id = dashboard_id + self.device_id = device_id + self.height = height + self.width = width + self.x = x + self.y = y + self.chart_type = chart_type + self.filters = filters + + def save(self): + """ + Stores this widget to database + This may raise errors + """ + db.session.add(self) + db.session.commit() + + def delete(self): + """ + Deletes this widget from database + """ + db.session.delete(self) + db.session.commit() + + @staticmethod + def exists_with_any_of(**kwargs): + """ + Checks if widget with any of the given arguments exists + """ + for key, value in kwargs.items(): + args = {key: value} + if DashboardWidget.query.filter_by(**args).first(): + return True + return False + + @staticmethod + def exists(**kwargs): + """ + Checks if widget with all of the given arguments exists + """ + if DashboardWidget.query.filter_by(**kwargs).first(): + return True + return False + + @staticmethod + def get_all(): + """ + Get all stored widgets + """ + return DashboardWidget.query.paginate(None, None, False).items + + @staticmethod + def get_many(**kwargs): + """ + Get widgets with given filters + + Available filters: + * id + * dashboard_id + * device_id + * dimensions and positions, but useless + * chart_type + * filters, but useless + """ + return DashboardWidget.query.filter_by(**kwargs).paginate( + None, None, False).items + + @staticmethod + def get_many_for_dashboard(dashboard_id): + """ + Get widgets for given dashboard + """ + query = DashboardWidget.query.filter( + DashboardWidget.dashboard_id == dashboard_id) + return query.paginate(None, None, False).items + + @staticmethod + def get(**kwargs): + """ + Get widget with given filters + + Available filters: + * id + * dashboard_id + * device_id + * dimensions and positions, but useless + * chart_type + * filters, but useless + """ + return DashboardWidget.query.filter_by(**kwargs).first_or_404() + + def __repr__(self): + return '' % ( + self.dashboard_data, self.account_id) diff --git a/app/swagger/template.yaml b/app/swagger/template.yaml index b389404..cd62c37 100644 --- a/app/swagger/template.yaml +++ b/app/swagger/template.yaml @@ -84,6 +84,16 @@ definitions: description: Dashboard data example: {} + filters: + type: object + description: Dashboard data + example: {} + + charttype: + type: string + description: Type of chart + example: line + Credentials: type: object required: @@ -222,6 +232,10 @@ definitions: $ref: '#/definitions/id' dashboard_data: $ref: '#/definitions/dashboarddata' + active: + type: boolean + name: + type: '#definitions/devicename' DashboardCreation: type: object @@ -234,6 +248,66 @@ definitions: $ref: '#/definitions/dashboarddata' active: type: boolean + name: + ref: '#definitions/genericname' + + Widget: + type: object + required: + - id + - dashboard_id + - device_id + - width + - height + - x + - y + - chart_type + - filters + properties: + id: + $ref: '#/definitions/id' + dashboard_id: + $ref: '#/definitions/id' + device_id: + $ref: '#/definitions/id' + width: + $ref: '#/definitions/id' + height: + $ref: '#/definitions/id' + x: + $ref: '#/definitions/id' + y: + $ref: '#/definitions/id' + chart_type: + $ref: '#/definitions/charttype' + filters: + $ref: '#/definitions/filters' + + WidgetCreation: + type: object + required: + - device_id + - width + - height + - x + - y + - chart_type + - filters + properties: + device_id: + $ref: '#/definitions/id' + width: + $ref: '#/definitions/id' + height: + $ref: '#/definitions/id' + x: + $ref: '#/definitions/id' + y: + $ref: '#/definitions/id' + chart_type: + $ref: '#/definitions/charttype' + filters: + $ref: '#/definitions/filters' UnauthorizedError: type: object diff --git a/migrations/versions/b5eb4a04c77e_.py b/migrations/versions/b5eb4a04c77e_.py new file mode 100644 index 0000000..56217b7 --- /dev/null +++ b/migrations/versions/b5eb4a04c77e_.py @@ -0,0 +1,43 @@ +"""empty message + +Revision ID: b5eb4a04c77e +Revises: 5cce35244087 +Create Date: 2018-10-22 23:11:50.584996 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b5eb4a04c77e' +down_revision = '5cce35244087' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('dashboard_widgets', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('dashboard_id', sa.Integer(), nullable=False), + sa.Column('device_id', sa.Integer(), nullable=True), + sa.Column('height', sa.Integer(), nullable=False), + sa.Column('width', sa.Integer(), nullable=False), + sa.Column('x', sa.Integer(), nullable=False), + sa.Column('y', sa.Integer(), nullable=False), + sa.Column('chart_type', sa.String(), nullable=False), + sa.Column('filters', postgresql.JSON(astext_type=sa.Text()), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('modified_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['dashboard_id'], ['dashboards.id'], ), + sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('dashboard_widgets') + # ### end Alembic commands ###