From 282f0f848fe5d473e36e91f8f58b0fc1da42db65 Mon Sep 17 00:00:00 2001 From: Randy Woods Date: Tue, 11 Feb 2025 07:49:18 -0700 Subject: [PATCH] Refactored Observation data gathering to support unassigned --- .../Reports/ObservationsToExcel.cs | 11 +- .../ReportsData/ReportsDataBusiness.cs | 145 ++++++++++++------ .../Reports/ReportsData/ReportsUtils.cs | 24 +-- .../Reports/IReportsDataBusiness.cs | 3 + .../Reports/BasicReportData.cs | 12 +- .../Reports/ObservationIngredients.cs | 20 +++ .../Controllers/ReportsController.cs | 3 + .../results/reports/reports.component.ts | 9 +- .../observation-tearouts.component.html | 4 +- 9 files changed, 158 insertions(+), 73 deletions(-) create mode 100644 CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/ObservationIngredients.cs diff --git a/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ObservationsToExcel.cs b/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ObservationsToExcel.cs index 02b1818324..94c6dc218b 100644 --- a/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ObservationsToExcel.cs +++ b/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ObservationsToExcel.cs @@ -95,7 +95,7 @@ public void GenerateSpreadsheet(int assessmentId, MemoryStream ms) _report.SetReportsAssessmentId(assessmentId); BasicReportData data = new BasicReportData(); data.information = _report.GetInformation(); - data.Individuals = _report.GetObservationIndividuals().OrderBy(x => x.FullName).ToList(); + data.Individuals = _report.GetObservationIndividuals().ToList(); foreach (var ind in data.Individuals) @@ -114,7 +114,7 @@ public void GenerateSpreadsheet(int assessmentId, MemoryStream ms) row.Append(new Cell { - CellValue = new CellValue(obs.Observation), + CellValue = new CellValue(obs.ObservationTitle), DataType = CellValues.String }); @@ -130,9 +130,14 @@ public void GenerateSpreadsheet(int assessmentId, MemoryStream ms) DataType = CellValues.String }); + string resolutionDateString = ""; + if (obs.ResolutionDate != null) + { + resolutionDateString = obs.ResolutionDate.ToString(); + } row.Append(new Cell { - CellValue = new CellValue((DateTimeOffset)obs.ResolutionDate), + CellValue = new CellValue(resolutionDateString), DataType = CellValues.String }); diff --git a/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsDataBusiness.cs b/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsDataBusiness.cs index 21196eac42..9defa5dbc8 100644 --- a/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsDataBusiness.cs +++ b/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsDataBusiness.cs @@ -36,7 +36,7 @@ public partial class ReportsDataBusiness : IReportsDataBusiness private readonly IAdminTabBusiness _adminTabBusiness; private readonly IMaturityBusiness _maturityBusiness; private readonly IQuestionRequirementManager _questionRequirement; - private readonly ITokenManager _tokenManager; + private ITokenManager _tokenManager; public List OutOfScopeQuestions = new List(); @@ -62,6 +62,17 @@ public ReportsDataBusiness(CSETContext context, IAssessmentUtil assessmentUtil, } + /// + /// Allows the token to be set after construction. This is needed + /// in the Reports realm, when tokens are passed as a param to the controller method. + /// + /// + public void SetToken(ITokenManager token) + { + _tokenManager = token; + } + + /// /// Returns an unfiltered list of MatRelevantAnswers for the current assessment. /// The optional modelId parameter is used to get a specific model's questions. If not @@ -675,7 +686,6 @@ orderby h.Question_Group_Heading /// public List GetQuestionsWithComments() { - var results = new List(); var rm = new Question.RequirementBusiness(_assessmentUtil, _questionRequirement, _context, _tokenManager); @@ -731,7 +741,6 @@ orderby h.Question_Group_Heading /// public List GetQuestionsMarkedForReview() { - var results = new List(); // get any "marked for review" or commented answers that currently apply @@ -785,7 +794,6 @@ orderby h.Question_Group_Heading /// public List GetQuestionsReviewed() { - var results = new List(); var rm = new Question.RequirementBusiness(_assessmentUtil, _questionRequirement, _context, _tokenManager); @@ -1087,24 +1095,34 @@ public BasicReportData.INFORMATION GetInformation() /// - /// Returns a list of individuals assigned to observations. + /// /// /// public List GetObservationIndividuals() { - var observations = (from a in _context.FINDING_CONTACT - join b in _context.FINDING on a.Finding_Id equals b.Finding_Id - join c in _context.ANSWER on b.Answer_Id equals c.Answer_Id - join mq in _context.MATURITY_QUESTIONS on c.Question_Or_Requirement_Id equals mq.Mat_Question_Id into mq1 + List individualList = []; + + var observations = (from f in _context.FINDING + join fc in _context.FINDING_CONTACT on f.Finding_Id equals fc.Finding_Id + join a in _context.ANSWER on f.Answer_Id equals a.Answer_Id + join mq in _context.MATURITY_QUESTIONS on a.Question_Or_Requirement_Id equals mq.Mat_Question_Id into mq1 from mq in mq1.DefaultIfEmpty() - join r in _context.NEW_REQUIREMENT on c.Question_Or_Requirement_Id equals r.Requirement_Id into r1 - from r in r1.DefaultIfEmpty() - join d in _context.ASSESSMENT_CONTACTS on a.Assessment_Contact_Id equals d.Assessment_Contact_Id - join i in _context.IMPORTANCE on b.Importance_Id equals i.Importance_Id into i1 + join nr in _context.NEW_REQUIREMENT on a.Question_Or_Requirement_Id equals nr.Requirement_Id into nr1 + from nr in nr1.DefaultIfEmpty() + join ac in _context.ASSESSMENT_CONTACTS on fc.Assessment_Contact_Id equals ac.Assessment_Contact_Id into ac1 + from ac in ac1.DefaultIfEmpty() + join i in _context.IMPORTANCE on f.Importance_Id equals i.Importance_Id into i1 from i in i1.DefaultIfEmpty() - where c.Assessment_Id == _assessmentId - orderby a.Assessment_Contact_Id, b.Answer_Id, b.Finding_Id - select new { a, b, c, mq, r, d, i.Value }).ToList(); + where a.Assessment_Id == _assessmentId + select new ObservationIngredients() { + Finding = f, + FC = fc, + Answer = a, + MaturityQuestion = mq, + NewRequirement = nr, + Importance = i }).ToList(); + + var acc = _context.ASSESSMENT_CONTACTS.Where(x => x.Assessment_Id == _assessmentId).OrderBy(x => x.Assessment_Contact_Id).ToList(); // Get any associated questions to get their display reference @@ -1112,57 +1130,84 @@ from i in i1.DefaultIfEmpty() var componentQuestions = GetComponentQuestions(); - List individualList = new List(); + + // First handle the 'assigned' Observations + foreach (var contact in acc) + { + Individual individual = new Individual() + { + FullName = FormatName(contact.FirstName, contact.LastName) + }; - int contactId = 0; - Individual individual = null; + individualList.Add(individual); - foreach (var f in observations) - { - if (contactId != f.a.Assessment_Contact_Id) + var obsList = observations.Where(x => x.FC?.Assessment_Contact_Id == contact.Assessment_Contact_Id).ToList(); + + foreach (var m in obsList) { - individual = new Individual() - { - Observations = new List(), - FullName = FormatName(f.d.FirstName, f.d.LastName) - }; + var obs = GenerateObservation(m, standardQuestions, componentQuestions); - individualList.Add(individual); + individual.Observations.Add(obs); } - contactId = f.a.Assessment_Contact_Id; + } - TinyMapper.Bind(); - Observations obs = TinyMapper.Map(f.b); - obs.Observation = f.b.Summary; - obs.ResolutionDate = f.b.Resolution_Date; - obs.Importance = f.Value; + // Include any 'unassigned' Observations + var ind = new Individual(); + ind.FullName = "Unassigned"; + var unnasignedObs = observations.Where(x => x.FC == null).ToList(); + foreach (var obs in unnasignedObs) + { + var observation = GenerateObservation(obs, standardQuestions, componentQuestions); + ind.Observations.Add(observation); + } - // get the question identifier and text - GetQuestionTitleAndText(f, standardQuestions, componentQuestions, f.c.Answer_Id, - out string qid, out string qtxt); + if (ind.Observations.Count > 0) + { + individualList.Add(ind); + } - if (_maturityBusiness.GetMaturityModel(_assessmentId)?.ModelName == "CIE") - { - GetQuestionTitleAndTextForCie(f, standardQuestions, componentQuestions, f.c.Answer_Id, - out qid, out qtxt); - } + return individualList; + } - obs.QuestionIdentifier = qid; - obs.QuestionText = qtxt; + /// + /// Creates and populates an instance of Observation + /// + /// + /// + private Observation GenerateObservation(ObservationIngredients oi, List standardQuestions, List componentQuestions) + { + TinyMapper.Bind(); + Observation obs = TinyMapper.Map(oi.Finding); + obs.ObservationTitle = oi.Finding.Summary; + obs.ResolutionDate = oi.Finding.Resolution_Date; + obs.Importance = oi.Importance.Value; - var othersList = (from a in f.b.FINDING_CONTACT - join b in _context.ASSESSMENT_CONTACTS on a.Assessment_Contact_Id equals b.Assessment_Contact_Id - select FormatName(b.FirstName, b.LastName)).ToList(); - obs.OtherContacts = string.Join(",", othersList); + // get the question identifier and text + GetQuestionTitleAndText(oi, standardQuestions, componentQuestions, oi.Answer.Answer_Id, + out string qid, out string qtxt); - individual.Observations.Add(obs); + if (_maturityBusiness.GetMaturityModel(_assessmentId)?.ModelName == "CIE") + { + GetQuestionTitleAndTextForCie(oi, standardQuestions, componentQuestions, oi.Answer.Answer_Id, + out qid, out qtxt); } - return individualList; + obs.QuestionIdentifier = qid; + obs.QuestionText = qtxt; + + + // list names of all people assigned to the observation + var othersList = (from a in oi.Finding.FINDING_CONTACT + join b in _context.ASSESSMENT_CONTACTS on a.Assessment_Contact_Id equals b.Assessment_Contact_Id + select FormatName(b.FirstName, b.LastName)).ToList(); + obs.Assignees = string.Join(",", othersList); + + + return obs; } diff --git a/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsUtils.cs b/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsUtils.cs index a3da3ccb49..18f07da196 100644 --- a/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsUtils.cs +++ b/CSETWebApi/CSETWeb_Api/CSETWebCore.Business/Reports/ReportsData/ReportsUtils.cs @@ -253,7 +253,7 @@ public List GetDocumentLibrary() /// question text with the translated version. /// /// - private void GetQuestionTitleAndText(dynamic f, + private void GetQuestionTitleAndText(ObservationIngredients f, List stdList, List compList, int answerId, out string identifier, out string questionText) @@ -262,12 +262,12 @@ private void GetQuestionTitleAndText(dynamic f, questionText = ""; var lang = _tokenManager.GetCurrentLanguage(); - switch (f.c.Question_Type) + switch (f.Answer.Question_Type) { case "Question": foreach (var s in stdList) { - var q1 = s.Questions.FirstOrDefault(x => x.QuestionId == f.c.Question_Or_Requirement_Id); + var q1 = s.Questions.FirstOrDefault(x => x.QuestionId == f.Answer.Question_Or_Requirement_Id); if (q1 != null) { identifier = q1.CategoryAndNumber; @@ -279,7 +279,7 @@ private void GetQuestionTitleAndText(dynamic f, return; case "Component": - var q2 = compList.FirstOrDefault(x => x.QuestionId == f.c.Question_Or_Requirement_Id); + var q2 = compList.FirstOrDefault(x => x.QuestionId == f.Answer.Question_Or_Requirement_Id); if (q2 != null) { identifier = q2.ComponentName; @@ -289,28 +289,28 @@ private void GetQuestionTitleAndText(dynamic f, return; case "Requirement": - identifier = f.r.Requirement_Title; + identifier = f.NewRequirement.Requirement_Title; var rb = new RequirementBusiness(_assessmentUtil, _questionRequirement, _context, _tokenManager); - questionText = rb.ResolveParameters(f.r.Requirement_Id, answerId, f.r.Requirement_Text); + questionText = rb.ResolveParameters(f.NewRequirement.Requirement_Id, answerId, f.NewRequirement.Requirement_Text); // translate - questionText = _overlay.GetRequirement(f.r.Requirement_Id, lang)?.RequirementText ?? questionText; + questionText = _overlay.GetRequirement(f.NewRequirement.Requirement_Id, lang)?.RequirementText ?? questionText; return; case "Maturity": - identifier = f.mq.Question_Title; - questionText = f.mq.Question_Text; + identifier = f.MaturityQuestion.Question_Title; + questionText = f.MaturityQuestion.Question_Text; // CPG is a special case - if (!String.IsNullOrEmpty(f.mq.Security_Practice)) + if (!String.IsNullOrEmpty(f.MaturityQuestion.Security_Practice)) { - questionText = f.mq.Security_Practice; + questionText = f.MaturityQuestion.Security_Practice; } // overlay - MaturityQuestionOverlay o = _overlay.GetMaturityQuestion(f.mq.Mat_Question_Id, lang); + MaturityQuestionOverlay o = _overlay.GetMaturityQuestion(f.MaturityQuestion.Mat_Question_Id, lang); if (o != null) { identifier = o.QuestionTitle; diff --git a/CSETWebApi/CSETWeb_Api/CSETWebCore.Interfaces/Reports/IReportsDataBusiness.cs b/CSETWebApi/CSETWeb_Api/CSETWebCore.Interfaces/Reports/IReportsDataBusiness.cs index e125fb1fe6..a9ffe09fce 100644 --- a/CSETWebApi/CSETWeb_Api/CSETWebCore.Interfaces/Reports/IReportsDataBusiness.cs +++ b/CSETWebApi/CSETWeb_Api/CSETWebCore.Interfaces/Reports/IReportsDataBusiness.cs @@ -6,6 +6,7 @@ //////////////////////////////// using CSETWebCore.Business.Reports; using CSETWebCore.DataLayer.Model; +using CSETWebCore.Interfaces.Helpers; using CSETWebCore.Model.Diagram; using CSETWebCore.Model.Maturity; using CSETWebCore.Model.Question; @@ -18,6 +19,8 @@ public interface IReportsDataBusiness { void SetReportsAssessmentId(int assessmentId); + void SetToken(ITokenManager token); + List GetMaturityDeficiencies(int? modelId = null); List GetCommentsList(int? modelId = null); List GetMarkedForReviewList(int? modelId = null); diff --git a/CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/BasicReportData.cs b/CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/BasicReportData.cs index cbe2c00642..85c4881ea9 100644 --- a/CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/BasicReportData.cs +++ b/CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/BasicReportData.cs @@ -114,9 +114,9 @@ public class Control_Questions } } - public class Observations + public class Observation { - public string Observation { get; set; } + public string ObservationTitle { get; set; } public string QuestionIdentifier { get; set; } public string QuestionText { get; set; } public string Importance { get; set; } @@ -125,7 +125,11 @@ public class Observations public string Impact { get; set; } public string Recommendations { get; set; } public string Vulnerabilities { get; set; } - public string OtherContacts { get; set; } + + /// + /// A comma list of all people assigned to the Observation + /// + public string Assignees { get; set; } } public class DocumentLibraryTable @@ -252,7 +256,7 @@ public List GetAnswersForAssessment(int assessmentID, CSETConte public class Individual { public string FullName { get; set; } - public List Observations { get; set; } + public List Observations { get; set; } = []; } public class QuestionsWithAltJust diff --git a/CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/ObservationIngredients.cs b/CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/ObservationIngredients.cs new file mode 100644 index 0000000000..bc7eace1bd --- /dev/null +++ b/CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Reports/ObservationIngredients.cs @@ -0,0 +1,20 @@ +using CSETWebCore.DataLayer.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CSETWebCore.Model.Reports +{ + public class ObservationIngredients + { + public FINDING Finding { get; set; } + public FINDING_CONTACT FC { get; set; } + public ANSWER Answer { get; set; } + public MATURITY_QUESTIONS MaturityQuestion { get; set; } + public NEW_REQUIREMENT NewRequirement { get; set; } + + public IMPORTANCE Importance { get; set; } + } +} diff --git a/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ReportsController.cs b/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ReportsController.cs index 16e65a0914..8d8d7c181f 100644 --- a/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ReportsController.cs +++ b/CSETWebApi/CSETWeb_Api/CSETWeb_ApiCore/Controllers/ReportsController.cs @@ -556,6 +556,9 @@ public IActionResult GetObservations() [Route("api/reports/observations/excel")] public IActionResult ExportObservationsCsv(string token) { + _token.SetToken(token); + _report.SetToken(_token); + int assessmentId = _token.AssessmentForUser(token); string lang = _token.GetCurrentLanguage(); diff --git a/CSETWebNg/src/app/assessment/results/reports/reports.component.ts b/CSETWebNg/src/app/assessment/results/reports/reports.component.ts index 0076215b8c..f2977fe55f 100644 --- a/CSETWebNg/src/app/assessment/results/reports/reports.component.ts +++ b/CSETWebNg/src/app/assessment/results/reports/reports.component.ts @@ -243,12 +243,17 @@ export class ReportsComponent implements OnInit, AfterViewInit { * */ clickExcelLink(reportType: string) { + let url = ''; if (reportType.toLowerCase() == 'poam') { - window.location.href = this.configSvc.apiUrl + 'reports/poam/excelexport?token=' + localStorage.getItem('userToken'); + url = this.configSvc.apiUrl + 'reports/poam/excelexport?token=' + localStorage.getItem('userToken'); } if (reportType.toLowerCase() == 'observations') { - window.location.href = this.configSvc.apiUrl + 'reports/observations/excel?token=' + localStorage.getItem('userToken'); + url = this.configSvc.apiUrl + 'reports/observations/excel?token=' + localStorage.getItem('userToken'); + } + + if (url.length > 0) { + window.open(url, '_blank'); } } diff --git a/CSETWebNg/src/app/reports/observation-tearouts/observation-tearouts.component.html b/CSETWebNg/src/app/reports/observation-tearouts/observation-tearouts.component.html index 56d96f6bbf..fe7cc2d48f 100644 --- a/CSETWebNg/src/app/reports/observation-tearouts/observation-tearouts.component.html +++ b/CSETWebNg/src/app/reports/observation-tearouts/observation-tearouts.component.html @@ -73,7 +73,7 @@

{{ 'reports.site information' | transloco }}

- + @@ -107,7 +107,7 @@

{{ 'reports.site information' | transloco }}

- +
{{obs.observation}}{{obs.observationTitle}}
{{ 'reports.observations tear-out sheets.question identifier' | transloco }}:
{{ 'contact.contacts' | transloco }}:{{obs.otherContacts.replace(',',', ')}}{{obs.assignees?.replace(',',', ')}}