Skip to content

Commit 04b8a6a

Browse files
authored
License expression display in Gallery (#6784)
* Generating markup for license expression display. * Test for controller to call license expression splitter when license expression is present. * More comments on why do we need helpers in DisplayPackage.cshtml
1 parent 20e0683 commit 04b8a6a

6 files changed

Lines changed: 133 additions & 7 deletions

File tree

src/NuGetGallery/App_Start/DefaultDependenciesModule.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Microsoft.WindowsAzure.ServiceRuntime;
2323
using NuGet.Services.Entities;
2424
using NuGet.Services.KeyVault;
25+
using NuGet.Services.Licenses;
2526
using NuGet.Services.Logging;
2627
using NuGet.Services.Messaging;
2728
using NuGet.Services.Messaging.Email;
@@ -345,6 +346,18 @@ protected override void Load(ContainerBuilder builder)
345346
.As<IContentFileMetadataService>()
346347
.InstancePerLifetimeScope();
347348

349+
builder.RegisterType<LicenseExpressionSplitter>()
350+
.As<ILicenseExpressionSplitter>()
351+
.InstancePerLifetimeScope();
352+
353+
builder.RegisterType<LicenseExpressionParser>()
354+
.As<ILicenseExpressionParser>()
355+
.InstancePerLifetimeScope();
356+
357+
builder.RegisterType<LicenseExpressionSegmentator>()
358+
.As<ILicenseExpressionSegmentator>()
359+
.InstancePerLifetimeScope();
360+
348361
RegisterMessagingService(builder, configuration);
349362

350363
builder.Register(c => HttpContext.Current.User)

src/NuGetGallery/Controllers/PackagesController.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Web.Mvc;
1717
using NuGet.Packaging;
1818
using NuGet.Services.Entities;
19+
using NuGet.Services.Licenses;
1920
using NuGet.Services.Messaging.Email;
2021
using NuGet.Versioning;
2122
using NuGetGallery.Areas.Admin;
@@ -96,6 +97,7 @@ public partial class PackagesController
9697
private readonly IDiagnosticsSource _trace;
9798
private readonly IFlatContainerService _flatContainerService;
9899
private readonly ICoreLicenseFileService _coreLicenseFileService;
100+
private readonly ILicenseExpressionSplitter _licenseExpressionSplitter;
99101

100102
public PackagesController(
101103
IPackageService packageService,
@@ -122,7 +124,8 @@ public PackagesController(
122124
ISymbolPackageUploadService symbolPackageUploadService,
123125
IDiagnosticsService diagnosticsService,
124126
IFlatContainerService flatContainerService,
125-
ICoreLicenseFileService coreLicenseFileService)
127+
ICoreLicenseFileService coreLicenseFileService,
128+
ILicenseExpressionSplitter licenseExpressionSplitter)
126129
{
127130
_packageService = packageService;
128131
_uploadFileService = uploadFileService;
@@ -149,6 +152,7 @@ public PackagesController(
149152
_trace = diagnosticsService?.SafeGetSource(nameof(PackagesController)) ?? throw new ArgumentNullException(nameof(diagnosticsService));
150153
_flatContainerService = flatContainerService;
151154
_coreLicenseFileService = coreLicenseFileService ?? throw new ArgumentNullException(nameof(coreLicenseFileService));
155+
_licenseExpressionSplitter = licenseExpressionSplitter ?? throw new ArgumentNullException(nameof(licenseExpressionSplitter));
152156
}
153157

154158
[HttpGet]
@@ -624,6 +628,21 @@ public virtual async Task<ActionResult> DisplayPackage(string id, string version
624628

625629
model.ReadMeHtml = await _readMeService.GetReadMeHtmlAsync(package);
626630

631+
if (!string.IsNullOrWhiteSpace(package.LicenseExpression))
632+
{
633+
try
634+
{
635+
model.LicenseExpressionSegments = _licenseExpressionSplitter.SplitExpression(package.LicenseExpression);
636+
}
637+
catch (Exception ex)
638+
{
639+
// Any exception thrown while trying to render license expression beautifully
640+
// is not severe enough to break the client experience, view will fall back to
641+
// display license url.
642+
_telemetryService.TraceException(ex);
643+
}
644+
}
645+
627646
var externalSearchService = _searchService as ExternalSearchService;
628647
if (_searchService.ContainsAllVersions && externalSearchService != null)
629648
{

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,9 @@
20492049
<PackageReference Include="Markdig.Signed">
20502050
<Version>0.15.4</Version>
20512051
</PackageReference>
2052+
<PackageReference Include="NuGet.Services.Licenses">
2053+
<Version>2.41.0-agr-licenses-2294033</Version>
2054+
</PackageReference>
20522055
<PackageReference Include="NuGet.StrongName.AnglicanGeek.MarkdownMailer">
20532056
<Version>1.2.0</Version>
20542057
</PackageReference>

src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using NuGet.Services.Entities;
8+
using NuGet.Services.Licenses;
89
using NuGet.Services.Validation.Issues;
910
using NuGet.Versioning;
1011

@@ -130,6 +131,7 @@ public bool HasNewerRelease
130131
public string LicenseUrl { get; set; }
131132
public IEnumerable<string> LicenseNames { get; set; }
132133
public string LicenseExpression { get; set; }
134+
public IReadOnlyCollection<CompositeLicenseExpressionSegment> LicenseExpressionSegments { get; set; }
133135
public EmbeddedLicenseFileType EmbeddedLicenseType { get; set; }
134136

135137
private IDictionary<User, string> _pushedByCache = new Dictionary<User, string>();

src/NuGetGallery/Views/Packages/DisplayPackage.cshtml

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
@using NuGet.Services.Validation;
1+
@using NuGet.Services.Validation
2+
@using NuGet.Services.Licenses
23

34
@model DisplayPackageViewModel
45
@{
@@ -69,6 +70,14 @@
6970
}
7071
}
7172

73+
@functions
74+
{
75+
static bool IsLicenseOrException(CompositeLicenseExpressionSegment segment)
76+
{
77+
return segment.Type == CompositeLicenseExpressionSegmentType.LicenseIdentifier || segment.Type == CompositeLicenseExpressionSegmentType.ExceptionIdentifier;
78+
}
79+
}
80+
7281
@section SocialMeta {
7382
@if (!String.IsNullOrWhiteSpace(ViewBag.FacebookAppID))
7483
{
@@ -146,6 +155,11 @@
146155
</div>
147156
}
148157

158+
@* The following two helpers must be on a single line each so no extra whitespace is introduced in the expression when rendered. *@
159+
@* Helpers themselves are needed not to introduce that extra whitespce, which happens if they are inlined. *@
160+
@helper MakeLicenseLink(CompositeLicenseExpressionSegment segment) {<a href="@LicenseExpressionRedirectUrlHelper.GetLicenseExpressionRedirectUrl(segment.Value)">@segment.Value</a>}
161+
@helper MakeLicenseSpan(CompositeLicenseExpressionSegment segment) {<span>@segment.Value</span>}
162+
149163
<section role="main" class="container main-container page-package-details">
150164
<div class="row">
151165
<aside aria-label="Package icon" class="col-sm-1">
@@ -638,8 +652,25 @@
638652
{
639653
if (Model.EmbeddedLicenseType == EmbeddedLicenseFileType.Absent || !Model.Validating)
640654
{
641-
<li>
642-
<i class="ms-Icon ms-Icon--Certificate" aria-hidden="true"></i>
655+
<li>
656+
<i class="ms-Icon ms-Icon--Certificate" aria-hidden="true"></i>
657+
@if (Model.LicenseExpressionSegments.AnySafe())
658+
{
659+
@:License:
660+
foreach (var segment in Model.LicenseExpressionSegments)
661+
{
662+
if (IsLicenseOrException(segment))
663+
{
664+
@MakeLicenseLink(segment);
665+
}
666+
else
667+
{
668+
@MakeLicenseSpan(segment);
669+
}
670+
}
671+
}
672+
else
673+
{
643674
<a href="@Model.LicenseUrl"
644675
data-track="outbound-license-url" title="Make sure you agree with the license" rel="nofollow">
645676
@if (Model.LicenseNames.AnySafe())
@@ -651,7 +682,8 @@
651682
@:License Info
652683
}
653684
</a>
654-
</li>
685+
}
686+
</li>
655687
}
656688
}
657689
<li>

tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Moq;
1717
using NuGet.Packaging;
1818
using NuGet.Services.Entities;
19+
using NuGet.Services.Licenses;
1920
using NuGet.Services.Messaging.Email;
2021
using NuGet.Services.Validation;
2122
using NuGet.Services.Validation.Issues;
@@ -67,7 +68,8 @@ private static PackagesController CreateController(
6768
Mock<IContentObjectService> contentObjectService = null,
6869
Mock<ISymbolPackageUploadService> symbolPackageUploadService = null,
6970
Mock<IFlatContainerService> flatContainerService = null,
70-
Mock<ICoreLicenseFileService> coreLicenseFileService = null)
71+
Mock<ICoreLicenseFileService> coreLicenseFileService = null,
72+
Mock<ILicenseExpressionSplitter> licenseExpressionSplitter = null)
7173
{
7274
packageService = packageService ?? new Mock<IPackageService>();
7375
if (uploadFileService == null)
@@ -185,6 +187,8 @@ private static PackagesController CreateController(
185187
.ReturnsAsync(() => new MemoryStream());
186188
}
187189

190+
licenseExpressionSplitter = licenseExpressionSplitter ?? new Mock<ILicenseExpressionSplitter>();
191+
188192
var diagnosticsService = new Mock<IDiagnosticsService>();
189193
var controller = new Mock<PackagesController>(
190194
packageService.Object,
@@ -211,7 +215,8 @@ private static PackagesController CreateController(
211215
symbolPackageUploadService.Object,
212216
diagnosticsService.Object,
213217
flatContainerService.Object,
214-
coreLicenseFileService.Object);
218+
coreLicenseFileService.Object,
219+
licenseExpressionSplitter.Object);
215220

216221
controller.CallBase = true;
217222
controller.Object.SetOwinContextOverride(Fakes.CreateOwinContext());
@@ -838,6 +843,58 @@ public async Task GetsValidationIssues()
838843
Assert.Equal(model.PackageValidationIssues, expectedIssues);
839844
}
840845

846+
[Fact]
847+
public async Task SplitsLicenseExpressionWhenProvided()
848+
{
849+
const string expression = "some expression";
850+
var splitterMock = new Mock<ILicenseExpressionSplitter>();
851+
var packageService = new Mock<IPackageService>();
852+
var indexingService = new Mock<IIndexingService>();
853+
854+
var segments = new List<CompositeLicenseExpressionSegment>();
855+
splitterMock
856+
.Setup(les => les.SplitExpression(expression))
857+
.Returns(segments);
858+
859+
var package = new Package()
860+
{
861+
PackageRegistration = new PackageRegistration()
862+
{
863+
Id = "Foo",
864+
Owners = new List<User>()
865+
},
866+
Version = "01.1.01",
867+
NormalizedVersion = "1.1.1",
868+
Title = "A test package!",
869+
LicenseExpression = expression,
870+
};
871+
872+
packageService.Setup(p => p.FindPackageByIdAndVersion(
873+
It.Is<string>(s => s == "Foo"),
874+
It.Is<string>(s => s == null),
875+
It.Is<int>(i => i == SemVerLevelKey.SemVer2),
876+
It.Is<bool>(b => b == true)))
877+
.Returns(package);
878+
879+
indexingService.Setup(i => i.GetLastWriteTime()).Returns(Task.FromResult((DateTime?)DateTime.UtcNow));
880+
881+
var controller = CreateController(
882+
GetConfigurationService(),
883+
packageService: packageService,
884+
indexingService: indexingService,
885+
licenseExpressionSplitter: splitterMock);
886+
887+
var result = await controller.DisplayPackage(id: "Foo", version: null);
888+
889+
splitterMock
890+
.Verify(les => les.SplitExpression(expression), Times.Once);
891+
splitterMock
892+
.Verify(les => les.SplitExpression(It.IsAny<string>()), Times.Once);
893+
894+
var model = ResultAssert.IsView<DisplayPackageViewModel>(result);
895+
Assert.Same(segments, model.LicenseExpressionSegments);
896+
}
897+
841898
private class TestIssue : ValidationIssue
842899
{
843900
private readonly string _message;

0 commit comments

Comments
 (0)