From 81d840a090ea8538f23522757cfca6fe8ec04d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brian=20Holmg=C3=A5rd=20Kristensen?= Date: Mon, 17 Oct 2022 21:50:25 +0200 Subject: [PATCH 1/3] feat: upgrade Relewise dependencies, update nullable checks, improve dashboard including more detailed descriptions, implement default anonymous user locator. --- .../Api/CatalogApi.cs | 5 + .../Api/SearchApi.cs | 4 +- .../BlogMapper.cs | 1 + .../CookieConsent/CookieConsent.cs | 23 +++-- .../RelewiseUserLocator.cs | 8 +- samples/UmbracoV10/Relewise.UmbracoV10.csproj | 4 +- samples/UmbracoV9/Relewise.UmbracoV9.csproj | 4 +- .../Relewise.Dashboard/dashboard.html | 94 ++++++++++++++++--- src/Integrations.Umbraco/Builders.cs | 2 +- .../Controllers/DashboardApiController.cs | 80 +++++++++++----- .../DefaultAnonymousUserLocator.cs | 12 +++ .../Middlewares/RelewiseContentMiddleware.cs | 39 ++++---- .../Integrations.Umbraco.csproj | 4 +- ...tDeletedNotificationNotificationHandler.cs | 2 +- ...RelewiseContentMovedNotificationHandler.cs | 2 +- ...seContentUnpublishedNotificationHandler.cs | 2 +- .../NestedContentPropertyValueConverter.cs | 12 ++- .../RelewisePropertyConverterContext.cs | 6 +- .../Services/ContentMapper.cs | 2 + .../Services/ExportContentService.cs | 4 +- .../Services/IRelewisePropertyConverter.cs | 4 +- .../Services/RelewisePropertyConverter.cs | 18 ++-- .../UmbracoBuilderExtensions.cs | 5 +- 23 files changed, 244 insertions(+), 93 deletions(-) create mode 100644 src/Integrations.Umbraco/DefaultAnonymousUserLocator.cs diff --git a/samples/Relewise.Umbraco.Application/Api/CatalogApi.cs b/samples/Relewise.Umbraco.Application/Api/CatalogApi.cs index 50ff4f3..db4edd7 100644 --- a/samples/Relewise.Umbraco.Application/Api/CatalogApi.cs +++ b/samples/Relewise.Umbraco.Application/Api/CatalogApi.cs @@ -7,6 +7,7 @@ using Relewise.Client; using Relewise.Client.DataTypes; using Relewise.Client.DataTypes.Search.Facets.Enums; +using Relewise.Client.DataTypes.Search.Facets.Queries; using Relewise.Client.DataTypes.Search.Facets.Result; using Relewise.Client.DataTypes.Search.Sorting.Enums; using Relewise.Client.DataTypes.Search.Sorting.Product; @@ -67,6 +68,10 @@ private static async Task Search( } }; + request.Facets ??= new ProductFacetQuery(); + request.Filters ??= new FilterCollection(); + request.Sorting ??= new ProductSortBySpecification(); + request.Facets.AddCategory(CategorySelectionStrategy.ImmediateParent, category2Id != null ? new[] { category2Id } : null); if (categoryId != null) diff --git a/samples/Relewise.Umbraco.Application/Api/SearchApi.cs b/samples/Relewise.Umbraco.Application/Api/SearchApi.cs index c483b89..1f6e02f 100644 --- a/samples/Relewise.Umbraco.Application/Api/SearchApi.cs +++ b/samples/Relewise.Umbraco.Application/Api/SearchApi.cs @@ -54,7 +54,7 @@ private static async Task Search( Currency.Undefined, user, DisplayedAtLocation, - q, + q ?? string.Empty, take: 5) { Settings = new SearchTermPredictionSettings @@ -68,7 +68,7 @@ private static async Task Search( Currency.Undefined, user, DisplayedAtLocation, - q, + q ?? string.Empty, skip: 0, take: 5) { diff --git a/samples/Relewise.Umbraco.Application/BlogMapper.cs b/samples/Relewise.Umbraco.Application/BlogMapper.cs index 3ac2fc4..1c5dac2 100644 --- a/samples/Relewise.Umbraco.Application/BlogMapper.cs +++ b/samples/Relewise.Umbraco.Application/BlogMapper.cs @@ -8,6 +8,7 @@ public class BlogMapper : IContentTypeMapping { public Task Map(ContentMappingContext context, CancellationToken token) { + context.ContentUpdate.Content.Data ??= new Dictionary(); context.ContentUpdate.Content.Data["Title"] = context.PublishedContent.GetProperty("title")?.GetValue(context.CulturesToPublish.First()); return Task.FromResult(context.ContentUpdate); diff --git a/samples/Relewise.Umbraco.Application/Infrastructure/CookieConsent/CookieConsent.cs b/samples/Relewise.Umbraco.Application/Infrastructure/CookieConsent/CookieConsent.cs index 90ef1aa..c991ced 100644 --- a/samples/Relewise.Umbraco.Application/Infrastructure/CookieConsent/CookieConsent.cs +++ b/samples/Relewise.Umbraco.Application/Infrastructure/CookieConsent/CookieConsent.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; @@ -5,6 +6,8 @@ namespace Relewise.Umbraco.Application.Infrastructure.CookieConsent; public class CookieConsent { + public readonly string CookieName = "_cookieConsent"; + private readonly IHttpContextAccessor _httpContextAccessor; public CookieConsent(IHttpContextAccessor httpContextAccessor) @@ -14,16 +17,16 @@ public CookieConsent(IHttpContextAccessor httpContextAccessor) public bool HasGivenConsentFor(CookieType type) { - string? cookie = _httpContextAccessor.HttpContext?.Request.Cookies["_cookieconsent"]; + string? cookie = _httpContextAccessor.HttpContext?.Request.Cookies[CookieName]; + if (cookie == null) return false; CookieData? data = JsonConvert.DeserializeObject(cookie); if (data == null) - throw new InvalidOperationException("Could not find 3rd party cookieconsent cookie"); - - + throw new InvalidOperationException($"Unable to deserialize cookie '{CookieName}' with data: '{cookie}'."); + return type switch { CookieType.Functional => data.Cookies.Functional, @@ -33,23 +36,27 @@ public bool HasGivenConsentFor(CookieType type) }; } - public string? UserId() + public string UserId() { - string? cookie = _httpContextAccessor.HttpContext?.Request.Cookies["_cookieconsent"]; + string? cookie = _httpContextAccessor.HttpContext?.Request.Cookies[CookieName]; + if (cookie == null) - return null; + throw new InvalidOperationException("UserId is null as cookie does not exist."); var data = JsonConvert.DeserializeObject(cookie); - return data?.UserId; + return data?.UserId ?? throw new InvalidOperationException($"Unable to deserialize cookie '{CookieName}' with data: '{cookie}'."); } + [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Local")] private class CookieData { public string UserId { get; set; } = default!; public CookieTypes Cookies { get; set; } = default!; } + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] private class CookieTypes { public bool Functional { get; set; } diff --git a/samples/Relewise.Umbraco.Application/RelewiseUserLocator.cs b/samples/Relewise.Umbraco.Application/RelewiseUserLocator.cs index 6628174..3acd677 100644 --- a/samples/Relewise.Umbraco.Application/RelewiseUserLocator.cs +++ b/samples/Relewise.Umbraco.Application/RelewiseUserLocator.cs @@ -15,8 +15,10 @@ public RelewiseUserLocator(CookieConsent cookieConsent) public Task GetUser() { - return Task.FromResult(!_cookieConsent.HasGivenConsentFor(CookieType.Marketing) - ? User.Anonymous() - : User.ByTemporaryId(_cookieConsent.UserId())); + bool consentGiven = _cookieConsent.HasGivenConsentFor(CookieType.Marketing); + + return Task.FromResult(consentGiven + ? User.ByTemporaryId(_cookieConsent.UserId()) + : User.Anonymous()); } } \ No newline at end of file diff --git a/samples/UmbracoV10/Relewise.UmbracoV10.csproj b/samples/UmbracoV10/Relewise.UmbracoV10.csproj index 7b2ae93..d73e80f 100644 --- a/samples/UmbracoV10/Relewise.UmbracoV10.csproj +++ b/samples/UmbracoV10/Relewise.UmbracoV10.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/samples/UmbracoV9/Relewise.UmbracoV9.csproj b/samples/UmbracoV9/Relewise.UmbracoV9.csproj index 8e30eeb..b3f08c2 100644 --- a/samples/UmbracoV9/Relewise.UmbracoV9.csproj +++ b/samples/UmbracoV9/Relewise.UmbracoV9.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html b/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html index 195c321..443af0c 100644 --- a/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html +++ b/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html @@ -23,25 +23,72 @@

Relewise

Settings

-

- These are the settings registered with Relewise. -

+
+ Unexpected error occured. Please check the response on the XHR request or check logs.
+
-

- No options have been configured. Please check your call to the 'services.AddRelewise(options => { /* options goes here */ })'-method in 'Startup.cs'. -

- Tracked content types are: {{vm.configuration.trackedContentTypes.join(', ')}} -
(Page views for these content types are being tracked to Relewise, if the automatic page view tracking has been enabled)
-
- Exported content types are: {{vm.configuration.exportedContentTypes.join(', ')}} -
(These are configured for being exported to Relewise on publish or when the export is triggered manually)
+ +
+ No settings have been configured. Please check your call to the 'services.AddRelewise(options => { /* options goes here */ })'-method in 'Startup.cs':
+
+public void ConfigureServices(IServiceCollection services)
+{
+	services.AddRelewise(options => options.ReadFromConfiguration(_config));
+...			
+}
+ Which then reads from the following configuration in appsettings.json: +
+"Relewise": {
+   "DatasetId": "00000000-0000-0000-0000-000000000000",
+   "ApiKey": "ApiKey",
+   "Timeout": "00:00:03"
+}
+
+ Error message from server: +
{{vm.configuration.errorMessage}}
+ You can read more about configuring Relewise here: https://github.com/Relewise/relewise-sdk-csharp-extensions +
+ +
+ Tracked content types are: + {{vm.configuration.trackedContentTypes.join(', ')}} + No tracked content type have been configured +
(Page views on these content types are automatically being tracked to Relewise by the RelewiseContentMiddleware)
+
To have Page views being tracked automatically to Relewise, please ensure a call to the 'TrackContentViews()'-method in 'Startup.cs':
+
+app.UseUmbraco()
+	.WithMiddleware(u =>
+	{
+		u.UseBackOffice();
+		u.UseWebsite();
+		u.TrackContentViews();
+	})
+	.WithEndpoints(u =>
+	{
+		u.UseInstallerEndpoints();
+		u.UseBackOfficeEndpoints();
+		u.UseWebsiteEndpoints();
+	});
+
+ +
+ Exported content types are: + {{vm.configuration.exportedContentTypes.join(', ')}} + No exported content type have been configured +
(Any publish on these content types will automatically export the data to Relewise)
+
+ +
+
+
+ Find more information about the Umbraco integration here: https://github.com/Relewise/relewise-integrations-umbraco
-

Clients

- +

Clients

+
@@ -81,6 +128,27 @@

Clients

{{ named.searcher.datasetId }}
{{ named.searcher.timeout }}
+
+
+ Search Administrator +
+
{{ named.searchAdministrator.datasetId }}
+
{{ named.searchAdministrator.timeout }}
+
+
+
+ Analyzer +
+
{{ named.analyzer.datasetId }}
+
{{ named.analyzer.timeout }}
+
+
+
+ DataAccessor +
+
{{ named.dataAccessor.datasetId }}
+
{{ named.dataAccessor.timeout }}
+
diff --git a/src/Integrations.Umbraco/Builders.cs b/src/Integrations.Umbraco/Builders.cs index 466cbb2..75f1884 100644 --- a/src/Integrations.Umbraco/Builders.cs +++ b/src/Integrations.Umbraco/Builders.cs @@ -50,7 +50,7 @@ public class ContentTypeBuilder internal IContentTypeMapping? Mapper { get; private set; } /// - /// Set the contentType to use the builtin automapping of properties + /// Set the contentType to use the builtin auto-mapping of properties /// public void AutoMap() { diff --git a/src/Integrations.Umbraco/Controllers/DashboardApiController.cs b/src/Integrations.Umbraco/Controllers/DashboardApiController.cs index 26a4237..c7c6370 100644 --- a/src/Integrations.Umbraco/Controllers/DashboardApiController.cs +++ b/src/Integrations.Umbraco/Controllers/DashboardApiController.cs @@ -1,14 +1,15 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; -using J2N.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Relewise.Client; using Relewise.Client.Extensions; using Relewise.Client.Search; +using Relewise.Integrations.Umbraco.Infrastructure.Mvc.Middlewares; using Relewise.Integrations.Umbraco.Services; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.Attributes; @@ -61,41 +62,65 @@ public async Task ContentExport([FromQuery] bool permanentlyDelet [HttpGet] public IActionResult Configuration() { - IRelewiseClientFactory clientFactory = _provider.GetRequiredService(); + IRelewiseClientFactory clientFactory; - var named = new List(); + try + { + clientFactory = _provider.GetRequiredService(); + } + catch (Exception ex) + { + return Ok(new + { + FactoryFailed = true, + ErrorMessage = ex.Message + }); + } + + List clients = clientFactory.ClientNames + .Where(x => !Constants.NamedClientName.Equals(x, StringComparison.OrdinalIgnoreCase)) + .Select(name => + { + RelewiseClientOptions trackerOptions = clientFactory.GetOptions(name); + RelewiseClientOptions recommenderOptions = clientFactory.GetOptions(name); + RelewiseClientOptions searcherOptions = clientFactory.GetOptions(name); + RelewiseClientOptions searchAdministratorOptions = clientFactory.GetOptions(name); + RelewiseClientOptions analyzerOptions = clientFactory.GetOptions(name); + RelewiseClientOptions dataAccessorOptions = clientFactory.GetOptions(name); + + return new NamedOptionsViewObject( + name, + new ClientOptionsViewObject(trackerOptions), + new ClientOptionsViewObject(recommenderOptions), + new ClientOptionsViewObject(searcherOptions), + new ClientOptionsViewObject(searchAdministratorOptions), + new ClientOptionsViewObject(analyzerOptions), + new ClientOptionsViewObject(dataAccessorOptions)); + }) + .ToList(); try { - named.Add(new NamedOptionsViewObject( + clients.Insert(0, new NamedOptionsViewObject( "Default", new ClientOptionsViewObject(clientFactory.GetOptions()), new ClientOptionsViewObject(clientFactory.GetOptions()), - new ClientOptionsViewObject(clientFactory.GetOptions()))); + new ClientOptionsViewObject(clientFactory.GetOptions()), + new ClientOptionsViewObject(clientFactory.GetOptions()), + new ClientOptionsViewObject(clientFactory.GetOptions()), + new ClientOptionsViewObject(clientFactory.GetOptions()))); } catch (ArgumentException) { - // we just swallow the exception here as this just means that there is no default configured, which is okay - } - - foreach (string name in clientFactory.ClientNames.Where(x => !Constants.NamedClientName.Equals(x, StringComparison.OrdinalIgnoreCase))) - { - RelewiseClientOptions trackerOptions = clientFactory.GetOptions(name); - RelewiseClientOptions recommenderOptions = clientFactory.GetOptions(name); - RelewiseClientOptions searcherOptions = clientFactory.GetOptions(name); - - named.Add(new NamedOptionsViewObject( - name, - new ClientOptionsViewObject(trackerOptions), - new ClientOptionsViewObject(recommenderOptions), - new ClientOptionsViewObject(searcherOptions))); + // we'll just ignore this exception as it just means that there no default client has been configured, which is okay } return Ok(new { _configuration.TrackedContentTypes, _configuration.ExportedContentTypes, - Named = named + Named = clients, + ContentMiddlewareEnabled = RelewiseContentMiddleware.IsEnabled }); } @@ -103,12 +128,22 @@ public IActionResult Configuration() [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] private class NamedOptionsViewObject { - public NamedOptionsViewObject(string name, ClientOptionsViewObject tracker, ClientOptionsViewObject recommender, ClientOptionsViewObject searcher) + public NamedOptionsViewObject(string name, + ClientOptionsViewObject tracker, + ClientOptionsViewObject recommender, + ClientOptionsViewObject searcher, + ClientOptionsViewObject searchAdministrator, + ClientOptionsViewObject analyzer, + ClientOptionsViewObject dataAccessor) { Name = name; + Tracker = tracker; Recommender = recommender; Searcher = searcher; + SearchAdministrator = searchAdministrator; + Analyzer = analyzer; + DataAccessor = dataAccessor; } public string Name { get; } @@ -116,6 +151,9 @@ public NamedOptionsViewObject(string name, ClientOptionsViewObject tracker, Clie public ClientOptionsViewObject Tracker { get; } public ClientOptionsViewObject Recommender { get; } public ClientOptionsViewObject Searcher { get; } + public ClientOptionsViewObject SearchAdministrator { get; } + public ClientOptionsViewObject Analyzer { get; } + public ClientOptionsViewObject DataAccessor { get; } } [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")] diff --git a/src/Integrations.Umbraco/DefaultAnonymousUserLocator.cs b/src/Integrations.Umbraco/DefaultAnonymousUserLocator.cs new file mode 100644 index 0000000..26d61c6 --- /dev/null +++ b/src/Integrations.Umbraco/DefaultAnonymousUserLocator.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Relewise.Client.DataTypes; + +namespace Relewise.Integrations.Umbraco; + +internal class DefaultAnonymousUserLocator : IRelewiseUserLocator +{ + public Task GetUser() + { + return Task.FromResult(User.Anonymous()); + } +} \ No newline at end of file diff --git a/src/Integrations.Umbraco/Infrastructure/Mvc/Middlewares/RelewiseContentMiddleware.cs b/src/Integrations.Umbraco/Infrastructure/Mvc/Middlewares/RelewiseContentMiddleware.cs index 7570db1..ee5f6db 100644 --- a/src/Integrations.Umbraco/Infrastructure/Mvc/Middlewares/RelewiseContentMiddleware.cs +++ b/src/Integrations.Umbraco/Infrastructure/Mvc/Middlewares/RelewiseContentMiddleware.cs @@ -23,32 +23,39 @@ public RelewiseContentMiddleware(RequestDelegate next, RelewiseUmbracoConfigurat _next = next; _configuration = configuration; _userLocator = userLocator; + + IsEnabled = true; } + internal static bool IsEnabled { get; private set; } + public async Task InvokeAsync(HttpContext context) { IUmbracoContextFactory umbracoContextFactory = context.RequestServices.GetRequiredService(); using (UmbracoContextReference umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext()) { - IPublishedContent? content = umbracoContextReference.UmbracoContext?.PublishedRequest?.PublishedContent; - - if (IsNotInPreview(umbracoContextReference) && EnsureContentAndIsTrackable(content)) + if (IsNotInPreview(umbracoContextReference)) { - ITracker tracker = context.RequestServices.GetRequiredService(); - - User user = await _userLocator.GetUser(); + IPublishedContent? content = umbracoContextReference.UmbracoContext?.PublishedRequest?.PublishedContent; - try + if (content != null && EnsureContentAndIsTrackable(content)) { - await tracker.TrackAsync(new ContentView(user, content?.Id.ToString())); - } - catch (HttpRequestException ex) - { - if (ex.StatusCode == HttpStatusCode.NotFound) - throw new InvalidOperationException($"The Dataset Id '{tracker.DatasetId}' is not known by Relewise - You can always find your available dataset id's on https://my.relewise.com", ex); + ITracker tracker = context.RequestServices.GetRequiredService(); + + User user = await _userLocator.GetUser(); + + try + { + await tracker.TrackAsync(new ContentView(user, content.Id.ToString())); + } + catch (HttpRequestException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + throw new InvalidOperationException($"The Dataset Id '{tracker.DatasetId}' is not known by Relewise - You can always find your available dataset id's on https://my.relewise.com", ex); - throw; + throw; + } } } } @@ -61,8 +68,8 @@ private static bool IsNotInPreview(UmbracoContextReference umbracoContextReferen return umbracoContextReference.UmbracoContext?.InPreviewMode == false; } - private bool EnsureContentAndIsTrackable(IPublishedContent? content) + private bool EnsureContentAndIsTrackable(IPublishedContent content) { - return content != null && _configuration.IsTrackable(content); + return _configuration.IsTrackable(content); } } \ No newline at end of file diff --git a/src/Integrations.Umbraco/Integrations.Umbraco.csproj b/src/Integrations.Umbraco/Integrations.Umbraco.csproj index f4eed4e..e2f5806 100644 --- a/src/Integrations.Umbraco/Integrations.Umbraco.csproj +++ b/src/Integrations.Umbraco/Integrations.Umbraco.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentDeletedNotificationNotificationHandler.cs b/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentDeletedNotificationNotificationHandler.cs index f201d21..95bbeb5 100644 --- a/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentDeletedNotificationNotificationHandler.cs +++ b/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentDeletedNotificationNotificationHandler.cs @@ -40,7 +40,7 @@ public async Task HandleAsync(ContentDeletedNotification notification, Cancellat Language.Undefined, Currency.Undefined, new FilterCollection(new ContentIdFilter(ids)), - ContentAdministrativeAction.UpdateKind.PermanentlyDelete); + ContentAdministrativeAction.UpdateKind.Delete); await tracker.TrackAsync(action, cancellationToken); } diff --git a/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentMovedNotificationHandler.cs b/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentMovedNotificationHandler.cs index 5ce85ec..1a9d6ab 100644 --- a/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentMovedNotificationHandler.cs +++ b/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentMovedNotificationHandler.cs @@ -42,7 +42,7 @@ public async Task HandleAsync(ContentMovedNotification notification, Cancellatio Language.Undefined, Currency.Undefined, new FilterCollection(new ContentIdFilter(ids)), - ContentAdministrativeAction.UpdateKind.DisableInRecommendations); + ContentAdministrativeAction.UpdateKind.Disable); await tracker.TrackAsync(action, cancellationToken); } diff --git a/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentUnpublishedNotificationHandler.cs b/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentUnpublishedNotificationHandler.cs index 6e232d7..31c09f9 100644 --- a/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentUnpublishedNotificationHandler.cs +++ b/src/Integrations.Umbraco/NotificationHandlers/RelewiseContentUnpublishedNotificationHandler.cs @@ -41,7 +41,7 @@ public async Task HandleAsync(ContentUnpublishedNotification notification, Cance Language.Undefined, Currency.Undefined, new FilterCollection(new ContentIdFilter(ids)), - ContentAdministrativeAction.UpdateKind.DisableInRecommendations); + ContentAdministrativeAction.UpdateKind.Disable); await tracker.TrackAsync(action, cancellationToken); } diff --git a/src/Integrations.Umbraco/PropertyValueConverters/NestedContentPropertyValueConverter.cs b/src/Integrations.Umbraco/PropertyValueConverters/NestedContentPropertyValueConverter.cs index 905472c..c11c2c8 100644 --- a/src/Integrations.Umbraco/PropertyValueConverters/NestedContentPropertyValueConverter.cs +++ b/src/Integrations.Umbraco/PropertyValueConverters/NestedContentPropertyValueConverter.cs @@ -32,11 +32,11 @@ public void Convert(RelewisePropertyConverterContext context) // NOTE: This needs to be resolved manually, since it would cause a circular dependency if injected through constructor var propertyConverter = _serviceProvider.GetRequiredService(); - List properties = new List(); + var properties = new List(); foreach (IPublishedElement prop in elementItems) { - Dictionary converted = propertyConverter.Convert(prop.Properties, context.Culture); + Dictionary converted = propertyConverter.Convert(prop.Properties, context.Culture); properties.AddRange(converted.Values); } @@ -44,7 +44,13 @@ public void Convert(RelewisePropertyConverterContext context) if (properties.Count == 0) return; - context.Add(context.Property.Alias, new DataValue(properties.Select(x => x.Value.ToString()).Where(x => !string.IsNullOrWhiteSpace(x)))); + string[] stringCollection = properties + .Select(x => x?.Value?.ToString()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x!) + .ToArray(); + + context.Add(context.Property.Alias, new DataValue(stringCollection)); } } } \ No newline at end of file diff --git a/src/Integrations.Umbraco/RelewisePropertyConverterContext.cs b/src/Integrations.Umbraco/RelewisePropertyConverterContext.cs index 2dbefba..ba71ce3 100644 --- a/src/Integrations.Umbraco/RelewisePropertyConverterContext.cs +++ b/src/Integrations.Umbraco/RelewisePropertyConverterContext.cs @@ -9,9 +9,9 @@ namespace Relewise.Integrations.Umbraco; /// public class RelewisePropertyConverterContext { - private readonly Dictionary _dataKeys; + private readonly Dictionary _dataKeys; - internal RelewisePropertyConverterContext(IPublishedProperty property, string culture, Dictionary dataKeys) + internal RelewisePropertyConverterContext(IPublishedProperty property, string culture, Dictionary dataKeys) { _dataKeys = dataKeys; Property = property; @@ -33,5 +33,5 @@ internal RelewisePropertyConverterContext(IPublishedProperty property, string cu /// /// /// - public void Add(string key, DataValue value) => _dataKeys.Add(key, value); + public void Add(string key, DataValue? value) => _dataKeys.Add(key, value); } \ No newline at end of file diff --git a/src/Integrations.Umbraco/Services/ContentMapper.cs b/src/Integrations.Umbraco/Services/ContentMapper.cs index 7a54c38..afe8f18 100644 --- a/src/Integrations.Umbraco/Services/ContentMapper.cs +++ b/src/Integrations.Umbraco/Services/ContentMapper.cs @@ -60,6 +60,7 @@ private async Task AutoMapOrUseMapper(MapContent content, List culturesT if (TryGetMapper(content, out IContentTypeMapping? mapping)) { contentUpdate = await mapping.Map(new ContentMappingContext(content.PublishedContent, contentUpdate, culturesToPublish, _provider), token); + if (contentUpdate == null) throw new InvalidOperationException("Content update can not be null when returned from a ContentTypeMapping"); } @@ -68,6 +69,7 @@ private async Task AutoMapOrUseMapper(MapContent content, List culturesT contentUpdate.Content.Data = _propertyConverter.Convert(content.PublishedContent.Properties, culturesToPublish.ToArray()); } + contentUpdate.Content.Data ??= new Dictionary(); contentUpdate.Content.Data.Add(Constants.VersionKey, content.Version); contentUpdate.Content.Data.Add("contentTypeAlias", content.PublishedContent.ContentType.Alias); contentUpdate.Content.Data.Add("url", content.PublishedContent.Url(null, UrlMode.Absolute)); diff --git a/src/Integrations.Umbraco/Services/ExportContentService.cs b/src/Integrations.Umbraco/Services/ExportContentService.cs index bf131be..fc12241 100644 --- a/src/Integrations.Umbraco/Services/ExportContentService.cs +++ b/src/Integrations.Umbraco/Services/ExportContentService.cs @@ -74,7 +74,7 @@ await tracker.TrackAsync(new ContentAdministrativeAction( Language.Undefined, Currency.Undefined, new FilterCollection(new ContentIdFilter(contentUpdates.Select(x => x.Content.Id))), - ContentAdministrativeAction.UpdateKind.EnableInRecommendations), token); + ContentAdministrativeAction.UpdateKind.Enable), token); return new ExportContentResult(); } @@ -111,7 +111,7 @@ await tracker.TrackAsync(new ContentAdministrativeAction( Language.Undefined, Currency.Undefined, new FilterCollection(new ContentDataFilter(Constants.VersionKey, new EqualsCondition(version, negated: true), filterOutIfKeyIsNotFound: false)), - exportAllContent.PermanentlyDelete ? ContentAdministrativeAction.UpdateKind.PermanentlyDelete : ContentAdministrativeAction.UpdateKind.DisableInRecommendations), + exportAllContent.PermanentlyDelete ? ContentAdministrativeAction.UpdateKind.Delete : ContentAdministrativeAction.UpdateKind.Disable), token); } diff --git a/src/Integrations.Umbraco/Services/IRelewisePropertyConverter.cs b/src/Integrations.Umbraco/Services/IRelewisePropertyConverter.cs index 1f7a2e1..c136de3 100644 --- a/src/Integrations.Umbraco/Services/IRelewisePropertyConverter.cs +++ b/src/Integrations.Umbraco/Services/IRelewisePropertyConverter.cs @@ -6,7 +6,7 @@ namespace Relewise.Integrations.Umbraco.Services; internal interface IRelewisePropertyConverter { - Dictionary Convert(IEnumerable properties, string[] cultures); + Dictionary Convert(IEnumerable properties, string[] cultures); - public Dictionary Convert(IEnumerable properties, string culture) => Convert(properties, new[] { culture }); + public Dictionary Convert(IEnumerable properties, string culture) => Convert(properties, new[] { culture }); } \ No newline at end of file diff --git a/src/Integrations.Umbraco/Services/RelewisePropertyConverter.cs b/src/Integrations.Umbraco/Services/RelewisePropertyConverter.cs index 8e0935d..5236c8a 100644 --- a/src/Integrations.Umbraco/Services/RelewisePropertyConverter.cs +++ b/src/Integrations.Umbraco/Services/RelewisePropertyConverter.cs @@ -16,36 +16,38 @@ public RelewisePropertyConverter(IEnumerable co _converters = converters; } - public Dictionary Convert(IEnumerable properties, string[] cultures) + public Dictionary Convert(IEnumerable properties, string[] cultures) { if (properties == null) throw new ArgumentNullException(nameof(properties)); if (cultures == null) throw new ArgumentNullException(nameof(cultures)); if (cultures.Length == 0) throw new ArgumentException("Value cannot be an empty collection.", nameof(cultures)); - var dataKeys = new Dictionary(); + var dataKeys = new Dictionary(); foreach (IPublishedProperty property in properties) { if (property.PropertyType.VariesByCulture()) { - var localizedProperties = new Dictionary>(); + var localizedProperties = new Dictionary>(); foreach (string culture in cultures) { - var localizedDataKeys = new Dictionary(); + var localizedDataKeys = new Dictionary(); Convert(property, culture, localizedDataKeys); localizedProperties.Add(culture, localizedDataKeys); } - IEnumerable<(string Lang, string Key, DataValue Value)> valueTuples = localizedProperties.SelectMany(x => x.Value.Select(y => (Lang: x.Key, Key: y.Key, Value: y.Value))); + IEnumerable<(string Lang, string Key, DataValue? Value)> valueTuples = localizedProperties + .SelectMany(x => x.Value.Select(y => (Lang: x.Key, Key: y.Key, y.Value))); - IEnumerable> groupBy = valueTuples.GroupBy(x => x.Key); + IEnumerable> groupBy = valueTuples + .GroupBy(x => x.Key); foreach (var values in groupBy) { if (values.Count() > 1) { - if (values.First().Value.Type == DataValue.DataValueTypes.StringList) + if (values.First().Value?.Type == DataValue.DataValueTypes.StringList) { dataKeys.Add(values.Key, new MultilingualCollection(values.Select(x => new MultilingualCollection.Value(x.Lang, x.Value)).ToArray())); } @@ -69,7 +71,7 @@ public Dictionary Convert(IEnumerable pro return dataKeys; } - private void Convert(IPublishedProperty property, string culture, Dictionary dataKeys) + private void Convert(IPublishedProperty property, string culture, Dictionary dataKeys) { var context = new RelewisePropertyConverterContext(property, culture, dataKeys); diff --git a/src/Integrations.Umbraco/UmbracoBuilderExtensions.cs b/src/Integrations.Umbraco/UmbracoBuilderExtensions.cs index 8b0835f..6faf0aa 100644 --- a/src/Integrations.Umbraco/UmbracoBuilderExtensions.cs +++ b/src/Integrations.Umbraco/UmbracoBuilderExtensions.cs @@ -29,9 +29,9 @@ public static class UmbracoBuilderExtensions /// The /// A delegate to configure /// The - public static IUmbracoBuilder AddRelewise(this IUmbracoBuilder builder, Action configure) + public static IUmbracoBuilder AddRelewise(this IUmbracoBuilder builder, Action? configure = null) { - ArgumentNullException.ThrowIfNull(configure, nameof(configure)); + configure ??= _ => { }; return builder.AddRelewise((optionsBuilder, _) => configure(optionsBuilder)); } @@ -59,6 +59,7 @@ public static IUmbracoBuilder AddRelewise(this IUmbracoBuilder builder, Action(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services .AddValueConverter() From d4ed430e6b66aa4234e9b100509a86719a1eccda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brian=20Holmg=C3=A5rd=20Kristensen?= Date: Tue, 18 Oct 2022 16:07:44 +0200 Subject: [PATCH 2/3] minor changes --- samples/UmbracoV10/Startup.cs | 6 +- .../Relewise.Dashboard/dashboard.html | 94 ++++++++++++++++--- samples/UmbracoV9/Startup.cs | 4 +- samples/UmbracoV9/Views/ContentPage.cshtml | 3 +- samples/UmbracoV9/appsettings.json | 14 ++- .../Relewise.Dashboard/dashboard.html | 2 +- .../Controllers/DashboardApiController.cs | 3 + .../Dashboards/RelewiseDashboard.cs | 2 +- 8 files changed, 103 insertions(+), 25 deletions(-) diff --git a/samples/UmbracoV10/Startup.cs b/samples/UmbracoV10/Startup.cs index 1cc801a..7d772b6 100644 --- a/samples/UmbracoV10/Startup.cs +++ b/samples/UmbracoV10/Startup.cs @@ -52,8 +52,8 @@ public Startup(IWebHostEnvironment webHostEnvironment, IConfiguration config) public void ConfigureServices(IServiceCollection services) { // This setups the needed configuration for you to be able to interact with our API. - // You need to add you own dataset id and api-key in the appsettings before recommendations and search works - services.AddRelewise(options => options.ReadFromConfiguration(_config)); + // You need to add you own dataset id and api-key in the appsettings.json before recommendations and search works + //services.AddRelewise(options => options.ReadFromConfiguration(_config)); services.AddHttpContextAccessor(); services.AddSingleton(); @@ -105,7 +105,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { u.UseBackOffice(); u.UseWebsite(); - // Enables tracking of all pageviews to Relewise + // Enables tracking of all page-views to Relewise u.TrackContentViews(); }) .WithEndpoints(u => diff --git a/samples/UmbracoV9/App_Plugins/Relewise.Dashboard/dashboard.html b/samples/UmbracoV9/App_Plugins/Relewise.Dashboard/dashboard.html index 195c321..443af0c 100644 --- a/samples/UmbracoV9/App_Plugins/Relewise.Dashboard/dashboard.html +++ b/samples/UmbracoV9/App_Plugins/Relewise.Dashboard/dashboard.html @@ -23,25 +23,72 @@

Relewise

Settings

-

- These are the settings registered with Relewise. -

+
+ Unexpected error occured. Please check the response on the XHR request or check logs.
+
-

- No options have been configured. Please check your call to the 'services.AddRelewise(options => { /* options goes here */ })'-method in 'Startup.cs'. -

- Tracked content types are: {{vm.configuration.trackedContentTypes.join(', ')}} -
(Page views for these content types are being tracked to Relewise, if the automatic page view tracking has been enabled)
-
- Exported content types are: {{vm.configuration.exportedContentTypes.join(', ')}} -
(These are configured for being exported to Relewise on publish or when the export is triggered manually)
+ +
+ No settings have been configured. Please check your call to the 'services.AddRelewise(options => { /* options goes here */ })'-method in 'Startup.cs':
+
+public void ConfigureServices(IServiceCollection services)
+{
+	services.AddRelewise(options => options.ReadFromConfiguration(_config));
+...			
+}
+ Which then reads from the following configuration in appsettings.json: +
+"Relewise": {
+   "DatasetId": "00000000-0000-0000-0000-000000000000",
+   "ApiKey": "ApiKey",
+   "Timeout": "00:00:03"
+}
+
+ Error message from server: +
{{vm.configuration.errorMessage}}
+ You can read more about configuring Relewise here: https://github.com/Relewise/relewise-sdk-csharp-extensions +
+ +
+ Tracked content types are: + {{vm.configuration.trackedContentTypes.join(', ')}} + No tracked content type have been configured +
(Page views on these content types are automatically being tracked to Relewise by the RelewiseContentMiddleware)
+
To have Page views being tracked automatically to Relewise, please ensure a call to the 'TrackContentViews()'-method in 'Startup.cs':
+
+app.UseUmbraco()
+	.WithMiddleware(u =>
+	{
+		u.UseBackOffice();
+		u.UseWebsite();
+		u.TrackContentViews();
+	})
+	.WithEndpoints(u =>
+	{
+		u.UseInstallerEndpoints();
+		u.UseBackOfficeEndpoints();
+		u.UseWebsiteEndpoints();
+	});
+
+ +
+ Exported content types are: + {{vm.configuration.exportedContentTypes.join(', ')}} + No exported content type have been configured +
(Any publish on these content types will automatically export the data to Relewise)
+
+ +
+
+
+ Find more information about the Umbraco integration here: https://github.com/Relewise/relewise-integrations-umbraco
-

Clients

- +

Clients

+
@@ -81,6 +128,27 @@

Clients

{{ named.searcher.datasetId }}
{{ named.searcher.timeout }}
+
+
+ Search Administrator +
+
{{ named.searchAdministrator.datasetId }}
+
{{ named.searchAdministrator.timeout }}
+
+
+
+ Analyzer +
+
{{ named.analyzer.datasetId }}
+
{{ named.analyzer.timeout }}
+
+
+
+ DataAccessor +
+
{{ named.dataAccessor.datasetId }}
+
{{ named.dataAccessor.timeout }}
+
diff --git a/samples/UmbracoV9/Startup.cs b/samples/UmbracoV9/Startup.cs index b2d1b64..9fdb526 100644 --- a/samples/UmbracoV9/Startup.cs +++ b/samples/UmbracoV9/Startup.cs @@ -55,7 +55,7 @@ public void ConfigureServices(IServiceCollection services) { // This setups the needed configuration for you to be able to interact with our API. // You need to add you own dataset id and api-key in the appsettings before recommendations and search works - services.AddRelewise(options => options.ReadFromConfiguration(_config)); + //services.AddRelewise(options => options.ReadFromConfiguration(_config)); services.AddHttpContextAccessor(); services.AddSingleton(); @@ -107,7 +107,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { u.UseBackOffice(); u.UseWebsite(); - // Enables tracking of all pageviews to Relewise + // Enables tracking of all page-views to Relewise u.TrackContentViews(); }) .WithEndpoints(u => diff --git a/samples/UmbracoV9/Views/ContentPage.cshtml b/samples/UmbracoV9/Views/ContentPage.cshtml index 547bf82..a48a690 100644 --- a/samples/UmbracoV9/Views/ContentPage.cshtml +++ b/samples/UmbracoV9/Views/ContentPage.cshtml @@ -10,13 +10,12 @@ @foreach (var block in (Model.Value>("blocks") ?? Array.Empty())) { IPublishedContent? blockSplashImage = block.Value("splashImage"); -

@(block.Value("headline"))

@if (blockSplashImage != null) { - + }

@(block.Value("text"))

diff --git a/samples/UmbracoV9/appsettings.json b/samples/UmbracoV9/appsettings.json index 8eabe73..b2eaf94 100644 --- a/samples/UmbracoV9/appsettings.json +++ b/samples/UmbracoV9/appsettings.json @@ -26,10 +26,18 @@ "Id": "7841fd23-322f-44f7-b295-a857b6f2fc45" } } - }, + } //"Relewise": { // "DatasetId": "", - // "ApiKey": "", - // "Timeout": "00:00:05" + // "ApiKey": "", + // "Timeout": "00:00:05", + // "Named": { + // "Browser": { + // "Tracker": { + // "ApiKey": "", + // "Timeout": "00:00:03" + // } + // } + // } //} } \ No newline at end of file diff --git a/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html b/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html index 443af0c..2216d20 100644 --- a/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html +++ b/src/Integrations.Umbraco/App_Plugins/Relewise.Dashboard/dashboard.html @@ -45,7 +45,7 @@

Settings

"Timeout": "00:00:03" } - Error message from server: + Error message from server:
{{vm.configuration.errorMessage}}
You can read more about configuring Relewise here: https://github.com/Relewise/relewise-sdk-csharp-extensions
diff --git a/src/Integrations.Umbraco/Controllers/DashboardApiController.cs b/src/Integrations.Umbraco/Controllers/DashboardApiController.cs index c7c6370..a3f3105 100644 --- a/src/Integrations.Umbraco/Controllers/DashboardApiController.cs +++ b/src/Integrations.Umbraco/Controllers/DashboardApiController.cs @@ -67,6 +67,9 @@ public IActionResult Configuration() try { clientFactory = _provider.GetRequiredService(); + + if (!clientFactory.Contains(Constants.NamedClientName)) + throw new InvalidOperationException("No clients registered."); } catch (Exception ex) { diff --git a/src/Integrations.Umbraco/Dashboards/RelewiseDashboard.cs b/src/Integrations.Umbraco/Dashboards/RelewiseDashboard.cs index 0b19dcc..386d1c1 100644 --- a/src/Integrations.Umbraco/Dashboards/RelewiseDashboard.cs +++ b/src/Integrations.Umbraco/Dashboards/RelewiseDashboard.cs @@ -8,7 +8,7 @@ namespace Relewise.Integrations.Umbraco.Dashboards; internal class RelewiseDashboard : IDashboard { public string Alias => "relewiseDashboard"; - public string View => "/App_Plugins/Relewise.Dashboard/dashboard.html?v=2"; + public string View => "/App_Plugins/Relewise.Dashboard/dashboard.html?v=3"; public string[] Sections => new[] { "Settings" }; public IAccessRule[] AccessRules => Array.Empty(); } \ No newline at end of file From 59b78ff792ace422b54349b4266fda71b1b149e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brian=20Holmg=C3=A5rd=20Kristensen?= Date: Tue, 18 Oct 2022 16:09:49 +0200 Subject: [PATCH 3/3] self review --- samples/UmbracoV9/Startup.cs | 2 +- samples/UmbracoV9/appsettings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/UmbracoV9/Startup.cs b/samples/UmbracoV9/Startup.cs index 9fdb526..5262d3b 100644 --- a/samples/UmbracoV9/Startup.cs +++ b/samples/UmbracoV9/Startup.cs @@ -54,7 +54,7 @@ public Startup(IWebHostEnvironment webHostEnvironment, IConfiguration config) public void ConfigureServices(IServiceCollection services) { // This setups the needed configuration for you to be able to interact with our API. - // You need to add you own dataset id and api-key in the appsettings before recommendations and search works + // You need to add you own dataset id and api-key in the appsettings.json before recommendations and search works //services.AddRelewise(options => options.ReadFromConfiguration(_config)); services.AddHttpContextAccessor(); diff --git a/samples/UmbracoV9/appsettings.json b/samples/UmbracoV9/appsettings.json index b2eaf94..5d63bfd 100644 --- a/samples/UmbracoV9/appsettings.json +++ b/samples/UmbracoV9/appsettings.json @@ -26,7 +26,7 @@ "Id": "7841fd23-322f-44f7-b295-a857b6f2fc45" } } - } + }, //"Relewise": { // "DatasetId": "", // "ApiKey": "",