Skip to content

Commit

Permalink
Tdrd 487 results of your metadata checks UI download errors in excel …
Browse files Browse the repository at this point in the history
…file (#4149)

Support flow to get metadata error download button
  • Loading branch information
ian-hoyle authored Sep 17, 2024
1 parent a760440 commit 355c1e7
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 65 deletions.
2 changes: 2 additions & 0 deletions app/configuration/ApplicationConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class ApplicationConfig @Inject() (configuration: Configuration) {

val draftMetadataFileName: String = configuration.get[String]("draftMetadata.fileName")

val draftMetadataErrorFileName: String = configuration.get[String]("draftMetadata.errorFileName")

val notificationSnsTopicArn: String = get("notificationSnsTopicArn")

val fileChecksTotalTimoutInSeconds: Int = configuration.get[Int]("fileChecksTotalTimoutInSeconds")
Expand Down
83 changes: 67 additions & 16 deletions app/controllers/DraftMetadataChecksResultsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import auth.TokenSecurity
import configuration.{ApplicationConfig, KeycloakConfiguration}
import graphql.codegen.GetConsignmentStatus.getConsignmentStatus.GetConsignment
import org.pac4j.play.scala.SecurityComponents
import play.api.i18n.I18nSupport
import play.api.i18n.{I18nSupport, Messages}
import play.api.mvc.{Action, AnyContent, Request}
import services.Statuses._
import services._
import services.{FileError, _}
import viewsapi.Caching.preventCaching

import java.util.UUID
Expand All @@ -20,7 +20,8 @@ class DraftMetadataChecksResultsController @Inject() (
val keycloakConfiguration: KeycloakConfiguration,
val consignmentService: ConsignmentService,
val applicationConfig: ApplicationConfig,
val consignmentStatusService: ConsignmentStatusService
val consignmentStatusService: ConsignmentStatusService,
val draftMetadataService: DraftMetadataService
)(implicit val ec: ExecutionContext)
extends TokenSecurity
with I18nSupport {
Expand All @@ -33,24 +34,74 @@ class DraftMetadataChecksResultsController @Inject() (
for {
reference <- consignmentService.getConsignmentRef(consignmentId, request.token.bearerAccessToken)
consignmentStatuses <- consignmentStatusService.getConsignmentStatuses(consignmentId, token)
errorType <- getErrorType(consignmentStatuses, consignmentId)
} yield {
Ok(
views.html.draftmetadata
.draftMetadataChecksResults(consignmentId, reference, getValue(consignmentStatuses, DraftMetadataType), request.token.name)
)
.uncache()
val resultsPage = {
// leaving original page for no errors
if (errorType == FileError.NONE) {
views.html.draftmetadata
.draftMetadataChecksResults(consignmentId, reference, DraftMetadataProgress("IMPORTED", "blue"), request.token.name)
} else {
if (isErrorReportAvailable(errorType)) {
views.html.draftmetadata
.draftMetadataChecksWithErrorDownload(
consignmentId,
reference,
request.token.name,
actionMessage(errorType),
detailsMessage(errorType)
)
} else {
views.html.draftmetadata
.draftMetadataChecksErrorsNoDownload(
consignmentId,
reference,
request.token.name,
actionMessage(errorType),
detailsMessage(errorType)
)
}
}
}
Ok(resultsPage).uncache()
}
}
}

def getValue(statuses: List[GetConsignment.ConsignmentStatuses], statusType: StatusType): DraftMetadataProgress = {
val failed = DraftMetadataProgress("FAILED", "red")
statuses.find(_.statusType == statusType.id).map(_.value).map {
case FailedValue.value => failed
case CompletedWithIssuesValue.value => DraftMetadataProgress("ERRORS", "red")
case CompletedValue.value => DraftMetadataProgress("IMPORTED", "blue")
case _ => failed
} getOrElse failed
private def actionMessage(fileError: FileError.FileError)(implicit messages: Messages): String = {
val key = s"draftMetadata.validation.action.$fileError"
if (Messages.isDefinedAt(key))
Messages(key)
else
s"Require action message for $key"
}

private def detailsMessage(fileError: FileError.FileError)(implicit messages: Messages): String = {
val key = s"draftMetadata.validation.details.$fileError"
if (Messages.isDefinedAt(key))
Messages(key)
else
s"Require details message for $key"
}

private def isErrorReportAvailable(fileError: FileError.FileError): Boolean = {
fileError match {
case FileError.SCHEMA_VALIDATION => true
case _ => false
}
}

private def getErrorType(consignmentStatuses: List[GetConsignment.ConsignmentStatuses], consignmentId: UUID): Future[FileError.Value] = {
val draftMetadataStatus = consignmentStatuses.find(_.statusType == DraftMetadataType.id).map(_.value)
if (draftMetadataStatus.isDefined) {
draftMetadataStatus.get match {
case CompletedValue.value => Future.successful(FileError.NONE)
case FailedValue.value => Future.successful(FileError.UNKNOWN)
case _ => draftMetadataService.getErrorTypeFromErrorJson(consignmentId)
}
} else {
Future.successful(FileError.UNKNOWN)
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions app/services/DownloadService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package services

import configuration.ApplicationConfig
import software.amazon.awssdk.core.ResponseBytes
import software.amazon.awssdk.core.async.AsyncResponseTransformer
import software.amazon.awssdk.services.s3.S3AsyncClient
import software.amazon.awssdk.services.s3.model.{GetObjectRequest, GetObjectResponse}
import uk.gov.nationalarchives.aws.utils.s3.S3Clients._

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.FutureConverters.CompletionStageOps

class DownloadService @Inject() (val applicationConfig: ApplicationConfig)(implicit val ec: ExecutionContext) {
private val s3Endpoint = applicationConfig.s3Endpoint

def downloadFile(bucket: String, key: String): Future[ResponseBytes[GetObjectResponse]] = {
downloadFile(bucket, key, s3Async(s3Endpoint))
}

def downloadFile(bucket: String, key: String, s3AsyncClient: S3AsyncClient): Future[ResponseBytes[GetObjectResponse]] = {
val getObjectRequest = GetObjectRequest.builder.bucket(bucket).key(key).build()
s3AsyncClient.getObject(getObjectRequest, AsyncResponseTransformer.toBytes[GetObjectResponse]).asScala
}
}
34 changes: 33 additions & 1 deletion app/services/DraftMetadataService.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
package services

import com.google.inject.Inject
import configuration.ApplicationConfig
import io.circe.Decoder
import io.circe.generic.auto._
import io.circe.parser.decode
import play.api.libs.ws.WSClient
import play.api.{Configuration, Logging}
import software.amazon.awssdk.core.ResponseBytes
import software.amazon.awssdk.services.s3.model.GetObjectResponse
import uk.gov.nationalarchives.tdr.validation.Metadata

import java.nio.charset.StandardCharsets
import java.util.UUID
import scala.concurrent.{ExecutionContext, Future}

class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: Configuration)(implicit val executionContext: ExecutionContext) extends Logging {
object FileError extends Enumeration {
type FileError = Value
val UTF_8, INVALID_CSV, SCHEMA_REQUIRED, SCHEMA_VALIDATION, UNKNOWN, NONE = Value
}

case class Error(validationProcess: String, property: String, errorKey: String, message: String)
case class ValidationErrors(assetId: String, errors: Set[Error], data: List[Metadata] = List.empty[Metadata])
case class ErrorFileData(consignmentId: UUID, date: String, fileError: FileError.FileError, validationErrors: List[ValidationErrors])

class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: Configuration, val applicationConfig: ApplicationConfig, val downloadService: DownloadService)(
implicit val executionContext: ExecutionContext
) extends Logging {

def triggerDraftMetadataValidator(consignmentId: UUID, uploadFileName: String, token: String): Future[Boolean] = {
val url = s"${configuration.get[String]("metadatavalidation.baseUrl")}/draft-metadata/validate/$consignmentId/$uploadFileName"
Expand All @@ -24,4 +43,17 @@ class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration:
}
)
}

def getErrorTypeFromErrorJson(consignmentId: UUID): Future[FileError.FileError] = {
implicit val FileErrorDecoder: Decoder[FileError.Value] = Decoder.decodeEnumeration(FileError)
val errorFile: Future[ResponseBytes[GetObjectResponse]] =
downloadService.downloadFile(applicationConfig.draft_metadata_s3_bucket_name, s"$consignmentId/${applicationConfig.draftMetadataErrorFileName}")

errorFile
.map(responseBytes => {
val errorJson = new String(responseBytes.asByteArray(), StandardCharsets.UTF_8)
decode[ErrorFileData](errorJson).fold(_ => FileError.UNKNOWN, errorFileData => errorFileData.fileError)
})
.recoverWith(_ => Future.successful(FileError.UNKNOWN))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import views.html.partials._

@import java.util.UUID
@(consignmentId: UUID, consignmentRef: String, name: String, actionMessage: String, detailsMessage: String)(implicit request: RequestHeader, messages: Messages)

@main("Results of CSV Checks", name = name) {
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
@draftMetadataChecksActionProcess(actionMessage, detailsMessage)
<p class="govuk-body">Once you have addressed this issue upload a revised metadata file.</p>

<div class="govuk-button-group">
<a class="govuk-button" href="@{
routes.DraftMetadataUploadController.draftMetadataUploadPage(consignmentId)
}" role="button" draggable="false" data-module="govuk-button">Re-upload metadata</a>
</div>

</div>
@transferReference(consignmentRef, isJudgmentUser = false)
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@import views.html.partials._

@import java.util.UUID
@(consignmentId: UUID, consignmentRef: String, name: String, actionMessage: String, detailsMessage: String)(implicit request: RequestHeader, messages: Messages)

@main("Results of CSV Checks", name = name) {
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
@draftMetadataChecksActionProcess(actionMessage, detailsMessage)

<p class="govuk-body">The report below contains details about issues found.</p>

<button class="govuk-button govuk-button--secondary govuk-!-margin-bottom-8" data-module="govuk-button">
<span aria-hidden="true" class="tna-button-icon tna-button-icon--download">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 23 23">
<path fill="#020202" d="m11.5 16.75-6.563-6.563 1.838-1.903 3.412 3.413V1h2.626v10.697l3.412-3.413 1.837 1.903L11.5 16.75ZM3.625 22c-.722 0-1.34-.257-1.853-.77A2.533 2.533 0 0 1 1 19.375v-3.938h2.625v3.938h15.75v-3.938H22v3.938c0 .722-.257 1.34-.77 1.855a2.522 2.522 0 0 1-1.855.77H3.625Z"/>
</svg>
</span>
Download report
</button>

<p class="govuk-body">Once you have addressed this issue upload a revised metadata file.</p>

<div class="govuk-button-group">
<a class="govuk-button" href="@{
routes.DraftMetadataUploadController.draftMetadataUploadPage(consignmentId)
}" role="button" draggable="false" data-module="govuk-button">Re-upload metadata</a>
</div>

</div>
@transferReference(consignmentRef, isJudgmentUser = false)
</div>
}
37 changes: 37 additions & 0 deletions app/views/partials/draftMetadataChecksActionProcess.scala.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@(actionMessage: String, detailsMessage: String)(implicit request: RequestHeader, messages: Messages)

<h1 class="govuk-heading-l">
Results of your metadata checks
</h1>
<p class="govuk-body">There was an issue with your uploaded metadata file.</p>

<p class="govuk-body"></p>

<dl class="govuk-summary-list">
<div class="govuk-summary-list__row">
<dt class="govuk-summary-list__key">
Status
</dt>
<dd class="govuk-summary-list__value">
<strong class="govuk-tag govuk-tag--orange">Issues found</strong>
</dd>
</div>

<div class="govuk-summary-list__row">
<dt class="govuk-summary-list__key">
Details
</dt>
<dd class="govuk-summary-list__value">
@detailsMessage
</dd>
</div>

<div class="govuk-summary-list__row">
<dt class="govuk-summary-list__key">
Action
</dt>
<dd class="govuk-summary-list__value">
@actionMessage
</dd>
</div>
</dl>
1 change: 1 addition & 0 deletions conf/application.base.conf
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ featureAccessBlock {

draftMetadata {
fileName = "draft-metadata.csv"
errorFileName = "draft-metadata-errors.json"
}

notificationSnsTopicArn = ${NOTIFICATION_SNS_TOPIC_ARN}
Expand Down
2 changes: 2 additions & 0 deletions conf/messages
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ notification.savedProgress.heading=Your progress has been saved
notification.savedProgress.metadataInfo=Your records and any metadata you added has been saved. You can leave at any time and return to this transfer by visiting the <a class="govuk-notification-banner__link" href="{0}">{1}</a> page.

additionalMetadata.descriptive.sensitive=If the description of a record contains sensitive information, you must enter the full uncensored version on the Descriptive metadata page before entering an alternative description on the Closure metadata page.
draftMetadata.validation.details.SCHEMA_VALIDATION=We found validation errors in the uploaded metadata.
draftMetadata.validation.action.SCHEMA_VALIDATION=Download the report below for details on individual validation errors.
Loading

0 comments on commit 355c1e7

Please sign in to comment.