Adds TestData project along with test endpoints for enabling/disabling segments

This commit is contained in:
Shannon
2020-01-21 13:33:39 +11:00
parent d378495942
commit 8cc78631a0
8 changed files with 538 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Umbraco.TestData")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Umbraco.TestData")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("fb5676ed-7a69-492c-b802-e7b24144c0fc")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Web.Mvc;
namespace Umbraco.TestData
{
public class SegmentTestController : SurfaceController
{
public ActionResult EnableDocTypeSegments(string alias, string propertyTypeAlias)
{
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
return HttpNotFound();
var ct = Services.ContentTypeService.Get(alias);
if (ct == null)
return Content($"No document type found by alias {alias}");
var propType = ct.PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias);
if (propType == null)
return Content($"The document type {alias} does not have a property type {propertyTypeAlias ?? "null"}");
if (ct.Variations.VariesBySegment())
return Content($"The document type {alias} already allows segments, nothing has been changed");
ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment);
propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment);
Services.ContentTypeService.Save(ct);
return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments");
}
public ActionResult DisableDocTypeSegments(string alias)
{
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
return HttpNotFound();
var ct = Services.ContentTypeService.Get(alias);
if (ct == null)
return Content($"No document type found by alias {alias}");
if (!ct.VariesBySegment())
return Content($"The document type {alias} does not allow segments, nothing has been changed");
ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment);
Services.ContentTypeService.Save(ct);
return Content($"The document type {alias} no longer allows segments");
}
public ActionResult AddSegmentData(int contentId, string propertyAlias, string value, string segment, string culture = null)
{
var content = Services.ContentService.GetById(contentId);
if (content == null)
return Content($"No content found by id {contentId}");
if (!content.HasProperty(propertyAlias))
return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias}");
if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace())
return Content($"The content by id {contentId} varies by culture but no culture was specified");
content.SetValue(propertyAlias, value, culture, segment);
Services.ContentService.Save(content);
return Content($"Segment value has been set on content {contentId} for property {propertyAlias}");
}
}
}

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{FB5676ED-7A69-492C-B802-E7B24144C0FC}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Umbraco.TestData</RootNamespace>
<AssemblyName>Umbraco.TestData</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="SegmentTestController.cs" />
<Compile Include="UmbracoTestDataController.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="readme.md" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">
<Project>{31785bc3-256c-4613-b2f5-a1b0bdded8c1}</Project>
<Name>Umbraco.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
<Name>Umbraco.Web</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Bogus">
<Version>28.4.4</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.Mvc">
<Version>5.2.7</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core;
using System.Web.Mvc;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web;
using Umbraco.Web.Mvc;
using System.Configuration;
using Bogus;
using Umbraco.Core.Scoping;
namespace Umbraco.TestData
{
/// <summary>
/// Creates test data
/// </summary>
public class UmbracoTestDataController : SurfaceController
{
private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE";
private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker";
private const string TextDataTypeName = "UmbracoTestDataContent.Text";
private const string TestDataContentTypeAlias = "umbTestDataContent";
private readonly IScopeProvider _scopeProvider;
private readonly PropertyEditorCollection _propertyEditors;
public UmbracoTestDataController(IScopeProvider scopeProvider, PropertyEditorCollection propertyEditors, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, UmbracoHelper umbracoHelper) : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger, umbracoHelper)
{
_scopeProvider = scopeProvider;
_propertyEditors = propertyEditors;
}
/// <summary>
/// Creates a content and associated media tree (hierarchy)
/// </summary>
/// <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 ActionResult CreateTree(int count, int depth, string locale = "en")
{
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
return HttpNotFound();
if (!Validate(count, depth, out var message, out var perLevel))
throw new InvalidOperationException(message);
var faker = new Faker(locale);
var company = faker.Company.CompanyName();
using (var scope = _scopeProvider.CreateScope())
{
var imageIds = CreateMediaTree(company, faker, count, depth).ToList();
var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList();
Services.ContentService.SaveAndPublishBranch(root, true);
scope.Complete();
}
return Content("Done");
}
private bool Validate(int count, int depth, out string message, out int perLevel)
{
perLevel = 0;
message = null;
if (count <= 0)
{
message = "Count must be more than 0";
return false;
}
perLevel = count / depth;
if (perLevel < 1)
{
message = "Count not high enough for specified for number of levels required";
return false;
}
return true;
}
/// <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();
// 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);
var tracked = new Stack<(T parent, int childCount)>();
var currChildCount = 0;
for (int i = 0; i < count; i++)
{
var created = create(parent);
var contentItem = created.content;
yield return contentItem.GetUdi();
currChildCount++;
if (currChildCount == perBranch)
{
// move back up...
var prev = tracked.Pop();
// 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));
// not at max depth, create below
parent = created.container();
currChildCount = 0;
}
}
}
/// <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);
return CreateHierarchy(parent, count, depth, currParent =>
{
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;
});
});
}
/// <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>
/// <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());
var docType = GetOrCreateContentType();
var parent = Services.ContentService.Create(company, -1, docType.Alias);
parent.SetValue("review", faker.Rant.Review());
parent.SetValue("desc", company);
parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]);
Services.ContentService.Save(parent);
root = parent;
return CreateHierarchy(parent, count, depth, currParent =>
{
var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias);
content.SetValue("review", 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)]);
Services.ContentService.Save(content);
return (content, () => content);
});
}
private IContentType GetOrCreateContentType()
{
var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias);
if (docType != null)
return docType;
docType = new ContentType(-1)
{
Alias = TestDataContentTypeAlias,
Name = "Umbraco Test Data Content",
Icon = "icon-science color-green"
};
docType.AddPropertyGroup("Content");
docType.AddPropertyType(new PropertyType(GetOrCreateRichText(), "review")
{
Name = "Review"
});
docType.AddPropertyType(new PropertyType(GetOrCreateMediaPicker(), "media")
{
Name = "Media"
});
docType.AddPropertyType(new PropertyType(GetOrCreateText(), "desc")
{
Name = "Description"
});
Services.ContentTypeService.Save(docType);
docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) };
Services.ContentTypeService.Save(docType);
return docType;
}
private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce);
private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker);
private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox);
private IDataType GetOrCreateDataType(string name, string editorAlias)
{
var dt = Services.DataTypeService.GetDataType(name);
if (dt != null) return dt;
var editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias);
if (editor == null)
throw new InvalidOperationException($"No {editorAlias} editor found");
dt = new DataType(editor)
{
Name = name,
Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject,
DatabaseType = ValueStorageType.Ntext
};
Services.DataTypeService.Save(dt);
return dt;
}
}
}

View File

@@ -0,0 +1,51 @@
## Umbraco Test Data
This project is a utility to be able to generate large amounts of content and media in an
Umbraco installation for testing.
Currently this project is referenced in the Umbraco.Web.UI project but only when it's being built
in Debug mode (i.e. when testing within Visual Studio).
## Usage
You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data.
It has to be enabled by an appSetting:
```xml
<add key="Umbraco.TestData.Enabled" value="true"/>
```
Once this is enabled this endpoint can be executed:
`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5`
The query string options are:
* `count` = the number of content and media nodes to create
* `depth` = how deep the trees created will be
* `locale` (optional, default = "en") = the language that the data will be generated in
This creates a content and associated media tree (hierarchy). Each content item created is associated
to a media item via a media picker and therefore a relation is created between the two. Each content and
media tree created have the same root node name so it's easy to know which content branch relates to
which media branch.
All values are generated using the very handy `Bogus` package.
## Schema
This will install some schema items:
* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type
* `UmbracoTestDataContent.RTE` Data Type
* `UmbracoTestDataContent.MediaPicker` Data Type
* `UmbracoTestDataContent.Text` Data Type
For media, the normal folder and image is used
## Media
This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value.
This works when viewing the media item in the media section and the image will show up and with recent changes this will also work
when editing content to view the thumbnail for the picked media.

View File

@@ -126,6 +126,10 @@
<Project>{52ac0ba8-a60e-4e36-897b-e8b97a54ed1c}</Project>
<Name>Umbraco.ModelsBuilder.Embedded</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.TestData\Umbraco.TestData.csproj">
<Project>{fb5676ed-7a69-492c-b802-e7b24144c0fc}</Project>
<Name>Umbraco.TestData</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
<Name>Umbraco.Web</Name>

View File

@@ -21,6 +21,11 @@
<Examine xdt:Transform="Remove" />
<ExamineLuceneIndexSets xdt:Transform="Remove" />
<appSettings>
<add key="Umbraco.TestData.Enabled" xdt:Transform="Remove" xdt:Locator="Match(key)"/>
<add key="Umbraco.TestData.Enabled" value="true" xdt:Transform="Insert" />
</appSettings>
<system.web>
<compilation debug="true" xdt:Transform="SetAttributes(debug)" />
</system.web>

View File

@@ -104,6 +104,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder.Embedded", "Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -140,6 +142,10 @@ Global
{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU
{FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -152,6 +158,7 @@ Global
{53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645}
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382}
{C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645}
{FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}