Merge conflicts resolved
This commit is contained in:
+4
-6
@@ -30,11 +30,6 @@ dotnet_naming_symbols.private_fields.applicable_accessibilities = private
|
||||
dotnet_naming_style.prefix_underscore.capitalization = camel_case
|
||||
dotnet_naming_style.prefix_underscore.required_prefix = _
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md
|
||||
[*.cs]
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
@@ -42,5 +37,8 @@ csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
csharp_prefer_braces = false : none
|
||||
|
||||
[*.{js,less}]
|
||||
[*.js]
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.less]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Microsoft Rich Code Navigation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ v8/dev ]
|
||||
|
||||
jobs:
|
||||
RichNavigation:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Run Umbraco Build Script with Powershell
|
||||
working-directory: 'build'
|
||||
run: .\build.ps1
|
||||
shell: powershell
|
||||
|
||||
- name: Microsoft Rich Code Navigation Indexer
|
||||
uses: microsoft/RichCodeNavIndexer@v0.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
languages: csharp,typescript
|
||||
@@ -36,6 +36,11 @@ namespace Umbraco.Core
|
||||
/// </summary>
|
||||
public static class Aliases
|
||||
{
|
||||
/// <summary>
|
||||
/// Block List.
|
||||
/// </summary>
|
||||
public const string BlockList = "Umbraco.BlockList";
|
||||
|
||||
/// <summary>
|
||||
/// CheckBox List.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
public const string AnyGuid = "any-guid"; // that one is for tests
|
||||
|
||||
public const string Element = "element";
|
||||
public const string Document = "document";
|
||||
|
||||
public const string DocumentBlueprint = "document-blueprint";
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
[DataContract(Name = "content", Namespace = "")]
|
||||
public class ContentItemSave : IContentSave<IContent>
|
||||
{
|
||||
protected ContentItemSave()
|
||||
public ContentItemSave()
|
||||
{
|
||||
UploadedFiles = new List<ContentPropertyFile>();
|
||||
Variants = new List<ContentVariantSave>();
|
||||
|
||||
@@ -44,6 +44,9 @@ namespace Umbraco.Web.Models.Mapping
|
||||
property.PropertyType.PropertyEditorAlias);
|
||||
|
||||
editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label];
|
||||
|
||||
if (editor == null)
|
||||
throw new InvalidOperationException($"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}");
|
||||
}
|
||||
|
||||
dest.Id = property.Id;
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an <see cref="IPublishedElement"/> type.
|
||||
/// </summary>
|
||||
/// <remarks>Instances implementing the <see cref="IPublishedContentType"/> interface should be
|
||||
/// immutable, ie if the content type changes, then a new instance needs to be created.</remarks>
|
||||
public interface IPublishedContentType2 : IPublishedContentType
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique key for the content type.
|
||||
/// </summary>
|
||||
Guid Key { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an <see cref="IPublishedElement"/> type.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace Umbraco. Core.Models.PublishedContent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Creates published content types.
|
||||
/// </summary>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Provides the published model creation service.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// </summary>
|
||||
/// <remarks>Instances of the <see cref="PublishedContentType"/> class are immutable, ie
|
||||
/// if the content type changes, then a new class needs to be created.</remarks>
|
||||
public class PublishedContentType : IPublishedContentType
|
||||
public class PublishedContentType : IPublishedContentType2
|
||||
{
|
||||
private readonly IPublishedPropertyType[] _propertyTypes;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// Initializes a new instance of the <see cref="PublishedContentType"/> class with a content type.
|
||||
/// </summary>
|
||||
public PublishedContentType(IContentTypeComposition contentType, IPublishedContentTypeFactory factory)
|
||||
: this(contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
|
||||
: this(contentType.Key, contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
|
||||
{
|
||||
var propertyTypes = contentType.CompositionPropertyTypes
|
||||
.Select(x => factory.CreatePropertyType(this, x))
|
||||
@@ -40,8 +40,20 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// <remarks>
|
||||
/// <para>Values are assumed to be consistent and are not checked.</para>
|
||||
/// </remarks>
|
||||
public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this(key, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
var propertyTypesA = propertyTypes.ToArray();
|
||||
foreach (var propertyType in propertyTypesA)
|
||||
propertyType.ContentType = this;
|
||||
_propertyTypes = propertyTypesA;
|
||||
|
||||
InitializeIndexes();
|
||||
}
|
||||
|
||||
[Obsolete("Use the overload specifying a key instead")]
|
||||
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this (id, alias, itemType, compositionAliases, variations, isElement)
|
||||
: this (Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
var propertyTypesA = propertyTypes.ToArray();
|
||||
foreach (var propertyType in propertyTypesA)
|
||||
@@ -57,16 +69,26 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// <remarks>
|
||||
/// <para>Values are assumed to be consistent and are not checked.</para>
|
||||
/// </remarks>
|
||||
public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this(key, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
_propertyTypes = propertyTypes(this).ToArray();
|
||||
|
||||
InitializeIndexes();
|
||||
}
|
||||
|
||||
[Obsolete("Use the overload specifying a key instead")]
|
||||
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations, bool isElement = false)
|
||||
: this(id, alias, itemType, compositionAliases, variations, isElement)
|
||||
: this(Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
|
||||
{
|
||||
_propertyTypes = propertyTypes(this).ToArray();
|
||||
|
||||
InitializeIndexes();
|
||||
}
|
||||
|
||||
private PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, ContentVariation variations, bool isElement)
|
||||
private PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, ContentVariation variations, bool isElement)
|
||||
{
|
||||
Key = key;
|
||||
Id = id;
|
||||
Alias = alias;
|
||||
ItemType = itemType;
|
||||
@@ -115,6 +137,9 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
|
||||
#region Content type
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Key { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
public static class PublishedContentTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the GUID key from an <see cref="IPublishedContentType"/>
|
||||
/// </summary>
|
||||
/// <param name="publishedContentType"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public static bool TryGetKey(this IPublishedContentType publishedContentType, out Guid key)
|
||||
{
|
||||
if (publishedContentType is IPublishedContentType2 contentTypeWithKey)
|
||||
{
|
||||
key = contentTypeWithKey.Key;
|
||||
return true;
|
||||
}
|
||||
key = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ namespace Umbraco.Core.Models
|
||||
|
||||
public Guid Key { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ITemplate DefaultTemplate { get; }
|
||||
|
||||
public ContentVariation Variations { get; }
|
||||
@@ -58,7 +59,7 @@ namespace Umbraco.Core.Models
|
||||
public string Icon { get; }
|
||||
|
||||
public bool IsContainer { get; }
|
||||
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public bool AllowedAsRoot { get; }
|
||||
@@ -93,5 +94,5 @@ namespace Umbraco.Core.Models
|
||||
return ((Alias != null ? Alias.GetHashCode() : 0) * 397) ^ Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +30,23 @@ namespace Umbraco.Core.PropertyEditors
|
||||
return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (object);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Snapshot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
=> source;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> inter;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> inter?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -235,5 +235,12 @@ namespace Umbraco.Web.PublishedCache
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <returns>The contents.</returns>
|
||||
IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content type identified by its alias.
|
||||
/// </summary>
|
||||
/// <param name="key">The content type key.</param>
|
||||
/// <returns>The content type, or null.</returns>
|
||||
IPublishedContentType GetContentType(Guid key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +96,8 @@ namespace Umbraco.Web.PublishedCache
|
||||
}
|
||||
|
||||
public abstract IPublishedContentType GetContentType(int id);
|
||||
|
||||
public abstract IPublishedContentType GetContentType(string alias);
|
||||
public abstract IPublishedContentType GetContentType(Guid key);
|
||||
|
||||
public virtual IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
|
||||
{
|
||||
|
||||
@@ -172,7 +172,7 @@ namespace Umbraco.Core
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom entity type.
|
||||
@@ -187,6 +187,7 @@ namespace Umbraco.Core
|
||||
{ Constants.UdiEntityType.Unknown, UdiType.Unknown },
|
||||
|
||||
{ Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi },
|
||||
{ Constants.UdiEntityType.Element, UdiType.GuidUdi },
|
||||
{ Constants.UdiEntityType.Document, UdiType.GuidUdi },
|
||||
{ Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi },
|
||||
{ Constants.UdiEntityType.Media, UdiType.GuidUdi },
|
||||
|
||||
@@ -228,7 +228,7 @@ namespace Umbraco.Web.Compose
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error<InstructionProcessTask>("Failed (will repeat).", e);
|
||||
_logger.Error<InstructionProcessTask>(e, "Failed (will repeat).");
|
||||
}
|
||||
return true; // repeat
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Examine;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
|
||||
namespace Umbraco.Examine
|
||||
{
|
||||
@@ -12,12 +14,14 @@ namespace Umbraco.Examine
|
||||
/// </summary>
|
||||
public class IndexRebuilder
|
||||
{
|
||||
private readonly IProfilingLogger _logger;
|
||||
private readonly IEnumerable<IIndexPopulator> _populators;
|
||||
public IExamineManager ExamineManager { get; }
|
||||
|
||||
public IndexRebuilder(IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
|
||||
public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
|
||||
{
|
||||
_populators = populators;
|
||||
_logger = logger;
|
||||
ExamineManager = examineManager;
|
||||
}
|
||||
|
||||
@@ -55,7 +59,14 @@ namespace Umbraco.Examine
|
||||
// run each populator over the indexes
|
||||
foreach(var populator in _populators)
|
||||
{
|
||||
populator.Populate(indexes);
|
||||
try
|
||||
{
|
||||
populator.Populate(indexes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error<IndexRebuilder>(e, "Index populating failed for populator {Populator}", populator.GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,9 +194,9 @@ namespace Umbraco.Core.Migrations.Upgrade
|
||||
To<MissingContentVersionsIndexes>("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
|
||||
To<AddMainDomLock>("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
|
||||
|
||||
|
||||
// to 8.7.0...
|
||||
To<MissingDictionaryIndex>("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}");
|
||||
|
||||
|
||||
//FINAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Convertable block data from json
|
||||
/// </summary>
|
||||
public class BlockEditorData
|
||||
{
|
||||
private readonly string _propertyEditorAlias;
|
||||
|
||||
public static BlockEditorData Empty { get; } = new BlockEditorData();
|
||||
|
||||
private BlockEditorData()
|
||||
{
|
||||
BlockValue = new BlockValue();
|
||||
}
|
||||
|
||||
public BlockEditorData(string propertyEditorAlias,
|
||||
IEnumerable<ContentAndSettingsReference> references,
|
||||
BlockValue blockValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propertyEditorAlias))
|
||||
throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias));
|
||||
_propertyEditorAlias = propertyEditorAlias;
|
||||
BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue));
|
||||
References = references != null ? new List<ContentAndSettingsReference>(references) : throw new ArgumentNullException(nameof(references));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the layout for this specific property editor
|
||||
/// </summary>
|
||||
public JToken Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the reference to the original BlockValue
|
||||
/// </summary>
|
||||
public BlockValue BlockValue { get; }
|
||||
|
||||
public List<ContentAndSettingsReference> References { get; } = new List<ContentAndSettingsReference>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Converts the block json data into objects
|
||||
/// </summary>
|
||||
public abstract class BlockEditorDataConverter
|
||||
{
|
||||
private readonly string _propertyEditorAlias;
|
||||
|
||||
protected BlockEditorDataConverter(string propertyEditorAlias)
|
||||
{
|
||||
_propertyEditorAlias = propertyEditorAlias;
|
||||
}
|
||||
|
||||
public BlockEditorData Deserialize(string json)
|
||||
{
|
||||
var value = JsonConvert.DeserializeObject<BlockValue>(json);
|
||||
|
||||
if (value.Layout == null)
|
||||
return BlockEditorData.Empty;
|
||||
|
||||
var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout)
|
||||
? GetBlockReferences(layout)
|
||||
: Enumerable.Empty<ContentAndSettingsReference>();
|
||||
|
||||
return new BlockEditorData(_propertyEditorAlias, references, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the collection of <see cref="IBlockReference"/> from the block editor's Layout (which could be an array or an object depending on the editor)
|
||||
/// </summary>
|
||||
/// <param name="jsonLayout"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// The base class for any strongly typed model for a Block editor implementation
|
||||
/// </summary>
|
||||
public abstract class BlockEditorModel
|
||||
{
|
||||
protected BlockEditorModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData)
|
||||
{
|
||||
ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData));
|
||||
SettingsData = settingsData ?? new List<IPublishedContent>();
|
||||
}
|
||||
|
||||
public BlockEditorModel()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The content data items of the Block List editor
|
||||
/// </summary>
|
||||
[DataMember(Name = "contentData")]
|
||||
public IEnumerable<IPublishedElement> ContentData { get; set; } = new List<IPublishedContent>();
|
||||
|
||||
/// <summary>
|
||||
/// The settings data items of the Block List editor
|
||||
/// </summary>
|
||||
[DataMember(Name = "settingsData")]
|
||||
public IEnumerable<IPublishedElement> SettingsData { get; set; } = new List<IPublishedContent>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single block's data in raw form
|
||||
/// </summary>
|
||||
public class BlockItemData
|
||||
{
|
||||
[JsonProperty("contentTypeKey")]
|
||||
public Guid ContentTypeKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// not serialized, manually set and used during internally
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ContentTypeAlias { get; set; }
|
||||
|
||||
[JsonProperty("udi")]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi Udi { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid Key => Udi != null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned");
|
||||
|
||||
/// <summary>
|
||||
/// The remaining properties will be serialized to a dictionary
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
|
||||
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
|
||||
/// NestedContent serializes to string, int, whatever eg
|
||||
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object> RawPropertyValues { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to convert the raw property data into data with a property type context
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IDictionary<string, BlockPropertyValue> PropertyValues { get; set; } = new Dictionary<string, BlockPropertyValue>();
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to populate the property value/property type of a block item content property
|
||||
/// </summary>
|
||||
public class BlockPropertyValue
|
||||
{
|
||||
public object Value { get; set; }
|
||||
public IPropertyType PropertyType { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Data converter for the block list property editor
|
||||
/// </summary>
|
||||
public class BlockListEditorDataConverter : BlockEditorDataConverter
|
||||
{
|
||||
public BlockListEditorDataConverter() : base(Constants.PropertyEditors.Aliases.BlockList)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout)
|
||||
{
|
||||
var blockListLayout = jsonLayout.ToObject<IEnumerable<BlockListLayoutItem>>();
|
||||
return blockListLayout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for deserializing the block list layout
|
||||
/// </summary>
|
||||
public class BlockListLayoutItem
|
||||
{
|
||||
[JsonProperty("contentUdi", Required = Required.Always)]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi ContentUdi { get; set; }
|
||||
|
||||
[JsonProperty("settingsUdi")]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi SettingsUdi { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a layout item for the Block List editor
|
||||
/// </summary>
|
||||
[DataContract(Name = "blockListLayout", Namespace = "")]
|
||||
public class BlockListLayoutReference : IBlockReference<IPublishedElement>
|
||||
{
|
||||
public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
|
||||
{
|
||||
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
Settings = settings; // can be null
|
||||
SettingsUdi = settingsUdi; // can be null
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Id of the content data item
|
||||
/// </summary>
|
||||
[DataMember(Name = "contentUdi")]
|
||||
public Udi ContentUdi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Id of the settings data item
|
||||
/// </summary>
|
||||
[DataMember(Name = "settingsUdi")]
|
||||
public Udi SettingsUdi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The content data item referenced
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is ignored from serialization since it is just a reference to the actual data element
|
||||
/// </remarks>
|
||||
[IgnoreDataMember]
|
||||
public IPublishedElement Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The settings data item referenced
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is ignored from serialization since it is just a reference to the actual data element
|
||||
/// </remarks>
|
||||
[IgnoreDataMember]
|
||||
public IPublishedElement Settings { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// The strongly typed model for the Block List editor
|
||||
/// </summary>
|
||||
[DataContract(Name = "blockList", Namespace = "")]
|
||||
public class BlockListModel : BlockEditorModel
|
||||
{
|
||||
public static BlockListModel Empty { get; } = new BlockListModel();
|
||||
|
||||
private BlockListModel()
|
||||
{
|
||||
}
|
||||
|
||||
public BlockListModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData, IEnumerable<BlockListLayoutReference> layout)
|
||||
: base(contentData, settingsData)
|
||||
{
|
||||
Layout = layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The layout items of the Block List editor
|
||||
/// </summary>
|
||||
[DataMember(Name = "layout")]
|
||||
public IEnumerable<BlockListLayoutReference> Layout { get; } = new List<BlockListLayoutReference>();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
public class BlockValue
|
||||
{
|
||||
[JsonProperty("layout")]
|
||||
public IDictionary<string, JToken> Layout { get; set; }
|
||||
|
||||
[JsonProperty("contentData")]
|
||||
public List<BlockItemData> ContentData { get; set; } = new List<BlockItemData>();
|
||||
|
||||
[JsonProperty("settingsData")]
|
||||
public List<BlockItemData> SettingsData { get; set; } = new List<BlockItemData>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
public struct ContentAndSettingsReference : IEquatable<ContentAndSettingsReference>
|
||||
{
|
||||
public ContentAndSettingsReference(Udi contentUdi, Udi settingsUdi)
|
||||
{
|
||||
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
|
||||
SettingsUdi = settingsUdi;
|
||||
}
|
||||
|
||||
public Udi ContentUdi { get; }
|
||||
public Udi SettingsUdi { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ContentAndSettingsReference reference && Equals(reference);
|
||||
}
|
||||
|
||||
public bool Equals(ContentAndSettingsReference other)
|
||||
{
|
||||
return EqualityComparer<Udi>.Default.Equals(ContentUdi, other.ContentUdi) &&
|
||||
EqualityComparer<Udi>.Default.Equals(SettingsUdi, other.SettingsUdi);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = 272556606;
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(ContentUdi);
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(SettingsUdi);
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data item reference for a Block editor implementation
|
||||
/// </summary>
|
||||
/// <typeparam name="TSettings"></typeparam>
|
||||
/// <remarks>
|
||||
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
|
||||
/// </remarks>
|
||||
public interface IBlockReference<TSettings> : IBlockReference
|
||||
{
|
||||
TSettings Settings { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data item reference for a Block Editor implementation
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
|
||||
/// </remarks>
|
||||
public interface IBlockReference
|
||||
{
|
||||
Udi ContentUdi { get; }
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,9 @@ namespace Umbraco.Web.Models.ContentEditing
|
||||
[DataMember(Name = "contentTypeId")]
|
||||
public int ContentTypeId { get; set; }
|
||||
|
||||
[DataMember(Name = "contentTypeKey")]
|
||||
public Guid ContentTypeKey { get; set; }
|
||||
|
||||
[DataMember(Name = "contentTypeAlias", IsRequired = true)]
|
||||
[Required(AllowEmptyStrings = false)]
|
||||
public string ContentTypeAlias { get; set; }
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace Umbraco.Core.Models
|
||||
/// Internal property to store the Id of the default template
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
internal int DefaultTemplateId
|
||||
public int DefaultTemplateId
|
||||
{
|
||||
get => _defaultTemplate;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId));
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Provides strongly typed published content models services.
|
||||
/// </summary>
|
||||
|
||||
@@ -35,18 +35,18 @@ namespace Umbraco.Core.Models.PublishedContent
|
||||
/// This method is for tests and is not intended to be used directly from application code.
|
||||
/// </summary>
|
||||
/// <remarks>Values are assumed to be consisted and are not checked.</remarks>
|
||||
internal IPublishedContentType CreateContentType(int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
internal IPublishedContentType CreateContentType(Guid key, int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
{
|
||||
return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, variations, isElement);
|
||||
return new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, variations, isElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is for tests and is not intended to be used directly from application code.
|
||||
/// </summary>
|
||||
/// <remarks>Values are assumed to be consisted and are not checked.</remarks>
|
||||
internal IPublishedContentType CreateContentType(int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
internal IPublishedContentType CreateContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
|
||||
{
|
||||
return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
|
||||
return new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Umbraco.Core.Models
|
||||
/// <summary>
|
||||
/// Returns true if the template is used as a layout for other templates (i.e. it has 'children')
|
||||
/// </summary>
|
||||
public bool IsMasterTemplate { get; internal set; }
|
||||
public bool IsMasterTemplate { get; set; }
|
||||
|
||||
public void SetMasterTemplate(ITemplate masterTemplate)
|
||||
{
|
||||
|
||||
+29
-15
@@ -4,6 +4,10 @@ using System.Data.SqlClient;
|
||||
|
||||
namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
{
|
||||
// See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues
|
||||
// Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ?
|
||||
// but i guess that's not netcore so we'll just leave it.
|
||||
|
||||
/// <summary>
|
||||
/// Provides the transient error detection logic for transient faults that are specific to SQL Azure.
|
||||
/// </summary>
|
||||
@@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception object to be verified.</param>
|
||||
/// <returns>True if the specified exception is considered as transient, otherwise false.</returns>
|
||||
/// <returns>true if the specified exception is considered as transient; otherwise, false.</returns>
|
||||
public bool IsTransient(Exception ex)
|
||||
{
|
||||
if (ex != null)
|
||||
@@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
|
||||
return true;
|
||||
|
||||
// SQL Error Code: 40197
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40197:
|
||||
// SQL Error Code: 10928
|
||||
// Resource ID: %d. The %s limit for the database is %d and has been reached.
|
||||
case 10928:
|
||||
// SQL Error Code: 10929
|
||||
// Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d.
|
||||
// However, the server is currently too busy to support requests greater than %d for this database.
|
||||
case 10929:
|
||||
// SQL Error Code: 10053
|
||||
// A transport-level error has occurred when receiving results from the server.
|
||||
// An established connection was aborted by the software in your host machine.
|
||||
case 10053:
|
||||
// SQL Error Code: 10054
|
||||
// A transport-level error has occurred when sending the request to the server.
|
||||
// A transport-level error has occurred when sending the request to the server.
|
||||
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
|
||||
case 10054:
|
||||
// SQL Error Code: 10060
|
||||
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
|
||||
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
|
||||
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
|
||||
// because the connected party did not properly respond after a period of time, or established connection failed
|
||||
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
|
||||
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
|
||||
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
|
||||
// because the connected party did not properly respond after a period of time, or established connection failed
|
||||
// because connected host has failed to respond.)"}
|
||||
case 10060:
|
||||
// SQL Error Code: 40197
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40197:
|
||||
// SQL Error Code: 40540
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40540:
|
||||
// SQL Error Code: 40613
|
||||
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
|
||||
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
|
||||
// support, and provide them the session tracing ID of ZZZZZ.
|
||||
case 40613:
|
||||
// SQL Error Code: 40143
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40143:
|
||||
// SQL Error Code: 233
|
||||
// The client was unable to establish a connection because of an error during connection initialization process before login.
|
||||
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
|
||||
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
|
||||
// The client was unable to establish a connection because of an error during connection initialization process before login.
|
||||
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
|
||||
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
|
||||
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
|
||||
case 233:
|
||||
// SQL Error Code: 64
|
||||
// A connection was successfully established with the server, but then an error occurred during the login process.
|
||||
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
|
||||
// A connection was successfully established with the server, but then an error occurred during the login process.
|
||||
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
|
||||
case 64:
|
||||
// DBNETLIB Error Code: 20
|
||||
// The instance of SQL Server you attempted to connect to does not support encryption.
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// </summary>
|
||||
void ClearSchedule(DateTime date);
|
||||
|
||||
void ClearSchedule(DateTime date, ContentScheduleAction action);
|
||||
|
||||
bool HasContentForExpiration(DateTime date);
|
||||
bool HasContentForRelease(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// Gets <see cref="IContent"/> objects having an expiration date before (lower than, or equal to) a specified date.
|
||||
/// </summary>
|
||||
|
||||
@@ -1027,6 +1027,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
Database.Execute(sql);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearSchedule(DateTime date, ContentScheduleAction action)
|
||||
{
|
||||
var a = action.ToString();
|
||||
var sql = Sql().Delete<ContentScheduleDto>().Where<ContentScheduleDto>(x => x.Date <= date && x.Action == a);
|
||||
Database.Execute(sql);
|
||||
}
|
||||
|
||||
private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql
|
||||
.SelectCount()
|
||||
.From<ContentScheduleDto>()
|
||||
.Where<ContentScheduleDto>(x => x.Action == SqlTemplate.Arg<string>("action") && x.Date <= SqlTemplate.Arg<DateTime>("date")));
|
||||
|
||||
var sql = template.Sql(action.ToString(), date);
|
||||
return sql;
|
||||
}
|
||||
|
||||
public bool HasContentForExpiration(DateTime date)
|
||||
{
|
||||
var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date);
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
public bool HasContentForRelease(DateTime date)
|
||||
{
|
||||
var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date);
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetContentForRelease(DateTime date)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Blocks;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using static Umbraco.Core.Models.Blocks.BlockItemData;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Abstract class for block editor based editors
|
||||
/// </summary>
|
||||
public abstract class BlockEditorPropertyEditor : DataEditor
|
||||
{
|
||||
public const string ContentTypeKeyPropertyKey = "contentTypeKey";
|
||||
public const string UdiPropertyKey = "udi";
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly Lazy<PropertyEditorCollection> _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
|
||||
public BlockEditorPropertyEditor(
|
||||
ILogger logger,
|
||||
Lazy<PropertyEditorCollection> propertyEditors,
|
||||
IDataTypeService dataTypeService,
|
||||
IContentTypeService contentTypeService,
|
||||
ILocalizedTextService localizedTextService,
|
||||
ILocalizationService localizationService,
|
||||
IShortStringHelper shortStringHelper)
|
||||
: base(logger, dataTypeService,localizationService,localizedTextService, shortStringHelper)
|
||||
{
|
||||
_localizedTextService = localizedTextService;
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_contentTypeService = contentTypeService;
|
||||
}
|
||||
|
||||
// has to be lazy else circular dep in ctor
|
||||
private PropertyEditorCollection PropertyEditors => _propertyEditors.Value;
|
||||
|
||||
#region Value Editor
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger, LocalizationService,ShortStringHelper);
|
||||
|
||||
internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService; // TODO: Not used yet but we'll need it to fill in the FromEditor/ToEditor
|
||||
private readonly ILogger _logger;
|
||||
private readonly BlockEditorValues _blockEditorValues;
|
||||
|
||||
public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
|
||||
: base(dataTypeService, localizationService, textService, shortStringHelper, attribute)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_logger = logger;
|
||||
_blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger);
|
||||
Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService));
|
||||
Validators.Add(new MinMaxValidator(_blockEditorValues, textService));
|
||||
}
|
||||
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object value)
|
||||
{
|
||||
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
|
||||
|
||||
var result = new List<UmbracoEntityReference>();
|
||||
var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson);
|
||||
if (blockEditorData == null)
|
||||
return Enumerable.Empty<UmbracoEntityReference>();
|
||||
|
||||
// loop through all content and settings data
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
|
||||
{
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
|
||||
var valueEditor = propEditor?.GetValueEditor();
|
||||
if (!(valueEditor is IDataValueReference reference)) continue;
|
||||
|
||||
var val = prop.Value.Value?.ToString();
|
||||
|
||||
var refs = reference.GetReferences(val);
|
||||
|
||||
result.AddRange(refs);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Convert database // editor
|
||||
|
||||
// note: there is NO variant support here
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their ToEditor methods
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="dataTypeService"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
public override object ToEditor(IProperty property, string culture = null, string segment = null)
|
||||
{
|
||||
var val = property.GetValue(culture, segment);
|
||||
|
||||
BlockEditorData blockEditorData;
|
||||
try
|
||||
{
|
||||
blockEditorData = _blockEditorValues.DeserializeAndClean(val);
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{
|
||||
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData)
|
||||
{
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
// create a temp property with the value
|
||||
// - force it to be culture invariant as the block editor can't handle culture variant element properties
|
||||
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
|
||||
var tempProp = new Property(prop.Value.PropertyType);
|
||||
|
||||
tempProp.SetValue(prop.Value.Value);
|
||||
|
||||
// convert that temp property, and store the converted value
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
if (propEditor == null)
|
||||
{
|
||||
// NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists.
|
||||
// if the property editor doesn't exist I think everything will break anyways?
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
|
||||
continue;
|
||||
}
|
||||
|
||||
var dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId);
|
||||
if (dataType == null)
|
||||
{
|
||||
// deal with weird situations by ignoring them (no comment)
|
||||
row.PropertyValues.Remove(prop.Key);
|
||||
_logger.Warn<BlockEditorPropertyValueEditor>(
|
||||
"ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
|
||||
prop.Key, row.Key, property.PropertyType.Alias);
|
||||
continue;
|
||||
}
|
||||
|
||||
var tempConfig = dataType.Configuration;
|
||||
var valEditor = propEditor.GetValueEditor(tempConfig);
|
||||
var convValue = valEditor.ToEditor(tempProp);
|
||||
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = convValue;
|
||||
}
|
||||
}
|
||||
|
||||
// return json convertable object
|
||||
return blockEditorData.BlockValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their FromEditor methods
|
||||
/// </summary>
|
||||
/// <param name="editorValue"></param>
|
||||
/// <param name="currentValue"></param>
|
||||
/// <returns></returns>
|
||||
public override object FromEditor(ContentPropertyData editorValue, object currentValue)
|
||||
{
|
||||
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
|
||||
return null;
|
||||
|
||||
BlockEditorData blockEditorData;
|
||||
try
|
||||
{
|
||||
blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value);
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{
|
||||
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData)
|
||||
{
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
// Fetch the property types prevalue
|
||||
var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
|
||||
|
||||
// Lookup the property editor
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
if (propEditor == null) continue;
|
||||
|
||||
// Create a fake content property data object
|
||||
var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration);
|
||||
|
||||
// Get the property editor to do it's conversion
|
||||
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value);
|
||||
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// return json
|
||||
return JsonConvert.SerializeObject(blockEditorData.BlockValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the min/max of the block editor
|
||||
/// </summary>
|
||||
private class MinMaxValidator : IValueValidator
|
||||
{
|
||||
private readonly BlockEditorValues _blockEditorValues;
|
||||
private readonly ILocalizedTextService _textService;
|
||||
|
||||
public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService)
|
||||
{
|
||||
_blockEditorValues = blockEditorValues;
|
||||
_textService = textService;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
|
||||
{
|
||||
var blockConfig = (BlockListConfiguration)dataTypeConfiguration;
|
||||
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
|
||||
if ((blockEditorData == null && blockConfig?.ValidationLimit?.Min > 0)
|
||||
|| (blockEditorData != null && blockEditorData.Layout.Count() < blockConfig?.ValidationLimit?.Min))
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
_textService.Localize("validation/entriesShort", new[] { blockConfig.ValidationLimit.Min.ToString(), (blockConfig.ValidationLimit.Min - blockEditorData.Layout.Count()).ToString() }),
|
||||
new[] { "minCount" });
|
||||
}
|
||||
|
||||
if (blockEditorData != null && blockEditorData.Layout.Count() > blockConfig?.ValidationLimit?.Max)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
_textService.Localize("validation/entriesExceed", new[] { blockConfig.ValidationLimit.Max.ToString(), (blockEditorData.Layout.Count() - blockConfig.ValidationLimit.Max).ToString() }),
|
||||
new[] { "maxCount" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class BlockEditorValidator : ComplexEditorValidator
|
||||
{
|
||||
private readonly BlockEditorValues _blockEditorValues;
|
||||
|
||||
public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) : base(propertyEditors, dataTypeService, textService)
|
||||
{
|
||||
_blockEditorValues = blockEditorValues;
|
||||
}
|
||||
|
||||
protected override IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value)
|
||||
{
|
||||
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
|
||||
if (blockEditorData != null)
|
||||
{
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
|
||||
{
|
||||
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key);
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
elementValidation.AddPropertyTypeValidation(
|
||||
new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value));
|
||||
}
|
||||
yield return elementValidation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to deserialize json values and clean up any values based on the existence of element types and layout structure
|
||||
/// </summary>
|
||||
internal class BlockEditorValues
|
||||
{
|
||||
private readonly Lazy<Dictionary<Guid, IContentType>> _contentTypes;
|
||||
private readonly BlockEditorDataConverter _dataConverter;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger)
|
||||
{
|
||||
_contentTypes = new Lazy<Dictionary<Guid, IContentType>>(() => contentTypeService.GetAll().ToDictionary(c => c.Key));
|
||||
_dataConverter = dataConverter;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private IContentType GetElementType(BlockItemData item)
|
||||
{
|
||||
_contentTypes.Value.TryGetValue(item.ContentTypeKey, out var contentType);
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public BlockEditorData DeserializeAndClean(object propertyValue)
|
||||
{
|
||||
if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString()))
|
||||
return null;
|
||||
|
||||
var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString());
|
||||
|
||||
if (blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
{
|
||||
// if there's no content ensure there's no settings too
|
||||
blockEditorData.BlockValue.SettingsData.Clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
|
||||
|
||||
// filter out any content that isn't referenced in the layout references
|
||||
foreach(var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => r.ContentUdi == x.Udi)))
|
||||
{
|
||||
ResolveBlockItemData(block, contentTypePropertyTypes);
|
||||
}
|
||||
// filter out any settings that isn't referenced in the layout references
|
||||
foreach (var block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => r.SettingsUdi == x.Udi)))
|
||||
{
|
||||
ResolveBlockItemData(block, contentTypePropertyTypes);
|
||||
}
|
||||
|
||||
// remove blocks that couldn't be resolved
|
||||
blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace());
|
||||
blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace());
|
||||
|
||||
return blockEditorData;
|
||||
}
|
||||
|
||||
private bool ResolveBlockItemData(BlockItemData block, Dictionary<string, Dictionary<string, IPropertyType>> contentTypePropertyTypes)
|
||||
{
|
||||
var contentType = GetElementType(block);
|
||||
if (contentType == null)
|
||||
return false;
|
||||
|
||||
// get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating
|
||||
// objects on each iteration.
|
||||
if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes))
|
||||
propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
var propValues = new Dictionary<string, BlockPropertyValue>();
|
||||
|
||||
// find any keys that are not real property types and remove them
|
||||
foreach (var prop in block.RawPropertyValues.ToList())
|
||||
{
|
||||
// doesn't exist so remove it
|
||||
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
|
||||
{
|
||||
block.RawPropertyValues.Remove(prop.Key);
|
||||
_logger.Warn<BlockEditorValues>("The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}",
|
||||
prop.Key, block.Key, prop.Key, contentType.Alias);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set the value to include the resolved property type
|
||||
propValues[prop.Key] = new BlockPropertyValue
|
||||
{
|
||||
PropertyType = propType,
|
||||
Value = prop.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
block.ContentTypeAlias = contentType.Alias;
|
||||
block.PropertyValues = propValues;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The configuration object for the Block List editor
|
||||
/// </summary>
|
||||
public class BlockListConfiguration
|
||||
{
|
||||
|
||||
|
||||
[ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")]
|
||||
public BlockConfiguration[] Blocks { get; set; }
|
||||
|
||||
public class BlockConfiguration
|
||||
{
|
||||
|
||||
[JsonProperty("backgroundColor")]
|
||||
public string BackgroundColor { get; set; }
|
||||
|
||||
[JsonProperty("iconColor")]
|
||||
public string IconColor { get; set; }
|
||||
|
||||
[JsonProperty("thumbnail")]
|
||||
public string Thumbnail { get; set; }
|
||||
|
||||
// TODO: This is named inconsistently in JS but renaming it needs to be done in quite a lot of places, this should be contentElementTypeKey
|
||||
[JsonProperty("contentTypeKey")]
|
||||
public Guid ContentElementTypeKey { get; set; }
|
||||
|
||||
[JsonProperty("settingsElementTypeKey")]
|
||||
public Guid? SettingsElementTypeKey { get; set; }
|
||||
|
||||
[JsonProperty("view")]
|
||||
public string View { get; set; }
|
||||
|
||||
[JsonProperty("stylesheet")]
|
||||
public string Stylesheet { get; set; }
|
||||
|
||||
[JsonProperty("label")]
|
||||
public string Label { get; set; }
|
||||
|
||||
[JsonProperty("editorSize")]
|
||||
public string EditorSize { get; set; }
|
||||
|
||||
[JsonProperty("forceHideContentEditorInOverlay")]
|
||||
public bool ForceHideContentEditorInOverlay { get; set; }
|
||||
}
|
||||
|
||||
[ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")]
|
||||
public NumberRange ValidationLimit { get; set; } = new NumberRange();
|
||||
|
||||
public class NumberRange
|
||||
{
|
||||
[JsonProperty("min")]
|
||||
public int? Min { get; set; }
|
||||
|
||||
[JsonProperty("max")]
|
||||
public int? Max { get; set; }
|
||||
}
|
||||
|
||||
[ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views.")]
|
||||
public bool UseLiveEditing { get; set; }
|
||||
|
||||
[ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")]
|
||||
public bool UseInlineEditingAsDefault { get; set; }
|
||||
|
||||
[ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")]
|
||||
public string MaxPropertyWidth { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
internal class BlockListConfigurationEditor : ConfigurationEditor<BlockListConfiguration>
|
||||
{
|
||||
public BlockListConfigurationEditor(IIOHelper ioHelper) : base(ioHelper)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.Blocks;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a block list property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.BlockList,
|
||||
"Block List",
|
||||
"blocklist",
|
||||
ValueType = ValueTypes.Json,
|
||||
Group = Constants.PropertyEditors.Groups.Lists,
|
||||
Icon = "icon-thumbnail-list")]
|
||||
public class BlockListPropertyEditor : BlockEditorPropertyEditor
|
||||
{
|
||||
private readonly IIOHelper _ioHelper;
|
||||
|
||||
public BlockListPropertyEditor(
|
||||
ILogger logger,
|
||||
Lazy<PropertyEditorCollection> propertyEditors,
|
||||
IDataTypeService dataTypeService,
|
||||
IContentTypeService contentTypeService,
|
||||
ILocalizedTextService localizedTextService,
|
||||
IIOHelper ioHelper,
|
||||
ILocalizationService localizationService,
|
||||
IShortStringHelper shortStringHelper)
|
||||
: base(logger, propertyEditors, dataTypeService, contentTypeService, localizedTextService, localizationService, shortStringHelper)
|
||||
{
|
||||
_ioHelper = ioHelper;
|
||||
}
|
||||
|
||||
#region Pre Value Editor
|
||||
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(_ioHelper);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.PropertyEditors.Validation;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to validate complex editors that contain nested editors
|
||||
/// </summary>
|
||||
public abstract class ComplexEditorValidator : IValueValidator
|
||||
{
|
||||
private readonly PropertyValidationService _propertyValidationService;
|
||||
|
||||
public ComplexEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
|
||||
{
|
||||
_propertyValidationService = new PropertyValidationService(propertyEditors, dataTypeService, textService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a single <see cref="ComplexEditorValidationResult"/> for all sub nested validation results in the complex editor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="valueType"></param>
|
||||
/// <param name="dataTypeConfiguration"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
|
||||
{
|
||||
var elementTypeValues = GetElementTypeValidation(value).ToList();
|
||||
var rowResults = GetNestedValidationResults(elementTypeValues).ToList();
|
||||
|
||||
if (rowResults.Count > 0)
|
||||
{
|
||||
var result = new ComplexEditorValidationResult();
|
||||
foreach(var rowResult in rowResults)
|
||||
{
|
||||
result.ValidationResults.Add(rowResult);
|
||||
}
|
||||
return result.Yield();
|
||||
}
|
||||
|
||||
return Enumerable.Empty<ValidationResult>();
|
||||
}
|
||||
|
||||
|
||||
protected abstract IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value);
|
||||
|
||||
/// <summary>
|
||||
/// Return a nested validation result per row (Element Type)
|
||||
/// </summary>
|
||||
/// <param name="rawValue"></param>
|
||||
/// <returns></returns>
|
||||
protected IEnumerable<ComplexEditorElementTypeValidationResult> GetNestedValidationResults(IEnumerable<ElementTypeValidationModel> elements)
|
||||
{
|
||||
foreach (var row in elements)
|
||||
{
|
||||
var elementTypeValidationResult = new ComplexEditorElementTypeValidationResult(row.ElementTypeAlias, row.Id);
|
||||
|
||||
foreach (var prop in row.PropertyTypeValidation)
|
||||
{
|
||||
var propValidationResult = new ComplexEditorPropertyTypeValidationResult(prop.PropertyType.Alias);
|
||||
|
||||
foreach (var validationResult in _propertyValidationService.ValidatePropertyValue(prop.PropertyType, prop.PostedValue))
|
||||
{
|
||||
// add the result to the property results
|
||||
propValidationResult.AddValidationResult(validationResult);
|
||||
}
|
||||
|
||||
// add the property results to the element type results
|
||||
if (propValidationResult.ValidationResults.Count > 0)
|
||||
elementTypeValidationResult.ValidationResults.Add(propValidationResult);
|
||||
}
|
||||
|
||||
if (elementTypeValidationResult.ValidationResults.Count > 0)
|
||||
{
|
||||
yield return elementTypeValidationResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PropertyTypeValidationModel
|
||||
{
|
||||
public PropertyTypeValidationModel(IPropertyType propertyType, object postedValue)
|
||||
{
|
||||
PostedValue = postedValue;
|
||||
PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
|
||||
}
|
||||
|
||||
public object PostedValue { get; }
|
||||
public IPropertyType PropertyType { get; }
|
||||
}
|
||||
|
||||
public class ElementTypeValidationModel
|
||||
{
|
||||
public ElementTypeValidationModel(string elementTypeAlias, Guid id)
|
||||
{
|
||||
ElementTypeAlias = elementTypeAlias;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
private List<PropertyTypeValidationModel> _list = new List<PropertyTypeValidationModel>();
|
||||
|
||||
public IEnumerable<PropertyTypeValidationModel> PropertyTypeValidation => _list;
|
||||
|
||||
public string ElementTypeAlias { get; }
|
||||
public Guid Id { get; }
|
||||
|
||||
public void AddPropertyTypeValidation(PropertyTypeValidationModel propValidation) => _list.Add(propValidation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for the nested content value editor.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core;
|
||||
@@ -16,6 +14,7 @@ using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents a nested content property editor.
|
||||
/// </summary>
|
||||
@@ -31,6 +30,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
private readonly Lazy<PropertyEditorCollection> _propertyEditors;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
|
||||
public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
_propertyEditors = propertyEditors;
|
||||
_contentTypeService = contentTypeService;
|
||||
_ioHelper = ioHelper;
|
||||
_localizedTextService = localizedTextService;
|
||||
}
|
||||
|
||||
// has to be lazy else circular dep in ctor
|
||||
@@ -61,25 +62,35 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
#region Value Editor
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(DataTypeService, LocalizationService, LocalizedTextService, _contentTypeService, ShortStringHelper, Attribute, PropertyEditors);
|
||||
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(DataTypeService, LocalizationService, LocalizedTextService, _contentTypeService, ShortStringHelper, Attribute, PropertyEditors, Logger);
|
||||
|
||||
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly NestedContentValues _nestedContentValues;
|
||||
|
||||
private readonly Lazy<Dictionary<string, IContentType>> _contentTypes;
|
||||
|
||||
public NestedContentPropertyValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, ILocalizedTextService localizedTextService, IContentTypeService contentTypeService, IShortStringHelper shortStringHelper, DataEditorAttribute attribute, PropertyEditorCollection propertyEditors)
|
||||
public NestedContentPropertyValueEditor(
|
||||
IDataTypeService dataTypeService,
|
||||
ILocalizationService localizationService,
|
||||
ILocalizedTextService localizedTextService,
|
||||
IContentTypeService contentTypeService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
DataEditorAttribute attribute,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
ILogger logger)
|
||||
: base(dataTypeService, localizationService, localizedTextService, shortStringHelper, attribute)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_contentTypeService = contentTypeService;
|
||||
_dataTypeService = dataTypeService;
|
||||
_logger = logger;
|
||||
_nestedContentValues = new NestedContentValues(contentTypeService);
|
||||
Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService, _nestedContentValues));
|
||||
Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, localizedTextService));
|
||||
|
||||
_contentTypes = new Lazy<Dictionary<string, IContentType>>(() =>
|
||||
_contentTypeService.GetAll().ToDictionary(c => c.Alias)
|
||||
@@ -106,41 +117,41 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
public override string ConvertDbToString(IPropertyType propertyType, object propertyValue, IDataTypeService dataTypeService)
|
||||
{
|
||||
var vals = _nestedContentValues.GetPropertyValues(propertyValue, out var deserialized).ToList();
|
||||
var rows = _nestedContentValues.GetPropertyValues(propertyValue);
|
||||
|
||||
if (vals.Count == 0)
|
||||
if (rows.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var row in vals)
|
||||
foreach (var row in rows.ToList())
|
||||
{
|
||||
if (row.PropType == null)
|
||||
{
|
||||
// type not found, and property is not system: just delete the value
|
||||
if (IsSystemPropertyKey(row.PropKey) == false)
|
||||
row.JsonRowValue[row.PropKey] = null;
|
||||
}
|
||||
else
|
||||
foreach(var prop in row.PropertyValues.ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
// convert the value, and store the converted value
|
||||
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
if (propEditor == null) continue;
|
||||
|
||||
var tempConfig = DataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
|
||||
var tempConfig = DataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
|
||||
var valEditor = propEditor.GetValueEditor(tempConfig);
|
||||
var convValue = valEditor.ConvertDbToString(row.PropType, row.JsonRowValue[row.PropKey]?.ToString(), dataTypeService);
|
||||
row.JsonRowValue[row.PropKey] = convValue;
|
||||
var convValue = valEditor.ConvertDbToString(prop.Value.PropertyType, prop.Value.Value, dataTypeService);
|
||||
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = convValue;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// deal with weird situations by ignoring them (no comment)
|
||||
row.JsonRowValue[row.PropKey] = null;
|
||||
row.RawPropertyValues.Remove(prop.Key);
|
||||
_logger.Warn<NestedContentPropertyValueEditor>(
|
||||
ex,
|
||||
"ConvertDbToString removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
|
||||
prop.Key, row.Id, propertyType.Alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(deserialized).ToXmlString<string>();
|
||||
return JsonConvert.SerializeObject(rows).ToXmlString<string>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -151,98 +162,108 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
// note: there is NO variant support here
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their ToEditor methods
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="dataTypeService"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
public override object ToEditor(IProperty property, string culture = null, string segment = null)
|
||||
{
|
||||
var val = property.GetValue(culture, segment);
|
||||
|
||||
var vals = _nestedContentValues.GetPropertyValues(val, out var deserialized).ToList();
|
||||
var rows = _nestedContentValues.GetPropertyValues(val);
|
||||
|
||||
if (vals.Count == 0)
|
||||
if (rows.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var row in vals)
|
||||
foreach (var row in rows.ToList())
|
||||
{
|
||||
if (row.PropType == null)
|
||||
{
|
||||
// type not found, and property is not system: just delete the value
|
||||
if (IsSystemPropertyKey(row.PropKey) == false)
|
||||
row.JsonRowValue[row.PropKey] = null;
|
||||
}
|
||||
else
|
||||
foreach(var prop in row.PropertyValues.ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
// create a temp property with the value
|
||||
// - force it to be culture invariant as NC can't handle culture variant element properties
|
||||
row.PropType.Variations = ContentVariation.Nothing;
|
||||
var tempProp = new Property(row.PropType);
|
||||
tempProp.SetValue(row.JsonRowValue[row.PropKey] == null ? null : row.JsonRowValue[row.PropKey].ToString());
|
||||
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
|
||||
var tempProp = new Property(prop.Value.PropertyType);
|
||||
|
||||
tempProp.SetValue(prop.Value.Value);
|
||||
|
||||
// convert that temp property, and store the converted value
|
||||
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
if (propEditor == null)
|
||||
{
|
||||
row.JsonRowValue[row.PropKey] = tempProp.GetValue()?.ToString();
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
|
||||
continue;
|
||||
}
|
||||
|
||||
var tempConfig = DataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
|
||||
var tempConfig = DataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
|
||||
var valEditor = propEditor.GetValueEditor(tempConfig);
|
||||
var convValue = valEditor.ToEditor(tempProp);
|
||||
row.JsonRowValue[row.PropKey] = convValue == null ? null : JToken.FromObject(convValue);
|
||||
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = convValue == null ? null : JToken.FromObject(convValue);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// deal with weird situations by ignoring them (no comment)
|
||||
row.JsonRowValue[row.PropKey] = null;
|
||||
row.RawPropertyValues.Remove(prop.Key);
|
||||
_logger.Warn<NestedContentPropertyValueEditor>(
|
||||
ex,
|
||||
"ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
|
||||
prop.Key, row.Id, property.PropertyType.Alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return json
|
||||
return deserialized;
|
||||
// return the object, there's a native json converter for this so it will serialize correctly
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their FromEditor methods
|
||||
/// </summary>
|
||||
/// <param name="editorValue"></param>
|
||||
/// <param name="currentValue"></param>
|
||||
/// <returns></returns>
|
||||
public override object FromEditor(ContentPropertyData editorValue, object currentValue)
|
||||
{
|
||||
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
|
||||
return null;
|
||||
|
||||
var vals = _nestedContentValues.GetPropertyValues(editorValue.Value, out var deserialized).ToList();
|
||||
var rows = _nestedContentValues.GetPropertyValues(editorValue.Value);
|
||||
|
||||
if (vals.Count == 0)
|
||||
if (rows.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var row in vals)
|
||||
foreach (var row in rows.ToList())
|
||||
{
|
||||
if (row.PropType == null)
|
||||
{
|
||||
// type not found, and property is not system: just delete the value
|
||||
if (IsSystemPropertyKey(row.PropKey) == false)
|
||||
row.JsonRowValue[row.PropKey] = null;
|
||||
}
|
||||
else
|
||||
foreach(var prop in row.PropertyValues.ToList())
|
||||
{
|
||||
// Fetch the property types prevalue
|
||||
var propConfiguration = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
|
||||
var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
|
||||
|
||||
// Lookup the property editor
|
||||
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
if (propEditor == null) continue;
|
||||
|
||||
// Create a fake content property data object
|
||||
var contentPropData = new ContentPropertyData(row.JsonRowValue[row.PropKey], propConfiguration);
|
||||
var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration);
|
||||
|
||||
// Get the property editor to do it's conversion
|
||||
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, row.JsonRowValue[row.PropKey]);
|
||||
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value);
|
||||
|
||||
// Store the value back
|
||||
row.JsonRowValue[row.PropKey] = (newValue == null) ? null : JToken.FromObject(newValue);
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = (newValue == null) ? null : JToken.FromObject(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
// return json
|
||||
return JsonConvert.SerializeObject(deserialized);
|
||||
return JsonConvert.SerializeObject(rows);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -252,98 +273,57 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
var result = new List<UmbracoEntityReference>();
|
||||
|
||||
foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _))
|
||||
foreach (var row in _nestedContentValues.GetPropertyValues(rawJson))
|
||||
{
|
||||
if (row.PropType == null) continue;
|
||||
foreach(var prop in row.PropertyValues)
|
||||
{
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
|
||||
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
|
||||
var valueEditor = propEditor?.GetValueEditor();
|
||||
if (!(valueEditor is IDataValueReference reference)) continue;
|
||||
|
||||
var valueEditor = propEditor?.GetValueEditor();
|
||||
if (!(valueEditor is IDataValueReference reference)) continue;
|
||||
var val = prop.Value.Value?.ToString();
|
||||
|
||||
var val = row.JsonRowValue[row.PropKey]?.ToString();
|
||||
var refs = reference.GetReferences(val);
|
||||
|
||||
var refs = reference.GetReferences(val);
|
||||
|
||||
result.AddRange(refs);
|
||||
result.AddRange(refs);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
internal class NestedContentValidator : IValueValidator
|
||||
/// <summary>
|
||||
/// Validator for nested content to ensure that all nesting of editors is validated
|
||||
/// </summary>
|
||||
internal class NestedContentValidator : ComplexEditorValidator
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly NestedContentValues _nestedContentValues;
|
||||
|
||||
public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, NestedContentValues nestedContentValues)
|
||||
public NestedContentValidator(NestedContentValues nestedContentValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
|
||||
: base(propertyEditors, dataTypeService, textService)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_nestedContentValues = nestedContentValues;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
|
||||
protected override IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value)
|
||||
{
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
foreach(var row in _nestedContentValues.GetPropertyValues(rawValue, out _))
|
||||
foreach (var row in _nestedContentValues.GetPropertyValues(value))
|
||||
{
|
||||
if (row.PropType == null) continue;
|
||||
|
||||
var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
|
||||
var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
|
||||
if (propertyEditor == null) continue;
|
||||
|
||||
foreach (var validator in propertyEditor.GetValueEditor().Validators)
|
||||
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Id);
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config))
|
||||
{
|
||||
result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage;
|
||||
validationResults.Add(result);
|
||||
elementValidation.AddPropertyTypeValidation(
|
||||
new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// Check mandatory
|
||||
if (row.PropType.Mandatory)
|
||||
{
|
||||
if (row.JsonRowValue[row.PropKey] == null)
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
|
||||
? $"'{row.PropType.Name}' cannot be null"
|
||||
: row.PropType.MandatoryMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues))
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
|
||||
? $"'{row.PropType.Name}' cannot be empty"
|
||||
: row.PropType.MandatoryMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
}
|
||||
|
||||
// Check regex
|
||||
if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace()
|
||||
&& row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace())
|
||||
{
|
||||
var regex = new Regex(row.PropType.ValidationRegExp);
|
||||
if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString()))
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage)
|
||||
? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern"
|
||||
: row.PropType.ValidationRegExpMessage;
|
||||
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
|
||||
}
|
||||
}
|
||||
yield return elementValidation;
|
||||
}
|
||||
|
||||
return validationResults;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to deserialize the nested content serialized value
|
||||
/// </summary>
|
||||
internal class NestedContentValues
|
||||
{
|
||||
private readonly Lazy<Dictionary<string, IContentType>> _contentTypes;
|
||||
@@ -353,84 +333,111 @@ namespace Umbraco.Web.PropertyEditors
|
||||
_contentTypes = new Lazy<Dictionary<string, IContentType>>(() => contentTypeService.GetAll().ToDictionary(c => c.Alias));
|
||||
}
|
||||
|
||||
private IContentType GetElementType(JObject item)
|
||||
private IContentType GetElementType(NestedContentRowValue item)
|
||||
{
|
||||
var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject<string>() ?? string.Empty;
|
||||
_contentTypes.Value.TryGetValue(contentTypeAlias, out var contentType);
|
||||
_contentTypes.Value.TryGetValue(item.ContentTypeAlias, out var contentType);
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public IEnumerable<RowValue> GetPropertyValues(object propertyValue, out List<JObject> deserialized)
|
||||
/// <summary>
|
||||
/// Deserialize the raw json property value
|
||||
/// </summary>
|
||||
/// <param name="propertyValue"></param>
|
||||
/// <returns></returns>
|
||||
public IReadOnlyList<NestedContentRowValue> GetPropertyValues(object propertyValue)
|
||||
{
|
||||
var rowValues = new List<RowValue>();
|
||||
|
||||
deserialized = null;
|
||||
|
||||
if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString()))
|
||||
return Enumerable.Empty<RowValue>();
|
||||
return new List<NestedContentRowValue>();
|
||||
|
||||
deserialized = JsonConvert.DeserializeObject<List<JObject>>(propertyValue.ToString());
|
||||
var rowValues = JsonConvert.DeserializeObject<List<NestedContentRowValue>>(propertyValue.ToString());
|
||||
|
||||
// There was a note here about checking if the result had zero items and if so it would return null, so we'll continue to do that
|
||||
// The original note was: "Issue #38 - Keep recursive property lookups working"
|
||||
// Which is from the original NC tracker: https://github.com/umco/umbraco-nested-content/issues/38
|
||||
// This check should be used everywhere when iterating NC prop values, instead of just the one previous place so that
|
||||
// empty values don't get persisted when there is nothing, it should actually be null.
|
||||
if (deserialized == null || deserialized.Count == 0)
|
||||
return Enumerable.Empty<RowValue>();
|
||||
if (rowValues == null || rowValues.Count == 0)
|
||||
return new List<NestedContentRowValue>();
|
||||
|
||||
var index = 0;
|
||||
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
|
||||
|
||||
foreach (var o in deserialized)
|
||||
foreach (var row in rowValues)
|
||||
{
|
||||
var propValues = o;
|
||||
|
||||
var contentType = GetElementType(propValues);
|
||||
var contentType = GetElementType(row);
|
||||
if (contentType == null)
|
||||
continue;
|
||||
|
||||
var propertyTypes = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
|
||||
var propAliases = propValues.Properties().Select(x => x.Name);
|
||||
foreach (var propAlias in propAliases)
|
||||
// get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating
|
||||
// objects on each iteration.
|
||||
if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes))
|
||||
propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
// find any keys that are not real property types and remove them
|
||||
foreach(var prop in row.RawPropertyValues.ToList())
|
||||
{
|
||||
propertyTypes.TryGetValue(propAlias, out var propType);
|
||||
rowValues.Add(new RowValue(propAlias, propType, propValues, index));
|
||||
if (IsSystemPropertyKey(prop.Key)) continue;
|
||||
|
||||
// doesn't exist so remove it
|
||||
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
|
||||
{
|
||||
row.RawPropertyValues.Remove(prop.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set the value to include the resolved property type
|
||||
row.PropertyValues[prop.Key] = new NestedContentPropertyValue
|
||||
{
|
||||
PropertyType = propType,
|
||||
Value = prop.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
return rowValues;
|
||||
}
|
||||
|
||||
internal class RowValue
|
||||
/// <summary>
|
||||
/// Used during deserialization to populate the property value/property type of a nested content row property
|
||||
/// </summary>
|
||||
internal class NestedContentPropertyValue
|
||||
{
|
||||
public RowValue(string propKey, IPropertyType propType, JObject propValues, int index)
|
||||
{
|
||||
PropKey = propKey ?? throw new ArgumentNullException(nameof(propKey));
|
||||
PropType = propType;
|
||||
JsonRowValue = propValues ?? throw new ArgumentNullException(nameof(propValues));
|
||||
RowIndex = index;
|
||||
}
|
||||
public object Value { get; set; }
|
||||
public IPropertyType PropertyType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current property key being iterated for the row value
|
||||
/// </summary>
|
||||
public string PropKey { get; }
|
||||
/// <summary>
|
||||
/// Used to deserialize a nested content row
|
||||
/// </summary>
|
||||
internal class NestedContentRowValue
|
||||
{
|
||||
[JsonProperty("key")]
|
||||
public Guid Id{ get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="PropertyType"/> of the value (if any), this may be null
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("ncContentTypeAlias")]
|
||||
public string ContentTypeAlias { get; set; }
|
||||
public IPropertyType PropType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The json values for the current row
|
||||
/// The remaining properties will be serialized to a dictionary
|
||||
/// </summary>
|
||||
public JObject JsonRowValue { get; }
|
||||
/// <remarks>
|
||||
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
|
||||
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
|
||||
/// NestedContent serializes to string, int, whatever eg
|
||||
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> RawPropertyValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Nested Content row index
|
||||
/// Used during deserialization to convert the raw property data into data with a property type context
|
||||
/// </summary>
|
||||
public int RowIndex { get; }
|
||||
[JsonIgnore]
|
||||
public IDictionary<string, NestedContentPropertyValue> PropertyValues { get; set; } = new Dictionary<string, NestedContentPropertyValue>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of <see cref="ComplexEditorPropertyTypeValidationResult"/> for an element type within complex editor represented by an Element Type
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
|
||||
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
|
||||
/// </remarks>
|
||||
public class ComplexEditorElementTypeValidationResult : ValidationResult
|
||||
{
|
||||
public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId)
|
||||
: base(string.Empty)
|
||||
{
|
||||
ElementTypeAlias = elementTypeAlias;
|
||||
BlockId = blockId;
|
||||
}
|
||||
|
||||
public IList<ComplexEditorPropertyTypeValidationResult> ValidationResults { get; } = new List<ComplexEditorPropertyTypeValidationResult>();
|
||||
|
||||
/// <summary>
|
||||
/// The element type alias of the validation result
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is useful for debugging purposes but it's not actively used in the angular app
|
||||
/// </remarks>
|
||||
public string ElementTypeAlias { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Block ID of the validation result
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the GUID id of the content item based on the element type
|
||||
/// </remarks>
|
||||
public Guid BlockId { get; }
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of <see cref="ValidationResult"/> for a property type within a complex editor represented by an Element Type
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
|
||||
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
|
||||
/// </remarks>
|
||||
public class ComplexEditorPropertyTypeValidationResult : ValidationResult
|
||||
{
|
||||
public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias)
|
||||
: base(string.Empty)
|
||||
{
|
||||
PropertyTypeAlias = propertyTypeAlias;
|
||||
}
|
||||
|
||||
private readonly List<ValidationResult> _validationResults = new List<ValidationResult>();
|
||||
|
||||
public void AddValidationResult(ValidationResult validationResult)
|
||||
{
|
||||
if (validationResult is ComplexEditorValidationResult && _validationResults.Any(x => x is ComplexEditorValidationResult))
|
||||
throw new InvalidOperationException($"Cannot add more than one {typeof(ComplexEditorValidationResult)}");
|
||||
|
||||
_validationResults.Add(validationResult);
|
||||
}
|
||||
|
||||
public IReadOnlyList<ValidationResult> ValidationResults => _validationResults;
|
||||
public string PropertyTypeAlias { get; }
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.Validation
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A collection of <see cref="ComplexEditorElementTypeValidationResult"/> for a complex editor represented by an Element Type
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For example, each <see cref="ComplexEditorValidationResult"/> represents validation results for a row in Nested Content.
|
||||
///
|
||||
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
|
||||
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
|
||||
/// </remarks>
|
||||
public class ComplexEditorValidationResult : ValidationResult
|
||||
{
|
||||
public ComplexEditorValidationResult()
|
||||
: base(string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public IList<ComplexEditorElementTypeValidationResult> ValidationResults { get; } = new List<ComplexEditorElementTypeValidationResult>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models.Blocks;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts json block objects into <see cref="IPublishedElement"/>
|
||||
/// </summary>
|
||||
public sealed class BlockEditorConverter
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
|
||||
public BlockEditorConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
}
|
||||
|
||||
public IPublishedElement ConvertToElement(
|
||||
BlockItemData data,
|
||||
PropertyCacheLevel referenceCacheLevel, bool preview)
|
||||
{
|
||||
// hack! we need to cast, we have no choice beacuse we cannot make breaking changes.
|
||||
var publishedContentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content;
|
||||
|
||||
// only convert element types - content types will cause an exception when PublishedModelFactory creates the model
|
||||
var publishedContentType = publishedContentCache.GetContentType(data.ContentTypeKey);
|
||||
if (publishedContentType == null || publishedContentType.IsElement == false)
|
||||
return null;
|
||||
|
||||
var propertyValues = data.RawPropertyValues;
|
||||
|
||||
// Get the udi from the deserialized object. If this is empty we can fallback to checking the 'key' if there is one
|
||||
var key = (data.Udi is GuidUdi gudi) ? gudi.Guid : Guid.Empty;
|
||||
if (propertyValues.TryGetValue("key", out var keyo))
|
||||
Guid.TryParse(keyo.ToString(), out key);
|
||||
|
||||
IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor);
|
||||
element = _publishedModelFactory.CreateModel(element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.Blocks;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.PropertyEditors.ValueConverters;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
|
||||
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
|
||||
public class BlockListPropertyValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
private readonly IProfilingLogger _proflog;
|
||||
private readonly BlockEditorConverter _blockConverter;
|
||||
private readonly BlockListEditorDataConverter _blockListEditorDataConverter;
|
||||
|
||||
public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
_blockListEditorDataConverter = new BlockListEditorDataConverter();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(BlockListModel);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
return source?.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
// NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string
|
||||
|
||||
using (_proflog.DebugDuration<BlockListPropertyValueConverter>($"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
|
||||
{
|
||||
var configuration = propertyType.DataType.ConfigurationAs<BlockListConfiguration>();
|
||||
var blockConfigMap = configuration.Blocks.ToDictionary(x => x.ContentElementTypeKey);
|
||||
var validSettingElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey).Where(x => x.HasValue).Distinct().ToList();
|
||||
|
||||
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
var settingsPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
|
||||
var layout = new List<BlockListLayoutReference>();
|
||||
|
||||
var value = (string)inter;
|
||||
if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty;
|
||||
|
||||
var converted = _blockListEditorDataConverter.Deserialize(value);
|
||||
if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty;
|
||||
|
||||
var blockListLayout = converted.Layout.ToObject<IEnumerable<BlockListLayoutItem>>();
|
||||
|
||||
// convert the content data
|
||||
foreach (var data in converted.BlockValue.ContentData)
|
||||
{
|
||||
if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) continue;
|
||||
|
||||
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
|
||||
if (element == null) continue;
|
||||
contentPublishedElements[element.Key] = element;
|
||||
}
|
||||
// convert the settings data
|
||||
foreach (var data in converted.BlockValue.SettingsData)
|
||||
{
|
||||
if (!validSettingElementTypes.Contains(data.ContentTypeKey)) continue;
|
||||
|
||||
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
|
||||
if (element == null) continue;
|
||||
settingsPublishedElements[element.Key] = element;
|
||||
}
|
||||
|
||||
// if there's no elements just return since if there's no data it doesn't matter what is stored in layout
|
||||
if (contentPublishedElements.Count == 0) return BlockListModel.Empty;
|
||||
|
||||
foreach (var layoutItem in blockListLayout)
|
||||
{
|
||||
// get the content reference
|
||||
var contentGuidUdi = (GuidUdi)layoutItem.ContentUdi;
|
||||
if (!contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData))
|
||||
continue;
|
||||
|
||||
// get the setting reference
|
||||
IPublishedElement settingsData = null;
|
||||
var settingGuidUdi = layoutItem.SettingsUdi != null ? (GuidUdi)layoutItem.SettingsUdi : null;
|
||||
if (settingGuidUdi != null)
|
||||
settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData);
|
||||
|
||||
if (!contentData.ContentType.TryGetKey(out var contentTypeKey))
|
||||
throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2));
|
||||
|
||||
if (!blockConfigMap.TryGetValue(contentTypeKey, out var blockConfig))
|
||||
continue;
|
||||
|
||||
// this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again
|
||||
// we also ensure that the content type's match since maybe the settings type has been changed after this has been persisted.
|
||||
if (settingsData != null)
|
||||
{
|
||||
if (!settingsData.ContentType.TryGetKey(out var settingsElementTypeKey))
|
||||
throw new InvalidOperationException("The settings element type was not of type " + typeof(IPublishedContentType2));
|
||||
|
||||
if (!blockConfig.SettingsElementTypeKey.HasValue || settingsElementTypeKey != blockConfig.SettingsElementTypeKey)
|
||||
settingsData = null;
|
||||
}
|
||||
|
||||
var layoutRef = new BlockListLayoutReference(contentGuidUdi, contentData, settingGuidUdi, settingsData);
|
||||
layout.Add(layoutRef);
|
||||
}
|
||||
|
||||
var model = new BlockListModel(contentPublishedElements.Values, settingsPublishedElements.Values, layout);
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -143,12 +143,11 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
// merge the crop values - the alias + width + height comes from
|
||||
// configuration, but each crop can store its own coordinates
|
||||
|
||||
if (Crops == null) return;
|
||||
|
||||
var configuredCrops = configuration?.Crops;
|
||||
if (configuredCrops == null) return;
|
||||
|
||||
var crops = Crops.ToList();
|
||||
//Use Crops if it's not null, otherwise create a new list
|
||||
var crops = Crops?.ToList() ?? new List<ImageCropperCrop>();
|
||||
|
||||
foreach (var configuredCrop in configuredCrops)
|
||||
{
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
var contentTypes = propertyType.DataType.ConfigurationAs<NestedContentConfiguration>().ContentTypes;
|
||||
return contentTypes.Length == 1
|
||||
? typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
|
||||
: typeof (IEnumerable<IPublishedElement>);
|
||||
? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
|
||||
: typeof(IEnumerable<IPublishedElement>);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
using (_proflog.DebugDuration<NestedContentSingleValueConverter>($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})"))
|
||||
{
|
||||
var value = (string) inter;
|
||||
var value = (string)inter;
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
|
||||
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
|
||||
|
||||
@@ -15,8 +15,10 @@ namespace Umbraco.Web.PublishedCache
|
||||
/// <remarks>This cache is not snapshotted, so it refreshes any time things change.</remarks>
|
||||
public class PublishedContentTypeCache
|
||||
{
|
||||
// NOTE: These are not concurrent dictionaries because all access is done within a lock
|
||||
private readonly Dictionary<string, IPublishedContentType> _typesByAlias = new Dictionary<string, IPublishedContentType>();
|
||||
private readonly Dictionary<int, IPublishedContentType> _typesById = new Dictionary<int, IPublishedContentType>();
|
||||
private readonly Dictionary<Guid, int> _keyToIdMap = new Dictionary<Guid, int>();
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
@@ -130,6 +132,42 @@ namespace Umbraco.Web.PublishedCache
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a published content type.
|
||||
/// </summary>
|
||||
/// <param name="itemType">An item type.</param>
|
||||
/// <param name="key">An key.</param>
|
||||
/// <returns>The published content type corresponding to the item key.</returns>
|
||||
public IPublishedContentType Get(PublishedItemType itemType, Guid key)
|
||||
{
|
||||
try
|
||||
{
|
||||
_lock.EnterUpgradeableReadLock();
|
||||
|
||||
if (_keyToIdMap.TryGetValue(key, out var id))
|
||||
return Get(itemType, id);
|
||||
|
||||
var type = CreatePublishedContentType(itemType, key);
|
||||
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
_keyToIdMap[key] = type.Id;
|
||||
return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsWriteLockHeld)
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_lock.IsUpgradeableReadLockHeld)
|
||||
_lock.ExitUpgradeableReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a published content type.
|
||||
/// </summary>
|
||||
@@ -152,7 +190,8 @@ namespace Umbraco.Web.PublishedCache
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
|
||||
if (type.TryGetKey(out var key))
|
||||
_keyToIdMap[key] = type.Id;
|
||||
return _typesByAlias[aliasKey] = _typesById[type.Id] = type;
|
||||
}
|
||||
finally
|
||||
@@ -188,7 +227,8 @@ namespace Umbraco.Web.PublishedCache
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
|
||||
if (type.TryGetKey(out var key))
|
||||
_keyToIdMap[key] = type.Id;
|
||||
return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type;
|
||||
}
|
||||
finally
|
||||
@@ -204,27 +244,32 @@ namespace Umbraco.Web.PublishedCache
|
||||
}
|
||||
}
|
||||
|
||||
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, Guid key)
|
||||
{
|
||||
IContentTypeComposition contentType = itemType switch
|
||||
{
|
||||
PublishedItemType.Content => _contentTypeService.Get(key),
|
||||
PublishedItemType.Media => _mediaTypeService.Get(key),
|
||||
PublishedItemType.Member => _memberTypeService.Get(key),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
|
||||
};
|
||||
if (contentType == null)
|
||||
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with key \"{key}\".");
|
||||
|
||||
return _publishedContentTypeFactory.CreateContentType(contentType);
|
||||
}
|
||||
|
||||
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias)
|
||||
{
|
||||
if (GetPublishedContentTypeByAlias != null)
|
||||
return GetPublishedContentTypeByAlias(alias);
|
||||
|
||||
IContentTypeComposition contentType;
|
||||
switch (itemType)
|
||||
IContentTypeComposition contentType = itemType switch
|
||||
{
|
||||
case PublishedItemType.Content:
|
||||
contentType = _contentTypeService.Get(alias);
|
||||
break;
|
||||
case PublishedItemType.Media:
|
||||
contentType = _mediaTypeService.Get(alias);
|
||||
break;
|
||||
case PublishedItemType.Member:
|
||||
contentType = _memberTypeService.Get(alias);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(itemType));
|
||||
}
|
||||
|
||||
PublishedItemType.Content => _contentTypeService.Get(alias),
|
||||
PublishedItemType.Media => _mediaTypeService.Get(alias),
|
||||
PublishedItemType.Member => _memberTypeService.Get(alias),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
|
||||
};
|
||||
if (contentType == null)
|
||||
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with alias \"{alias}\".");
|
||||
|
||||
@@ -235,23 +280,13 @@ namespace Umbraco.Web.PublishedCache
|
||||
{
|
||||
if (GetPublishedContentTypeById != null)
|
||||
return GetPublishedContentTypeById(id);
|
||||
|
||||
IContentTypeComposition contentType;
|
||||
switch (itemType)
|
||||
IContentTypeComposition contentType = itemType switch
|
||||
{
|
||||
case PublishedItemType.Content:
|
||||
contentType = _contentTypeService.Get(id);
|
||||
break;
|
||||
case PublishedItemType.Media:
|
||||
contentType = _mediaTypeService.Get(id);
|
||||
break;
|
||||
case PublishedItemType.Member:
|
||||
contentType = _memberTypeService.Get(id);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(itemType));
|
||||
}
|
||||
|
||||
PublishedItemType.Content => _contentTypeService.Get(id),
|
||||
PublishedItemType.Media => _mediaTypeService.Get(id),
|
||||
PublishedItemType.Member => _memberTypeService.Get(id),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
|
||||
};
|
||||
if (contentType == null)
|
||||
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with id {id}.");
|
||||
|
||||
@@ -259,6 +294,7 @@ namespace Umbraco.Web.PublishedCache
|
||||
}
|
||||
|
||||
// for unit tests - changing the callback must reset the cache obviously
|
||||
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
|
||||
private Func<string, IPublishedContentType> _getPublishedContentTypeByAlias;
|
||||
internal Func<string, IPublishedContentType> GetPublishedContentTypeByAlias
|
||||
{
|
||||
@@ -282,6 +318,7 @@ namespace Umbraco.Web.PublishedCache
|
||||
}
|
||||
|
||||
// for unit tests - changing the callback must reset the cache obviously
|
||||
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
|
||||
private Func<int, IPublishedContentType> _getPublishedContentTypeById;
|
||||
internal Func<int, IPublishedContentType> GetPublishedContentTypeById
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@ using Umbraco.Web.Media.EmbedProviders;
|
||||
using Umbraco.Web.Migrations.PostMigrations;
|
||||
using Umbraco.Web.Models.PublishedContent;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Web.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
using Umbraco.Web.Routing;
|
||||
using Umbraco.Web.Search;
|
||||
@@ -52,6 +53,7 @@ using Umbraco.Web.Services;
|
||||
using Umbraco.Web.Templates;
|
||||
using Umbraco.Web.Trees;
|
||||
using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator;
|
||||
using TextStringValueConverter = Umbraco.Core.PropertyEditors.ValueConverters.TextStringValueConverter;
|
||||
|
||||
namespace Umbraco.Core.Runtime
|
||||
{
|
||||
@@ -213,6 +215,7 @@ namespace Umbraco.Core.Runtime
|
||||
composition.RegisterUnique<HtmlImageSourceParser>();
|
||||
composition.RegisterUnique<HtmlUrlParser>();
|
||||
composition.RegisterUnique<RichTextEditorPastedImages>();
|
||||
composition.RegisterUnique<BlockEditorConverter>();
|
||||
|
||||
// both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be
|
||||
// discovered when CoreBootManager configures the converters. We HAVE to remove one of them
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Umbraco.Core.Runtime
|
||||
private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider();
|
||||
private bool _mainDomChanging = false;
|
||||
private readonly UmbracoDatabaseFactory _dbFactory;
|
||||
private bool _hasError;
|
||||
private bool _errorDuringAcquiring;
|
||||
private object _locker = new object();
|
||||
|
||||
public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment)
|
||||
@@ -63,25 +63,24 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Acquiring lock...");
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
var tempId = Guid.NewGuid().ToString();
|
||||
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
// wait to get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch(SqlException ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not acquire MainDom.");
|
||||
_hasError = true;
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -89,15 +88,12 @@ namespace Umbraco.Core.Runtime
|
||||
throw;
|
||||
}
|
||||
|
||||
var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown
|
||||
var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown
|
||||
if (result == RecordPersistenceType.Insert)
|
||||
{
|
||||
// if we've inserted, then there was no MainDom so we can instantly acquire
|
||||
|
||||
// TODO: see the other TODO, could we just delete the row and that would indicate that we
|
||||
// are MainDom? then we don't leave any orphan rows behind.
|
||||
|
||||
InsertLockRecord(_lockId); // so update with our appdomain id
|
||||
InsertLockRecord(_lockId, db); // so update with our appdomain id
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
@@ -107,23 +103,23 @@ namespace Umbraco.Core.Runtime
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, cannot acquire MainDom");
|
||||
_hasError = true;
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
transaction.Complete();
|
||||
}
|
||||
|
||||
|
||||
return await WaitForExistingAsync(tempId, millisecondsTimeout);
|
||||
}
|
||||
|
||||
public Task ListenAsync()
|
||||
{
|
||||
if (_hasError)
|
||||
if (_errorDuringAcquiring)
|
||||
{
|
||||
_logger.Warn<SqlMainDomLock>("Could not acquire MainDom, listening is canceled.");
|
||||
return Task.CompletedTask;
|
||||
@@ -149,8 +145,15 @@ namespace Umbraco.Core.Runtime
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// poll every 1 second
|
||||
Thread.Sleep(1000);
|
||||
// poll every couple of seconds
|
||||
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
|
||||
Thread.Sleep(2000);
|
||||
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
// if we aren't configured, we just keep looping since we can't query the db
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
@@ -167,20 +170,14 @@ namespace Umbraco.Core.Runtime
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that
|
||||
// we are still the maindom. An empty value might be better because then we won't have any orphan rows
|
||||
// if the app is terminated. Could that work?
|
||||
|
||||
if (!IsMainDomValue(_lockId))
|
||||
if (!IsMainDomValue(_lockId, db))
|
||||
{
|
||||
// we are no longer main dom, another one has come online, exit
|
||||
_mainDomChanging = true;
|
||||
@@ -190,38 +187,23 @@ namespace Umbraco.Core.Runtime
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, listening is canceled.");
|
||||
_hasError = true;
|
||||
return;
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during listening.");
|
||||
|
||||
// We need to keep on listening unless we've been notified by our own AppDomain to shutdown since
|
||||
// we don't want to shutdown resources controlled by MainDom inadvertently. We'll just keep listening otherwise.
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetDatabase()
|
||||
{
|
||||
if (_db.InTransaction)
|
||||
_db.AbortTransaction();
|
||||
_db.Dispose();
|
||||
_db = null;
|
||||
}
|
||||
|
||||
private IUmbracoDatabase GetDatabase()
|
||||
{
|
||||
if (_db != null)
|
||||
return _db;
|
||||
|
||||
_db = _dbFactory.CreateDatabase();
|
||||
return _db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for any existing MainDom to release so we can continue booting
|
||||
/// </summary>
|
||||
@@ -234,35 +216,51 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var db = GetDatabase();
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
while(true)
|
||||
while (true)
|
||||
{
|
||||
// poll very often, we need to take over as fast as we can
|
||||
Thread.Sleep(100);
|
||||
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
|
||||
Thread.Sleep(1000);
|
||||
|
||||
try
|
||||
var acquired = TryAcquire(db, tempId, updatedTempId);
|
||||
if (acquired.HasValue)
|
||||
return acquired.Value;
|
||||
|
||||
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
return AcquireWhenMaxWaitTimeElapsed(db);
|
||||
}
|
||||
}
|
||||
}, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId)
|
||||
{
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// the row
|
||||
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
|
||||
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
|
||||
{
|
||||
// the other main dom has updated our record
|
||||
// Or the other maindom shutdown super fast and just deleted the record
|
||||
// which indicates that we
|
||||
// can acquire it and it has shutdown.
|
||||
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
|
||||
{
|
||||
// the other main dom has updated our record
|
||||
// Or the other maindom shutdown super fast and just deleted the record
|
||||
// which indicates that we
|
||||
// can acquire it and it has shutdown.
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId, db);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
@@ -272,83 +270,77 @@ namespace Umbraco.Core.Runtime
|
||||
// another new AppDomain has come online and is wanting to take over. In that case, we will not
|
||||
// acquire.
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
|
||||
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
|
||||
{
|
||||
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
|
||||
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
|
||||
// and it's just been left as an orphan row.
|
||||
// There's really know way of knowing unless we are constantly updating the row for the current maindom
|
||||
// which isn't ideal.
|
||||
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
// something is wrong, we cannot acquire, not much we can do
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
}
|
||||
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
|
||||
return false;
|
||||
}
|
||||
}, _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex as SqlException))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
transaction.Complete();
|
||||
}
|
||||
|
||||
return null; // continue
|
||||
}
|
||||
|
||||
private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db)
|
||||
{
|
||||
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
|
||||
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
|
||||
// and it's just been left as an orphan row.
|
||||
// There's really know way of knowing unless we are constantly updating the row for the current maindom
|
||||
// which isn't ideal.
|
||||
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
|
||||
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId, db);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex as SqlException))
|
||||
{
|
||||
// something is wrong, we cannot acquire, not much we can do
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts or updates the key/value row
|
||||
/// </summary>
|
||||
private RecordPersistenceType InsertLockRecord(string id)
|
||||
private RecordPersistenceType InsertLockRecord(string id, IUmbracoDatabase db)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.InsertOrUpdate(new KeyValueDto
|
||||
{
|
||||
Key = MainDomKey,
|
||||
@@ -361,9 +353,8 @@ namespace Umbraco.Core.Runtime
|
||||
/// Checks if the DB row value is equals the value
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool IsMainDomValue(string val)
|
||||
private bool IsMainDomValue(string val, IUmbracoDatabase db)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val",
|
||||
new { key = MainDomKey, val = val }) == 1;
|
||||
}
|
||||
@@ -373,7 +364,7 @@ namespace Umbraco.Core.Runtime
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222;
|
||||
private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222;
|
||||
|
||||
#region IDisposable Support
|
||||
private bool _disposedValue = false; // To detect redundant calls
|
||||
@@ -392,11 +383,11 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
if (_dbFactory.Configured)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
@@ -409,24 +400,21 @@ namespace Umbraco.Core.Runtime
|
||||
if (_mainDomChanging)
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, updating row, new application is booting.");
|
||||
db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
var count = db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, deleting row, application is shutting down.");
|
||||
db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
var count = db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
|
||||
_hasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
ResetDatabase();
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration.HealthChecks;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Sync;
|
||||
using Umbraco.Web.HealthCheck;
|
||||
|
||||
@@ -14,20 +15,31 @@ namespace Umbraco.Web.Scheduling
|
||||
private readonly IMainDom _mainDom;
|
||||
private readonly HealthCheckCollection _healthChecks;
|
||||
private readonly HealthCheckNotificationMethodCollection _notifications;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IProfilingLogger _logger;
|
||||
private readonly IHealthChecksSettings _healthChecksSettingsConfig;
|
||||
private readonly IServerRegistrar _serverRegistrar;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
public HealthCheckNotifier(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
|
||||
HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications,
|
||||
IMainDom mainDom, IProfilingLogger logger, IHealthChecksSettings healthChecksSettingsConfig, IServerRegistrar serverRegistrar,
|
||||
IRuntimeState runtimeState)
|
||||
public HealthCheckNotifier(
|
||||
IBackgroundTaskRunner<RecurringTaskBase> runner,
|
||||
int delayMilliseconds,
|
||||
int periodMilliseconds,
|
||||
HealthCheckCollection healthChecks,
|
||||
HealthCheckNotificationMethodCollection notifications,
|
||||
IMainDom mainDom,
|
||||
IProfilingLogger logger,
|
||||
IHealthChecksSettings healthChecksSettingsConfig,
|
||||
IServerRegistrar serverRegistrar,
|
||||
IRuntimeState runtimeState,
|
||||
IScopeProvider scopeProvider)
|
||||
: base(runner, delayMilliseconds, periodMilliseconds)
|
||||
{
|
||||
_healthChecks = healthChecks;
|
||||
_notifications = notifications;
|
||||
_mainDom = mainDom;
|
||||
_scopeProvider = scopeProvider;
|
||||
_runtimeState = runtimeState;
|
||||
_logger = logger;
|
||||
_healthChecksSettingsConfig = healthChecksSettingsConfig;
|
||||
_serverRegistrar = serverRegistrar;
|
||||
@@ -56,6 +68,10 @@ namespace Umbraco.Web.Scheduling
|
||||
return false; // do NOT repeat, going down
|
||||
}
|
||||
|
||||
// Ensure we use an explicit scope since we are running on a background thread and plugin health
|
||||
// checks can be making service/database calls so we want to ensure the CallContext/Ambient scope
|
||||
// isn't used since that can be problematic.
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
using (_logger.DebugDuration<HealthCheckNotifier>("Health checks executing", "Health checks complete"))
|
||||
{
|
||||
var healthCheckConfig = _healthChecksSettingsConfig;
|
||||
|
||||
@@ -72,8 +72,7 @@ namespace Umbraco.Web.Scheduling
|
||||
return false; // do NOT repeat, going down
|
||||
}
|
||||
|
||||
// running on a background task, and Log.CleanLogs uses the old SqlHelper,
|
||||
// better wrap in a scope and ensure it's all cleaned up and nothing leaks
|
||||
// Ensure we use an explicit scope since we are running on a background thread.
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
using (_logger.DebugDuration<LogScrubber>("Log scrubbing executing", "Log scrubbing complete"))
|
||||
{
|
||||
|
||||
@@ -9,16 +9,18 @@ namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
public class ScheduledPublishing : RecurringTaskBase
|
||||
{
|
||||
private readonly IRuntimeState _runtime;
|
||||
private readonly IMainDom _mainDom;
|
||||
private readonly IServerRegistrar _serverRegistrar;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMainDom _mainDom;
|
||||
private readonly IRuntimeState _runtime;
|
||||
private readonly IServerMessenger _serverMessenger;
|
||||
private readonly IServerRegistrar _serverRegistrar;
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
|
||||
public ScheduledPublishing(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
|
||||
IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService, IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger)
|
||||
public ScheduledPublishing(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds,
|
||||
int periodMilliseconds,
|
||||
IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService,
|
||||
IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger)
|
||||
: base(runner, delayMilliseconds, periodMilliseconds)
|
||||
{
|
||||
_runtime = runtime;
|
||||
@@ -30,6 +32,8 @@ namespace Umbraco.Web.Scheduling
|
||||
_serverMessenger = serverMessenger;
|
||||
}
|
||||
|
||||
public override bool IsAsync => false;
|
||||
|
||||
public override bool PerformRun()
|
||||
{
|
||||
if (Suspendable.ScheduledPublishing.CanRun == false)
|
||||
@@ -61,24 +65,27 @@ namespace Umbraco.Web.Scheduling
|
||||
|
||||
try
|
||||
{
|
||||
// ensure we run with an UmbracoContext, because this may run in a background task,
|
||||
// yet developers may be using the 'current' UmbracoContext in the event handlers
|
||||
//
|
||||
// We don't need an explicit scope here because PerformScheduledPublish creates it's own scope
|
||||
// so it's safe as it will create it's own ambient scope.
|
||||
// Ensure we run with an UmbracoContext, because this will run in a background task,
|
||||
// and developers may be using the UmbracoContext in the event handlers.
|
||||
|
||||
// TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events
|
||||
// - UmbracoContext 'current' needs to be refactored and cleaned up
|
||||
// - batched messenger should not depend on a current HttpContext
|
||||
// but then what should be its "scope"? could we attach it to scopes?
|
||||
// - and we should definitively *not* have to flush it here (should be auto)
|
||||
//
|
||||
using (
|
||||
var contextReference = _umbracoContextFactory.EnsureUmbracoContext())
|
||||
using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext())
|
||||
{
|
||||
try
|
||||
{
|
||||
// run
|
||||
var result = _contentService.PerformScheduledPublish(DateTime.Now);
|
||||
foreach (var grouped in result.GroupBy(x => x.Result))
|
||||
_logger.Info<ScheduledPublishing>("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key);
|
||||
_logger.Info<ScheduledPublishing>(
|
||||
"Scheduled publishing result: '{StatusCount}' items with status {Status}",
|
||||
grouped.Count(), grouped.Key);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -96,7 +103,5 @@ namespace Umbraco.Web.Scheduling
|
||||
|
||||
return true; // repeat
|
||||
}
|
||||
|
||||
public override bool IsAsync => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ namespace Umbraco.Web.Scheduling
|
||||
}
|
||||
|
||||
var periodInMilliseconds = healthCheckSettingsConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000;
|
||||
var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _mainDom, logger, _healthChecksSettingsConfig, _serverRegistrar, _runtime);
|
||||
var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _mainDom, logger, _healthChecksSettingsConfig, _serverRegistrar, _runtime, _scopeProvider);
|
||||
_healthCheckRunner.TryAdd(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
@@ -881,7 +881,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types.");
|
||||
}
|
||||
|
||||
if(content.Name != null && content.Name.Length > 255)
|
||||
if (content.Name != null && content.Name.Length > 255)
|
||||
{
|
||||
throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
|
||||
}
|
||||
@@ -1258,7 +1258,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (publishResult == null)
|
||||
throw new PanicException("publishResult == null - should not happen");
|
||||
|
||||
switch(publishResult.Result)
|
||||
switch (publishResult.Result)
|
||||
{
|
||||
case PublishResultType.FailedPublishMandatoryCultureMissing:
|
||||
//occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
|
||||
@@ -1366,24 +1366,90 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PublishResult> PerformScheduledPublish(DateTime date)
|
||||
=> PerformScheduledPublishInternal(date).ToList();
|
||||
|
||||
// beware! this method yields results, so the returned IEnumerable *must* be
|
||||
// enumerated for anything to happen - dangerous, so private + exposed via
|
||||
// the public method above, which forces ToList().
|
||||
private IEnumerable<PublishResult> PerformScheduledPublishInternal(DateTime date)
|
||||
{
|
||||
var allLangs = new Lazy<List<ILanguage>>(() => _languageRepository.GetMany().ToList());
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
var results = new List<PublishResult>();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
|
||||
PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void PerformScheduledPublishingExpiration(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
|
||||
{
|
||||
using var scope = ScopeProvider.CreateScope();
|
||||
|
||||
// do a fast read without any locks since this executes often to see if we even need to proceed
|
||||
if (_documentRepository.HasContentForExpiration(date))
|
||||
{
|
||||
// now take a write lock since we'll be updating
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
var allLangs = _languageRepository.GetMany().ToList();
|
||||
foreach (var d in _documentRepository.GetContentForExpiration(date))
|
||||
{
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
|
||||
.Select(x => x.Culture)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
continue; //shouldn't happen but no point in processing this document if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
{
|
||||
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var c in pendingCultures)
|
||||
{
|
||||
//Clear this schedule for this culture
|
||||
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
|
||||
//set the culture to be published
|
||||
d.UnpublishCulture(c);
|
||||
}
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
results.Add(result);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
|
||||
var result = Unpublish(d, userId: d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
_documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private void PerformScheduledPublishingRelease(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
|
||||
{
|
||||
using var scope = ScopeProvider.CreateScope();
|
||||
|
||||
// do a fast read without any locks since this executes often to see if we even need to proceed
|
||||
if (_documentRepository.HasContentForRelease(date))
|
||||
{
|
||||
// now take a write lock since we'll be updating
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
foreach (var d in _documentRepository.GetContentForRelease(date))
|
||||
{
|
||||
PublishResult result;
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
@@ -1393,11 +1459,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
continue; //shouldn't happen but no point in processing this document if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
{
|
||||
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
|
||||
continue; // this document is canceled move next
|
||||
}
|
||||
|
||||
var publishing = true;
|
||||
foreach (var culture in pendingCultures)
|
||||
@@ -1409,94 +1478,51 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
//publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
|
||||
IProperty[] invalidProperties = null;
|
||||
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture));
|
||||
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture));
|
||||
var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
|
||||
if (invalidProperties != null && invalidProperties.Length > 0)
|
||||
Logger.Warn<ContentService>("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
|
||||
d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
|
||||
|
||||
publishing &= tryPublish; //set the culture to be published
|
||||
if (!publishing) break; // no point continuing
|
||||
if (!publishing) continue; // move to next document
|
||||
}
|
||||
|
||||
PublishResult result;
|
||||
|
||||
if (d.Trashed)
|
||||
result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
|
||||
else if (!publishing)
|
||||
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
|
||||
else
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
|
||||
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
|
||||
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
|
||||
yield return result;
|
||||
results.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Release, date);
|
||||
|
||||
result = d.Trashed
|
||||
var result = d.Trashed
|
||||
? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d)
|
||||
: SaveAndPublish(d, userId: d.WriterId);
|
||||
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
|
||||
yield return result;
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var d in _documentRepository.GetContentForExpiration(date))
|
||||
{
|
||||
PublishResult result;
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
|
||||
.Select(x => x.Culture)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
_documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
|
||||
foreach (var c in pendingCultures)
|
||||
{
|
||||
//Clear this schedule for this culture
|
||||
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
|
||||
//set the culture to be published
|
||||
d.UnpublishCulture(c);
|
||||
}
|
||||
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
|
||||
result = Unpublish(d, userId: d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
_documentRepository.ClearSchedule(date);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
// utility 'PublishCultures' func used by SaveAndPublishBranch
|
||||
|
||||
@@ -1,18 +1,77 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Core.Services
|
||||
{
|
||||
internal class PropertyValidationService : IPropertyValidationService
|
||||
public class PropertyValidationService : IPropertyValidationService
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizedTextService _textService;
|
||||
|
||||
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService)
|
||||
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_textService = textService;
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<ValidationResult> ValidatePropertyValue(
|
||||
IPropertyType propertyType,
|
||||
object postedValue)
|
||||
{
|
||||
if (propertyType is null) throw new ArgumentNullException(nameof(propertyType));
|
||||
var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId);
|
||||
if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId);
|
||||
|
||||
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
|
||||
if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias);
|
||||
|
||||
return ValidatePropertyValue(_textService, editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage);
|
||||
}
|
||||
|
||||
public static IEnumerable<ValidationResult> ValidatePropertyValue(
|
||||
ILocalizedTextService textService,
|
||||
IDataEditor editor,
|
||||
IDataType dataType,
|
||||
object postedValue,
|
||||
bool isRequired,
|
||||
string validationRegExp,
|
||||
string isRequiredMessage,
|
||||
string validationRegExpMessage)
|
||||
{
|
||||
// Retrieve default messages used for required and regex validatation. We'll replace these
|
||||
// if set with custom ones if they've been provided for a given property.
|
||||
var requiredDefaultMessages = new[]
|
||||
{
|
||||
textService.Localize("validation", "invalidNull"),
|
||||
textService.Localize("validation", "invalidEmpty")
|
||||
};
|
||||
var formatDefaultMessages = new[]
|
||||
{
|
||||
textService.Localize("validation", "invalidPattern"),
|
||||
};
|
||||
|
||||
var valueEditor = editor.GetValueEditor(dataType.Configuration);
|
||||
foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp))
|
||||
{
|
||||
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
|
||||
if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
validationResult.ErrorMessage = isRequiredMessage;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
validationResult.ErrorMessage = validationRegExpMessage;
|
||||
}
|
||||
yield return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -553,8 +553,10 @@ namespace Umbraco.ModelsBuilder.Embedded
|
||||
if (modelInfos.TryGetValue(typeName, out var modelInfo))
|
||||
throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\".");
|
||||
|
||||
// fixme use Core's ReflectionUtilities.EmitCtor !!
|
||||
// TODO: use Core's ReflectionUtilities.EmitCtor !!
|
||||
// Yes .. DynamicMethod is uber slow
|
||||
// TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still?
|
||||
// See CtorInvokeBenchmarks
|
||||
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
|
||||
var gen = meth.GetILGenerator();
|
||||
gen.Emit(OpCodes.Ldarg_0);
|
||||
|
||||
@@ -384,15 +384,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
#region Content types
|
||||
|
||||
public override IPublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _snapshot.GetContentType(id);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id);
|
||||
|
||||
public override IPublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _snapshot.GetContentType(alias);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias);
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -37,9 +37,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
private readonly ConcurrentDictionary<int, LinkedNode<ContentNode>> _contentNodes;
|
||||
private LinkedNode<ContentNode> _root;
|
||||
private readonly ConcurrentDictionary<int, LinkedNode<IPublishedContentType>> _contentTypesById;
|
||||
|
||||
// We must keep separate dictionaries for by id and by alias because we track these in snapshot/layers
|
||||
// and it is possible that the alias of a content type can be different for the same id in another layer
|
||||
// whereas the GUID -> INT cross reference can never be different
|
||||
private readonly ConcurrentDictionary<int, LinkedNode<IPublishedContentType>> _contentTypesById;
|
||||
private readonly ConcurrentDictionary<string, LinkedNode<IPublishedContentType>> _contentTypesByAlias;
|
||||
private readonly ConcurrentDictionary<Guid, int> _xmap;
|
||||
private readonly ConcurrentDictionary<Guid, int> _contentTypeKeyToIdMap;
|
||||
private readonly ConcurrentDictionary<Guid, int> _contentKeyToIdMap;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
@@ -76,7 +81,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
_root = new LinkedNode<ContentNode>(new ContentNode(), 0);
|
||||
_contentTypesById = new ConcurrentDictionary<int, LinkedNode<IPublishedContentType>>();
|
||||
_contentTypesByAlias = new ConcurrentDictionary<string, LinkedNode<IPublishedContentType>>(StringComparer.InvariantCultureIgnoreCase);
|
||||
_xmap = new ConcurrentDictionary<Guid, int>();
|
||||
_contentTypeKeyToIdMap = new ConcurrentDictionary<Guid, int>();
|
||||
_contentKeyToIdMap = new ConcurrentDictionary<Guid, int>();
|
||||
|
||||
_genObjs = new ConcurrentQueue<GenObj>();
|
||||
_genObj = null; // no initial gen exists
|
||||
@@ -139,7 +145,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
Monitor.Enter(_wlocko, ref lockInfo.Taken);
|
||||
|
||||
lock(_rlocko)
|
||||
lock (_rlocko)
|
||||
{
|
||||
// see SnapDictionary
|
||||
try { }
|
||||
@@ -294,8 +300,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
SetValueLocked(_contentTypesById, type.Id, type);
|
||||
SetValueLocked(_contentTypesByAlias, type.Alias, type);
|
||||
SetContentTypeLocked(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,8 +326,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
foreach (var type in index.Values)
|
||||
{
|
||||
SetValueLocked(_contentTypesById, type.Id, type);
|
||||
SetValueLocked(_contentTypesByAlias, type.Alias, type);
|
||||
SetContentTypeLocked(type);
|
||||
}
|
||||
|
||||
foreach (var link in _contentNodes.Values)
|
||||
@@ -357,8 +361,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// set all new content types
|
||||
foreach (var type in types)
|
||||
{
|
||||
SetValueLocked(_contentTypesById, type.Id, type);
|
||||
SetValueLocked(_contentTypesByAlias, type.Alias, type);
|
||||
SetContentTypeLocked(type);
|
||||
}
|
||||
|
||||
// beware! at that point the cache is inconsistent,
|
||||
@@ -422,8 +425,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// perform update of refreshed content types
|
||||
foreach (var type in refreshedTypesA)
|
||||
{
|
||||
SetValueLocked(_contentTypesById, type.Id, type);
|
||||
SetValueLocked(_contentTypesByAlias, type.Alias, type);
|
||||
SetContentTypeLocked(type);
|
||||
}
|
||||
|
||||
// perform update of content with refreshed content type - from the kits
|
||||
@@ -641,7 +643,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId;
|
||||
}
|
||||
|
||||
_xmap[kit.Node.Uid] = kit.Node.Id;
|
||||
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -737,7 +739,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// this node becomes the previous node
|
||||
previousNode = thisNode;
|
||||
|
||||
_xmap[kit.Node.Uid] = kit.Node.Id;
|
||||
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
|
||||
}
|
||||
|
||||
return ok;
|
||||
@@ -781,7 +783,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
|
||||
AddTreeNodeLocked(kit.Node, parent);
|
||||
|
||||
_xmap[kit.Node.Uid] = kit.Node.Id;
|
||||
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
|
||||
}
|
||||
|
||||
return ok;
|
||||
@@ -836,7 +838,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
|
||||
AddTreeNodeLocked(kit.Node, parent);
|
||||
|
||||
_xmap[kit.Node.Uid] = kit.Node.Id;
|
||||
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
|
||||
}
|
||||
|
||||
return ok;
|
||||
@@ -888,11 +890,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// This should never be null, all code that calls this method is null checking but we've seen
|
||||
// issues of null ref exceptions in issue reports so we'll double check here
|
||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||
|
||||
|
||||
SetValueLocked(_contentNodes, content.Id, null);
|
||||
if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null);
|
||||
|
||||
_xmap.TryRemove(content.Uid, out _);
|
||||
_contentKeyToIdMap.TryRemove(content.Uid, out _);
|
||||
|
||||
var id = content.FirstChildContentId;
|
||||
while (id > 0)
|
||||
@@ -1157,6 +1159,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
|
||||
private void SetContentTypeLocked(IPublishedContentType type)
|
||||
{
|
||||
SetValueLocked(_contentTypesById, type.Id, type);
|
||||
SetValueLocked(_contentTypesByAlias, type.Alias, type);
|
||||
// ensure the key/id map is accurate
|
||||
if (type.TryGetKey(out var key))
|
||||
_contentTypeKeyToIdMap[key] = type.Id;
|
||||
}
|
||||
|
||||
// set a node (just the node, not the tree)
|
||||
private void SetValueLocked<TKey, TValue>(ConcurrentDictionary<TKey, LinkedNode<TValue>> dict, TKey key, TValue value)
|
||||
where TValue : class
|
||||
@@ -1214,7 +1225,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
public ContentNode Get(Guid uid, long gen)
|
||||
{
|
||||
return _xmap.TryGetValue(uid, out var id)
|
||||
return _contentKeyToIdMap.TryGetValue(uid, out var id)
|
||||
? GetValue(_contentNodes, id, gen)
|
||||
: null;
|
||||
}
|
||||
@@ -1277,13 +1288,20 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return GetValue(_contentTypesByAlias, alias, gen);
|
||||
}
|
||||
|
||||
public IPublishedContentType GetContentType(Guid key, long gen)
|
||||
{
|
||||
if (!_contentTypeKeyToIdMap.TryGetValue(key, out var id))
|
||||
return null;
|
||||
return GetContentType(id, gen);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snapshots
|
||||
|
||||
public Snapshot CreateSnapshot()
|
||||
{
|
||||
lock(_rlocko)
|
||||
lock (_rlocko)
|
||||
{
|
||||
// if no next generation is required, and we already have one,
|
||||
// use it and create a new snapshot
|
||||
@@ -1609,6 +1627,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return _store.GetContentType(alias, _gen);
|
||||
}
|
||||
|
||||
public IPublishedContentType GetContentType(Guid key)
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
return _store.GetContentType(key, _gen);
|
||||
}
|
||||
|
||||
// this code is here just so you don't try to implement it
|
||||
// the only way we can iterate over "all" without locking the entire cache forever
|
||||
// is by shallow cloning the cache, which is quite expensive, so we should probably not do it,
|
||||
|
||||
@@ -214,7 +214,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
if (dto.EditData == null)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding.");
|
||||
throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding.");
|
||||
_logger.Warn<DatabaseDataSource>("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id);
|
||||
}
|
||||
else
|
||||
@@ -241,7 +241,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
if (dto.PubData == null)
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding.");
|
||||
throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding.");
|
||||
_logger.Warn<DatabaseDataSource>("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id);
|
||||
}
|
||||
else
|
||||
@@ -280,7 +280,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto)
|
||||
{
|
||||
if (dto.EditData == null)
|
||||
throw new Exception("No data for media " + dto.Id);
|
||||
throw new InvalidOperationException("No data for media " + dto.Id);
|
||||
|
||||
var nested = DeserializeNestedData(dto.EditData);
|
||||
|
||||
|
||||
@@ -155,15 +155,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
#region Content types
|
||||
|
||||
public override IPublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _snapshot.GetContentType(id);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id);
|
||||
|
||||
public override IPublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _snapshot.GetContentType(alias);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias);
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Models;
|
||||
using System.Web;
|
||||
using System.Web.Hosting;
|
||||
using System.Web.Routing;
|
||||
using System.Diagnostics;
|
||||
using Umbraco.Core.Composing;
|
||||
using System.Configuration;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting
|
||||
|
||||
namespace Umbraco.TestData
|
||||
{
|
||||
public class LoadTestController : Controller
|
||||
{
|
||||
public LoadTestController(ServiceContext serviceContext, IShortStringHelper shortStringHelper)
|
||||
{
|
||||
_serviceContext = serviceContext;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
}
|
||||
|
||||
private static readonly Random _random = new Random();
|
||||
private static readonly object _locko = new object();
|
||||
|
||||
private static volatile int _containerId = -1;
|
||||
|
||||
private const string _containerAlias = "LoadTestContainer";
|
||||
private const string _contentAlias = "LoadTestContent";
|
||||
private const int _textboxDefinitionId = -88;
|
||||
private const int _maxCreate = 1000;
|
||||
|
||||
private static readonly string HeadHtml = @"<html>
|
||||
<head>
|
||||
<title>LoadTest</title>
|
||||
<style>
|
||||
body { font-family: arial; }
|
||||
a,a:visited { color: blue; }
|
||||
h1 { margin: 0; padding: 0; font-size: 120%; font-weight: bold; }
|
||||
h1 a { text-decoration: none; }
|
||||
div.block { margin: 20px 0; }
|
||||
ul { margin:0; }
|
||||
div.ver { font-size: 80%; }
|
||||
div.head { padding:0 0 10px 0; margin: 0 0 20px 0; border-bottom: 1px solid #cccccc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""head"">
|
||||
<h1><a href=""/LoadTest"">LoadTest</a></h1>
|
||||
<div class=""ver"">" + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"</div>
|
||||
</div>
|
||||
";
|
||||
|
||||
private const string FootHtml = @"</body>
|
||||
</html>";
|
||||
|
||||
private static readonly string _containerTemplateText = @"
|
||||
@inherits Umbraco.Web.Mvc.UmbracoViewPage
|
||||
@{
|
||||
Layout = null;
|
||||
var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault();
|
||||
var contents = container.Children().ToArray();
|
||||
var groups = contents.GroupBy(x => x.Value<string>(""origin""));
|
||||
var id = contents.Length > 0 ? contents[0].Id : -1;
|
||||
var wurl = Request.QueryString[""u""] == ""1"";
|
||||
var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length;
|
||||
}
|
||||
" + HeadHtml + @"
|
||||
<div class=""block"">
|
||||
<span @Html.Raw(missing ? ""style=\""color:red;\"""" : """")>@contents.Length items</span>
|
||||
<ul>
|
||||
@foreach (var group in groups)
|
||||
{
|
||||
<li>@group.Key: @group.Count()</li>
|
||||
}
|
||||
</ul></div>
|
||||
<div class=""block"">
|
||||
@foreach (var content in contents)
|
||||
{
|
||||
while (content.Id > id)
|
||||
{
|
||||
<div style=""color:red;"">@id :: MISSING</div>
|
||||
id++;
|
||||
}
|
||||
if (wurl)
|
||||
{
|
||||
<div>@content.Id :: @content.Name :: @content.Url</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>@content.Id :: @content.Name</div>
|
||||
} id++;
|
||||
}
|
||||
</div>
|
||||
" + FootHtml;
|
||||
private readonly ServiceContext _serviceContext;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
|
||||
private ActionResult ContentHtml(string s)
|
||||
{
|
||||
return Content(HeadHtml + s + FootHtml);
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
var html = @"Welcome. You can:
|
||||
<ul>
|
||||
<li><a href=""/LoadTestContainer"">List existing contents</a> (u:url)</li>
|
||||
<li><a href=""/LoadTest/Create?o=browser"">Create a content</a> (o:origin, r:restart, n:number)</li>
|
||||
<li><a href=""/LoadTest/Clear"">Clear all contents</a></li>
|
||||
<li><a href=""/LoadTest/Domains"">List the current domains in w3wp.exe</a></li>
|
||||
<li><a href=""/LoadTest/Restart"">Restart the current AppDomain</a></li>
|
||||
<li><a href=""/LoadTest/Recycle"">Recycle the AppPool</a></li>
|
||||
<li><a href=""/LoadTest/Die"">Cause w3wp.exe to die</a></li>
|
||||
</ul>
|
||||
";
|
||||
|
||||
return ContentHtml(html);
|
||||
}
|
||||
|
||||
private ActionResult EnsureInitialize()
|
||||
{
|
||||
if (_containerId > 0) return null;
|
||||
|
||||
lock (_locko)
|
||||
{
|
||||
if (_containerId > 0) return null;
|
||||
|
||||
var contentTypeService = _serviceContext.ContentTypeService;
|
||||
var contentType = contentTypeService.Get(_contentAlias);
|
||||
if (contentType == null)
|
||||
return ContentHtml("Not installed, first you must <a href=\"/LoadTest/Install\">install</a>.");
|
||||
|
||||
var containerType = contentTypeService.Get(_containerAlias);
|
||||
if (containerType == null)
|
||||
return ContentHtml("Panic! Container type is missing.");
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
var container = contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault();
|
||||
if (container == null)
|
||||
return ContentHtml("Panic! Container is missing.");
|
||||
|
||||
_containerId = container.Id;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Install()
|
||||
{
|
||||
var dataTypeService = _serviceContext.DataTypeService;
|
||||
|
||||
//var dataType = dataTypeService.GetAll(Constants.DataTypes.DefaultContentListView);
|
||||
|
||||
|
||||
//if (!dict.ContainsKey("pageSize")) dict["pageSize"] = new PreValue("10");
|
||||
//dict["pageSize"].Value = "200";
|
||||
//dataTypeService.SavePreValues(dataType, dict);
|
||||
|
||||
var contentTypeService = _serviceContext.ContentTypeService;
|
||||
|
||||
var contentType = new ContentType(_shortStringHelper, -1)
|
||||
{
|
||||
Alias = _contentAlias,
|
||||
Name = "LoadTest Content",
|
||||
Description = "Content for LoadTest",
|
||||
Icon = "icon-document"
|
||||
};
|
||||
var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId);
|
||||
contentType.AddPropertyType(new PropertyType(_shortStringHelper, def)
|
||||
{
|
||||
Name = "Origin",
|
||||
Alias = "origin",
|
||||
Description = "The origin of the content.",
|
||||
});
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var containerTemplate = ImportTemplate(_serviceContext, _shortStringHelper,
|
||||
"LoadTestContainer", "LoadTestContainer", _containerTemplateText);
|
||||
|
||||
var containerType = new ContentType(_shortStringHelper, -1)
|
||||
{
|
||||
Alias = _containerAlias,
|
||||
Name = "LoadTest Container",
|
||||
Description = "Container for LoadTest content",
|
||||
Icon = "icon-document",
|
||||
AllowedAsRoot = true,
|
||||
IsContainer = true
|
||||
};
|
||||
containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[]
|
||||
{
|
||||
new ContentTypeSort(new Lazy<int>(() => contentType.Id), 0, contentType.Alias),
|
||||
});
|
||||
containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate });
|
||||
containerType.SetDefaultTemplate(containerTemplate);
|
||||
contentTypeService.Save(containerType);
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
var content = contentService.Create("LoadTestContainer", -1, _containerAlias);
|
||||
contentService.SaveAndPublish(content);
|
||||
|
||||
return ContentHtml("Installed.");
|
||||
}
|
||||
|
||||
public ActionResult Create(int n = 1, int r = 0, string o = null)
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
if (r < 0) r = 0;
|
||||
if (r > 100) r = 100;
|
||||
var restart = GetRandom(0, 100) > (100 - r);
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
|
||||
if (n < 1) n = 1;
|
||||
if (n > _maxCreate) n = _maxCreate;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o;
|
||||
var content = contentService.Create(name, _containerId, _contentAlias);
|
||||
content.SetValue("origin", o);
|
||||
contentService.SaveAndPublish(content);
|
||||
}
|
||||
|
||||
if (restart)
|
||||
DoRestart();
|
||||
|
||||
return ContentHtml("Created " + n + " content"
|
||||
+ (restart ? ", and restarted" : "")
|
||||
+ ".");
|
||||
}
|
||||
|
||||
private int GetRandom(int minValue, int maxValue)
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
return _random.Next(minValue, maxValue);
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Clear()
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
var contentType = _serviceContext.ContentTypeService.Get(_contentAlias);
|
||||
_serviceContext.ContentService.DeleteOfType(contentType.Id);
|
||||
|
||||
return ContentHtml("Cleared.");
|
||||
}
|
||||
|
||||
private void DoRestart()
|
||||
{
|
||||
HttpContext.User = null;
|
||||
System.Web.HttpContext.Current.User = null;
|
||||
Thread.CurrentPrincipal = null;
|
||||
HttpRuntime.UnloadAppDomain();
|
||||
}
|
||||
|
||||
public ActionResult Restart()
|
||||
{
|
||||
DoRestart();
|
||||
|
||||
return ContentHtml("Restarted.");
|
||||
}
|
||||
|
||||
public ActionResult Die()
|
||||
{
|
||||
var timer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
throw new Exception("die!");
|
||||
});
|
||||
timer.Change(100, 0);
|
||||
|
||||
return ContentHtml("Dying.");
|
||||
}
|
||||
|
||||
public ActionResult Domains()
|
||||
{
|
||||
var currentDomain = AppDomain.CurrentDomain;
|
||||
var currentName = currentDomain.FriendlyName;
|
||||
var pos = currentName.IndexOf('-');
|
||||
if (pos > 0) currentName = currentName.Substring(0, pos);
|
||||
|
||||
var text = new System.Text.StringBuilder();
|
||||
text.Append("<div class=\"block\">Process ID: " + Process.GetCurrentProcess().Id + "</div>");
|
||||
text.Append("<div class=\"block\">");
|
||||
text.Append("<div>IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "</div>");
|
||||
text.Append("<div>App ID: " + currentName + "</div>");
|
||||
//text.Append("<div>AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "</div>");
|
||||
text.Append("</div>");
|
||||
|
||||
text.Append("<div class=\"block\">Domains:<ul>");
|
||||
text.Append("<li>Not implemented.</li>");
|
||||
/*
|
||||
foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id))
|
||||
{
|
||||
var name = domain.FriendlyName;
|
||||
pos = name.IndexOf('-');
|
||||
if (pos > 0) name = name.Substring(0, pos);
|
||||
text.Append("<li style=\""
|
||||
+ (name != currentName ? "color: #cccccc;" : "")
|
||||
//+ (domain.Id == currentDomain.Id ? "" : "")
|
||||
+ "\">"
|
||||
+"[" + domain.Id + "] " + name
|
||||
+ (domain.IsDefaultAppDomain() ? " (default)" : "")
|
||||
+ (domain.Id == currentDomain.Id ? " (current)" : "")
|
||||
+ "</li>");
|
||||
}
|
||||
*/
|
||||
text.Append("</ul></div>");
|
||||
|
||||
return ContentHtml(text.ToString());
|
||||
}
|
||||
|
||||
public ActionResult Recycle()
|
||||
{
|
||||
return ContentHtml("Not implemented—please use IIS console.");
|
||||
}
|
||||
|
||||
private static Template ImportTemplate(ServiceContext svces, IShortStringHelper shortStringHelper, string name, string alias, string text, ITemplate master = null)
|
||||
{
|
||||
var t = new Template(shortStringHelper, name, alias) { Content = text };
|
||||
if (master != null)
|
||||
t.SetMasterTemplate(master);
|
||||
svces.FileService.SaveTemplate(t);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestComponent : IComponent
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
|
||||
return;
|
||||
|
||||
RouteTable.Routes.MapRoute(
|
||||
name: "LoadTest",
|
||||
url: "LoadTest/{action}",
|
||||
defaults: new
|
||||
{
|
||||
controller = "LoadTest",
|
||||
action = "Index"
|
||||
},
|
||||
namespaces: new[] { "Umbraco.TestData" }
|
||||
);
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class TestComposer : ComponentComposer<TestComponent>, IUserComposer
|
||||
{
|
||||
public override void Compose(Composition composition)
|
||||
{
|
||||
base.Compose(composition);
|
||||
|
||||
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
|
||||
return;
|
||||
|
||||
composition.Register(typeof(LoadTestController), Lifetime.Request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="LoadTestController.cs" />
|
||||
<Compile Include="SegmentTestController.cs" />
|
||||
<Compile Include="UmbracoTestDataController.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
||||
@@ -34,7 +34,7 @@ context('Document Types', () => {
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
@@ -34,7 +34,7 @@ context('Media Types', () => {
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
@@ -32,7 +32,7 @@ context('Member Types', () => {
|
||||
cy.get('.umb-search-field').type('Textstring');
|
||||
|
||||
// Choose first item
|
||||
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
|
||||
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
|
||||
|
||||
// Save property
|
||||
cy.get('.btn-success').last().click();
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace Umbraco.Tests.Benchmarks
|
||||
// - it's faster to get+invoke the ctor
|
||||
// - emitting the ctor is unless if invoked only 1
|
||||
|
||||
// TODO: Check out https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 ?
|
||||
|
||||
//[Config(typeof(Config))]
|
||||
[MemoryDiagnoser]
|
||||
public class CtorInvokeBenchmarks
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace Umbraco.Tests.Cache.PublishedCache
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var type = new AutoPublishedContentType(22, "myType", new PublishedPropertyType[] { });
|
||||
var image = new AutoPublishedContentType(23, "Image", new PublishedPropertyType[] { });
|
||||
var testMediaType = new AutoPublishedContentType(24, "TestMediaType", new PublishedPropertyType[] { });
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "myType", new PublishedPropertyType[] { });
|
||||
var image = new AutoPublishedContentType(Guid.NewGuid(), 23, "Image", new PublishedPropertyType[] { });
|
||||
var testMediaType = new AutoPublishedContentType(Guid.NewGuid(), 24, "TestMediaType", new PublishedPropertyType[] { });
|
||||
_mediaTypes = new Dictionary<string, PublishedContentType>
|
||||
{
|
||||
{ type.Alias, type },
|
||||
|
||||
@@ -275,7 +275,7 @@ AnotherContentFinder
|
||||
public void GetDataEditors()
|
||||
{
|
||||
var types = _typeLoader.GetDataEditors();
|
||||
Assert.AreEqual(39, types.Count());
|
||||
Assert.AreEqual(40, types.Count());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -536,15 +536,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
|
||||
|
||||
#region Content types
|
||||
|
||||
public override IPublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Content, id);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Content, id);
|
||||
|
||||
public override IPublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Content, alias);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Content, alias);
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Content, key);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -600,15 +600,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
|
||||
|
||||
#region Content types
|
||||
|
||||
public override IPublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Media, id);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Media, id);
|
||||
|
||||
public override IPublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Media, alias);
|
||||
}
|
||||
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Media, alias);
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Media, key);
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
|
||||
{
|
||||
|
||||
@@ -448,7 +448,10 @@ namespace Umbraco.Tests.Models
|
||||
[Test]
|
||||
public void ContentPublishValuesWithMixedPropertyTypeVariations()
|
||||
{
|
||||
var propertyValidationService = new PropertyValidationService(_factory.GetInstance<PropertyEditorCollection>(), _factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(
|
||||
_factory.GetInstance<PropertyEditorCollection>(),
|
||||
_factory.GetInstance<ServiceContext>().DataTypeService,
|
||||
_factory.GetInstance<ServiceContext>().TextService);
|
||||
const string langFr = "fr-FR";
|
||||
|
||||
// content type varies by Culture
|
||||
@@ -580,7 +583,11 @@ namespace Umbraco.Tests.Models
|
||||
prop.SetValue("a");
|
||||
Assert.AreEqual("a", prop.GetValue());
|
||||
Assert.IsNull(prop.GetValue(published: true));
|
||||
var propertyValidationService = new PropertyValidationService(_factory.GetInstance<PropertyEditorCollection>(), _factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(
|
||||
_factory.GetInstance<PropertyEditorCollection>(),
|
||||
_factory.GetInstance<ServiceContext>().DataTypeService,
|
||||
_factory.GetInstance<ServiceContext>().TextService
|
||||
);
|
||||
|
||||
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
|
||||
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.Blocks;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Web.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
|
||||
namespace Umbraco.Tests.PropertyEditors
|
||||
{
|
||||
[TestFixture]
|
||||
public class BlockListPropertyValueConverterTests
|
||||
{
|
||||
private readonly Guid ContentKey1 = Guid.NewGuid();
|
||||
private readonly Guid ContentKey2 = Guid.NewGuid();
|
||||
private const string ContentAlias1 = "Test1";
|
||||
private const string ContentAlias2 = "Test2";
|
||||
private readonly Guid SettingKey1 = Guid.NewGuid();
|
||||
private readonly Guid SettingKey2 = Guid.NewGuid();
|
||||
private const string SettingAlias1 = "Setting1";
|
||||
private const string SettingAlias2 = "Setting2";
|
||||
|
||||
/// <summary>
|
||||
/// Setup mocks for IPublishedSnapshotAccessor
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor()
|
||||
{
|
||||
var test1ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == ContentKey1
|
||||
&& x.Alias == ContentAlias1);
|
||||
var test2ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == ContentKey2
|
||||
&& x.Alias == ContentAlias2);
|
||||
var test3ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == SettingKey1
|
||||
&& x.Alias == SettingAlias1);
|
||||
var test4ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == SettingKey2
|
||||
&& x.Alias == SettingAlias2);
|
||||
var contentCache = new Mock<IPublishedContentCache>();
|
||||
contentCache.Setup(x => x.GetContentType(ContentKey1)).Returns(test1ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(ContentKey2)).Returns(test2ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(SettingKey1)).Returns(test3ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(SettingKey2)).Returns(test4ContentType);
|
||||
var publishedSnapshot = Mock.Of<IPublishedSnapshot>(x => x.Content == contentCache.Object);
|
||||
var publishedSnapshotAccessor = Mock.Of<IPublishedSnapshotAccessor>(x => x.PublishedSnapshot == publishedSnapshot);
|
||||
return publishedSnapshotAccessor;
|
||||
}
|
||||
|
||||
private BlockListPropertyValueConverter CreateConverter()
|
||||
{
|
||||
var publishedSnapshotAccessor = GetPublishedSnapshotAccessor();
|
||||
var publishedModelFactory = new NoopPublishedModelFactory();
|
||||
var editor = new BlockListPropertyValueConverter(
|
||||
Mock.Of<IProfilingLogger>(),
|
||||
new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory));
|
||||
return editor;
|
||||
}
|
||||
|
||||
private BlockListConfiguration ConfigForMany() => new BlockListConfiguration
|
||||
{
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey1,
|
||||
SettingsElementTypeKey = SettingKey2
|
||||
},
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey2,
|
||||
SettingsElementTypeKey = SettingKey1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private BlockListConfiguration ConfigForSingle() => new BlockListConfiguration
|
||||
{
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private IPublishedPropertyType GetPropertyType(BlockListConfiguration config)
|
||||
{
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
var propertyType = Mock.Of<IPublishedPropertyType>(x =>
|
||||
x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList
|
||||
&& x.DataType == dataType);
|
||||
return propertyType;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Is_Converter_For()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
Assert.IsTrue(editor.IsConverter(Mock.Of<IPublishedPropertyType>(x => x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList)));
|
||||
Assert.IsFalse(editor.IsConverter(Mock.Of<IPublishedPropertyType>(x => x.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Value_Type_Multiple()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
var propType = Mock.Of<IPublishedPropertyType>(x => x.DataType == dataType);
|
||||
|
||||
var valueType = editor.GetPropertyValueType(propType);
|
||||
|
||||
// the result is always block list model
|
||||
Assert.AreEqual(typeof(BlockListModel), valueType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Value_Type_Single()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForSingle();
|
||||
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
var propType = Mock.Of<IPublishedPropertyType>(x => x.DataType == dataType);
|
||||
|
||||
var valueType = editor.GetPropertyValueType(propType);
|
||||
|
||||
// the result is always block list model
|
||||
Assert.AreEqual(typeof(BlockListModel), valueType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Convert_Null_Empty()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
string json = null;
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
json = string.Empty;
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Convert_Valid_Empty_Json()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = "{}";
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
json = @"{
|
||||
layout: {},
|
||||
data: []}";
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
// Even though there is a layout, there is no data, so the conversion will result in zero elements in total
|
||||
json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: []
|
||||
}";
|
||||
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
// Even though there is a layout and data, the data is invalid (missing required keys) so the conversion will result in zero elements in total
|
||||
json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
// Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty
|
||||
json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'key': '1304E1DD-0000-4396-84FE-8A399231CB3D'
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(1, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Convert_Valid_Json()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
}";
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(1, converted.ContentData.Count());
|
||||
var item0 = converted.ContentData.ElementAt(0);
|
||||
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key);
|
||||
Assert.AreEqual("Test1", item0.ContentType.Alias);
|
||||
Assert.AreEqual(1, converted.Layout.Count());
|
||||
var layout0 = converted.Layout.ElementAt(0);
|
||||
Assert.IsNull(layout0.Settings);
|
||||
Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.ContentUdi);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Data_From_Layout_Item()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForMany();
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
|
||||
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
|
||||
}
|
||||
],
|
||||
settingsData: [
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey1 + @"',
|
||||
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
|
||||
}
|
||||
],
|
||||
}";
|
||||
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(3, converted.ContentData.Count());
|
||||
Assert.AreEqual(3, converted.SettingsData.Count());
|
||||
Assert.AreEqual(2, converted.Layout.Count());
|
||||
|
||||
var item0 = converted.Layout.ElementAt(0);
|
||||
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key);
|
||||
Assert.AreEqual("Test1", item0.Content.ContentType.Alias);
|
||||
Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key);
|
||||
Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias);
|
||||
|
||||
var item1 = converted.Layout.ElementAt(1);
|
||||
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key);
|
||||
Assert.AreEqual("Test2", item1.Content.ContentType.Alias);
|
||||
Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key);
|
||||
Assert.AreEqual("Setting1", item1.Settings.ContentType.Alias);
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Data_Item_Removed_If_Removed_From_Config()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
|
||||
// The data below expects that ContentKey1 + ContentKey2 + SettingsKey1 + SettingsKey2 exist but only ContentKey2 exists so
|
||||
// the data should all be filtered.
|
||||
var config = new BlockListConfiguration
|
||||
{
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey2,
|
||||
SettingsElementTypeKey = null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
|
||||
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
|
||||
}
|
||||
],
|
||||
settingsData: [
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey1 + @"',
|
||||
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
|
||||
}
|
||||
],
|
||||
}";
|
||||
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(2, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.SettingsData.Count());
|
||||
Assert.AreEqual(1, converted.Layout.Count());
|
||||
|
||||
var item0 = converted.Layout.ElementAt(0);
|
||||
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item0.Content.Key);
|
||||
Assert.AreEqual("Test2", item0.Content.ContentType.Alias);
|
||||
Assert.IsNull(item0.Settings);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
})));
|
||||
|
||||
var publishedPropType = new PublishedPropertyType(
|
||||
new PublishedContentType(1234, "test", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing),
|
||||
new PublishedContentType(Guid.NewGuid(),1234, "test", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing),
|
||||
new PropertyType(TestHelper.ShortStringHelper, "test", ValueStorageType.Nvarchar) { DataTypeId = 123 },
|
||||
new PropertyValueConverterCollection(Enumerable.Empty<IPropertyValueConverter>()),
|
||||
Mock.Of<IPublishedModelFactory>(), mockPublishedContentTypeFactory.Object);
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes);
|
||||
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes);
|
||||
|
||||
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace Umbraco.Tests.Published
|
||||
=> propertyType.EditorAlias.InvariantEquals("Umbraco.Void");
|
||||
|
||||
public Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (int);
|
||||
=> typeof(int);
|
||||
|
||||
public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
@@ -87,10 +87,10 @@ namespace Umbraco.Tests.Published
|
||||
=> int.TryParse(source as string, out int i) ? i : 0;
|
||||
|
||||
public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> (int) inter;
|
||||
=> (int)inter;
|
||||
|
||||
public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> ((int) inter).ToString();
|
||||
=> ((int)inter).ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -124,11 +124,11 @@ namespace Umbraco.Tests.Published
|
||||
yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes);
|
||||
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes);
|
||||
|
||||
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
|
||||
|
||||
var cntType1 = contentTypeFactory.CreateContentType(1001, "cnt1", t => Enumerable.Empty<PublishedPropertyType>());
|
||||
var cntType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "cnt1", t => Enumerable.Empty<PublishedPropertyType>());
|
||||
var cnt1 = new SolidPublishedContent(cntType1) { Id = 1234 };
|
||||
cacheContent[cnt1.Id] = cnt1;
|
||||
|
||||
@@ -147,7 +147,7 @@ namespace Umbraco.Tests.Published
|
||||
}
|
||||
|
||||
public bool? IsValue(object value, PropertyValueLevel level)
|
||||
=> value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false);
|
||||
=> value != null && (!(value is string) || string.IsNullOrWhiteSpace((string)value) == false);
|
||||
|
||||
public bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals("Umbraco.Void");
|
||||
@@ -166,10 +166,10 @@ namespace Umbraco.Tests.Published
|
||||
=> int.TryParse(source as string, out int i) ? i : -1;
|
||||
|
||||
public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int) inter);
|
||||
=> _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int)inter);
|
||||
|
||||
public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
=> ((int) inter).ToString();
|
||||
=> ((int)inter).ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -219,10 +219,10 @@ namespace Umbraco.Tests.Published
|
||||
yield return contentTypeFactory.CreatePropertyType(contentType, "prop" + i, i);
|
||||
}
|
||||
|
||||
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", t => CreatePropertyTypes(t, 1));
|
||||
var elementType2 = contentTypeFactory.CreateContentType(1001, "element2", t => CreatePropertyTypes(t, 2));
|
||||
var contentType1 = contentTypeFactory.CreateContentType(1002, "content1", t => CreatePropertyTypes(t, 1));
|
||||
var contentType2 = contentTypeFactory.CreateContentType(1003, "content2", t => CreatePropertyTypes(t, 2));
|
||||
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", t => CreatePropertyTypes(t, 1));
|
||||
var elementType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "element2", t => CreatePropertyTypes(t, 2));
|
||||
var contentType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1002, "content1", t => CreatePropertyTypes(t, 1));
|
||||
var contentType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1003, "content2", t => CreatePropertyTypes(t, 2));
|
||||
|
||||
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "val1" } }, false);
|
||||
var element2 = new PublishedElement(elementType2, Guid.NewGuid(), new Dictionary<string, object> { { "prop2", "1003" } }, false);
|
||||
@@ -244,22 +244,22 @@ namespace Umbraco.Tests.Published
|
||||
// can get the actual property Clr type
|
||||
// ie ModelType gets properly mapped by IPublishedContentModelFactory
|
||||
// must test ModelClrType with special equals 'cos they are not ref-equals
|
||||
Assert.IsTrue(ModelType.Equals(typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
|
||||
Assert.AreEqual(typeof (IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
|
||||
Assert.IsTrue(ModelType.Equals(typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
|
||||
Assert.AreEqual(typeof(IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
|
||||
|
||||
// can create a model for an element
|
||||
var model1 = factory.CreateModel(element1);
|
||||
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestElementModel1>(model1);
|
||||
Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1) model1).Prop1);
|
||||
Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1)model1).Prop1);
|
||||
|
||||
// can create a model for a published content
|
||||
var model2 = factory.CreateModel(element2);
|
||||
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestElementModel2>(model2);
|
||||
var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2) model2;
|
||||
var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2)model2;
|
||||
|
||||
// and get direct property
|
||||
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestContentModel1[]>(model2.Value("prop2"));
|
||||
Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[]) model2.Value("prop2")).Length);
|
||||
Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[])model2.Value("prop2")).Length);
|
||||
|
||||
// and get model property
|
||||
Assert.IsInstanceOf<IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>>(mmodel2.Prop2);
|
||||
@@ -276,7 +276,7 @@ namespace Umbraco.Tests.Published
|
||||
=> propertyType.EditorAlias == "Umbraco.Void";
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (string);
|
||||
=> typeof(string);
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
@@ -295,7 +295,7 @@ namespace Umbraco.Tests.Published
|
||||
=> propertyType.EditorAlias == "Umbraco.Void.2";
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1"));
|
||||
=> typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1"));
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Elements;
|
||||
@@ -308,7 +308,7 @@ namespace Umbraco.Tests.Published
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
return ((int[]) inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1) _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray();
|
||||
return ((int[])inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1)_publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ namespace Umbraco.Tests.Published
|
||||
.Returns((string alias) =>
|
||||
{
|
||||
return alias == "contentN1"
|
||||
? (IList) new List<TestElementModel>()
|
||||
: (IList) new List<IPublishedElement>();
|
||||
? (IList)new List<TestElementModel>()
|
||||
: (IList)new List<IPublishedElement>();
|
||||
});
|
||||
|
||||
var contentCache = new Mock<IPublishedContentCache>();
|
||||
@@ -140,9 +140,9 @@ namespace Umbraco.Tests.Published
|
||||
yield return factory.CreatePropertyType(contentType, "propertyN1", 3);
|
||||
}
|
||||
|
||||
var contentType1 = factory.CreateContentType(1, "content1", CreatePropertyTypes1);
|
||||
var contentType2 = factory.CreateContentType(2, "content2", CreatePropertyTypes2);
|
||||
var contentTypeN1 = factory.CreateContentType(2, "contentN1", CreatePropertyTypesN1, isElement: true);
|
||||
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "content1", CreatePropertyTypes1);
|
||||
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "content2", CreatePropertyTypes2);
|
||||
var contentTypeN1 = factory.CreateContentType(Guid.NewGuid(), 2, "contentN1", CreatePropertyTypesN1, isElement: true);
|
||||
|
||||
// mocked content cache returns content types
|
||||
contentCache
|
||||
@@ -162,7 +162,7 @@ namespace Umbraco.Tests.Published
|
||||
(var contentType1, _) = CreateContentTypes();
|
||||
|
||||
// nested single converter returns the proper value clr type TestModel, and cache level
|
||||
Assert.AreEqual(typeof (TestElementModel), contentType1.GetPropertyType("property1").ClrType);
|
||||
Assert.AreEqual(typeof(TestElementModel), contentType1.GetPropertyType("property1").ClrType);
|
||||
Assert.AreEqual(PropertyCacheLevel.Element, contentType1.GetPropertyType("property1").CacheLevel);
|
||||
|
||||
var key = Guid.NewGuid();
|
||||
@@ -170,7 +170,7 @@ namespace Umbraco.Tests.Published
|
||||
var content = new SolidPublishedContent(contentType1)
|
||||
{
|
||||
Key = key,
|
||||
Properties = new []
|
||||
Properties = new[]
|
||||
{
|
||||
new TestPublishedProperty(contentType1.GetPropertyType("property1"), $@"[
|
||||
{{ ""key"": ""{keyA}"", ""propertyN1"": ""foo"", ""ncContentTypeAlias"": ""contentN1"" }}
|
||||
@@ -181,7 +181,7 @@ namespace Umbraco.Tests.Published
|
||||
|
||||
// nested single converter returns proper TestModel value
|
||||
Assert.IsInstanceOf<TestElementModel>(value);
|
||||
var valueM = (TestElementModel) value;
|
||||
var valueM = (TestElementModel)value;
|
||||
Assert.AreEqual("foo", valueM.PropValue);
|
||||
Assert.AreEqual(keyA, valueM.Key);
|
||||
}
|
||||
@@ -192,7 +192,7 @@ namespace Umbraco.Tests.Published
|
||||
(_, var contentType2) = CreateContentTypes();
|
||||
|
||||
// nested many converter returns the proper value clr type IEnumerable<TestModel>, and cache level
|
||||
Assert.AreEqual(typeof (IEnumerable<TestElementModel>), contentType2.GetPropertyType("property2").ClrType);
|
||||
Assert.AreEqual(typeof(IEnumerable<TestElementModel>), contentType2.GetPropertyType("property2").ClrType);
|
||||
Assert.AreEqual(PropertyCacheLevel.Element, contentType2.GetPropertyType("property2").CacheLevel);
|
||||
|
||||
var key = Guid.NewGuid();
|
||||
@@ -214,7 +214,7 @@ namespace Umbraco.Tests.Published
|
||||
// nested many converter returns proper IEnumerable<TestModel> value
|
||||
Assert.IsInstanceOf<IEnumerable<IPublishedElement>>(value);
|
||||
Assert.IsInstanceOf<IEnumerable<TestElementModel>>(value);
|
||||
var valueM = ((IEnumerable<TestElementModel>) value).ToArray();
|
||||
var valueM = ((IEnumerable<TestElementModel>)value).ToArray();
|
||||
Assert.AreEqual("foo", valueM[0].PropValue);
|
||||
Assert.AreEqual(keyA, valueM[0].Key);
|
||||
Assert.AreEqual("bar", valueM[1].PropValue);
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
|
||||
|
||||
// PublishedElementPropertyBase.GetCacheLevels:
|
||||
//
|
||||
@@ -125,7 +125,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
|
||||
|
||||
var elementsCache = new FastDictionaryAppCache();
|
||||
var snapshotCache = new FastDictionaryAppCache();
|
||||
@@ -202,7 +202,7 @@ namespace Umbraco.Tests.Published
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
|
||||
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
|
||||
|
||||
Assert.Throws<Exception>(() =>
|
||||
{
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
var doc = GetContent(true, 1);
|
||||
//change a doc type alias
|
||||
var c = (SolidPublishedContent)doc.Children.ElementAt(0);
|
||||
c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
c.ContentType = new PublishedContentType(Guid.NewGuid(), 22, "DontMatch", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
|
||||
var dt = doc.ChildrenAsTable(ServiceContext, "Child");
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
|
||||
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
|
||||
var contentTypeAlias = createChildren ? "Parent" : "Child";
|
||||
var contentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var d = new SolidPublishedContent(contentType)
|
||||
{
|
||||
CreateDate = DateTime.Now,
|
||||
|
||||
@@ -75,14 +75,14 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return factory.CreatePropertyType(contentType, "noprop", 1, variations: ContentVariation.Culture);
|
||||
}
|
||||
|
||||
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes1);
|
||||
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes1);
|
||||
|
||||
IEnumerable<IPublishedPropertyType> CreatePropertyTypes2(IPublishedContentType contentType)
|
||||
{
|
||||
yield return factory.CreatePropertyType(contentType, "prop3", 1, variations: ContentVariation.Culture);
|
||||
}
|
||||
|
||||
var contentType2 = factory.CreateContentType(2, "contentType2", Enumerable.Empty<string>(), CreatePropertyTypes2);
|
||||
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "contentType2", Enumerable.Empty<string>(), CreatePropertyTypes2);
|
||||
|
||||
var prop1 = new SolidPublishedPropertyWithLanguageVariants
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ using Umbraco.Tests.Testing;
|
||||
using Umbraco.Web.Composing;
|
||||
using Moq;
|
||||
using Examine;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Tests.PublishedContent
|
||||
{
|
||||
@@ -23,9 +24,9 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return factory.CreatePropertyType(contentType, "prop1", 1);
|
||||
}
|
||||
|
||||
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "ContentType2", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
var contentType2Sub = factory.CreateContentType(Guid.NewGuid(), 3, "ContentType2Sub", Enumerable.Empty<string>(), CreatePropertyTypes);
|
||||
|
||||
var content = new SolidPublishedContent(contentType1)
|
||||
{
|
||||
|
||||
@@ -5,18 +5,16 @@ using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using Moq;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.Templates;
|
||||
using Umbraco.Web.Models;
|
||||
using Umbraco.Web.Routing;
|
||||
using Umbraco.Core.Media;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Tests.PublishedContent
|
||||
{
|
||||
@@ -73,7 +71,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "content", 1);
|
||||
}
|
||||
|
||||
var type = new AutoPublishedContentType(0, "anything", CreatePropertyTypes);
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", CreatePropertyTypes);
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => type;
|
||||
|
||||
var umbracoContext = GetUmbracoContext("/test");
|
||||
|
||||
@@ -89,8 +89,8 @@ namespace Umbraco.Tests.PublishedContent
|
||||
}
|
||||
|
||||
var compositionAliases = new[] { "MyCompositionAlias" };
|
||||
var anythingType = new AutoPublishedContentType(0, "anything", compositionAliases, CreatePropertyTypes);
|
||||
var homeType = new AutoPublishedContentType(0, "home", compositionAliases, CreatePropertyTypes);
|
||||
var anythingType = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", compositionAliases, CreatePropertyTypes);
|
||||
var homeType = new AutoPublishedContentType(Guid.NewGuid(), 0, "home", compositionAliases, CreatePropertyTypes);
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => alias.InvariantEquals("home") ? homeType : anythingType;
|
||||
}
|
||||
|
||||
@@ -404,8 +404,8 @@ namespace Umbraco.Tests.PublishedContent
|
||||
[Test]
|
||||
public void Children_GroupBy_DocumentTypeAlias()
|
||||
{
|
||||
var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var contentTypes = new Dictionary<string, PublishedContentType>
|
||||
{
|
||||
{ home.Alias, home },
|
||||
@@ -425,8 +425,8 @@ namespace Umbraco.Tests.PublishedContent
|
||||
[Test]
|
||||
public void Children_Where_DocumentTypeAlias()
|
||||
{
|
||||
var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
|
||||
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var contentTypes = new Dictionary<string, PublishedContentType>
|
||||
{
|
||||
{ home.Alias, home },
|
||||
@@ -909,7 +909,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
yield return factory.CreatePropertyType(contentType, "detached", 1003);
|
||||
}
|
||||
|
||||
var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes);
|
||||
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
|
||||
var pt = ct.GetPropertyType("detached");
|
||||
var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548);
|
||||
Assert.IsInstanceOf<int>(prop.GetValue());
|
||||
@@ -941,7 +941,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes);
|
||||
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
|
||||
|
||||
var c = new ImageWithLegendModel(ct, guid, new Dictionary<string, object>
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
pc.Setup(content => content.Path).Returns("-1,1");
|
||||
pc.Setup(content => content.Parent).Returns(() => null);
|
||||
pc.Setup(content => content.Properties).Returns(new Collection<IPublishedProperty>());
|
||||
pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing));
|
||||
pc.Setup(content => content.ContentType).Returns(new PublishedContentType(Guid.NewGuid(), 22, "anything", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing));
|
||||
return pc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,11 @@ namespace Umbraco.Tests.PublishedContent
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IPublishedContentType GetContentType(Guid key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
@@ -376,7 +381,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
#endregion
|
||||
}
|
||||
|
||||
class PublishedContentStrong1 : PublishedContentModel
|
||||
internal class PublishedContentStrong1 : PublishedContentModel
|
||||
{
|
||||
public PublishedContentStrong1(IPublishedContent content)
|
||||
: base(content)
|
||||
@@ -385,7 +390,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
public int StrongValue => this.Value<int>("strongValue");
|
||||
}
|
||||
|
||||
class PublishedContentStrong1Sub : PublishedContentStrong1
|
||||
internal class PublishedContentStrong1Sub : PublishedContentStrong1
|
||||
{
|
||||
public PublishedContentStrong1Sub(IPublishedContent content)
|
||||
: base(content)
|
||||
@@ -394,7 +399,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
public int AnotherValue => this.Value<int>("anotherValue");
|
||||
}
|
||||
|
||||
class PublishedContentStrong2 : PublishedContentModel
|
||||
internal class PublishedContentStrong2 : PublishedContentModel
|
||||
{
|
||||
public PublishedContentStrong2(IPublishedContent content)
|
||||
: base(content)
|
||||
@@ -403,7 +408,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
public int StrongValue => this.Value<int>("strongValue");
|
||||
}
|
||||
|
||||
class AutoPublishedContentType : PublishedContentType
|
||||
internal class AutoPublishedContentType : PublishedContentType
|
||||
{
|
||||
private static readonly IPublishedPropertyType Default;
|
||||
|
||||
@@ -416,20 +421,20 @@ namespace Umbraco.Tests.PublishedContent
|
||||
Default = factory.CreatePropertyType("*", 666);
|
||||
}
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public AutoPublishedContentType(int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
|
||||
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
|
||||
{ }
|
||||
|
||||
public override IPublishedPropertyType GetPropertyType(string alias)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
@@ -27,7 +28,7 @@ namespace Umbraco.Tests.Routing
|
||||
Mock.Of<IPublishedModelFactory>(),
|
||||
Mock.Of<IPublishedContentTypeFactory>()),
|
||||
};
|
||||
_publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
}
|
||||
|
||||
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
@@ -31,7 +32,7 @@ namespace Umbraco.Tests.Routing
|
||||
factory:Mock.Of<IPublishedContentTypeFactory>()
|
||||
)
|
||||
};
|
||||
_publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
|
||||
}
|
||||
|
||||
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Umbraco.Tests.Routing
|
||||
property.SetSourceValue("en", enMediaUrl, true);
|
||||
property.SetSourceValue("da", daMediaUrl);
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}};
|
||||
|
||||
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto, "da");
|
||||
@@ -162,7 +162,7 @@ namespace Umbraco.Tests.Routing
|
||||
{
|
||||
var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing);
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(),
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(),
|
||||
new[] {umbracoFilePropertyType}, ContentVariation.Nothing);
|
||||
|
||||
return new SolidPublishedContent(contentType)
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace Umbraco.Tests.Routing
|
||||
frequest.TemplateModel = template;
|
||||
|
||||
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
|
||||
var type = new AutoPublishedContentType(22, "CustomDocument", new PublishedPropertyType[] { });
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "CustomDocument", new PublishedPropertyType[] { });
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => type;
|
||||
|
||||
var handler = new RenderRouteHandler(umbracoContext, new TestControllerFactory(umbracoContextAccessor, Mock.Of<ILogger>(), context =>
|
||||
|
||||
@@ -174,7 +174,7 @@ namespace Umbraco.Tests.Routing
|
||||
var requestHandlerSettings = TestHelpers.SettingsForTests.GenerateMockRequestHandlerSettings();
|
||||
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
|
||||
|
||||
var publishedContentCache = new Mock<IPublishedContentCache>();
|
||||
@@ -221,7 +221,7 @@ namespace Umbraco.Tests.Routing
|
||||
|
||||
var requestHandlerSettings = TestHelpers.SettingsForTests.GenerateMockRequestHandlerSettings();
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
|
||||
|
||||
var publishedContentCache = new Mock<IPublishedContentCache>();
|
||||
@@ -277,7 +277,7 @@ namespace Umbraco.Tests.Routing
|
||||
|
||||
var requestHandlerSettings = TestHelpers.SettingsForTests.GenerateMockRequestHandlerSettings();
|
||||
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
|
||||
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
|
||||
|
||||
var publishedContentCache = new Mock<IPublishedContentCache>();
|
||||
|
||||
@@ -1197,7 +1197,7 @@ namespace Umbraco.Tests.Services
|
||||
Assert.IsFalse(content.HasIdentity);
|
||||
|
||||
// content cannot publish values because they are invalid
|
||||
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService);
|
||||
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService, ServiceContext.TextService);
|
||||
var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties, CultureImpact.Invariant);
|
||||
Assert.IsFalse(isValid);
|
||||
Assert.IsNotEmpty(invalidProperties);
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
var propEditors = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
|
||||
|
||||
validationService = new PropertyValidationService(propEditors, dataTypeService.Object);
|
||||
validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of<ILocalizedTextService>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Umbraco.Tests.Templates
|
||||
{
|
||||
//setup a mock url provider which we'll use for testing
|
||||
|
||||
var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var media = new Mock<IPublishedContent>();
|
||||
media.Setup(x => x.ContentType).Returns(mediaType);
|
||||
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
|
||||
|
||||
@@ -55,12 +55,12 @@ namespace Umbraco.Tests.Templates
|
||||
contentUrlProvider
|
||||
.Setup(x => x.GetUrl( It.IsAny<IPublishedContent>(), It.IsAny<UrlMode>(), It.IsAny<string>(), It.IsAny<Uri>()))
|
||||
.Returns(UrlInfo.Url("/my-test-url"));
|
||||
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var publishedContent = new Mock<IPublishedContent>();
|
||||
publishedContent.Setup(x => x.Id).Returns(1234);
|
||||
publishedContent.Setup(x => x.ContentType).Returns(contentType);
|
||||
|
||||
var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
|
||||
var media = new Mock<IPublishedContent>();
|
||||
media.Setup(x => x.ContentType).Returns(mediaType);
|
||||
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -6,8 +7,11 @@ using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Tests.Common;
|
||||
using Umbraco.Tests.PublishedContent;
|
||||
using Umbraco.Tests.TestHelpers.Stubs;
|
||||
@@ -38,7 +42,11 @@ namespace Umbraco.Tests.TestHelpers
|
||||
// need to specify a custom callback for unit tests
|
||||
// AutoPublishedContentTypes generates properties automatically
|
||||
|
||||
var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] { });
|
||||
var dataTypeService = new TestObjects.TestDataTypeService(
|
||||
new DataType(new VoidEditor(Mock.Of<ILogger>(), Mock.Of<IDataTypeService>(), Mock.Of<ILocalizationService>(),Mock.Of<ILocalizedTextService>(), Mock.Of<IShortStringHelper>())) { Id = 1 });
|
||||
|
||||
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
|
||||
var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", new PublishedPropertyType[] { });
|
||||
ContentTypesCache.GetPublishedContentTypeByAlias = alias => GetPublishedContentTypeByAlias(alias) ?? type;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user