diff --git a/compendium_v2/routes/api.py b/compendium_v2/routes/api.py index 3707e116df9f7754e16feb9f218b3113986c8fe8..97eac240383e1478f546bece6b057aab899d8c91 100644 --- a/compendium_v2/routes/api.py +++ b/compendium_v2/routes/api.py @@ -18,9 +18,10 @@ import time from flask import Blueprint, jsonify 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.register_blueprint(data_entry_routes, url_prefix='/data-entries/') THING_LIST_SCHEMA = { '$schema': 'http://json-schema.org/draft-07/schema#', diff --git a/compendium_v2/routes/data_entry.py b/compendium_v2/routes/data_entry.py new file mode 100644 index 0000000000000000000000000000000000000000..c2a03b59dd2505ce3f94fa16636d6db7ca42e036 --- /dev/null +++ b/compendium_v2/routes/data_entry.py @@ -0,0 +1,228 @@ +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)) diff --git a/config-example.json b/config-example.json index 03c0451aa13163afe7ee0cbea79c32db9106b1c7..5a9ddebf6d02d12a1f3d9e7036d22ea85fc98ba3 100644 --- a/config-example.json +++ b/config-example.json @@ -1,3 +1,3 @@ { - "SQLALCHEMY_DATABASE_URI": "psql://username:password@hostname/db_name" + "SQLALCHEMY_DATABASE_URI": "postgresql://compendium_v2:password@localhost/compendium_v2" } diff --git a/test/test_routes.py b/test/test_routes.py index 2e42f3e438e6eddb5215e25af7e343d605bb9b4e..5d2d12d62d5ce4f87f07cdad3cbb1e158d4cb96a 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -4,6 +4,9 @@ import jsonschema import pytest 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 @@ -18,7 +21,6 @@ def test_bad_accept(endpoint, client): def test_version_request(client): - rv = client.post( 'version', headers={'Accept': ['application/json']}) @@ -28,10 +30,36 @@ def test_version_request(client): def test_things(client): - rv = client.post( 'api/things', headers={'Accept': ['application/json']}) assert rv.status_code == 200 result = json.loads(rv.data.decode('utf-8')) 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)