Skip to content

Commit

Permalink
Merge pull request #3972 from cisagov/feature/CSET-2773
Browse files Browse the repository at this point in the history
Implement "Resume Questions" feature
  • Loading branch information
mattrwins committed Jun 20, 2024
2 parents 49d3da2 + 278d690 commit 1bc23b6
Show file tree
Hide file tree
Showing 19 changed files with 421 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public partial class ASSESSMENT_CONTACTS

public bool Is_Site_Participant { get; set; }

[StringLength(100)]
public string Last_Q_Answered { get; set; }

[ForeignKey("Assessment_Id")]
[InverseProperty("ASSESSMENT_CONTACTS")]
public virtual ASSESSMENTS Assessment { get; set; }
Expand Down
78 changes: 78 additions & 0 deletions CSETWebApi/CSETWeb_Api/CSETWebCore.Helpers/LastAnsweredHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
////////////////////////////////
//
// Copyright 2024 Battelle Energy Alliance, LLC
//
//
////////////////////////////////
using System.Linq;
using CSETWebCore.Model.Question;
using CSETWebCore.DataLayer.Model;


namespace CSETWebCore.Helpers
{
/// <summary>
/// Saves the last-answered question/option.
/// </summary>
public class LastAnsweredHelper
{
private readonly CSETContext _context;


public LastAnsweredHelper(CSETContext context)
{
_context = context;
}


/// <summary>
/// Save the group/requirement/question/option so that the user can return to the last question answered
/// on a subsequent rerun of the assessment.
/// </summary>
/// <param name="questionId"></param>
public void Save(int assessmentId, int? userId, Answer ans)
{
var ac = _context.ASSESSMENT_CONTACTS.Where(x => x.UserId == userId && x.Assessment_Id == assessmentId).FirstOrDefault();
if (ac == null)
{
// no contact record - should never happen
return;
}


if (ans.QuestionType.ToLower() == "maturity")
{
// get the question ID if not in the answer
if (ans.QuestionId == 0)
{
var dbOption = _context.MATURITY_ANSWER_OPTIONS.FirstOrDefault(x => x.Mat_Option_Id == ans.OptionId);
ans.QuestionId = dbOption.Mat_Question_Id;
}

var dbQuestion = _context.MATURITY_QUESTIONS.Where(x => x.Mat_Question_Id == ans.QuestionId).FirstOrDefault();

ac.Last_Q_Answered = $"MG:{dbQuestion.Grouping_Id},MQ:{ans.QuestionId}";

if (ans.OptionId != null && ans.OptionId != 0)
{
ac.Last_Q_Answered += $",MO:{ans.OptionId}";
}
}


if (ans.QuestionType.ToLower() == "requirement")
{
ac.Last_Q_Answered = $"R:{ans.QuestionId}";
}


if (ans.QuestionType.ToLower() == "question")
{
ac.Last_Q_Answered = $"Q:{ans.QuestionId}";
}


_context.SaveChanges();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using CSETWebCore.DataLayer.Model;
using CSETWebCore.Model.Authentication;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
//using Microsoft.AspNetCore.Authentication.OpenIdConnect;


namespace CSETWebCore.Helpers
{
public class UserAuthenticationAzure
{
CSETContext _context;


/// <summary>
///
/// </summary>
/// <param name="context"></param>
public UserAuthenticationAzure(
CSETContext context)
{
_context = context;
}


/// <summary>
///
/// </summary>
/// <param name="login"></param>
/// <returns></returns>
public LoginResponse AuthenticateUser(Login login)
{
// authenticate against Azure




// Read directly from the database; UserManager does not read password and salt, in order to keep them more private
var loginUser = _context.USERS.Where(x => x.PrimaryEmail == login.Email).FirstOrDefault();

// Build response object
LoginResponse resp = new LoginResponse
{
UserId = loginUser.UserId,
Email = login.Email,
Lang = loginUser.Lang,
UserFirstName = loginUser.FirstName,
UserLastName = loginUser.LastName,
IsSuperUser = loginUser.IsSuperUser,
ResetRequired = loginUser.PasswordResetRequired,
ExportExtension = IOHelper.GetExportFileExtension(login.Scope),
ImportExtensions = IOHelper.GetImportFileExtensions(login.Scope),
LinkerTime = new BuildNumberHelper().GetLinkerTime(),
IsFirstLogin = loginUser.IsFirstLogin
};

return resp;
}
}
}
13 changes: 13 additions & 0 deletions CSETWebApi/CSETWeb_Api/CSETWebCore.Model/Contact/Bookmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSETWebCore.Model.Contact
{
public class BookmarkRequest
{
public string Bookmark { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -608,5 +608,26 @@ public IActionResult ValidateMyRemoval(int assessmentId)

return Ok(true);
}


/// <summary>
///
/// </summary>
[HttpGet]
[Route("api/contacts/bookmark")]
public IActionResult GetBookmark()
{
int? currentUserId = _token.GetUserId();
int assessmentId = _token.AssessmentForUser();

var ac = _context.ASSESSMENT_CONTACTS.Where(x => x.UserId == currentUserId && x.Assessment_Id == assessmentId).FirstOrDefault();
if (ac == null)
{
// no contact record - just do nothing
return Ok();
}

return Ok(ac.Last_Q_Answered);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
using System.Collections.Generic;
using System.Linq;
using CSETWebCore.Business.Malcolm;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using CSETWebCore.Helpers;


namespace CSETWebCore.Api.Controllers
{
Expand Down Expand Up @@ -113,7 +116,7 @@ public IActionResult GetComponentQuestionsList([FromQuery] string skin, string g
}
var manager = new ComponentQuestionBusiness(_context, _assessmentUtil, _token, _questionRequirement);
QuestionResponse resp = manager.GetResponse();

return Ok(resp);
}

Expand Down Expand Up @@ -220,6 +223,8 @@ protected string GetApplicationMode(int assessmentId)
[Route("api/AnswerQuestion")]
public IActionResult StoreAnswer([FromBody] Answer answer)
{
int assessmentId = _token.AssessmentForUser();

var mb = new MaturityBusiness(_context, _assessmentUtil, _adminTabBusiness);

if (answer == null)
Expand All @@ -239,8 +244,12 @@ public IActionResult StoreAnswer([FromBody] Answer answer)
answer.QuestionType = "Question";
}

int assessmentId = _token.AssessmentForUser();
string applicationMode = GetApplicationMode(assessmentId);

// Save the last answered question
var lah = new LastAnsweredHelper(_context);
lah.Save(assessmentId, _token.GetCurrentUserId(), answer);



if (answer.Is_Component)
{
Expand All @@ -256,7 +265,6 @@ public IActionResult StoreAnswer([FromBody] Answer answer)

if (answer.Is_Maturity)
{
//var mb = new MaturityBusiness(_context, _assessmentUtil, _adminTabBusiness);
return Ok(mb.StoreAnswer(assessmentId, answer));
}

Expand All @@ -265,6 +273,9 @@ public IActionResult StoreAnswer([FromBody] Answer answer)
}





/// <summary>
/// Persists multiple answers. It is being build specifically
/// to handle answer cleanup in CIS but can be enhanced if
Expand All @@ -283,6 +294,8 @@ public IActionResult StoreAnswers([FromBody] List<Answer> answers, [FromQuery] i

var cisBiz = new CisQuestionsBusiness(_context, _assessmentUtil, assessmentId);

var lah = new LastAnsweredHelper(_context);

foreach (var answer in answers)
{
if (String.IsNullOrWhiteSpace(answer.QuestionType))
Expand All @@ -299,6 +312,9 @@ public IActionResult StoreAnswers([FromBody] List<Answer> answers, [FromQuery] i

if (answer.Is_Maturity)
{
// save the last answered question
lah.Save(assessmentId, _token.GetCurrentUserId(), answer);

cisBiz.StoreAnswer(answer);
}
}
Expand All @@ -325,14 +341,20 @@ public IActionResult StoreAnswers([FromBody] List<Answer> answers, [FromQuery] i
public IActionResult StoreAllAnswers([FromBody] List<Answer> answers)
{
int assessmentId = _token.AssessmentForUser();
int? userId = _token.GetCurrentUserId();

if (answers == null || answers.Count == 0)
{
return Ok(0);
}

var lah = new LastAnsweredHelper(_context);

foreach (var answer in answers)
{
// save the last answered question
lah.Save(assessmentId, userId, answer);

var mb = new MaturityBusiness(_context, _assessmentUtil, _adminTabBusiness);
mb.StoreAnswer(assessmentId, answer);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
// SOFTWARE.
//
////////////////////////////////
import { Component, OnInit } from '@angular/core';
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { NavigationService } from '../../../services/navigation/navigation.service';
import { AssessmentService } from '../../../services/assessment.service';
import { MaturityService } from '../../../services/maturity.service';
Expand All @@ -44,7 +44,7 @@ import { TranslocoService } from '@ngneat/transloco';
selector: 'app-maturity-questions',
templateUrl: './maturity-questions.component.html'
})
export class MaturityQuestionsComponent implements OnInit {
export class MaturityQuestionsComponent implements OnInit, AfterViewInit {

groupings: QuestionGrouping[] = [];
pageTitle: string = '';
Expand Down Expand Up @@ -122,6 +122,12 @@ export class MaturityQuestionsComponent implements OnInit {
});
}

ngAfterViewInit() {
setTimeout(() => {
this.scrollToResumeQuestionsTarget();
}, 500);
}

/**
*
*/
Expand Down Expand Up @@ -259,6 +265,47 @@ export class MaturityQuestionsComponent implements OnInit {
});
}

/**
* If a "resume questions" target is defined, attempt to scroll to it.
*/
scrollToResumeQuestionsTarget() {
// scroll to the target question if we have one
const scrollTarget = this.navSvc.resumeQuestionsTarget;
this.navSvc.resumeQuestionsTarget = null;
if (!scrollTarget) {
return;
}

var mg = scrollTarget.split(',').find(x => x.startsWith('MG:'))?.replace('MG:', '');
let mq = scrollTarget.split(',').find(x => x.startsWith('MQ:'))?.replace('MQ:', '');

// expand the question's group
var groupToExpand = this.findGroupingById(Number(mg), this.groupings);
if (!!groupToExpand) {
groupToExpand.expanded = true;
}

// scroll to the question
let qqElement = document.getElementById(`mq${mq}`);
setTimeout(() => {
qqElement.scrollIntoView({ behavior: 'smooth' });
return;
}, 1000);
}

/**
* Recurse grouping tree, looking for the ID
*/
findGroupingById(id: number, groupings: any[]) {
var grp = groupings.find(x => x.groupingID == id);
if (!!grp) {
return grp;
}
for (var i = 0; i < groupings.length; i++) {
return this.findGroupingById(id, groupings[i].subGroupings);
}
}

/**
*
*/
Expand Down
Loading

0 comments on commit 1bc23b6

Please sign in to comment.