2025-09-23 13:22:29 +02:00
|
|
|
|
using Bogus;
|
2021-07-26 09:32:08 +02:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
|
using Microsoft.Extensions.Options;
|
2021-02-09 10:22:42 +01:00
|
|
|
|
using Umbraco.Cms.Core;
|
|
|
|
|
|
using Umbraco.Cms.Core.Cache;
|
|
|
|
|
|
using Umbraco.Cms.Core.Logging;
|
|
|
|
|
|
using Umbraco.Cms.Core.Models;
|
|
|
|
|
|
using Umbraco.Cms.Core.PropertyEditors;
|
2021-07-26 09:32:08 +02:00
|
|
|
|
using Umbraco.Cms.Core.Routing;
|
2021-02-15 11:41:12 +01:00
|
|
|
|
using Umbraco.Cms.Core.Scoping;
|
2021-02-09 10:22:42 +01:00
|
|
|
|
using Umbraco.Cms.Core.Services;
|
|
|
|
|
|
using Umbraco.Cms.Core.Strings;
|
|
|
|
|
|
using Umbraco.Cms.Core.Web;
|
2021-02-12 13:36:50 +01:00
|
|
|
|
using Umbraco.Cms.Infrastructure.Persistence;
|
2021-07-26 09:32:08 +02:00
|
|
|
|
using Umbraco.Cms.Web.Website.Controllers;
|
2021-02-09 13:32:34 +01:00
|
|
|
|
using Umbraco.Extensions;
|
2021-07-26 09:32:08 +02:00
|
|
|
|
using Umbraco.TestData.Configuration;
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
namespace Umbraco.TestData;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Creates test data
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class UmbracoTestDataController : SurfaceController
|
2020-01-21 13:33:39 +11:00
|
|
|
|
{
|
2022-06-21 08:09:38 +02:00
|
|
|
|
private const string TestDataContentTypeAlias = "umbTestDataContent";
|
|
|
|
|
|
private readonly PropertyEditorCollection _propertyEditors;
|
|
|
|
|
|
private readonly ICoreScopeProvider _scopeProvider;
|
|
|
|
|
|
private readonly IShortStringHelper _shortStringHelper;
|
|
|
|
|
|
private readonly TestDataSettings _testDataSettings;
|
|
|
|
|
|
|
|
|
|
|
|
public UmbracoTestDataController(
|
|
|
|
|
|
IUmbracoContextAccessor umbracoContextAccessor,
|
|
|
|
|
|
IUmbracoDatabaseFactory databaseFactory,
|
|
|
|
|
|
ServiceContext services,
|
|
|
|
|
|
AppCaches appCaches,
|
|
|
|
|
|
IProfilingLogger profilingLogger,
|
|
|
|
|
|
IPublishedUrlProvider publishedUrlProvider,
|
|
|
|
|
|
ICoreScopeProvider scopeProvider,
|
|
|
|
|
|
PropertyEditorCollection propertyEditors,
|
|
|
|
|
|
IShortStringHelper shortStringHelper,
|
|
|
|
|
|
IOptions<TestDataSettings> testDataSettings)
|
|
|
|
|
|
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
|
|
|
|
|
|
{
|
|
|
|
|
|
_scopeProvider = scopeProvider;
|
|
|
|
|
|
_propertyEditors = propertyEditors;
|
|
|
|
|
|
_shortStringHelper = shortStringHelper;
|
|
|
|
|
|
_testDataSettings = testDataSettings.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-01-21 13:33:39 +11:00
|
|
|
|
/// <summary>
|
2022-06-21 08:09:38 +02:00
|
|
|
|
/// Creates a content and associated media tree (hierarchy)
|
2020-01-21 13:33:39 +11:00
|
|
|
|
/// </summary>
|
2022-06-21 08:09:38 +02:00
|
|
|
|
/// <param name="count"></param>
|
|
|
|
|
|
/// <param name="depth"></param>
|
|
|
|
|
|
/// <param name="locale"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// Each content item created is associated to a media item via a media picker and therefore a relation is created
|
|
|
|
|
|
/// between the two
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
public IActionResult CreateTree(int count, int depth, string locale = "en")
|
2020-01-21 13:33:39 +11:00
|
|
|
|
{
|
2022-06-21 08:09:38 +02:00
|
|
|
|
if (_testDataSettings.Enabled == false)
|
2020-01-21 13:33:39 +11:00
|
|
|
|
{
|
2022-06-21 08:09:38 +02:00
|
|
|
|
return NotFound();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
if (!Validate(count, depth, out var message, out var perLevel))
|
2020-01-21 13:33:39 +11:00
|
|
|
|
{
|
2022-06-21 08:09:38 +02:00
|
|
|
|
throw new InvalidOperationException(message);
|
|
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
var faker = new Faker(locale);
|
|
|
|
|
|
var company = faker.Company.CompanyName();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
using (var scope = _scopeProvider.CreateCoreScope())
|
|
|
|
|
|
{
|
|
|
|
|
|
var imageIds = CreateMediaTree(company, faker, count, depth).ToList();
|
|
|
|
|
|
var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2025-02-12 12:04:58 +01:00
|
|
|
|
Services.ContentService.PublishBranch(root, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
2020-01-21 10:18:03 +01:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
scope.Complete();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
return Content("Done");
|
|
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
private static bool Validate(int count, int depth, out string message, out int perLevel)
|
|
|
|
|
|
{
|
|
|
|
|
|
perLevel = 0;
|
|
|
|
|
|
message = null;
|
2020-01-21 10:18:03 +01:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
if (count <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
message = "Count must be more than 0";
|
|
|
|
|
|
return false;
|
2020-01-21 13:33:39 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
perLevel = count / depth;
|
|
|
|
|
|
if (perLevel < 1)
|
2020-01-21 13:33:39 +11:00
|
|
|
|
{
|
2022-06-21 08:09:38 +02:00
|
|
|
|
message = "Count not high enough for specified for number of levels required";
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Utility to create a tree hierarchy
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <typeparam name="T"></typeparam>
|
|
|
|
|
|
/// <param name="parent"></param>
|
|
|
|
|
|
/// <param name="count"></param>
|
|
|
|
|
|
/// <param name="depth"></param>
|
|
|
|
|
|
/// <param name="create">
|
|
|
|
|
|
/// A callback that returns a tuple of Content and another callback to produce a Container.
|
|
|
|
|
|
/// For media, a container will be another folder, for content the container will be the Content itself.
|
|
|
|
|
|
/// </param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
private IEnumerable<Udi> CreateHierarchy<T>(T parent, int count, int depth, Func<T, (T content, Func<T> container)> create)
|
|
|
|
|
|
where T : class, IContentBase
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return parent.GetUdi();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
// This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree.
|
|
|
|
|
|
var totalDescendants = count - 1;
|
|
|
|
|
|
var perLevel = Math.Ceiling(totalDescendants / (double)depth);
|
|
|
|
|
|
var perBranch = Math.Ceiling(perLevel / depth);
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
var tracked = new Stack<(T parent, int childCount)>();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
var currChildCount = 0;
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
for (var i = 0; i < count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (content, container) = create(parent);
|
|
|
|
|
|
var contentItem = content;
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
yield return contentItem.GetUdi();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
currChildCount++;
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
if (currChildCount == perBranch)
|
|
|
|
|
|
{
|
|
|
|
|
|
// move back up...
|
2020-01-21 10:18:03 +01:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
var prev = tracked.Pop();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
// restore child count
|
|
|
|
|
|
currChildCount = prev.childCount;
|
|
|
|
|
|
// restore the parent
|
|
|
|
|
|
parent = prev.parent;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (contentItem.Level < depth)
|
|
|
|
|
|
{
|
|
|
|
|
|
// track the current parent and it's current child count
|
|
|
|
|
|
tracked.Push((parent, currChildCount));
|
2020-01-21 10:18:03 +01:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
// not at max depth, create below
|
|
|
|
|
|
parent = container();
|
2020-01-21 10:18:03 +01:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
currChildCount = 0;
|
2020-01-21 13:33:39 +11:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-06-21 08:09:38 +02:00
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Creates the media tree hiearachy
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="company"></param>
|
|
|
|
|
|
/// <param name="faker"></param>
|
|
|
|
|
|
/// <param name="count"></param>
|
|
|
|
|
|
/// <param name="depth"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
private IEnumerable<Udi> CreateMediaTree(string company, Faker faker, int count, int depth)
|
|
|
|
|
|
{
|
|
|
|
|
|
var parent =
|
|
|
|
|
|
Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder);
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
return CreateHierarchy(parent, count, depth, currParent =>
|
2020-01-21 13:33:39 +11:00
|
|
|
|
{
|
2022-06-21 08:09:38 +02:00
|
|
|
|
var imageUrl = faker.Image.PicsumUrl();
|
|
|
|
|
|
|
|
|
|
|
|
// we are appending a &ext=.jpg to the end of this for a reason. The result of this URL will be something like:
|
|
|
|
|
|
// https://picsum.photos/640/480/?image=106
|
|
|
|
|
|
// and due to the way that we detect images there must be an extension so we'll change it to
|
|
|
|
|
|
// https://picsum.photos/640/480/?image=106&ext=.jpg
|
|
|
|
|
|
// which will trick our app into parsing this and thinking it's an image ... which it is so that's good.
|
|
|
|
|
|
// if we don't do this we don't get thumbnails in the back office.
|
|
|
|
|
|
imageUrl += "&ext=.jpg";
|
|
|
|
|
|
|
|
|
|
|
|
var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image);
|
|
|
|
|
|
media.SetValue(Constants.Conventions.Media.File, imageUrl);
|
|
|
|
|
|
Services.MediaService.Save(media);
|
|
|
|
|
|
return (media, () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// create a folder to contain child media
|
|
|
|
|
|
var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder);
|
|
|
|
|
|
return container;
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Creates the content tree hiearachy
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="company"></param>
|
|
|
|
|
|
/// <param name="faker"></param>
|
|
|
|
|
|
/// <param name="count"></param>
|
|
|
|
|
|
/// <param name="depth"></param>
|
|
|
|
|
|
/// <param name="imageIds"></param>
|
|
|
|
|
|
/// <param name="root"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
private IEnumerable<Udi> CreateContentTree(string company, Faker faker, int count, int depth, List<Udi> imageIds, out IContent root)
|
|
|
|
|
|
{
|
|
|
|
|
|
var random = new Random(company.GetHashCode());
|
2021-07-26 09:32:08 +02:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
var docType = GetOrCreateContentType();
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
var parent = Services.ContentService.Create(company, -1, docType.Alias);
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
// give it some reasonable data (100 reviews)
|
|
|
|
|
|
parent.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review())));
|
|
|
|
|
|
parent.SetValue("desc", company);
|
|
|
|
|
|
parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]);
|
|
|
|
|
|
Services.ContentService.Save(parent);
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
root = parent;
|
2021-07-26 09:32:08 +02:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
return CreateHierarchy(parent, count, depth, currParent =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias);
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
// give it some reasonable data (100 reviews)
|
|
|
|
|
|
content.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review())));
|
|
|
|
|
|
content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective())));
|
|
|
|
|
|
content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]);
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
Services.ContentService.Save(content);
|
|
|
|
|
|
return (content, () => content);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
private IContentType GetOrCreateContentType()
|
|
|
|
|
|
{
|
|
|
|
|
|
var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias);
|
|
|
|
|
|
if (docType != null)
|
2020-01-21 13:33:39 +11:00
|
|
|
|
{
|
|
|
|
|
|
return docType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
docType = new ContentType(_shortStringHelper, -1)
|
|
|
|
|
|
{
|
|
|
|
|
|
Alias = TestDataContentTypeAlias,
|
|
|
|
|
|
Name = "Umbraco Test Data Content",
|
|
|
|
|
|
Icon = "icon-science color-green"
|
|
|
|
|
|
};
|
|
|
|
|
|
docType.AddPropertyGroup("content", "Content");
|
2025-09-23 13:22:29 +02:00
|
|
|
|
docType.AddPropertyType(
|
|
|
|
|
|
new PropertyType(_shortStringHelper, Constants.PropertyEditors.Aliases.RichText, ValueStorageType.Ntext, "review")
|
2022-06-21 08:09:38 +02:00
|
|
|
|
{
|
2025-09-23 13:22:29 +02:00
|
|
|
|
Name = "Review",
|
|
|
|
|
|
}, "content");
|
|
|
|
|
|
docType.AddPropertyType(new PropertyType(_shortStringHelper, Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "desc") { Name = "Description" }, "content");
|
|
|
|
|
|
docType.AddPropertyType(new PropertyType(_shortStringHelper, Constants.PropertyEditors.Aliases.MediaPicker3, ValueStorageType.Integer, "media") { Name = "Media" }, "content");
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-06-21 08:09:38 +02:00
|
|
|
|
Services.ContentTypeService.Save(docType);
|
2023-08-17 12:28:16 +02:00
|
|
|
|
docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Key, 0, docType.Alias) };
|
2022-06-21 08:09:38 +02:00
|
|
|
|
Services.ContentTypeService.Save(docType);
|
|
|
|
|
|
return docType;
|
|
|
|
|
|
}
|
2020-01-21 13:33:39 +11:00
|
|
|
|
}
|