Skip to content

Commit

Permalink
Add an optional footer for the generated PDF (#792)
Browse files Browse the repository at this point in the history
* add PDF footer

* fix timezone, reference id and move footer check to PdfService

* update mocks of GeneratePdf()

* change FooterContet to camelCase

* update styling according to new design

* add logger and try catch for norwegian time zone

* use explicit exception
  • Loading branch information
Magnusrm authored Oct 1, 2024
1 parent eedd776 commit 90042d7
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,27 @@ IHttpContextAccessor httpContextAccessor

/// <inheritdoc/>
public async Task<Stream> GeneratePdf(Uri uri, CancellationToken ct)
{
return await GeneratePdf(uri, null, ct);
}

/// <inheritdoc/>
public async Task<Stream> GeneratePdf(Uri uri, string? footerContent, CancellationToken ct)
{
bool hasWaitForSelector = !string.IsNullOrWhiteSpace(_pdfGeneratorSettings.WaitForSelector);
PdfGeneratorRequest generatorRequest =
new()
{
Url = uri.AbsoluteUri,
WaitFor = hasWaitForSelector ? _pdfGeneratorSettings.WaitForSelector : _pdfGeneratorSettings.WaitForTime
WaitFor = hasWaitForSelector
? _pdfGeneratorSettings.WaitForSelector
: _pdfGeneratorSettings.WaitForTime,
Options =
{
HeaderTemplate = "<div/>",
FooterTemplate = footerContent ?? "<div/>",
DisplayHeaderFooter = footerContent != null,
},
};

generatorRequest.Cookies.Add(
Expand Down
6 changes: 6 additions & 0 deletions src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ public interface IPdfGeneratorClient
/// </summary>
/// <returns>A stream with the binary content of the generated PDF</returns>
Task<Stream> GeneratePdf(Uri uri, CancellationToken ct);

/// <summary>
/// Generates a PDF.
/// </summary>
/// <returns>A stream with the binary content of the generated PDF with a footer</returns>
Task<Stream> GeneratePdf(Uri uri, string? footerContent, CancellationToken ct);
}
5 changes: 5 additions & 0 deletions src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public class PdfGeneratorSettings
/// </summary>
/// <remarks>This will be ignored if <see cref="WaitForSelector"/> has been assigned a value.</remarks>
public int WaitForTime { get; set; } = 5000;

/// <summary>
/// Shows a footer on each page in the PDF with the date, altinn-referance, page number and total pages.
/// </summary>
public bool DisplayFooter { get; set; }
}
50 changes: 49 additions & 1 deletion src/Altinn.App.Core/Internal/Pdf/PdfService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using System.Security.Claims;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Extensions;
Expand All @@ -11,6 +12,7 @@
using Altinn.Platform.Profile.Models;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

Expand All @@ -28,6 +30,7 @@ public class PdfService : IPdfService

private readonly IPdfGeneratorClient _pdfGeneratorClient;
private readonly PdfGeneratorSettings _pdfGeneratorSettings;
private readonly ILogger<PdfService> _logger;
private readonly GeneralSettings _generalSettings;
private readonly Telemetry? _telemetry;
private const string PdfElementType = "ref-data-as-pdf";
Expand All @@ -43,6 +46,7 @@ public class PdfService : IPdfService
/// <param name="pdfGeneratorClient">PDF generator client for the experimental PDF generator service</param>
/// <param name="pdfGeneratorSettings">PDF generator related settings.</param>
/// <param name="generalSettings">The app general settings.</param>
/// <param name="logger">The logger.</param>
/// <param name="telemetry">Telemetry for metrics and traces.</param>
public PdfService(
IAppResources appResources,
Expand All @@ -52,6 +56,7 @@ public PdfService(
IPdfGeneratorClient pdfGeneratorClient,
IOptions<PdfGeneratorSettings> pdfGeneratorSettings,
IOptions<GeneralSettings> generalSettings,
ILogger<PdfService> logger,
Telemetry? telemetry = null
)
{
Expand All @@ -62,6 +67,7 @@ public PdfService(
_pdfGeneratorClient = pdfGeneratorClient;
_pdfGeneratorSettings = pdfGeneratorSettings.Value;
_generalSettings = generalSettings.Value;
_logger = logger;
_telemetry = telemetry;
}

Expand Down Expand Up @@ -113,7 +119,10 @@ CancellationToken ct

Uri uri = BuildUri(baseUrl, pagePath, language);

Stream pdfContent = await _pdfGeneratorClient.GeneratePdf(uri, ct);
bool displayFooter = _pdfGeneratorSettings.DisplayFooter;
string? footerContent = displayFooter ? GetFooterContent(instance) : null;

Stream pdfContent = await _pdfGeneratorClient.GeneratePdf(uri, footerContent, ct);

return pdfContent;
}
Expand Down Expand Up @@ -225,4 +234,43 @@ private static string GetValidFileName(string fileName)
fileName = Uri.EscapeDataString(fileName.AsFileName(false));
return fileName;
}

private string GetFooterContent(Instance instance)
{
TimeZoneInfo timeZone = TimeZoneInfo.Utc;
try
{
// attempt to set timezone to norwegian
timeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Oslo");
}
catch (TimeZoneNotFoundException e)
{
_logger.LogWarning($"Could not find timezone Europe/Oslo. Defaulting to UTC. {e.Message}");
}

DateTimeOffset now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZone);

string dateGenerated = now.ToString("G", new CultureInfo("nb-NO"));
string altinnReferenceId = instance.Id.Split("/")[1].Split("-")[4];

string footerTemplate =
$@"<div style='font-family: Inter; font-size: 12px; width: 100%; display: flex; flex-direction: row; align-items: center; gap: 12px; padding: 0 70px 0 70px;'>
<div style='display: flex; flex-direction: row; width: 100%; align-items: center'>
<span class='title'></span>
<div
id='header-template'
style='color: #F00; font-weight: 700; border: 1px solid #F00; padding: 6px 8px; margin-left: auto;'
>
<span>{dateGenerated} </span>
<span>ID:{altinnReferenceId}</span>
</div>
</div>
<div style='display: flex; flex-direction-row; align-items: center;'>
<span class='pageNumber'></span>
/
<span class='totalPages'></span>
</div>
</div>";
return footerTemplate;
}
}
15 changes: 15 additions & 0 deletions src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ internal class PdfGeneratorRequestOptions
/// </summary>
public bool DisplayHeaderFooter { get; set; } = false;

/// <summary>
/// HTML template for the print header. Should be valid HTML with the following classes used to inject values into them:
/// * `date` formatted print date
/// * `title` document title
/// * `url` document location
/// * `pageNumber` current page number
/// * `totalPages` total pages in the document
/// </summary>
public string HeaderTemplate { get; set; } = string.Empty;

/// <summary>
/// HTML template for the print footer. Has the same constraints and support for special classes as HeaderTemplate.
/// </summary>
public string FooterTemplate { get; set; } = string.Empty;

/// <summary>
/// Indicate wheter the background should be included.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,9 @@ public async Task InstationAllowedByOrg_Returns_Ok_For_User_When_Copying_Simplif
{
var pdfMock = new Mock<IPdfGeneratorClient>(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
pdfMock.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(pdfReturnStream);
pdfMock
.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pdfReturnStream);

// Setup test data
string org = "tdd";
Expand Down
16 changes: 12 additions & 4 deletions test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
using Altinn.App.Core.Internal.Pdf;
using Altinn.App.Core.Internal.Profile;
using Altinn.Platform.Storage.Interface.Models;
using Castle.Core.Logging;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Moq.Protected;
Expand Down Expand Up @@ -42,6 +44,8 @@ public class PdfControllerTests
new() { }
);

private readonly Mock<ILogger<PdfService>> _logger = new();

public PdfControllerTests()
{
_instanceClient
Expand Down Expand Up @@ -92,7 +96,8 @@ public async Task Request_In_Prod_Should_Be_Blocked()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
generalSettingsOptions
generalSettingsOptions,
_logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
Expand Down Expand Up @@ -146,7 +151,8 @@ public async Task Request_In_Dev_Should_Generate()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
generalSettingsOptions
generalSettingsOptions,
_logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
Expand Down Expand Up @@ -225,7 +231,8 @@ public async Task Request_In_Dev_Should_Include_Frontend_Version()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
generalSettingsOptions
generalSettingsOptions,
_logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
Expand Down Expand Up @@ -306,7 +313,8 @@ public async Task Request_In_TT02_Should_Ignore_Frontend_Version()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
generalSettingsOptions
generalSettingsOptions,
_logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,9 @@ public async Task RunProcessNext_NonErrorValidations_ReturnsOk()
);
var pdfMock = new Mock<IPdfGeneratorClient>(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
pdfMock.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(pdfReturnStream);
pdfMock
.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pdfReturnStream);
OverrideServicesForThisTest = (services) =>
{
services.AddSingleton(dataValidator.Object);
Expand All @@ -366,7 +368,9 @@ public async Task RunCompleteTask_GoesToEndEvent()
{
var pdfMock = new Mock<IPdfGeneratorClient>(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
pdfMock.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(pdfReturnStream);
pdfMock
.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(pdfReturnStream);
OverrideServicesForThisTest = (services) =>
{
services.AddSingleton(pdfMock.Object);
Expand Down
15 changes: 13 additions & 2 deletions test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
using Altinn.Platform.Profile.Models;
using Altinn.Platform.Storage.Interface.Models;
using AltinnCore.Authentication.Constants;
using Castle.Core.Logging;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Moq;
Expand Down Expand Up @@ -43,6 +45,8 @@ public class PdfServiceTests

private readonly Mock<IUserTokenProvider> _userTokenProvider;

private readonly Mock<ILogger<PdfService>> _logger = new();

public PdfServiceTests()
{
var resource = new TextResource()
Expand Down Expand Up @@ -134,7 +138,9 @@ public async Task GenerateAndStorePdf()
{
// Arrange
TelemetrySink telemetrySink = new();
_pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny<Uri>(), It.IsAny<CancellationToken>()));
_pdfGeneratorClient.Setup(s =>
s.GeneratePdf(It.IsAny<Uri>(), It.IsAny<string?>(), It.IsAny<CancellationToken>())
);
_generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}";

var target = SetupPdfService(
Expand Down Expand Up @@ -164,6 +170,7 @@ public async Task GenerateAndStorePdf()
&& u.AbsoluteUri.Contains(instance.AppId)
&& u.AbsoluteUri.Contains(instance.Id)
),
It.Is<string?>(s => s == null),
It.IsAny<CancellationToken>()
),
Times.Once
Expand All @@ -189,7 +196,9 @@ public async Task GenerateAndStorePdf()
public async Task GenerateAndStorePdf_with_generatedFrom()
{
// Arrange
_pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny<Uri>(), It.IsAny<CancellationToken>()));
_pdfGeneratorClient.Setup(s =>
s.GeneratePdf(It.IsAny<Uri>(), It.IsAny<string?>(), It.IsAny<CancellationToken>())
);

_generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}";

Expand Down Expand Up @@ -228,6 +237,7 @@ public async Task GenerateAndStorePdf_with_generatedFrom()
&& u.AbsoluteUri.Contains(instance.AppId)
&& u.AbsoluteUri.Contains(instance.Id)
),
It.Is<string?>(s => s == null),
It.IsAny<CancellationToken>()
),
Times.Once
Expand Down Expand Up @@ -394,6 +404,7 @@ private PdfService SetupPdfService(
pdfGeneratorClient?.Object ?? _pdfGeneratorClient.Object,
pdfGeneratorSettingsOptions ?? _pdfGeneratorSettingsOptions,
generalSettingsOptions ?? _generalSettingsOptions,
_logger.Object,
telemetrySink?.Object
);
}
Expand Down

0 comments on commit 90042d7

Please sign in to comment.