Skip to content

Simplify creating content from a blueprint programmatically #19528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Umbraco.Core/Services/ContentBlueprintEditingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ public ContentBlueprintEditingService(
return Task.FromResult<IContent?>(null);
}

IContent scaffold = blueprint.DeepCloneWithResetIdentities();

using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages()));
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, scaffold, Constants.System.Root, new EventMessages()));
scope.Complete();

return Task.FromResult<IContent?>(blueprint);
return Task.FromResult<IContent?>(scaffold);
}

public async Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take)
Expand Down Expand Up @@ -112,7 +114,7 @@ public async Task<Attempt<ContentCreateResult, ContentEditingOperationStatus>> C

// Create Blueprint
var currentUserId = await GetUserIdAsync(userKey);
IContent blueprint = ContentService.CreateContentFromBlueprint(content, name, currentUserId);
IContent blueprint = ContentService.CreateBlueprintFromContent(content, name, currentUserId);

if (key.HasValue)
{
Expand Down
27 changes: 15 additions & 12 deletions src/Umbraco.Core/Services/ContentService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;

Check notice on line 1 in src/Umbraco.Core/Services/ContentService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 4.14 to 4.10, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.
using System.Runtime.InteropServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -3654,33 +3654,31 @@

private static readonly string?[] ArrayOfOneNullString = { null };

public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
public IContent CreateBlueprintFromContent(
IContent blueprint,
string name,
int userId = Constants.Security.SuperUserId)
{
if (blueprint == null)
{
throw new ArgumentNullException(nameof(blueprint));
}
ArgumentNullException.ThrowIfNull(blueprint);

IContentType contentType = GetContentType(blueprint.ContentType.Alias);
var content = new Content(name, -1, contentType);
content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id);

content.CreatorId = userId;
content.WriterId = userId;

IEnumerable<string?> cultures = ArrayOfOneNullString;
if (blueprint.CultureInfos?.Count > 0)
{
cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
{
if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
{
defaultCulture.Name = name;
}

scope.Complete();
defaultCulture.Name = name;
}

scope.Complete();

Check notice on line 3681 in src/Umbraco.Core/Services/ContentService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Bumpy Road Ahead

CreateContentFromBlueprint is no longer above the threshold for logical blocks with deeply nested code. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
}

DateTime now = DateTime.Now;
Expand All @@ -3701,6 +3699,11 @@
return content;
}

/// <inheritdoc />
[Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")]
public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
=> CreateBlueprintFromContent(blueprint, name, userId);

public IEnumerable<IContent> GetBlueprintsForContentTypes(params int[] contentTypeId)
{
using (ScopeProvider.CreateCoreScope(autoComplete: true))
Expand Down
12 changes: 11 additions & 1 deletion src/Umbraco.Core/Services/IContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,18 @@ public interface IContentService : IContentServiceBase<IContent>
void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);

/// <summary>
/// Creates a new content item from a blueprint.
/// Creates a blueprint from a content item.
/// </summary>
// TODO: Remove the default implementation when CreateContentFromBlueprint is removed.
IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
=> throw new NotImplementedException();

/// <summary>
/// (Deprecated) Creates a new content item from a blueprint.
/// </summary>
/// <remarks>If creating content from a blueprint, use <see cref="IContentBlueprintEditingService.GetScaffoldedAsync"/>
/// instead. If creating a blueprint from content use <see cref="CreateBlueprintFromContent"/> instead.</remarks>
[Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")]
IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);

/// <summary>
Expand Down
101 changes: 100 additions & 1 deletion tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.

using System;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Builders.Interfaces;

namespace Umbraco.Cms.Tests.Common.Builders;
Expand Down Expand Up @@ -155,4 +158,100 @@

return dataType;
}

public static DataType CreateSimpleElementDataType(
IIOHelper ioHelper,
string editorAlias,
Guid elementKey,
Guid? elementSettingKey)
{
Dictionary<string, object> configuration = editorAlias switch
{
Constants.PropertyEditors.Aliases.BlockGrid => GetBlockGridBaseConfiguration(),
Constants.PropertyEditors.Aliases.RichText => GetRteBaseConfiguration(),
_ => [],
};

SetBlockConfiguration(
configuration,
elementKey,
elementSettingKey,
editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null);


var dataTypeBuilder = new DataTypeBuilder()
.WithId(0)
.WithDatabaseType(ValueStorageType.Nvarchar)
.AddEditor()
.WithAlias(editorAlias);

switch (editorAlias)
{
case Constants.PropertyEditors.Aliases.BlockGrid:
dataTypeBuilder.WithConfigurationEditor(
new BlockGridConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
break;
case Constants.PropertyEditors.Aliases.BlockList:
dataTypeBuilder.WithConfigurationEditor(
new BlockListConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
break;
case Constants.PropertyEditors.Aliases.RichText:
dataTypeBuilder.WithConfigurationEditor(
new RichTextConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
break;
}

return dataTypeBuilder.Done().Build();
}

Check warning on line 205 in tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Complex Method

CreateSimpleElementDataType has a cyclomatic complexity of 9, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

private static void SetBlockConfiguration(
Dictionary<string, object> dictionary,
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
if (elementKey is null)
{
return;
}

dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
}

private static Dictionary<string, object> GetBlockGridBaseConfiguration() => new() { ["gridColumns"] = 12 };

private static Dictionary<string, object> GetRteBaseConfiguration()
{
var dictionary = new Dictionary<string, object>
{
["maxImageSize"] = 500,
["mode"] = "Classic",
["toolbar"] = new[]
{
"styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent",
"indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog"
},
};
return dictionary;
}

private static Dictionary<string, object> BuildBlockConfiguration(
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
var dictionary = new Dictionary<string, object>();
if (allowAtRoot is not null)
{
dictionary.Add("allowAtRoot", allowAtRoot.Value);
}

dictionary.Add("contentElementTypeKey", elementKey.ToString());
if (elementSettingKey is not null)
{
dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString());
}

return dictionary;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest

protected IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();

protected IDataTypeService DataTypeService => GetRequiredService<IDataTypeService>();

protected IFileService FileService => GetRequiredService<IFileService>();

protected ContentService ContentService => (ContentService)GetRequiredService<IContentService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public void Delete_Blueprint()
}

[Test]
public void Create_Content_From_Blueprint()
public void Create_Blueprint_From_Content()
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
Expand All @@ -140,22 +140,21 @@ public void Create_Content_From_Blueprint()
var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id);
ContentTypeService.Save(contentType);

var blueprint = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root);
blueprint.SetValue("title", "blueprint 1");
blueprint.SetValue("bodyText", "blueprint 2");
blueprint.SetValue("keywords", "blueprint 3");
blueprint.SetValue("description", "blueprint 4");

ContentService.SaveBlueprint(blueprint);

var fromBlueprint = ContentService.CreateContentFromBlueprint(blueprint, "hello world");
ContentService.Save(fromBlueprint);

Assert.IsTrue(fromBlueprint.HasIdentity);
Assert.AreEqual("blueprint 1", fromBlueprint.Properties["title"].GetValue());
Assert.AreEqual("blueprint 2", fromBlueprint.Properties["bodyText"].GetValue());
Assert.AreEqual("blueprint 3", fromBlueprint.Properties["keywords"].GetValue());
Assert.AreEqual("blueprint 4", fromBlueprint.Properties["description"].GetValue());
var originalPage = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root);
originalPage.SetValue("title", "blueprint 1");
originalPage.SetValue("bodyText", "blueprint 2");
originalPage.SetValue("keywords", "blueprint 3");
originalPage.SetValue("description", "blueprint 4");
ContentService.Save(originalPage);

var fromContent = ContentService.CreateBlueprintFromContent(originalPage, "hello world");
ContentService.SaveBlueprint(fromContent);

Assert.IsTrue(fromContent.HasIdentity);
Assert.AreEqual("blueprint 1", fromContent.Properties["title"]?.GetValue());
Assert.AreEqual("blueprint 2", fromContent.Properties["bodyText"]?.GetValue());
Assert.AreEqual("blueprint 3", fromContent.Properties["keywords"]?.GetValue());
Assert.AreEqual("blueprint 4", fromContent.Properties["description"]?.GetValue());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NUnit.Framework;

Check notice on line 1 in tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Code Duplication

The module no longer contains too many functions with similar structure

Check notice on line 1 in tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Primitive Obsession

The ratio of primivite types in function arguments is no longer above the threshold
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
Expand Down Expand Up @@ -286,104 +286,12 @@
Guid elementKey,
Guid? elementSettingKey)
{
Dictionary<string, object> configuration;
switch (editorAlias)
{
case Constants.PropertyEditors.Aliases.BlockGrid:
configuration = GetBlockGridBaseConfiguration();
break;
case Constants.PropertyEditors.Aliases.RichText:
configuration = GetRteBaseConfiguration();
break;
default:
configuration = new Dictionary<string, object>();
break;
}

SetBlockConfiguration(
configuration,
var dataType = DataTypeBuilder.CreateSimpleElementDataType(
IOHelper,
editorAlias,
elementKey,
elementSettingKey,
editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null);


var dataTypeBuilder = new DataTypeBuilder()
.WithId(0)
.WithDatabaseType(ValueStorageType.Nvarchar)
.AddEditor()
.WithAlias(editorAlias);

switch (editorAlias)
{
case Constants.PropertyEditors.Aliases.BlockGrid:
dataTypeBuilder.WithConfigurationEditor(
new BlockGridConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
break;
case Constants.PropertyEditors.Aliases.BlockList:
dataTypeBuilder.WithConfigurationEditor(
new BlockListConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
break;
case Constants.PropertyEditors.Aliases.RichText:
dataTypeBuilder.WithConfigurationEditor(
new RichTextConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
break;
}

var dataType = dataTypeBuilder.Done()
.Build();
elementSettingKey);

await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey);
}

private void SetBlockConfiguration(
Dictionary<string, object> dictionary,
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
if (elementKey is null)
{
return;
}

dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
}

private Dictionary<string, object> GetBlockGridBaseConfiguration()
=> new Dictionary<string, object> { ["gridColumns"] = 12 };

private Dictionary<string, object> GetRteBaseConfiguration()
{
var dictionary = new Dictionary<string, object>
{
["maxImageSize"] = 500,
["mode"] = "Classic",
["toolbar"] = new[]
{
"styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist",
"outdent", "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog"
},
};
return dictionary;
}

private Dictionary<string, object> BuildBlockConfiguration(
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
var dictionary = new Dictionary<string, object>();
if (allowAtRoot is not null)
{
dictionary.Add("allowAtRoot", allowAtRoot.Value);
}

dictionary.Add("contentElementTypeKey", elementKey.ToString());
if (elementSettingKey is not null)
{
dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString());
}

return dictionary;
}
}
Loading