From 690e73fa1712088210fe51529cf8c69189c7f9ef Mon Sep 17 00:00:00 2001 From: John Harvey <10814889+john681611@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:09:51 +0100 Subject: [PATCH] Split ga response (#434) * add time metrics * map analysis through redis * move hash to utils and make db method write to redis optionally * lint * Added: Front end support for weaker links calls * Added: DB seperation for smaller inital response and cache extras * Added: DB side of part split response logic * Refactor and tests --------- Co-authored-by: Spyros --- .gitignore | 3 +- application/database/db.py | 47 ++- application/frontend/src/const.ts | 2 + .../src/pages/GapAnalysis/GapAnalysis.tsx | 86 +++--- application/frontend/src/types.ts | 2 + application/tests/db_test.py | 271 +++++++++++++++++- application/tests/web_main_test.py | 136 ++++++++- application/web/web_main.py | 20 +- 8 files changed, 491 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index a1f5606a8..ace3d96b2 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ coverage/ standards_cache.sqlite ### Neo4j -neo4j/ \ No newline at end of file +neo4j/ +.neo4j/ \ No newline at end of file diff --git a/application/database/db.py b/application/database/db.py index 0a4a54653..5ffd454f8 100644 --- a/application/database/db.py +++ b/application/database/db.py @@ -494,15 +494,12 @@ def format_path_record(rec): @classmethod def standards(self) -> List[str]: - tools = [] + results = [] for x in db.cypher_query("""MATCH (n:NeoTool) RETURN DISTINCT n.name""")[0]: - tools.extend(x) - standards = [] - for x in db.cypher_query("""MATCH (n:NeoStandard) RETURN DISTINCT n.name""")[ - 0 - ]: # 0 is the results, 1 is the "n.name" param - standards.extend(x) - return list(set([x for x in tools] + [x for x in standards])) + results.extend(x) + for x in db.cypher_query("""MATCH (n:NeoStandard) RETURN DISTINCT n.name""")[0]: + results.extend(x) + return list(set(results)) @staticmethod def parse_node(node: NeoDocument) -> cre_defs.Document: @@ -1774,21 +1771,38 @@ def gap_analysis( if base_standard is None: return None grouped_paths = {} + extra_paths_dict = {} + GA_STRONG_UPPER_LIMIT = 2 + for node in base_standard: key = node.id if key not in grouped_paths: - grouped_paths[key] = {"start": node, "paths": {}} + grouped_paths[key] = {"start": node, "paths": {}, "extra": 0} + extra_paths_dict[key] = {"paths": {}} for path in paths: key = path["start"].id end_key = path["end"].id path["score"] = get_path_score(path) del path["start"] - if end_key in grouped_paths[key]["paths"]: - if grouped_paths[key]["paths"][end_key]["score"] > path["score"]: + if path["score"] <= GA_STRONG_UPPER_LIMIT: + if end_key in extra_paths_dict[key]["paths"]: + del extra_paths_dict[key]["paths"][end_key] + grouped_paths[key]["extra"] -= 1 + if end_key in grouped_paths[key]["paths"]: + if grouped_paths[key]["paths"][end_key]["score"] > path["score"]: + grouped_paths[key]["paths"][end_key] = path + else: grouped_paths[key]["paths"][end_key] = path else: - grouped_paths[key]["paths"][end_key] = path + if end_key in grouped_paths[key]["paths"]: + continue + if end_key in extra_paths_dict[key]: + if extra_paths_dict[key]["paths"][end_key]["score"] > path["score"]: + extra_paths_dict[key]["paths"][end_key] = path + else: + extra_paths_dict[key]["paths"][end_key] = path + grouped_paths[key]["extra"] += 1 if ( store_in_cache @@ -1799,6 +1813,11 @@ def gap_analysis( cache_key = make_array_hash(node_names) conn.set(cache_key, flask_json.dumps({"result": grouped_paths})) - return (node_names, {}) + for key in extra_paths_dict: + conn.set( + cache_key + "->" + key, + flask_json.dumps({"result": extra_paths_dict[key]}), + ) + return (node_names, {}, {}) - return (node_names, grouped_paths) + return (node_names, grouped_paths, extra_paths_dict) diff --git a/application/frontend/src/const.ts b/application/frontend/src/const.ts index 6f176723f..8022b9138 100644 --- a/application/frontend/src/const.ts +++ b/application/frontend/src/const.ts @@ -37,3 +37,5 @@ export const GRAPH = '/graph'; export const DEEPLINK = '/deeplink'; export const BROWSEROOT = '/root_cres'; export const GAP_ANALYSIS = '/map_analysis'; + +export const GA_STRONG_UPPER_LIMIT = 2; // remember to change this in the Python code too diff --git a/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx b/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx index c5b517d4c..4f2d12a5f 100644 --- a/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx +++ b/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx @@ -1,20 +1,10 @@ import axios from 'axios'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; -import { - Accordion, - Button, - Container, - Dropdown, - DropdownItemProps, - Grid, - Icon, - Label, - Popup, - Table, -} from 'semantic-ui-react'; +import { Button, Dropdown, DropdownItemProps, Icon, Popup, Table } from 'semantic-ui-react'; import { LoadingAndErrorIndicator } from '../../components/LoadingAndErrorIndicator'; +import { GA_STRONG_UPPER_LIMIT } from '../../const'; import { useEnvironment } from '../../hooks'; import { GapAnalysisPathStart } from '../../types'; import { getDocumentDisplayName } from '../../utils'; @@ -51,14 +41,14 @@ function useQuery() { const GetStrength = (score) => { if (score == 0) return 'Direct'; - if (score <= 2) return 'Strong'; + if (score <= GA_STRONG_UPPER_LIMIT) return 'Strong'; if (score >= 20) return 'Weak'; return 'Average'; }; const GetStrengthColor = (score) => { if (score === 0) return 'darkgreen'; - if (score <= 2) return '#93C54B'; + if (score <= GA_STRONG_UPPER_LIMIT) return '#93C54B'; if (score >= 20) return 'Red'; return 'Orange'; }; @@ -100,8 +90,10 @@ const GetResultLine = (path, gapAnalysis, key) => {
{GetStrength(0)}: Directly Linked
- {GetStrength(2)}: Closely connected likely to have - majority overlap + + {GetStrength(GA_STRONG_UPPER_LIMIT)} + + : Closely connected likely to have majority overlap
{GetStrength(6)}: Connected likely to have partial overlap @@ -127,21 +119,12 @@ export const GapAnalysis = () => { ); const [gaJob, setgaJob] = useState(''); const [gapAnalysis, setGapAnalysis] = useState>(); - const [activeIndex, SetActiveIndex] = useState(); const [loadingStandards, setLoadingStandards] = useState(false); const [loadingGA, setLoadingGA] = useState(false); const [error, setError] = useState(null); const { apiUrl } = useEnvironment(); const timerIdRef = useRef(); - const GetStrongPathsCount = (paths) => - Math.max( - Object.values(paths).filter( - (x) => GetStrength(x.score) === 'Strong' || GetStrength(x.score) === 'Direct' - ).length, - 3 - ); - useEffect(() => { const fetchData = async () => { const result = await axios.get(`${apiUrl}/standards`); @@ -225,11 +208,20 @@ export const GapAnalysis = () => { }); }, [BaseStandard, CompareStandard, setGapAnalysis, setLoadingGA, setError]); - const handleAccordionClick = (e, titleProps) => { - const { index } = titleProps; - const newIndex = activeIndex === index ? -1 : index; - SetActiveIndex(newIndex); - }; + const getWeakLinks = useCallback( + async (key) => { + if (!gapAnalysis) return; + const result = await axios.get( + `${apiUrl}/map_analysis_weak_links?standard=${BaseStandard}&standard=${CompareStandard}&key=${key}` + ); + if (result.data.result) { + gapAnalysis[key].weakLinks = result.data.result.paths; + setGapAnalysis(undefined); //THIS HAS TO BE THE WRONG WAY OF DOING THIS + setGapAnalysis(gapAnalysis); + } + }, + [gapAnalysis, setGapAnalysis] + ); return (
@@ -299,29 +291,19 @@ export const GapAnalysis = () => { {Object.values(gapAnalysis[key].paths) .sort((a, b) => a.score - b.score) - .slice(0, GetStrongPathsCount(gapAnalysis[key].paths)) .map((path) => GetResultLine(path, gapAnalysis, key))} - {Object.keys(gapAnalysis[key].paths).length > 3 && ( - - - - - - {Object.values(gapAnalysis[key].paths) - .sort((a, b) => a.score - b.score) - .slice( - GetStrongPathsCount(gapAnalysis[key].paths), - Object.keys(gapAnalysis[key].paths).length - ) - .map((path) => GetResultLine(path, gapAnalysis, key))} - - + {gapAnalysis[key].weakLinks && + Object.values(gapAnalysis[key].weakLinks) + .sort((a, b) => a.score - b.score) + .map((path) => GetResultLine(path, gapAnalysis, key))} + {gapAnalysis[key].extra > 0 && !gapAnalysis[key].weakLinks && ( + + )} + {Object.keys(gapAnalysis[key].paths).length === 0 && gapAnalysis[key].extra === 0 && ( + No links Found )} - {Object.keys(gapAnalysis[key].paths).length === 0 && No links Found} ))} diff --git a/application/frontend/src/types.ts b/application/frontend/src/types.ts index f8d2e1df7..dc8e5f977 100644 --- a/application/frontend/src/types.ts +++ b/application/frontend/src/types.ts @@ -39,4 +39,6 @@ interface GapAnalysisPath { export interface GapAnalysisPathStart { start: Document; paths: Record; + extra: number; + weakLinks: Record; } diff --git a/application/tests/db_test.py b/application/tests/db_test.py index 6ab6402c9..5142d5011 100644 --- a/application/tests/db_test.py +++ b/application/tests/db_test.py @@ -3,13 +3,14 @@ import os import tempfile import unittest +from unittest import mock from unittest.mock import patch import uuid from copy import copy, deepcopy from pprint import pprint -from pydoc import doc from typing import Any, Dict, List, Union -import neo4j +import redis +from flask import json as flask_json import yaml from application import create_app, sqla # type: ignore @@ -1155,7 +1156,7 @@ def test_gap_analysis_no_nodes(self, gap_mock): gap_mock.return_value = ([], []) self.assertEqual( - db.gap_analysis(collection.neo_db, ["a", "b"]), (["a", "b"], {}) + db.gap_analysis(collection.neo_db, ["a", "b"]), (["a", "b"], {}, {}) ) @patch.object(db.NEO_DB, "gap_analysis") @@ -1166,7 +1167,11 @@ def test_gap_analysis_no_links(self, gap_mock): gap_mock.return_value = ([defs.CRE(name="bob", id=1)], []) self.assertEqual( db.gap_analysis(collection.neo_db, ["a", "b"]), - (["a", "b"], {1: {"start": defs.CRE(name="bob", id=1), "paths": {}}}), + ( + ["a", "b"], + {1: {"start": defs.CRE(name="bob", id=1), "paths": {}, "extra": 0}}, + {1: {"paths": {}}}, + ), ) @patch.object(db.NEO_DB, "gap_analysis") @@ -1203,9 +1208,61 @@ def test_gap_analysis_one_link(self, gap_mock): "paths": { 2: {"end": defs.CRE(name="bob", id=2), "path": path, "score": 0} }, + "extra": 0, + } + }, + {1: {"paths": {}}}, + ) + self.assertEqual(db.gap_analysis(collection.neo_db, ["a", "b"]), expected) + + @patch.object(db.NEO_DB, "gap_analysis") + def test_gap_analysis_one_weak_link(self, gap_mock): + collection = db.Node_collection() + collection.neo_db.connected = True + path = [ + { + "end": defs.CRE(name="bob", id=1), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id=1), + }, + { + "end": defs.CRE(name="bob", id=1), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id=2), + }, + { + "end": defs.CRE(name="bob", id=3), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id=2), + }, + ] + gap_mock.return_value = ( + [defs.CRE(name="bob", id=1)], + [ + { + "start": defs.CRE(name="bob", id=1), + "end": defs.CRE(name="bob", id=2), + "path": path, + } + ], + ) + expected = ( + ["a", "b"], + {1: {"start": defs.CRE(name="bob", id=1), "paths": {}, "extra": 1}}, + { + 1: { + "paths": { + 2: {"end": defs.CRE(name="bob", id=2), "path": path, "score": 4} + } } }, ) + self.assertEqual(db.gap_analysis(collection.neo_db, ["a", "b"]), expected) @patch.object(db.NEO_DB, "gap_analysis") @@ -1259,8 +1316,75 @@ def test_gap_analysis_duplicate_link_path_existing_lower(self, gap_mock): "paths": { 2: {"end": defs.CRE(name="bob", id=2), "path": path, "score": 0} }, - } + "extra": 0, + }, }, + {1: {"paths": {}}}, + ) + self.assertEqual(db.gap_analysis(collection.neo_db, ["a", "b"]), expected) + + @patch.object(db.NEO_DB, "gap_analysis") + def test_gap_analysis_duplicate_link_path_existing_lower_new_in_extras( + self, gap_mock + ): + collection = db.Node_collection() + collection.neo_db.connected = True + path = [ + { + "end": defs.CRE(name="bob", id=1), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + ] + path2 = [ + { + "end": defs.CRE(name="bob", id=1), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id="a"), + }, + ] + gap_mock.return_value = ( + [defs.CRE(name="bob", id=1)], + [ + { + "start": defs.CRE(name="bob", id=1), + "end": defs.CRE(name="bob", id=2), + "path": path, + }, + { + "start": defs.CRE(name="bob", id=1), + "end": defs.CRE(name="bob", id=2), + "path": path2, + }, + ], + ) + expected = ( + ["a", "b"], + { + 1: { + "start": defs.CRE(name="bob", id=1), + "paths": { + 2: {"end": defs.CRE(name="bob", id=2), "path": path, "score": 0} + }, + "extra": 0, + }, + }, + {1: {"paths": {}}}, ) self.assertEqual(db.gap_analysis(collection.neo_db, ["a", "b"]), expected) @@ -1315,11 +1439,148 @@ def test_gap_analysis_duplicate_link_path_existing_higher(self, gap_mock): "paths": { 2: {"end": defs.CRE(name="bob", id=2), "path": path, "score": 0} }, + "extra": 0, } }, + {1: {"paths": {}}}, ) self.assertEqual(db.gap_analysis(collection.neo_db, ["a", "b"]), expected) + @patch.object(db.NEO_DB, "gap_analysis") + def test_gap_analysis_duplicate_link_path_existing_higher_and_in_extras( + self, gap_mock + ): + collection = db.Node_collection() + collection.neo_db.connected = True + path = [ + { + "end": defs.CRE(name="bob", id=1), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + ] + path2 = [ + { + "end": defs.CRE(name="bob", id=1), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id="a"), + }, + ] + gap_mock.return_value = ( + [defs.CRE(name="bob", id=1)], + [ + { + "start": defs.CRE(name="bob", id=1), + "end": defs.CRE(name="bob", id=2), + "path": path2, + }, + { + "start": defs.CRE(name="bob", id=1), + "end": defs.CRE(name="bob", id=2), + "path": path, + }, + ], + ) + expected = ( + ["a", "b"], + { + 1: { + "start": defs.CRE(name="bob", id=1), + "paths": { + 2: {"end": defs.CRE(name="bob", id=2), "path": path, "score": 0} + }, + "extra": 0, + } + }, + {1: {"paths": {}}}, + ) + self.assertEqual(db.gap_analysis(collection.neo_db, ["a", "b"]), expected) + + @patch.object(redis, "from_url") + @patch.object(db.NEO_DB, "gap_analysis") + def test_gap_analysis_dump_to_cache(self, gap_mock, redis_conn_mock): + collection = db.Node_collection() + collection.neo_db.connected = True + path = [ + { + "end": defs.CRE(name="bob", id=1), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id="a"), + }, + { + "end": defs.CRE(name="bob", id=2), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id=1), + }, + { + "end": defs.CRE(name="bob", id=1), + "relationship": "RELATED", + "start": defs.CRE(name="bob", id=2), + }, + { + "end": defs.CRE(name="bob", id=3), + "relationship": "LINKED_TO", + "start": defs.CRE(name="bob", id=2), + }, + ] + gap_mock.return_value = ( + [defs.CRE(name="bob", id="a")], + [ + { + "start": defs.CRE(name="bob", id="a"), + "end": defs.CRE(name="bob", id="b"), + "path": path, + } + ], + ) + + expected_response = ( + ["a", "b"], + {"a": {"start": defs.CRE(name="bob", id="a"), "paths": {}, "extra": 1}}, + { + "a": { + "paths": { + "b": { + "end": defs.CRE(name="bob", id="b"), + "path": path, + "score": 4, + } + } + } + }, + ) + response = db.gap_analysis(collection.neo_db, ["a", "b"], True) + + self.assertEqual(response, (expected_response[0], {}, {})) + + redis_conn_mock.return_value.set.assert_has_calls( + [ + mock.call( + "d8160c9b3dc20d4e931aeb4f45262155", + flask_json.dumps({"result": expected_response[1]}), + ), + mock.call( + "d8160c9b3dc20d4e931aeb4f45262155->a", + flask_json.dumps({"result": expected_response[2]["a"]}), + ), + ] + ) + def test_neo_db_parse_node_code(self): name = "name" id = "id" diff --git a/application/tests/web_main_test.py b/application/tests/web_main_test.py index f35492168..3540d41db 100644 --- a/application/tests/web_main_test.py +++ b/application/tests/web_main_test.py @@ -4,15 +4,22 @@ import os import tempfile import unittest -from pprint import pprint -from typing import Any, Dict, List +from unittest.mock import patch + +import redis +import rq from application import create_app, sqla # type: ignore from application.database import db from application.defs import cre_defs as defs from application.defs import osib_defs from application.web import web_main -from application.utils import mdutils + + +class MockJob: + @property + def id(self): + return "ABC" class TestMain(unittest.TestCase): @@ -570,3 +577,126 @@ def test_smartlink(self) -> None: location, "https://cwe.mitre.org/data/definitions/999.html" ) self.assertEqual(302, response.status_code) + + @patch.object(redis, "from_url") + def test_gap_analysis_from_cache_full_response(self, redis_conn_mock) -> None: + expected = {"result": "hello"} + redis_conn_mock.return_value.exists.return_value = True + redis_conn_mock.return_value.get.return_value = json.dumps(expected) + with self.app.test_client() as client: + response = client.get( + "/rest/v1/map_analysis?standard=aaa&standard=bbb", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, json.loads(response.data)) + + @patch.object(rq.Queue, "enqueue_call") + @patch.object(redis, "from_url") + def test_gap_analysis_from_cache_job_id( + self, redis_conn_mock, enqueue_call_mock + ) -> None: + expected = {"job_id": "hello"} + redis_conn_mock.return_value.exists.return_value = True + redis_conn_mock.return_value.get.return_value = json.dumps(expected) + with self.app.test_client() as client: + response = client.get( + "/rest/v1/map_analysis?standard=aaa&standard=bbb", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, json.loads(response.data)) + self.assertFalse(enqueue_call_mock.called) + + @patch.object(db, "Node_collection") + @patch.object(rq.Queue, "enqueue_call") + @patch.object(redis, "from_url") + def test_gap_analysis_create_job_id( + self, redis_conn_mock, enqueue_call_mock, db_mock + ) -> None: + expected = {"job_id": "ABC"} + redis_conn_mock.return_value.exists.return_value = False + enqueue_call_mock.return_value = MockJob() + with self.app.test_client() as client: + response = client.get( + "/rest/v1/map_analysis?standard=aaa&standard=bbb", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, json.loads(response.data)) + enqueue_call_mock.assert_called_with( + db.gap_analysis, + kwargs={ + "neo_db": db_mock().neo_db, + "node_names": ["aaa", "bbb"], + "store_in_cache": True, + "cache_key": "7aa45d88f69a131890f8e4a769bbb07b", + }, + ) + redis_conn_mock.return_value.set.assert_called_with( + "7aa45d88f69a131890f8e4a769bbb07b", '{"job_id": "ABC", "result": ""}' + ) + + @patch.object(redis, "from_url") + def test_standards_from_cache(self, redis_conn_mock) -> None: + expected = ["A", "B"] + redis_conn_mock.return_value.exists.return_value = True + redis_conn_mock.return_value.get.return_value = json.dumps(expected) + with self.app.test_client() as client: + response = client.get( + "/rest/v1/standards", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, json.loads(response.data)) + + @patch.object(redis, "from_url") + @patch.object(db, "Node_collection") + def test_standards_from_db(self, node_mock, redis_conn_mock) -> None: + expected = ["A", "B"] + redis_conn_mock.return_value.get.return_value = None + node_mock.return_value.standards.return_value = expected + with self.app.test_client() as client: + response = client.get( + "/rest/v1/standards", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, json.loads(response.data)) + + @patch.object(redis, "from_url") + @patch.object(db, "Node_collection") + def test_standards_from_db_off(self, node_mock, redis_conn_mock) -> None: + expected = { + "message": "Backend services connected to this feature are not running at the moment." + } + redis_conn_mock.return_value.get.return_value = None + node_mock.return_value.standards.return_value = None + with self.app.test_client() as client: + response = client.get( + "/rest/v1/standards", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(500, response.status_code) + self.assertEqual(expected, json.loads(response.data)) + + def test_gap_analysis_weak_links_no_cache(self) -> None: + with self.app.test_client() as client: + response = client.get( + "/rest/v1/map_analysis_weak_links?standard=aaa&standard=bbb&key=ccc`", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(404, response.status_code) + + @patch.object(redis, "from_url") + def test_gap_analysis_weak_links_response(self, redis_conn_mock) -> None: + expected = {"result": "hello"} + redis_conn_mock.return_value.exists.return_value = True + redis_conn_mock.return_value.get.return_value = json.dumps(expected) + with self.app.test_client() as client: + response = client.get( + "/rest/v1/map_analysis_weak_links?standard=aaa&standard=bbb&key=ccc`", + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, json.loads(response.data)) diff --git a/application/web/web_main.py b/application/web/web_main.py index abeb10ca1..cdd5fd593 100644 --- a/application/web/web_main.py +++ b/application/web/web_main.py @@ -249,6 +249,24 @@ def gap_analysis() -> Any: # TODO (spyros): add export result to spreadsheet return jsonify({"job_id": gap_analysis_job.id}) +@app.route("/rest/v1/map_analysis_weak_links", methods=["GET"]) +@cache.cached(timeout=50, query_string=True) +def gap_analysis_weak_links() -> Any: + standards = request.args.getlist("standard") + key = request.args.get("key") + redis_url = os.getenv("REDIS_URL", "redis://localhost:6379") + conn = redis.from_url(redis_url) + standards_hash = make_array_hash(standards) + cache_key = standards_hash + "->" + key + if conn.exists(cache_key): + gap_analysis_results = conn.get(cache_key) + if gap_analysis_results: + gap_analysis_dict = json.loads(gap_analysis_results) + if gap_analysis_dict.get("result"): + return jsonify({"result": gap_analysis_dict.get("result")}) + abort(404, "No such Cache") + + @app.route("/rest/v1/ma_job_results", methods=["GET"]) def fetch_job() -> Any: logger.info("fetching job results") @@ -319,7 +337,7 @@ def standards() -> Any: database = db.Node_collection() standards = database.standards() if standards is None: - neo4j_not_running_rejection() + return neo4j_not_running_rejection() conn.set("NodeNames", flask_json.dumps(standards)) return standards