From fafb81fb7a9a3930ae178252198472b525bd8018 Mon Sep 17 00:00:00 2001 From: Luigi Pellecchia Date: Thu, 12 Sep 2024 23:38:44 +0200 Subject: [PATCH] Show and manage unmapped Document. Avoid to increate the api version in case of last_coverage (field used as cached coverage value) changes. Fix warning of Document work items. Signed-off-by: Luigi Pellecchia --- api/api.py | 42 +++++++++++++++++-- .../app/Dashboard/Modal/APICheckSpecModal.tsx | 38 +++++++++++++++++ app/src/app/Mapping/Mapping.tsx | 14 ++++++- app/src/app/Mapping/MappingListingTable.tsx | 9 +++- .../app/Mapping/Menu/UnmappedMenuKebab.tsx | 9 ++++ db/models/api.py | 17 +++++++- 6 files changed, 122 insertions(+), 7 deletions(-) diff --git a/api/api.py b/api/api.py index 42a4773..bfe7412 100644 --- a/api/api.py +++ b/api/api.py @@ -752,6 +752,7 @@ def get_api_sw_requirements_mapping_sections(dbi, api): mapped_sections = get_split_sections(api_specification, mapping, [_SR, _J, _D]) unmapped_sections = [x for x in mapping[_SRs] if not x['match']] unmapped_sections += [x for x in mapping[_Js] if not x['match']] + unmapped_sections += [x for x in mapping[_Ds] if not x['match']] for iMS in range(len(mapped_sections)): for iSR in range(len(mapped_sections[iMS][_SRs])): @@ -777,9 +778,15 @@ def check_direct_work_items_against_another_spec_file(db_session, spec, api): 'warning': []}, 'justifications': {'ok': [], 'ko': [], - 'warning': []} + 'warning': []}, + 'documents': {'ok': [], + 'ko': [], + 'warning': []} } + if not spec: + return ret + # ApiSwRequirement api_srs = db_session.query(ApiSwRequirementModel).filter( ApiSwRequirementModel.api_id == api.id @@ -853,6 +860,26 @@ def check_direct_work_items_against_another_spec_file(db_session, spec, api): else: ret['justifications']['ko'].append({'id': api_j.id, 'title': api_j.justification.description}) + + # ApiDocument + api_docs = db_session.query(ApiDocumentModel).filter( + ApiDocumentModel.api_id == api.id + ).all() + for api_doc in api_docs: + if api_doc.section in spec: + if spec.index(api_doc.section) == api_doc.offset: + ret['documents']['ok'].append({'id': api_doc.id, + 'title': api_doc.document.title}) + else: + ret['documents']['warning'].append({'id': api_doc.id, + 'old-offset': api_doc.offset, + 'new-offset': spec.index( + api_doc.section), + 'title': api_doc.document.title}) + else: + ret['documents']['ko'].append({'id': api_doc.id, + 'title': api_doc.document.title}) + return ret @@ -1180,6 +1207,15 @@ def get(self): j_all[0].offset = analysis['justifications']['warning'][i]['new-offset'] dbi.session.commit() + # ApiDocument + for i in range(len(analysis['documents']['warning'])): + doc_all = dbi.session.query(ApiDocumentModel).filter( + ApiDocumentModel.id == analysis['documents']['warning'][i]['id'] + ).all() + if len(doc_all) == 1: + doc_all[0].offset = analysis['documents']['warning'][i]['new-offset'] + dbi.session.commit() + dbi.engine.dispose() return True @@ -1765,6 +1801,7 @@ def get(self): mapped_sections = get_split_sections(api_specification, mapping, [_TS, _J, _D]) unmapped_sections = [x for x in mapping[_TSs] if not x['match']] unmapped_sections += [x for x in mapping[_Js] if not x['match']] + unmapped_sections += [x for x in mapping[_Ds] if not x['match']] ret = {'mapped': mapped_sections, 'unmapped': unmapped_sections} @@ -2081,6 +2118,7 @@ def get(self): mapped_sections = get_split_sections(api_specification, mapping, [_TC, _J, _D]) unmapped_sections = [x for x in mapping[_TCs] if not x['match']] unmapped_sections += [x for x in mapping[_Js] if not x['match']] + unmapped_sections += [x for x in mapping[_Ds] if not x['match']] ret = {'mapped': mapped_sections, 'unmapped': unmapped_sections} @@ -3133,8 +3171,6 @@ def put(self): # Update only modified fields modified_d = False request_document_data = get_dict_with_db_format_keys(request_data["document"]) - print(f"request_document_data: {request_document_data}") - print(f"document: {document}") for field in document_fields: if field in request_document_data.keys(): if getattr(document, field) != request_document_data[field]: diff --git a/app/src/app/Dashboard/Modal/APICheckSpecModal.tsx b/app/src/app/Dashboard/Modal/APICheckSpecModal.tsx index f8079e3..7168f11 100644 --- a/app/src/app/Dashboard/Modal/APICheckSpecModal.tsx +++ b/app/src/app/Dashboard/Modal/APICheckSpecModal.tsx @@ -35,6 +35,9 @@ export const CheckSpecResults: React.FunctionComponent = const [showJustificationsOk, setShowJustificationsOk] = React.useState(false) const [showJustificationsKo, setShowJustificationsKo] = React.useState(false) const [showJustificationsWarning, setShowJustificationsWarning] = React.useState(false) + const [showDocumentsOk, setShowDocumentsOk] = React.useState(false) + const [showDocumentsKo, setShowDocumentsKo] = React.useState(false) + const [showDocumentsWarning, setShowDocumentsWarning] = React.useState(false) React.useEffect(() => { setShowSwRequirementsOk(false) @@ -49,6 +52,9 @@ export const CheckSpecResults: React.FunctionComponent = setShowJustificationsOk(false) setShowJustificationsKo(false) setShowJustificationsWarning(false) + setShowDocumentsOk(false) + setShowDocumentsKo(false) + setShowDocumentsWarning(false) if (checkResultData['sw-requirements']['ok'].length > 0) { setShowSwRequirementsOk(true) @@ -89,6 +95,16 @@ export const CheckSpecResults: React.FunctionComponent = if (checkResultData['justifications']['warning'].length > 0) { setShowJustificationsWarning(true) } + + if (checkResultData['documents']['ok'].length > 0) { + setShowDocumentsOk(true) + } + if (checkResultData['documents']['ko'].length > 0) { + setShowDocumentsKo(true) + } + if (checkResultData['documents']['warning'].length > 0) { + setShowDocumentsWarning(true) + } }, [checkResultData]) return ( @@ -181,6 +197,28 @@ export const CheckSpecResults: React.FunctionComponent = ))} + + + Documents + {showDocumentsOk ? * OK : } + {checkResultData['documents']['ok'].map((item, index) => ( + + {item.id} - {item.title} + + ))} + {showDocumentsKo ? * KO : } + {checkResultData['documents']['ko'].map((item, index) => ( + + {item.id} - {item.title} + + ))} + {showDocumentsWarning ? * WARNING : } + {checkResultData['documents']['warning'].map((item, index) => ( + + {item.id} - {item.title} + + ))} + ) diff --git a/app/src/app/Mapping/Mapping.tsx b/app/src/app/Mapping/Mapping.tsx index 45aa105..28151b4 100644 --- a/app/src/app/Mapping/Mapping.tsx +++ b/app/src/app/Mapping/Mapping.tsx @@ -14,7 +14,7 @@ const Mapping: React.FunctionComponent = () => { const [apiData, setApiData] = React.useState(null) const [mappingData, setMappingData] = React.useState([]) const [unmappingData, setUnmappingData] = React.useState([]) - const [totalCoverage, setTotalCoverage] = React.useState(0) + const [totalCoverage, setTotalCoverage] = React.useState(-1) const { api_id } = useParams<{ api_id: string }>() //view @@ -88,11 +88,17 @@ const Mapping: React.FunctionComponent = () => { if (mappingData == null) { return } + if (mappingData.length == 0) { + return + } let total_len = 0 let wa = 0 for (let i = 0; i < mappingData['length']; i++) { total_len = total_len + mappingData[i]['section']['length'] } + if (total_len == 0) { + return + } for (let i = 0; i < mappingData['length']; i++) { wa = wa + (mappingData[i]['section']['length'] / total_len) * (mappingData[i]['covered'] / 100.0) } @@ -101,6 +107,12 @@ const Mapping: React.FunctionComponent = () => { }, [mappingData]) const updateLastCoverage = (new_coverage) => { + if (apiData == null) { + return + } + if (new_coverage == apiData['last_coverage']) { + return + } let data = { 'api-id': api_id, 'last-coverage': new_coverage } if (auth.isLogged()) { diff --git a/app/src/app/Mapping/MappingListingTable.tsx b/app/src/app/Mapping/MappingListingTable.tsx index fcb7f5f..2f262bd 100644 --- a/app/src/app/Mapping/MappingListingTable.tsx +++ b/app/src/app/Mapping/MappingListingTable.tsx @@ -102,8 +102,8 @@ const MappingListingTable: React.FunctionComponent = ( const auth = useAuth() const getWorkItemDescription = (_work_item_type) => { - const work_item_types = [Constants._A, Constants._J, Constants._SR, Constants._TS, Constants._TC] - const work_item_descriptions = [Constants._A, Constants._J, 'Software Requirement', 'Test Specification', 'Test Case'] + const work_item_types = [Constants._A, Constants._J, Constants._D, Constants._SR, Constants._TS, Constants._TC] + const work_item_descriptions = [Constants._A, Constants._J, Constants._D, 'Software Requirement', 'Test Specification', 'Test Case'] return work_item_descriptions[work_item_types.indexOf(_work_item_type)] } @@ -829,6 +829,10 @@ const MappingListingTable: React.FunctionComponent = ( work_item_type = Constants._J work_item_description = snippet[Constants._J]['description'] work_item_id = snippet[Constants._J]['id'] + } else if (Object.prototype.hasOwnProperty.call(snippet, 'document')) { + work_item_type = Constants._D + work_item_description = snippet[Constants._D]['title'] + work_item_id = snippet[Constants._D]['id'] } else if (Object.prototype.hasOwnProperty.call(snippet, 'sw_requirement')) { work_item_type = Constants._SR work_item_description = snippet[Constants._SR_]['title'] @@ -867,6 +871,7 @@ const MappingListingTable: React.FunctionComponent = ( api={api} srModalShowState={srModalShowState} setDeleteModalInfo={setDeleteModalInfo} + setDocModalInfo={setDocModalInfo} setTcModalInfo={setTcModalInfo} setTsModalInfo={setTsModalInfo} setSrModalInfo={setSrModalInfo} diff --git a/app/src/app/Mapping/Menu/UnmappedMenuKebab.tsx b/app/src/app/Mapping/Menu/UnmappedMenuKebab.tsx index efb42c3..eaf66d7 100644 --- a/app/src/app/Mapping/Menu/UnmappedMenuKebab.tsx +++ b/app/src/app/Mapping/Menu/UnmappedMenuKebab.tsx @@ -9,6 +9,7 @@ import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-ico export interface UnmappedMenuKebabProps { srModalShowState setDeleteModalInfo + setDocModalInfo setTcModalInfo setTsModalInfo setSrModalInfo @@ -24,6 +25,7 @@ export interface UnmappedMenuKebabProps { export const UnmappedMenuKebab: React.FunctionComponent = ({ setDeleteModalInfo, + setDocModalInfo, setTcModalInfo, setTsModalInfo, setSrModalInfo, @@ -60,6 +62,13 @@ export const UnmappedMenuKebab: React.FunctionComponent //list = list_item; //list = mappingList[mappingIndex] setJModalInfo(true, 'edit', api, mappingSection, mappingOffset, mappingList, mappingIndex) + } else if (mappingType == Constants._D) { + //list_item = mappingList[mappingIndex]; + //list_item['coverage'] = mappingList[mappingIndex]['coverage']; + //list_item['relation_id'] = mappingList[mappingIndex]['relation_id']; + //list = [list_item]; + //list = mappingList[mappingIndex] + setDocModalInfo(true, 'edit', api, mappingSection, mappingOffset, mappingList, mappingIndex) } else if (mappingType == Constants._SR) { //list_item = mappingList[mappingIndex]; //list_item['coverage'] = mappingList[mappingIndex]['coverage']; diff --git a/db/models/api.py b/db/models/api.py index 3d377eb..e08dda7 100644 --- a/db/models/api.py +++ b/db/models/api.py @@ -2,7 +2,7 @@ from db.models.db_base import Base from db.models.user import UserModel from sqlalchemy import BigInteger, DateTime, Integer, String -from sqlalchemy import event, insert, select +from sqlalchemy import event, insert, inspect, select from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column @@ -147,6 +147,21 @@ def as_dict(self, full_data=False, db_session=None): @event.listens_for(ApiModel, "after_update") def receive_after_update(mapper, connection, target): + # Avoid to update the version if the only change is related to last_coverage + state = inspect(target) + changes = {} + for attr in state.attrs: + hist = state.get_history(attr.key, True) + old_value = hist.deleted[0] if hist.deleted else None + new_value = hist.added[0] if hist.added else None + if old_value is not None or new_value is not None: + if old_value != new_value: + changes[attr.key] = [old_value, new_value] + affected_fields = list(changes.keys()) + if len(affected_fields) == 1: + if affected_fields[0] == 'last_coverage': + return + last_query = select(ApiHistoryModel.version).where(ApiHistoryModel.id == target.id).order_by(ApiHistoryModel.version.desc()).limit(1) version = -1