Skip to content
Snippets Groups Projects
Commit d30c24b0 authored by Ian Galpin's avatar Ian Galpin
Browse files

Added data entry API endpoints

parent bbed5a3e
No related branches found
No related tags found
No related merge requests found
...@@ -18,9 +18,10 @@ import time ...@@ -18,9 +18,10 @@ import time
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from compendium_v2.routes import common from compendium_v2.routes import common
from compendium_v2.routes.data_entry import routes as data_entry_routes
routes = Blueprint('compendium-v2-api', __name__) routes = Blueprint('compendium-v2-api', __name__)
routes.register_blueprint(data_entry_routes, url_prefix='/data-entries/')
THING_LIST_SCHEMA = { THING_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#', '$schema': 'http://json-schema.org/draft-07/schema#',
......
from typing import Any
from flask import Blueprint, abort, jsonify, url_for
from compendium_v2.db import db
from compendium_v2.db.models import (DataEntryItem, DataEntrySection,
DataSourceType)
from compendium_v2.db.survey import get_budget_by_year
from compendium_v2.routes import common
routes = Blueprint('data-entry', __name__)
col_pal = ['#fd7f6f', '#7eb0d5', '#b2e061',
'#bd7ebe', '#ffb55a', '#ffee65',
'#beb9db', '#fdcce5', '#8bd3c7']
DATA_ENTRY_SECTIONS_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'section': {
'type': 'object',
'properties': {
'id': {'type': 'number'},
'name': {'type': 'string'},
'description': {'type': 'string'},
'url': {'type': 'string'}
},
'required': ['id', 'name', 'description', 'url'],
'additionalProperties': False
}
},
'type': 'array',
'items': {'$ref': '#/definitions/section'}
}
DATA_ENTRY_SECTIONS_DETAIL_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'item': {
'type': 'object',
'properties': {
'id': {'type': 'number'},
'title': {'type': 'string'},
'url': {'type': 'string'}
},
'required': ['id', 'title', 'url'],
'additionalProperties': False
}
},
'type': 'object',
'properties': {
'name': {'type': 'string'},
'description': {'type': 'string'},
'items': {
'type': 'array',
'items': {'$ref': '#/definitions/item'}
}
},
'required': ['name', 'description', 'items'],
'additionalProperties': False
}
DATA_ENTRY_ITEM_DETAIL_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'settings': {
'type': 'object',
'properties': {
},
'additionalProperties': False
},
'dataset': {
'type': 'object',
'properties': {
'data': {
'type': 'array',
'items': {'type': ['number', 'null']}
},
'backgroundColor': {
'type': 'string'
},
'label': {
'type': 'string'
}
}
},
'data': {
'type': 'object',
'properties': {
'labels': {
'type': 'array',
'items': {'type': 'string'}},
'datasets': {
'type': 'array',
'items': {
'$ref': '#/definitions/dataset'
}
}
},
'required': ['labels', 'datasets'],
'additionalProperties': False
}
},
'type': 'object',
'properties': {
'id': {'type': 'number'},
'title': {'type': 'string'},
'description': {'type': 'string'},
'settings': {
'type': 'object',
'$ref': '#/definitions/settings'
},
'data': {
'type': 'object',
'$ref': '#/definitions/data'
}
},
'required': ['id', 'title', 'description', 'settings', 'data'],
'additionalProperties': False
}
def load_data(data_source_id: DataSourceType) -> Any:
response_data = {}
if data_source_id == DataSourceType.BUDGETS_BY_YEAR:
response_data = get_budget_by_year()
# Enrich response data
# Add the colour formatting
for index, dataset in enumerate(response_data['datasets']):
dataset['backgroundColor'] = col_pal[index % len(col_pal)]
return response_data
@routes.route('/item/<int:item_id>', methods=['GET'])
@common.require_accepts_json
def item_view(item_id):
"""
handler for /api/data-entries/item/<item_id> requests
response will be formatted as:
.. asjson::
compendium_v2.routes.data_entry.DATA_ENTRY_ITEM_DETAIL_SCHEMA
:return:
"""
de_item = db.get_or_404(DataEntryItem, item_id)
# Confirm that only active sections can be loaded
if not de_item.is_active:
return abort(404)
return jsonify({
'id': de_item.id,
'title': de_item.title,
'description': de_item.description,
'settings': {},
'data': load_data(de_item.data_source)
})
@routes.route('/sections/<int:section_id>', methods=['GET'])
@common.require_accepts_json
def section_view(section_id):
"""
handler for /api/data-entries/sections/<section_id> requests
response will be formatted as:
.. asjson::
compendium_v2.routes.data_entry.DATA_ENTRY_SECTIONS_DETAIL_SCHEMA
:return:
"""
de_section = db.get_or_404(DataEntrySection, section_id)
# Confirm that only active sections can be loaded
if not de_section.is_active:
return abort(404)
items = [
{
'id': item.id,
'url': url_for('.item_view', item_id=item.id),
'title': item.title
} for item in de_section.items if item.is_active
]
response_section = {
'name': de_section.name,
'description': de_section.description,
'items': items
}
return jsonify(response_section)
@routes.route('/sections', methods=['GET'])
@common.require_accepts_json
def sections_view():
"""
handler for /api/data-entries/sections requests
response will be formatted as:
.. asjson::
compendium_v2.routes.data_entry.DATA_ENTRY_SECTIONS_LIST_SCHEMA
:return:
"""
model_sections = db.session.execute(
db.select(DataEntrySection)
.filter(DataEntrySection.is_active)
.order_by(DataEntrySection.sort_order)
).scalars()
de_sections = [{'id': de_section.id,
'name': de_section.name,
'description': de_section.description,
'url': url_for('.section_view', section_id=de_section.id)
} for de_section in model_sections]
return jsonify(list(de_sections))
{ {
"SQLALCHEMY_DATABASE_URI": "psql://username:password@hostname/db_name" "SQLALCHEMY_DATABASE_URI": "postgresql://compendium_v2:password@localhost/compendium_v2"
} }
...@@ -4,6 +4,9 @@ import jsonschema ...@@ -4,6 +4,9 @@ import jsonschema
import pytest import pytest
from compendium_v2.routes.api import THING_LIST_SCHEMA from compendium_v2.routes.api import THING_LIST_SCHEMA
from compendium_v2.routes.data_entry import (DATA_ENTRY_ITEM_DETAIL_SCHEMA,
DATA_ENTRY_SECTIONS_DETAIL_SCHEMA,
DATA_ENTRY_SECTIONS_LIST_SCHEMA)
from compendium_v2.routes.default import VERSION_SCHEMA from compendium_v2.routes.default import VERSION_SCHEMA
...@@ -18,7 +21,6 @@ def test_bad_accept(endpoint, client): ...@@ -18,7 +21,6 @@ def test_bad_accept(endpoint, client):
def test_version_request(client): def test_version_request(client):
rv = client.post( rv = client.post(
'version', 'version',
headers={'Accept': ['application/json']}) headers={'Accept': ['application/json']})
...@@ -28,10 +30,36 @@ def test_version_request(client): ...@@ -28,10 +30,36 @@ def test_version_request(client):
def test_things(client): def test_things(client):
rv = client.post( rv = client.post(
'api/things', 'api/things',
headers={'Accept': ['application/json']}) headers={'Accept': ['application/json']})
assert rv.status_code == 200 assert rv.status_code == 200
result = json.loads(rv.data.decode('utf-8')) result = json.loads(rv.data.decode('utf-8'))
jsonschema.validate(result, THING_LIST_SCHEMA) jsonschema.validate(result, THING_LIST_SCHEMA)
def test_api_data_entry_sections(client):
rv = client.get(
'api/data-entries/sections',
headers={'Accept': ['application/json']})
assert rv.status_code == 200
result = json.loads(rv.data.decode('utf-8'))
jsonschema.validate(result, DATA_ENTRY_SECTIONS_LIST_SCHEMA)
def test_api_data_entry_sections_detail(client):
rv = client.get(
'api/data-entries/sections/1',
headers={'Accept': ['application/json']})
assert rv.status_code == 200
result = json.loads(rv.data.decode('utf-8'))
jsonschema.validate(result, DATA_ENTRY_SECTIONS_DETAIL_SCHEMA)
def test_api_data_entry_item_detail(client):
rv = client.get(
'api/data-entries/item/1',
headers={'Accept': ['application/json']})
assert rv.status_code == 200
result = json.loads(rv.data.decode('utf-8'))
jsonschema.validate(result, DATA_ENTRY_ITEM_DETAIL_SCHEMA)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment