Skip to content

Commit 8ca2740

Browse files
authored
Feature/upload feature (#32)
1 parent d255129 commit 8ca2740

18 files changed

Lines changed: 620 additions & 28 deletions

src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.13" />
1011
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.3.0" />
1112
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.0" />
1213
</ItemGroup>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using DfE.CoreLibs.Contracts.ExternalApplications.Models.Response;
2+
using GovUK.Dfe.ExternalApplications.Api.Client.Contracts;
3+
4+
namespace DfE.ExternalApplications.Application.Interfaces
5+
{
6+
/// <summary>
7+
/// Service for managing file uploads for applications
8+
/// </summary>
9+
public interface IFileUploadService
10+
{
11+
Task UploadFileAsync(Guid applicationId, string? name = null, string? description = null, FileParameter file = null!, CancellationToken cancellationToken = default);
12+
Task<IReadOnlyList<UploadDto>> GetFilesForApplicationAsync(Guid applicationId, CancellationToken cancellationToken = default);
13+
Task<FileResponse> DownloadFileAsync(Guid fileId, Guid applicationId, CancellationToken cancellationToken = default);
14+
Task DeleteFileAsync(Guid fileId, Guid applicationId, CancellationToken cancellationToken = default);
15+
}
16+
}

src/DfE.ExternalApplications.Infrastructure/DfE.ExternalApplications.Infrastructure.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="DfE.CoreLibs.Caching" Version="1.0.10" />
1111
<PackageReference Include="DfE.CoreLibs.Contracts" Version="1.0.34" />
12-
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.10" />
12+
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.13" />
1313
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.3.0" />
1414
</ItemGroup>
1515

src/DfE.ExternalApplications.Infrastructure/Services/ApplicationResponseService.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,36 @@ private object CleanFormValue(object value)
129129
{
130130
if (value == null)
131131
return string.Empty;
132-
132+
133133
if (value is JsonElement jsonElement)
134134
{
135-
return jsonElement.ValueKind switch
135+
switch (jsonElement.ValueKind)
136136
{
137-
JsonValueKind.String => jsonElement.GetString() ?? string.Empty,
138-
JsonValueKind.Array => jsonElement.GetArrayLength() == 1
139-
? jsonElement[0].GetString() ?? string.Empty
140-
: jsonElement.EnumerateArray().Select(x => x.GetString() ?? string.Empty).ToArray(),
141-
JsonValueKind.Number => jsonElement.GetDecimal().ToString(),
142-
JsonValueKind.True => "true",
143-
JsonValueKind.False => "false",
144-
_ => value.ToString() ?? string.Empty
145-
};
137+
case JsonValueKind.String:
138+
return jsonElement.GetString() ?? string.Empty;
139+
case JsonValueKind.Array:
140+
var allStrings = jsonElement.EnumerateArray().All(e => e.ValueKind == JsonValueKind.String);
141+
if (allStrings)
142+
{
143+
return jsonElement.GetArrayLength() == 1
144+
? jsonElement[0].GetString() ?? string.Empty
145+
: jsonElement.EnumerateArray().Select(x => x.GetString() ?? string.Empty).ToArray();
146+
}
147+
// return raw JSON for arrays of objects
148+
return jsonElement.ToString();
149+
case JsonValueKind.Number:
150+
return jsonElement.GetDecimal().ToString();
151+
case JsonValueKind.True:
152+
return "true";
153+
case JsonValueKind.False:
154+
return "false";
155+
case JsonValueKind.Object:
156+
return jsonElement.ToString();
157+
default:
158+
return value.ToString() ?? string.Empty;
159+
}
146160
}
147-
161+
148162
if (value is string[] stringArray && stringArray.Length == 1)
149163
{
150164
return stringArray[0];

src/DfE.ExternalApplications.Infrastructure/Services/ComplexFieldRendererFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class ComplexFieldRendererFactory(IEnumerable<IComplexFieldRenderer> rend
99
public IComplexFieldRenderer GetRenderer(string fieldType)
1010
{
1111
var renderer = renderers.FirstOrDefault(r => r.FieldType.Equals(fieldType, StringComparison.OrdinalIgnoreCase));
12-
return renderer ?? renderers.FirstOrDefault(r => r.FieldType == "autocomplete"); // Default to autocomplete
12+
return renderer ?? renderers.FirstOrDefault(r => r.FieldType == "autocomplete" || r.FieldType == "upload"); // Default to autocomplete or upload
1313
}
1414
}
1515
}

src/DfE.ExternalApplications.Infrastructure/Services/FieldFormattingService.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ public string GetFormattedFieldValue(string fieldId, Dictionary<string, object>
5656
// Try to format as autocomplete data if it looks like JSON
5757
if (fieldValue.StartsWith("{") || fieldValue.StartsWith("["))
5858
{
59+
if (LooksLikeUploadData(fieldValue))
60+
{
61+
return FormatUploadValue(fieldValue);
62+
}
63+
5964
return FormatAutocompleteValue(fieldValue);
6065
}
6166

@@ -71,9 +76,13 @@ public List<string> GetFormattedFieldValues(string fieldId, Dictionary<string, o
7176
return new List<string>();
7277
}
7378

74-
// Try to format as autocomplete data if it looks like JSON
7579
if (fieldValue.StartsWith("{") || fieldValue.StartsWith("["))
7680
{
81+
if (LooksLikeUploadData(fieldValue))
82+
{
83+
return FormatUploadValuesList(fieldValue);
84+
}
85+
7786
return FormatAutocompleteValuesList(fieldValue);
7887
}
7988

@@ -233,5 +242,65 @@ private string FormatSingleAutocompleteValue(JsonElement element)
233242

234243
return System.Web.HttpUtility.HtmlEncode(element.ToString());
235244
}
245+
246+
private bool LooksLikeUploadData(string value)
247+
{
248+
try
249+
{
250+
using var doc = JsonDocument.Parse(value);
251+
if (doc.RootElement.ValueKind == JsonValueKind.Array && doc.RootElement.GetArrayLength() > 0)
252+
{
253+
var first = doc.RootElement[0];
254+
return first.ValueKind == JsonValueKind.Object && first.TryGetProperty("OriginalFileName", out _);
255+
}
256+
}
257+
catch
258+
{
259+
// ignore parse errors
260+
}
261+
262+
return false;
263+
}
264+
265+
private string FormatUploadValue(string value)
266+
{
267+
try
268+
{
269+
using var doc = JsonDocument.Parse(value);
270+
if (doc.RootElement.ValueKind == JsonValueKind.Array)
271+
{
272+
var names = doc.RootElement.EnumerateArray()
273+
.Select(e => e.TryGetProperty("OriginalFileName", out var n) ? n.GetString() ?? string.Empty : string.Empty)
274+
.Where(n => !string.IsNullOrEmpty(n));
275+
return string.Join("<br />", names);
276+
}
277+
}
278+
catch
279+
{
280+
// ignore and return raw value
281+
}
282+
return value;
283+
}
284+
285+
private List<string> FormatUploadValuesList(string value)
286+
{
287+
try
288+
{
289+
using var doc = JsonDocument.Parse(value);
290+
if (doc.RootElement.ValueKind == JsonValueKind.Array)
291+
{
292+
var names = doc.RootElement.EnumerateArray()
293+
.Select(e => e.TryGetProperty("OriginalFileName", out var n) ? n.GetString() ?? string.Empty : string.Empty)
294+
.Where(n => !string.IsNullOrEmpty(n))
295+
.ToList();
296+
return names;
297+
}
298+
}
299+
catch
300+
{
301+
// ignore
302+
}
303+
return new List<string> { value };
304+
}
236305
}
237306
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using DfE.ExternalApplications.Application.Interfaces;
2+
using GovUK.Dfe.ExternalApplications.Api.Client.Contracts;
3+
using DfE.CoreLibs.Contracts.ExternalApplications.Models.Response;
4+
using Microsoft.Extensions.Logging;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace DfE.ExternalApplications.Infrastructure.Services
11+
{
12+
public class FileUploadService(IApplicationsClient applicationsClient, ILogger<FileUploadService> logger)
13+
: IFileUploadService
14+
{
15+
public async Task UploadFileAsync(Guid applicationId, string? name = null, string? description = null, FileParameter file = null!, CancellationToken cancellationToken = default)
16+
{
17+
try
18+
{
19+
logger.LogInformation("Uploading file for application {ApplicationId}", applicationId);
20+
await applicationsClient.UploadFileAsync(applicationId, name, description, file, cancellationToken);
21+
logger.LogInformation("File uploaded successfully for application {ApplicationId}", applicationId);
22+
}
23+
catch (Exception ex)
24+
{
25+
logger.LogError(ex, "Error uploading file for application {ApplicationId}", applicationId);
26+
throw;
27+
}
28+
}
29+
30+
public async Task<IReadOnlyList<UploadDto>> GetFilesForApplicationAsync(Guid applicationId, CancellationToken cancellationToken = default)
31+
{
32+
try
33+
{
34+
logger.LogInformation("Getting files for application {ApplicationId}", applicationId);
35+
var files = await applicationsClient.GetFilesForApplicationAsync(applicationId, cancellationToken);
36+
return files;
37+
}
38+
catch (Exception ex)
39+
{
40+
logger.LogError(ex, "Error getting files for application {ApplicationId}", applicationId);
41+
return new List<UploadDto>().AsReadOnly();
42+
}
43+
}
44+
45+
public async Task<FileResponse> DownloadFileAsync(Guid fileId, Guid applicationId, CancellationToken cancellationToken = default)
46+
{
47+
try
48+
{
49+
logger.LogInformation("Downloading file {FileId} for application {ApplicationId}", fileId, applicationId);
50+
return await applicationsClient.DownloadFileAsync(fileId, applicationId, cancellationToken);
51+
}
52+
catch (Exception ex)
53+
{
54+
logger.LogError(ex, "Error downloading file {FileId} for application {ApplicationId}", fileId, applicationId);
55+
throw;
56+
}
57+
}
58+
59+
public async Task DeleteFileAsync(Guid fileId, Guid applicationId, CancellationToken cancellationToken = default)
60+
{
61+
try
62+
{
63+
logger.LogInformation("Deleting file {FileId} for application {ApplicationId}", fileId, applicationId);
64+
await applicationsClient.DeleteFileAsync(fileId, applicationId, cancellationToken);
65+
logger.LogInformation("File {FileId} deleted for application {ApplicationId}", fileId, applicationId);
66+
}
67+
catch (Exception ex)
68+
{
69+
logger.LogError(ex, "Error deleting file {FileId} for application {ApplicationId}", fileId, applicationId);
70+
throw;
71+
}
72+
}
73+
}
74+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using DfE.ExternalApplications.Application.Interfaces;
2+
using DfE.ExternalApplications.Domain.Models;
3+
4+
namespace DfE.ExternalApplications.Infrastructure.Services
5+
{
6+
public class UploadComplexFieldRenderer : IComplexFieldRenderer
7+
{
8+
public string FieldType => "upload";
9+
10+
public string Render(ComplexFieldConfiguration configuration, string fieldId, string currentValue, string errorMessage, string label, string tooltip, bool isRequired)
11+
{
12+
var isRequiredAttr = isRequired ? "required" : "";
13+
var errorClass = !string.IsNullOrEmpty(errorMessage) ? "govuk-form-group--error" : "";
14+
var labelClasses = "govuk-label";
15+
16+
// Generate unique IDs
17+
var inputId = $"{fieldId}-upload-input";
18+
var nameId = $"{fieldId}-name";
19+
var descId = $"{fieldId}-desc";
20+
var uploadBtnId = $"{fieldId}-upload-btn";
21+
22+
return $@"
23+
<div class=""govuk-form-group {errorClass}"">
24+
<fieldset class=""govuk-fieldset"">
25+
<legend class=""govuk-fieldset__legend {labelClasses}"">
26+
{label}
27+
{(isRequired ? "<span class=\"govuk-visually-hidden\">required</span>" : "")}
28+
</legend>
29+
{(string.IsNullOrEmpty(tooltip) ? "" : $@"<div class=""govuk-hint"">{tooltip}</div>")}
30+
31+
{(string.IsNullOrEmpty(errorMessage) ? "" : $@"<div class=""govuk-error-message"">{errorMessage}</div>")}
32+
33+
<input class=""govuk-input"" id=""{nameId}"" name=""{nameId}"" type=""text"" placeholder=""File name (optional)"" />
34+
<input class=""govuk-input"" id=""{descId}"" name=""{descId}"" type=""text"" placeholder=""Description (optional)"" />
35+
<input class=""govuk-file-upload"" id=""{inputId}"" name=""{inputId}"" type=""file"" {isRequiredAttr} />
36+
<button class=""govuk-button"" id=""{uploadBtnId}"" type=""submit"">Upload</button>
37+
<!-- Placeholder for file list and actions -->
38+
<div id=""{fieldId}-file-list""></div>
39+
</div>";
40+
}
41+
}
42+
}

src/DfE.ExternalApplications.Web/DfE.ExternalApplications.Web.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="DfE.CoreLibs.Caching" Version="1.0.10" />
12-
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.10" />
12+
<PackageReference Include="GovUK.Dfe.ExternalApplications.Api.Client" Version="0.1.13" />
1313
<PackageReference Include="GovUk.Frontend.AspNetCore" Version="3.2.2" />
1414
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
1515
<PackageReference Include="DfE.CoreLibs.Security" Version="1.1.13" />

src/DfE.ExternalApplications.Web/Pages/Applications/TaskSummary.cshtml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@page "/applications/{referenceNumber}/{taskId}/summary"
22
@model TaskSummaryModel
3+
@inject Application.Interfaces.IComplexFieldConfigurationService ComplexFieldConfigurationService
34
@{
45
ViewData["Title"] = Model.CurrentTask?.TaskName ?? "Task Summary";
56
}
@@ -16,7 +17,7 @@
1617
var fieldValue = Model.GetFieldValue(field.FieldId);
1718
var hasValue = Model.HasFieldValue(field.FieldId);
1819
var changeUrl = $"/form-engine/render-form/{Model.ReferenceNumber}/{page.PageId}";
19-
20+
2021
if ((field.Type == "autocomplete" || field.Type == "complexField") && hasValue)
2122
{
2223
// Handle multiple autocomplete/complex field values as separate rows

0 commit comments

Comments
 (0)