Merged in develop (pull request #5)

Version 0.2.0 release
master
Ensar Sarajcic 2018-10-08 20:09:57 +00:00
commit 9da4d3eea9
23 changed files with 83 additions and 153 deletions

View File

@ -7,9 +7,10 @@ import app.accounts.api as accounts
from app.api.auth_protection import ProtectedResource from app.api.auth_protection import ProtectedResource
from app.api.permission_protection import (requires_permission, from app.api.permission_protection import (requires_permission,
valid_permissions) valid_permissions)
from app.api.schemas import BaseResourceSchema
class UserSchema(Schema): class UserSchema(BaseResourceSchema):
username = fields.Str(required=True) username = fields.Str(required=True)
email = fields.Email(required=True) email = fields.Email(required=True)
password = fields.Str(required=True, load_only=True) password = fields.Str(required=True, load_only=True)
@ -23,7 +24,7 @@ def validate_role_permissions(permissions_list):
return set(permissions_list).issubset(valid_permissions) return set(permissions_list).issubset(valid_permissions)
class RoleSchema(Schema): class RoleSchema(BaseResourceSchema):
id = fields.Integer(required=True, location='json') id = fields.Integer(required=True, location='json')
display_name = fields.String(required=True, location='json') display_name = fields.String(required=True, location='json')
permissions = fields.List(fields.String, required=True, permissions = fields.List(fields.String, required=True,
@ -31,50 +32,32 @@ class RoleSchema(Schema):
validate=validate_role_permissions) validate=validate_role_permissions)
class RoleWrapperSchema(Schema):
role = fields.Nested(RoleSchema, required=True, location='json')
class RolesWrapperSchema(Schema):
roles = fields.Nested(RoleSchema, required=True,
location='json', many=True)
class RoleCreationSchema(Schema): class RoleCreationSchema(Schema):
display_name = fields.String(required=True, location='json') display_name = fields.String(required=True, location='json')
permissions = fields.List(fields.String, required=True, permissions = fields.List(fields.String, required=True,
location='json', many=True) location='json', many=True)
class RoleCreationWrapperSchema(Schema):
role = fields.Nested(RoleCreationSchema, required=True, location='json')
class UserWrapperSchema(Schema):
user = fields.Nested(UserSchema, required=True, location='json')
class AccountResource(ProtectedResource): class AccountResource(ProtectedResource):
@swag_from('swagger/get_account_spec.yaml') @swag_from('swagger/get_account_spec.yaml')
def get(self, account_id): def get(self, account_id):
if g.current_account.id == account_id: if g.current_account.id == account_id:
return UserWrapperSchema().dump({'user': g.current_account}), 200 return UserSchema().dump(g.current_account), 200
abort(403, message='You can only get your own account', status='error') abort(403, message='You can only get your own account', status='error')
class RoleResource(ProtectedResource): class RoleResource(ProtectedResource):
@swag_from('swagger/get_role_spec.yaml') @swag_from('swagger/get_role_spec.yaml')
def get(self, role_id): def get(self, role_id):
return RoleWrapperSchema().dump( return RoleSchema().dump(
{'role': accounts.get_role(role_id)}), 200 accounts.get_role(role_id)), 200
class RolesResource(ProtectedResource): class RolesResource(ProtectedResource):
@requires_permission('CREATE_ROLE', 'Role creation') @requires_permission('CREATE_ROLE', 'Role creation')
@use_args(RoleCreationWrapperSchema()) @use_args(RoleCreationSchema(), locations=('json',))
@swag_from('swagger/create_role_spec.yaml') @swag_from('swagger/create_role_spec.yaml')
def post(self, args): def post(self, args):
args = args['role']
success = accounts.create_role(args['display_name'], success = accounts.create_role(args['display_name'],
args['permissions']) args['permissions'])
if success: if success:
@ -82,12 +65,11 @@ class RolesResource(ProtectedResource):
@swag_from('swagger/get_roles_spec.yaml') @swag_from('swagger/get_roles_spec.yaml')
def get(self): def get(self):
return RolesWrapperSchema().dump( return RoleSchema().dump(accounts.get_all_roles(), many=True), 200
{'roles': accounts.get_all_roles()}), 200
class AccountRoleResource(ProtectedResource): class AccountRoleResource(ProtectedResource):
@use_args(RoleUpdateSchema()) @use_args(RoleUpdateSchema(), locations=('json',))
@swag_from('swagger/update_account_role_spec.yaml') @swag_from('swagger/update_account_role_spec.yaml')
def put(self, args, account_id): def put(self, args, account_id):
if g.current_account.id == account_id: if g.current_account.id == account_id:
@ -99,11 +81,10 @@ class AccountRoleResource(ProtectedResource):
class AccountListResource(Resource): class AccountListResource(Resource):
@use_args(UserWrapperSchema()) @use_args(UserSchema(), locations=('json',))
@swag_from('swagger/create_account_spec.yaml') @swag_from('swagger/create_account_spec.yaml')
def post(self, args): def post(self, args):
try: try:
args = args['user']
success = accounts.create_account( success = accounts.create_account(
args['username'], args['username'],
args['email'], args['email'],

View File

@ -1,26 +1,18 @@
from flask import g from flask import g
from flask_restful import abort from flask_restful import abort
from marshmallow import Schema, fields from marshmallow import fields
from webargs.flaskparser import use_args from webargs.flaskparser import use_args
from flasgger import swag_from from flasgger import swag_from
import app.dashboards.api as dashboard import app.dashboards.api as dashboard
from app.api.auth_protection import ProtectedResource from app.api.auth_protection import ProtectedResource
from app.api.schemas import BaseResourceSchema
class DashboardSchema(Schema): class DashboardSchema(BaseResourceSchema):
id = fields.Integer(dump_only=True) id = fields.Integer(dump_only=True)
dashboard_data = fields.Raw() dashboard_data = fields.Raw()
class DashboardWrapperSchema(Schema):
dashboard = fields.Nested(DashboardSchema, required=True, location='json')
class DashboardsSchema(Schema):
dashboards = fields.Nested(DashboardSchema, required=True, location='json',
many=True)
class DashboardResource(ProtectedResource): class DashboardResource(ProtectedResource):
@swag_from('swagger/get_dashboard_spec.yaml') @swag_from('swagger/get_dashboard_spec.yaml')
def get(self, dashboard_id): def get(self, dashboard_id):
@ -28,17 +20,15 @@ class DashboardResource(ProtectedResource):
if requested_dashboard.account_id != g.current_account.id: if requested_dashboard.account_id != g.current_account.id:
abort(403, message='You are not allowed to access this dashboard', abort(403, message='You are not allowed to access this dashboard',
status='error') status='error')
return DashboardWrapperSchema().dump( return DashboardSchema().dump(requested_dashboard), 200
{'dashboard': requested_dashboard}), 200
@use_args(DashboardWrapperSchema()) @use_args(DashboardSchema(), locations=('json',))
@swag_from('swagger/update_dashboard_spec.yaml') @swag_from('swagger/update_dashboard_spec.yaml')
def put(self, args, dashboard_id): def put(self, args, dashboard_id):
requested_dashboard = dashboard.get_dashboard(dashboard_id) requested_dashboard = dashboard.get_dashboard(dashboard_id)
if requested_dashboard.account_id != g.current_account.id: if requested_dashboard.account_id != g.current_account.id:
abort(403, message='You are not allowed to access this dashboard', abort(403, message='You are not allowed to access this dashboard',
status='error') status='error')
args = args['dashboard']
success = dashboard.update_dashboard( success = dashboard.update_dashboard(
dashboard_id, dashboard_id,
args['dashboard_data']) args['dashboard_data'])
@ -47,10 +37,9 @@ class DashboardResource(ProtectedResource):
class DashboardListResource(ProtectedResource): class DashboardListResource(ProtectedResource):
@use_args(DashboardWrapperSchema()) @use_args(DashboardSchema(), locations=('json',))
@swag_from('swagger/create_dashboard_spec.yaml') @swag_from('swagger/create_dashboard_spec.yaml')
def post(self, args): def post(self, args):
args = args['dashboard']
success = dashboard.create_dashboard( success = dashboard.create_dashboard(
args['dashboard_data'], args['dashboard_data'],
g.current_account.id) g.current_account.id)
@ -59,6 +48,5 @@ class DashboardListResource(ProtectedResource):
@swag_from('swagger/get_dashboards_spec.yaml') @swag_from('swagger/get_dashboards_spec.yaml')
def get(self): def get(self):
return DashboardsSchema().dump( return DashboardSchema().dump(
{'dashboards': dashboard.get_dashboards(g.current_account.id), many=True), 200
dashboard.get_dashboards(g.current_account.id)}), 200

View File

@ -5,17 +5,22 @@ from flasgger import swag_from
from flask import g, request from flask import g, request
import app.devices.api as devices import app.devices.api as devices
from app.api.auth_protection import ProtectedResource from app.api.auth_protection import ProtectedResource
from app.api.schemas import BaseResourceSchema
class DeviceTypeSchema(Schema): class BasicDeviceTypeSchema(Schema):
id = fields.Integer(dump_only=True) id = fields.Integer(dump_only=True)
name = fields.Str(required=True) name = fields.Str(required=True)
class DeviceSchema(Schema): class DeviceTypeSchema(BaseResourceSchema, BasicDeviceTypeSchema):
pass
class DeviceSchema(BaseResourceSchema):
id = fields.Integer(dump_only=True) id = fields.Integer(dump_only=True)
name = fields.Str(required=True) name = fields.Str(required=True)
device_type = fields.Nested(DeviceTypeSchema, dump_only=True) device_type = fields.Nested(BasicDeviceTypeSchema, dump_only=True)
device_type_id = fields.Integer(load_only=True, missing=1) device_type_id = fields.Integer(load_only=True, missing=1)
@ -23,37 +28,12 @@ class DeviceWithConfigurationSchema(DeviceSchema):
configuration = fields.Raw(dump_only=True) configuration = fields.Raw(dump_only=True)
class DeviceWrapperSchema(Schema): class RecordingsSchema(BaseResourceSchema):
device = fields.Nested(DeviceWithConfigurationSchema,
required=True, location='json')
class DevicesWrapperSchema(Schema):
devices = fields.Nested(DeviceSchema, required=True,
location='json', many=True)
class DeviceTypeWrapperSchema(Schema):
device_type = fields.Nested(DeviceTypeSchema, required=True,
location='json')
class DeviceTypesWrapperSchema(Schema):
device_types = fields.Nested(DeviceTypeSchema, required=True,
location='json', many=True)
class RecordingsSchema(Schema):
recorded_at = fields.DateTime() recorded_at = fields.DateTime()
record_type = fields.Integer() record_type = fields.Integer()
record_value = fields.String() record_value = fields.String()
class RecordingsWrapperSchema(Schema):
recordings = fields.Nested(RecordingsSchema, required=True,
location='json', many=True)
def validate_device_ownership(device_id): def validate_device_ownership(device_id):
if not devices.can_user_access_device(g.current_account.id, device_id): if not devices.can_user_access_device(g.current_account.id, device_id):
abort(403, message='You are not allowed to access this device', abort(403, message='You are not allowed to access this device',
@ -64,8 +44,8 @@ class DeviceResource(ProtectedResource):
@swag_from('swagger/get_device_spec.yaml') @swag_from('swagger/get_device_spec.yaml')
def get(self, device_id): def get(self, device_id):
validate_device_ownership(device_id) validate_device_ownership(device_id)
return DeviceWrapperSchema().dump( return DeviceSchema().dump(
{'device': devices.get_device(device_id)}), 200 devices.get_device(device_id)), 200
@swag_from('swagger/delete_device_spec.yaml') @swag_from('swagger/delete_device_spec.yaml')
def delete(self, device_id): def delete(self, device_id):
@ -77,18 +57,17 @@ class DeviceResource(ProtectedResource):
class DeviceTypeResource(ProtectedResource): class DeviceTypeResource(ProtectedResource):
@swag_from('swagger/get_device_type_spec.yaml') @swag_from('swagger/get_device_type_spec.yaml')
def get(self, device_type_id): def get(self, device_type_id):
return DeviceTypeWrapperSchema().dump( return DeviceTypeSchema().dump(
{'device_type': devices.get_device_type(device_type_id)}), 200 devices.get_device_type(device_type_id)), 200
class DeviceTypeListResource(ProtectedResource): class DeviceTypeListResource(ProtectedResource):
@use_args(DeviceTypeWrapperSchema()) @use_args(DeviceTypeSchema(), locations=('json',))
@swag_from('swagger/create_device_type_spec.yaml') @swag_from('swagger/create_device_type_spec.yaml')
def post(self, args): def post(self, args):
if g.current_account.role_id != 1: if g.current_account.role_id != 1:
abort(403, message='Only admin may create device types', abort(403, message='Only admin may create device types',
status='error') status='error')
args = args['device_type']
success = devices.create_device_type( success = devices.create_device_type(
args['name']) args['name'])
if success: if success:
@ -96,8 +75,8 @@ class DeviceTypeListResource(ProtectedResource):
@swag_from('swagger/get_device_types_spec.yaml') @swag_from('swagger/get_device_types_spec.yaml')
def get(self): def get(self):
return DeviceTypesWrapperSchema().dump( return DeviceTypeSchema().dump(devices.get_device_types(),
{'device_types': devices.get_device_types()}), 200 many=True), 200
class DeviceRecordingResource(ProtectedResource): class DeviceRecordingResource(ProtectedResource):
@ -105,13 +84,12 @@ class DeviceRecordingResource(ProtectedResource):
def get(self, device_id): def get(self, device_id):
validate_device_ownership(device_id) validate_device_ownership(device_id)
request_args = request.args request_args = request.args
return RecordingsWrapperSchema().dump( return RecordingsSchema().dump(
{'recordings':
devices.get_device_recordings_filtered( devices.get_device_recordings_filtered(
device_id, device_id,
request_args.get('record_type'), request_args.get('record_type'),
request_args.get('start_date'), request_args.get('start_date'),
request_args.get('end_date'))}), 200 request_args.get('end_date')), many=True), 200
@swag_from('swagger/create_device_recording_spec.yaml') @swag_from('swagger/create_device_recording_spec.yaml')
def post(self, device_id): def post(self, device_id):
@ -122,10 +100,9 @@ class DeviceRecordingResource(ProtectedResource):
class DeviceListResource(ProtectedResource): class DeviceListResource(ProtectedResource):
@use_args(DeviceWrapperSchema()) @use_args(DeviceSchema(), locations=('json',))
@swag_from('swagger/create_device_spec.yaml') @swag_from('swagger/create_device_spec.yaml')
def post(self, args): def post(self, args):
args = args['device']
success = devices.create_device( success = devices.create_device(
args['name'], args['name'],
g.current_account.id, g.current_account.id,
@ -135,8 +112,8 @@ class DeviceListResource(ProtectedResource):
@swag_from('swagger/get_devices_spec.yaml') @swag_from('swagger/get_devices_spec.yaml')
def get(self): def get(self):
return DevicesWrapperSchema().dump( return DeviceSchema().dump(
{'devices': devices.get_devices(g.current_account.id)}), 200 devices.get_devices(g.current_account.id), many=True), 200
class DeviceConfigurationResource(ProtectedResource): class DeviceConfigurationResource(ProtectedResource):

View File

@ -10,11 +10,7 @@ parameters:
required: true required: true
schema: schema:
type: object type: object
required: $ref: '#/definitions/User'
- user
properties:
user:
$ref: '#/definitions/User'
security: [] security: []
responses: responses:
201: 201:

View File

@ -9,11 +9,7 @@ parameters:
required: true required: true
schema: schema:
type: object type: object
required: $ref: '#/definitions/DashboardCreation'
- dashboard
properties:
dashboard:
$ref: '#/definitions/DashboardCreation'
responses: responses:
201: 201:
description: Successful creation description: Successful creation

View File

@ -9,11 +9,7 @@ parameters:
required: true required: true
schema: schema:
type: object type: object
required: $ref: '#/definitions/DeviceCreation'
- device
properties:
device:
$ref: '#/definitions/DeviceCreation'
responses: responses:
201: 201:
description: Successful creation description: Successful creation

View File

@ -9,11 +9,7 @@ parameters:
required: true required: true
schema: schema:
type: object type: object
required: $ref: '#/definitions/DeviceType'
- device_type
properties:
device_type:
$ref: '#/definitions/DeviceType'
responses: responses:
201: 201:
description: Successful creation description: Successful creation

View File

@ -9,11 +9,7 @@ parameters:
required: true required: true
schema: schema:
type: object type: object
required: $ref: '#/definitions/Role'
- role
properties:
device:
$ref: '#/definitions/Role'
responses: responses:
201: 201:
description: Successful creation description: Successful creation

View File

@ -11,11 +11,7 @@ parameters:
required: true required: true
schema: schema:
type: object type: object
required: $ref: '#/definitions/Credentials'
- user
properties:
user:
$ref: '#/definitions/Credentials'
security: [] security: []
responses: responses:
200: 200:

View File

@ -15,9 +15,9 @@ responses:
schema: schema:
type: object type: object
required: required:
- user - content
properties: properties:
user: content:
$ref: '#/definitions/User' $ref: '#/definitions/User'
403: 403:
description: Accessed a different account description: Accessed a different account

View File

@ -14,8 +14,8 @@ responses:
schema: schema:
type: object type: object
required: required:
- dashboard - content
properties: properties:
device: content:
$ref: '#/definitions/Dashboard' $ref: '#/definitions/Dashboard'

View File

@ -8,9 +8,9 @@ responses:
schema: schema:
type: object type: object
required: required:
- dashboards - content
properties: properties:
devices: content:
type: array type: array
items: items:
$ref: '#/definitions/Dashboard' $ref: '#/definitions/Dashboard'

View File

@ -33,9 +33,9 @@ responses:
schema: schema:
type: object type: object
required: required:
- recordings - content
properties: properties:
recordings: content:
type: array type: array
items: items:
$ref: '#/definitions/Recording' $ref: '#/definitions/Recording'

View File

@ -14,8 +14,8 @@ responses:
schema: schema:
type: object type: object
required: required:
- device - content
properties: properties:
device: content:
$ref: '#/definitions/DeviceWithConfig' $ref: '#/definitions/DeviceWithConfig'

View File

@ -14,8 +14,8 @@ responses:
schema: schema:
type: object type: object
required: required:
- device_type - content
properties: properties:
device_type: content:
$ref: '#/definitions/DeviceType' $ref: '#/definitions/DeviceType'

View File

@ -23,9 +23,9 @@ responses:
schema: schema:
type: object type: object
required: required:
- device_types - content
properties: properties:
device_types: content:
type: array type: array
items: items:
$ref: '#/definitions/DeviceType' $ref: '#/definitions/DeviceType'

View File

@ -23,9 +23,9 @@ responses:
schema: schema:
type: object type: object
required: required:
- devices - content
properties: properties:
devices: content:
type: array type: array
items: items:
$ref: '#/definitions/Device' $ref: '#/definitions/Device'

View File

@ -14,8 +14,8 @@ responses:
schema: schema:
type: object type: object
required: required:
- role - content
properties: properties:
device: content:
$ref: '#/definitions/Role' $ref: '#/definitions/Role'

View File

@ -8,9 +8,9 @@ responses:
schema: schema:
type: object type: object
required: required:
- roles - content
properties: properties:
devices: content:
type: array type: array
items: items:
$ref: '#/definitions/Role' $ref: '#/definitions/Role'

View File

@ -1,24 +1,23 @@
from flask_restful import Resource, abort from flask_restful import Resource, abort
from webargs import fields from webargs import fields
from marshmallow import Schema
from webargs.flaskparser import use_args from webargs.flaskparser import use_args
from flasgger import swag_from from flasgger import swag_from
from app.api.auth_protection import ProtectedResource from app.api.auth_protection import ProtectedResource
import app.accounts.api as accounts import app.accounts.api as accounts
class TokenResource(Resource): class UserInfoSchema(Schema):
user_args = { username = fields.Str(required=True)
'user': fields.Nested({ password = fields.Str(required=True)
'username': fields.Str(required=True),
'password': fields.Str(required=True)
}, required=True, location='json')
}
@use_args(user_args)
class TokenResource(Resource):
@use_args(UserInfoSchema(), locations=('json',))
@swag_from('swagger/create_token_spec.yaml') @swag_from('swagger/create_token_spec.yaml')
def post(self, args): def post(self, args):
try: try:
args = args['user']
token = accounts.create_token( token = accounts.create_token(
args['username'], args['username'],
args['password']) args['password'])

View File

@ -0,0 +1,7 @@
from marshmallow import Schema, post_dump
class BaseResourceSchema(Schema):
@post_dump(pass_many=True)
def wrap_with_envelope(self, data, many):
return {'content': data}

View File

@ -12,6 +12,7 @@ app.config.from_pyfile('config.py', silent=True)
db = SQLAlchemy(app) db = SQLAlchemy(app)
bcrypt = Bcrypt(app) bcrypt = Bcrypt(app)
swagger = Swagger(app, template_file='swagger/template.yaml') swagger = Swagger(app, template_file='swagger/template.yaml')
swagger.template['info']['version'] = app.config['APP_VERSION']
CORS(app) CORS(app)
celery = celery_configurator.make_celery(app) celery = celery_configurator.make_celery(app)

View File

@ -2,6 +2,7 @@ import os
# App configuration # App configuration
DEBUG = os.environ['DEBUG'] DEBUG = os.environ['DEBUG']
APP_VERSION = '0.2.0'
# Define the application directory # Define the application directory
BASE_DIR = os.path.abspath(os.path.dirname(__file__)) BASE_DIR = os.path.abspath(os.path.dirname(__file__))
@ -39,7 +40,7 @@ MQTT_REFRESH_TIME = 1.0 # refresh time in seconds
CELERY_BROKER_URL = os.environ['REDIS_URL'] CELERY_BROKER_URL = os.environ['REDIS_URL']
CELERY_RESULT_BACKEND = os.environ['REDIS_URL'] CELERY_RESULT_BACKEND = os.environ['REDIS_URL']
# Flassger config # Flasgger config
SWAGGER = { SWAGGER = {
'uiversion': 3 'uiversion': 3
} }