Merge branch 'v8/dev' into v8/contrib

# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js
#	src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
#	src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
#	src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html
#	src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html
#	src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
This commit is contained in:
Sebastiaan Janssen
2020-08-19 13:12:13 +02:00
245 changed files with 30050 additions and 4227 deletions

View File

@@ -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>

View File

@@ -25,6 +25,7 @@ namespace Umbraco.Core
{ Unknown, UdiType.Unknown },
{ AnyGuid, UdiType.GuidUdi },
{ Element, UdiType.GuidUdi },
{ Document, UdiType.GuidUdi },
{ DocumentBlueprint, UdiType.GuidUdi },
{ Media, UdiType.GuidUdi },
@@ -64,6 +65,8 @@ namespace Umbraco.Core
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";

View File

@@ -191,9 +191,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
}
}

View File

@@ -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>();
}
}

View File

@@ -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);
}
}

View File

@@ -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>();
}
}

View File

@@ -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 PropertyType PropertyType { get; set; }
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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>();
}
}

View File

@@ -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>();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -25,6 +25,7 @@ namespace Umbraco.Core.Models
/// <summary>
/// Gets a value indicating whether the content is published.
/// </summary>
/// <remarks>The <see cref="PublishedVersionId"/> property tells you which version of the content is currently published.</remarks>
bool Published { get; set; }
PublishedState PublishedState { get; set; }
@@ -32,10 +33,11 @@ namespace Umbraco.Core.Models
/// <summary>
/// Gets a value indicating whether the content has been edited.
/// </summary>
/// <remarks>Will return `true` once unpublished edits have been made after the version with <see cref="PublishedVersionId"/> has been published.</remarks>
bool Edited { get; set; }
/// <summary>
/// Gets the published version identifier.
/// Gets the version identifier for the currently published version of the content.
/// </summary>
int PublishedVersionId { get; set; }
@@ -129,6 +131,6 @@ namespace Umbraco.Core.Models
/// </summary>
/// <returns></returns>
IContent DeepCloneWithResetIdentities();
}
}

View File

@@ -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>

View File

@@ -1,5 +1,6 @@
namespace Umbraco. Core.Models.PublishedContent
{
/// <summary>
/// Creates published content types.
/// </summary>

View File

@@ -3,6 +3,7 @@ using System.Collections;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Provides the published model creation service.
/// </summary>

View File

@@ -3,6 +3,7 @@ using Umbraco.Core.Composing;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Provides strongly typed published content models services.
/// </summary>

View File

@@ -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;
@@ -116,6 +138,9 @@ namespace Umbraco.Core.Models.PublishedContent
#region Content type
/// <inheritdoc />
public Guid Key { get; }
/// <inheritdoc />
public int Id { get; }

View File

@@ -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;
}
}
}

View File

@@ -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 />

View File

@@ -37,6 +37,7 @@ namespace Umbraco.Core.Models
if (contentType == null) throw new ArgumentNullException(nameof(contentType));
Id = contentType.Id;
Key = contentType.Key;
Alias = contentType.Alias;
Variations = contentType.Variations;
Icon = contentType.Icon;
@@ -51,6 +52,8 @@ namespace Umbraco.Core.Models
public int Id { get; }
public Guid Key { get; }
/// <inheritdoc />
public ITemplate DefaultTemplate { get; }
@@ -109,13 +112,14 @@ namespace Umbraco.Core.Models
string ITreeEntity.Name { get => this.Name; set => throw new NotImplementedException(); }
int IEntity.Id { get => this.Id; set => throw new NotImplementedException(); }
bool IEntity.HasIdentity => this.Id != default;
Guid IEntity.Key { get => this.Key; set => throw new NotImplementedException(); }
int ITreeEntity.CreatorId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
int ITreeEntity.ParentId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
int ITreeEntity.Level { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
string ITreeEntity.Path { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
int ITreeEntity.SortOrder { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
bool ITreeEntity.Trashed => throw new NotImplementedException();
Guid IEntity.Key { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
bool ITreeEntity.Trashed => throw new NotImplementedException();
DateTime IEntity.CreateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
DateTime IEntity.UpdateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
DateTime? IEntity.DeleteDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

View File

@@ -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.

View File

@@ -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>

View File

@@ -1017,6 +1017,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)
{

View File

@@ -157,12 +157,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)
{

View File

@@ -21,12 +21,11 @@ namespace Umbraco.Core.Runtime
private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom";
private const string UpdatedSuffix = "_updated";
private readonly ILogger _logger;
private IUmbracoDatabase _db;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
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)
@@ -56,25 +55,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;
}
@@ -82,15 +80,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;
}
@@ -100,23 +95,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;
@@ -142,8 +137,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)
{
@@ -160,20 +162,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;
@@ -183,38 +179,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>
@@ -227,121 +208,131 @@ 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
{
db.BeginTransaction(IsolationLevel.ReadCommitted);
// 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.
_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;
}
else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
{
// in this case, the prefixed ID is different which means
// 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();
}
var acquired = TryAcquire(db, tempId, updatedTempId);
if (acquired.HasValue)
return acquired.Value;
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();
}
return AcquireWhenMaxWaitTimeElapsed(db);
}
}
}, _cancellationTokenSource.Token);
}
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.
_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;
}
else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
{
// in this case, the prefixed ID is different which means
// 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)
{
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,
@@ -354,9 +345,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;
}
@@ -366,7 +356,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
@@ -385,11 +375,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);
@@ -402,24 +392,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();
}
}
}

View File

@@ -30,7 +30,7 @@ namespace Umbraco.Core.Services.Implement
private IQuery<IContent> _queryNotTrashed;
//TODO: The non-lazy object should be injected
private readonly Lazy<PropertyValidationService> _propertyValidationService = new Lazy<PropertyValidationService>(() => new PropertyValidationService());
#region Constructors
@@ -879,7 +879,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.");
}
@@ -1247,7 +1247,7 @@ namespace Umbraco.Core.Services.Implement
if (culturesUnpublishing != null)
{
// This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
var langs = string.Join(", ", allLangs
.Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
.Select(x => x.CultureName));
@@ -1256,7 +1256,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)
@@ -1270,7 +1270,7 @@ namespace Umbraco.Core.Services.Implement
Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)");
return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content);
}
}
Audit(AuditType.Unpublish, userId, content.Id);
@@ -1290,7 +1290,7 @@ namespace Umbraco.Core.Services.Implement
changeType = TreeChangeTypes.RefreshBranch; // whole branch
else if (isNew == false && previouslyPublished)
changeType = TreeChangeTypes.RefreshNode; // single node
// invalidate the node/branch
if (!branchOne) // for branches, handled by SaveAndPublishBranch
@@ -1364,24 +1364,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
@@ -1391,11 +1457,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)
@@ -1407,94 +1476,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
Property[] 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
@@ -2650,7 +2676,7 @@ namespace Umbraco.Core.Services.Implement
// there will be nothing to publish/unpublish.
return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
}
// missing mandatory culture = cannot be published
var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
@@ -3165,6 +3191,6 @@ namespace Umbraco.Core.Services.Implement
#endregion
}
}

View File

@@ -511,6 +511,16 @@ namespace Umbraco.Core.Services.Implement
// delete content
DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id));
// Next find all other document types that have a reference to this content type
var referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes.Any(p=>p.Id.Value==item.Id));
foreach (var reference in referenceToAllowedContentTypes)
{
reference.AllowedContentTypes = reference.AllowedContentTypes.Where(p => p.Id.Value != item.Id);
var changedRef = new List<ContentTypeChange<TItem>>() { new ContentTypeChange<TItem>(reference, ContentTypeChangeTypes.RefreshMain) };
// Fire change event
OnChanged(scope, changedRef.ToEventArgs());
}
// finally delete the content type
// - recursively deletes all descendants
@@ -518,7 +528,7 @@ namespace Umbraco.Core.Services.Implement
// (contents of any descendant type have been deleted but
// contents of any composed (impacted) type remain but
// need to have their property data cleared)
Repository.Delete(item);
Repository.Delete(item);
//...
var changes = descendantsAndSelf.Select(x => new ContentTypeChange<TItem>(x, ContentTypeChangeTypes.Remove))

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Collections;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
@@ -15,19 +13,73 @@ namespace Umbraco.Core.Services
{
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;
}
//TODO: Remove this method in favor of the overload specifying all dependencies
public PropertyValidationService()
: this(Current.PropertyEditors, Current.Services.DataTypeService)
: this(Current.PropertyEditors, Current.Services.DataTypeService, Current.Services.TextService)
{
}
public IEnumerable<ValidationResult> ValidatePropertyValue(
PropertyType 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);
}
internal 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;
}
}
/// <summary>
/// Validates the content item's properties pass validation rules
/// </summary>

View File

@@ -132,15 +132,27 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\ContentTypeDto80.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\PropertyDataDto80.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\PropertyTypeDto80.cs" />
<Compile Include="Models\Blocks\BlockEditorDataConverter.cs" />
<Compile Include="Models\Blocks\BlockEditorData.cs" />
<Compile Include="Models\Blocks\BlockItemData.cs" />
<Compile Include="Models\Blocks\BlockListLayoutItem.cs" />
<Compile Include="Models\Blocks\BlockListEditorDataConverter.cs" />
<Compile Include="Models\Blocks\BlockValue.cs" />
<Compile Include="Models\Blocks\ContentAndSettingsReference.cs" />
<Compile Include="Models\Blocks\IBlockReference.cs" />
<Compile Include="Models\ContentDataIntegrityReport.cs" />
<Compile Include="Models\ContentDataIntegrityReportEntry.cs" />
<Compile Include="Models\ContentDataIntegrityReportOptions.cs" />
<Compile Include="Models\InstallLog.cs" />
<Compile Include="Models\PublishedContent\PublishedContentTypeExtensions.cs" />
<Compile Include="Models\RelationTypeExtensions.cs" />
<Compile Include="Persistence\Repositories\IInstallationRepository.cs" />
<Compile Include="Persistence\Repositories\Implement\InstallationRepository.cs" />
<Compile Include="Services\Implement\InstallationService.cs" />
<Compile Include="Migrations\Upgrade\V_8_6_0\AddMainDomLock.cs" />
<Compile Include="Models\Blocks\BlockEditorModel.cs" />
<Compile Include="Models\Blocks\BlockListLayoutReference.cs" />
<Compile Include="Models\Blocks\BlockListModel.cs" />
<Compile Include="Models\UpgradeResult.cs" />
<Compile Include="Persistence\Repositories\Implement\UpgradeCheckRepository.cs" />
<Compile Include="Persistence\Repositories\IUpgradeCheckRepository.cs" />

View File

@@ -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,20 @@ namespace Umbraco.Examine
/// </summary>
public class IndexRebuilder
{
private readonly IProfilingLogger _logger;
private readonly IEnumerable<IIndexPopulator> _populators;
public IExamineManager ExamineManager { get; }
[Obsolete("Use constructor with all dependencies")]
public IndexRebuilder(IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
: this(Current.ProfilingLogger, examineManager, populators)
{
}
public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
{
_populators = populators;
_logger = logger;
ExamineManager = examineManager;
}
@@ -50,8 +60,18 @@ namespace Umbraco.Examine
index.CreateIndex(); // clear the index
}
//run the populators in parallel against all indexes
Parallel.ForEach(_populators, populator => populator.Populate(indexes));
// run each populator over the indexes
foreach(var populator in _populators)
{
try
{
populator.Populate(indexes);
}
catch (Exception e)
{
_logger.Error<IndexRebuilder>(e, "Index populating failed for populator {Populator}", populator.GetType());
}
}
}
}

View File

@@ -542,8 +542,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);

View File

@@ -0,0 +1,371 @@
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;
// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting
namespace Umbraco.TestData
{
public class LoadTestController : Controller
{
public LoadTestController(ServiceContext serviceContext)
{
_serviceContext = serviceContext;
}
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 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(-1)
{
Alias = _contentAlias,
Name = "LoadTest Content",
Description = "Content for LoadTest",
Icon = "icon-document"
};
var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId);
contentType.AddPropertyType(new PropertyType(def)
{
Name = "Origin",
Alias = "origin",
Description = "The origin of the content.",
});
contentTypeService.Save(contentType);
var containerTemplate = ImportTemplate(_serviceContext,
"LoadTestContainer", "LoadTestContainer", _containerTemplateText);
var containerType = new ContentType(-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&mdash;please use IIS console.");
}
private static Template ImportTemplate(ServiceContext svces, string name, string alias, string text, ITemplate master = null)
{
var t = new Template(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);
}
}
}

View File

@@ -41,6 +41,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="LoadTestController.cs" />
<Compile Include="SegmentTestController.cs" />
<Compile Include="UmbracoTestDataController.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

View File

@@ -0,0 +1,32 @@
context('User Groups', () => {
beforeEach(() => {
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
});
it('Create member group', () => {
const name = "Test Group";
cy.umbracoEnsureMemberGroupNameNotExists(name);
cy.umbracoSection('member');
cy.get('li .umb-tree-root:contains("Members")').should("be.visible");
cy.umbracoTreeItem("member", ["Member Groups"]).rightclick();
cy.umbracoContextMenuAction("action-create").click();
//Type name
cy.umbracoEditorHeaderName(name);
// Save
cy.get('.btn-success').click();
//Assert
cy.umbracoSuccessNotification().should('be.visible');
//Clean up
cy.umbracoEnsureMemberGroupNameNotExists(name);
});
});

View File

@@ -0,0 +1,41 @@
/// <reference types="Cypress" />
context('Members', () => {
beforeEach(() => {
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
});
it('Create member', () => {
const name = "Alice Bobson";
const email = "alice-bobson@acceptancetest.umbraco";
const password = "$AUlkoF*St0kgPiyyVEk5iU5JWdN*F7&@OSl5Y4pOofnidfifkBj5Ns2ONv%FzsTl36V1E924Gw97zcuSeT7UwK&qb5l&O9h!d!w";
cy.umbracoEnsureMemberEmailNotExists(email);
cy.umbracoSection('member');
cy.get('li .umb-tree-root:contains("Members")').should("be.visible");
cy.umbracoTreeItem("member", ["Members"]).rightclick();
cy.umbracoContextMenuAction("action-create").click();
cy.get('.menu-label').first().click();
//Type name
cy.umbracoEditorHeaderName(name);
cy.get('input#_umb_login').clear().type(email);
cy.get('input#_umb_email').clear().type(email);
cy.get('input#password').clear().type(password);
cy.get('input#confirmPassword').clear().type(password);
// Save
cy.get('.btn-success').click();
//Assert
cy.umbracoSuccessNotification().should('be.visible');
//Clean up
cy.umbracoEnsureMemberEmailNotExists(email);
});
});

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -6,8 +6,8 @@
"devDependencies": {
"cross-env": "^7.0.2",
"ncp": "^2.0.0",
"cypress": "^4.6.0",
"umbraco-cypress-testhelpers": "1.0.0-beta-39"
"cypress": "^4.9.0",
"umbraco-cypress-testhelpers": "1.0.0-beta-44"
},
"dependencies": {
"typescript": "^3.9.2"

View File

@@ -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

View File

@@ -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 },

View File

@@ -268,7 +268,7 @@ AnotherContentFinder
public void GetDataEditors()
{
var types = _typeLoader.GetDataEditors();
Assert.AreEqual(38, types.Count());
Assert.AreEqual(39, types.Count());
}
/// <summary>

View File

@@ -14,7 +14,7 @@ using Umbraco.Web.Routing;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache
internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache2
{
private readonly IAppCache _appCache;
private readonly IGlobalSettings _globalSettings;
@@ -532,15 +532,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
}

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
/// <remarks>
/// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly.
/// </remarks>
internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache
internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache2
{
private readonly IMediaService _mediaService;
private readonly IUserService _userService;
@@ -612,15 +612,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)
{

View File

@@ -442,7 +442,10 @@ namespace Umbraco.Tests.Models
[Test]
public void ContentPublishValuesWithMixedPropertyTypeVariations()
{
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
var propertyValidationService = new PropertyValidationService(
Current.Factory.GetInstance<PropertyEditorCollection>(),
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
Current.Factory.GetInstance<ServiceContext>().TextService);
const string langFr = "fr-FR";
// content type varies by Culture
@@ -574,7 +577,10 @@ namespace Umbraco.Tests.Models
prop.SetValue("a");
Assert.AreEqual("a", prop.GetValue());
Assert.IsNull(prop.GetValue(published: true));
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
var propertyValidationService = new PropertyValidationService(
Current.Factory.GetInstance<PropertyEditorCollection>(),
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
Current.Factory.GetInstance<ServiceContext>().TextService);
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));

View File

@@ -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<IPublishedContentCache2>();
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(Udi.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);
}
}
}

View File

@@ -0,0 +1,405 @@
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Web.Compose;
namespace Umbraco.Tests.PropertyEditors
{
[TestFixture]
public class NestedContentPropertyComponentTests
{
[Test]
public void Invalid_Json()
{
var component = new NestedContentPropertyComponent();
Assert.DoesNotThrow(() => component.CreateNestedContentKeys("this is not json", true));
}
[Test]
public void No_Nesting()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
var json = @"[
{""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""},
{""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void One_Level_Nesting_Unescaped()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"": [{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]
}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void One_Level_Nesting_Escaped()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + subJsonEscaped + @"
}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void Nested_In_Complex_Editor_Escaped()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
// Complex editor such as the grid
var complexEditorJsonEscaped = @"{
""name"": ""1 column layout"",
""sections"": [
{
""grid"": ""12"",
""rows"": [
{
""name"": ""Article"",
""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"",
""areas"": [
{
""grid"": ""4"",
""controls"": [
{
""value"": ""I am quote"",
""editor"": {
""alias"": ""quote"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
},
{
""grid"": ""8"",
""controls"": [
{
""value"": ""Header"",
""editor"": {
""alias"": ""headline"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
},
{
""value"": " + subJsonEscaped + @",
""editor"": {
""alias"": ""madeUpNestedContent"",
""view"": ""madeUpNestedContentInGrid""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}]
}]
}";
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + complexEditorJsonEscaped + @"
}
]";
var expected = json
.Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString())
.Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString())
.Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString())
.Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString());
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, false, guidFactory);
Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString());
}
[Test]
public void No_Nesting_Generates_Keys_For_Missing_Items()
{
var guids = new[] { Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
var json = @"[
{""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1 my key wont change"",""ncContentTypeAlias"":""nested"",""text"":""woot""},
{""name"":""Item 2 was copied and has no key prop"",""ncContentTypeAlias"":""nested"",""text"":""zoot""}
]";
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, true, guidFactory);
// Ensure the new GUID is put in a key into the JSON
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
// Ensure that the original key is NOT changed/modified & still exists
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08"));
}
[Test]
public void One_Level_Nesting_Escaped_Generates_Keys_For_Missing_Items()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""name"": ""Nested Item 2 was copied and has no key"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
var json = @"[{
""name"": ""Item 1 was copied and has no key"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",
""name"": ""Item 2"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + subJsonEscaped + @"
}
]";
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, true, guidFactory);
// Ensure the new GUID is put in a key into the JSON for each item
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString()));
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString()));
}
[Test]
public void Nested_In_Complex_Editor_Escaped_Generates_Keys_For_Missing_Items()
{
var guids = new[] { Guid.NewGuid(), Guid.NewGuid() };
var guidCounter = 0;
Func<Guid> guidFactory = () => guids[guidCounter++];
// we need to ensure the escaped json is consistent with how it will be re-escaped after parsing
// and this is how to do that, the result will also include quotes around it.
var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{
""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""name"": ""Nested Item 2 was copied and has no key"",
""ncContentTypeAlias"": ""text"",
""text"": ""zoot""
}
]").ToString());
// Complex editor such as the grid
var complexEditorJsonEscaped = @"{
""name"": ""1 column layout"",
""sections"": [
{
""grid"": ""12"",
""rows"": [
{
""name"": ""Article"",
""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"",
""areas"": [
{
""grid"": ""4"",
""controls"": [
{
""value"": ""I am quote"",
""editor"": {
""alias"": ""quote"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
},
{
""grid"": ""8"",
""controls"": [
{
""value"": ""Header"",
""editor"": {
""alias"": ""headline"",
""view"": ""textstring""
},
""styles"": null,
""config"": null
},
{
""value"": " + subJsonEscaped + @",
""editor"": {
""alias"": ""madeUpNestedContent"",
""view"": ""madeUpNestedContentInGrid""
},
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}],
""styles"": null,
""config"": null
}]
}]
}";
var json = @"[{
""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"",
""name"": ""Item 1"",
""ncContentTypeAlias"": ""text"",
""text"": ""woot""
}, {
""name"": ""Item 2 was copied and has no key"",
""ncContentTypeAlias"": ""list"",
""text"": ""zoot"",
""subItems"":" + complexEditorJsonEscaped + @"
}
]";
var component = new NestedContentPropertyComponent();
var result = component.CreateNestedContentKeys(json, true, guidFactory);
// Ensure the new GUID is put in a key into the JSON for each item
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString()));
Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString()));
}
}
}

View File

@@ -93,7 +93,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("test", ValueStorageType.Nvarchar) { DataTypeId = 123 },
new PropertyValueConverterCollection(Enumerable.Empty<IPropertyValueConverter>()),
Mock.Of<IPublishedModelFactory>(), mockPublishedContentTypeFactory.Object);

View File

@@ -40,7 +40,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);
@@ -74,7 +74,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;
@@ -83,10 +83,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
@@ -120,11 +120,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;
@@ -143,7 +143,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");
@@ -162,10 +162,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
@@ -215,10 +215,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);
@@ -239,22 +239,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);
@@ -271,7 +271,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;
@@ -290,7 +290,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;
@@ -303,7 +303,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();
}
}

View File

@@ -34,7 +34,7 @@ namespace Umbraco.Tests.Published
var proflog = new ProfilingLogger(logger, profiler);
PropertyEditorCollection editors = null;
var editor = new NestedContentPropertyEditor(logger, new Lazy<PropertyEditorCollection>(() => editors), Mock.Of<IDataTypeService>(), Mock.Of<IContentTypeService>());
var editor = new NestedContentPropertyEditor(logger, new Lazy<PropertyEditorCollection>(() => editors), Mock.Of<IDataTypeService>(), Mock.Of<IContentTypeService>(), Mock.Of<ILocalizedTextService>());
editors = new PropertyEditorCollection(new DataEditorCollection(new DataEditor[] { editor }));
var dataType1 = new DataType(editor)
@@ -100,8 +100,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>();
@@ -142,9 +142,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
@@ -164,7 +164,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();
@@ -172,7 +172,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"" }}
@@ -183,7 +183,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);
}
@@ -194,7 +194,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();
@@ -216,7 +216,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);

View File

@@ -41,7 +41,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:
//
@@ -122,7 +122,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();
@@ -199,7 +199,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>(() =>
{

View File

@@ -98,7 +98,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(Current.Services, "Child");
@@ -129,7 +129,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,

View File

@@ -73,14 +73,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
{

View File

@@ -7,6 +7,7 @@ using Umbraco.Web;
using Umbraco.Core;
using Umbraco.Tests.Testing;
using Umbraco.Web.Composing;
using System;
namespace Umbraco.Tests.PublishedContent
{
@@ -21,9 +22,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)
{

View File

@@ -13,6 +13,7 @@ using Umbraco.Core.Services;
using Umbraco.Web;
using Umbraco.Web.Templates;
using Umbraco.Web.Models;
using System;
namespace Umbraco.Tests.PublishedContent
{
@@ -56,7 +57,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");

View File

@@ -83,8 +83,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;
}
@@ -398,8 +398,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 },
@@ -419,8 +419,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 },
@@ -903,7 +903,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());
@@ -935,7 +935,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>
{

View File

@@ -66,7 +66,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;
}
}

View File

@@ -44,7 +44,7 @@ namespace Umbraco.Tests.PublishedContent
{ }
}
class SolidPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache
class SolidPublishedContentCache : PublishedCacheBase, IPublishedContentCache2, IPublishedMediaCache2
{
private readonly Dictionary<int, IPublishedContent> _content = new Dictionary<int, IPublishedContent>();
@@ -150,6 +150,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();
@@ -378,7 +383,7 @@ namespace Umbraco.Tests.PublishedContent
#endregion
}
class PublishedContentStrong1 : PublishedContentModel
internal class PublishedContentStrong1 : PublishedContentModel
{
public PublishedContentStrong1(IPublishedContent content)
: base(content)
@@ -387,7 +392,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)
@@ -396,7 +401,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)
@@ -405,7 +410,7 @@ namespace Umbraco.Tests.PublishedContent
public int StrongValue => this.Value<int>("strongValue");
}
class AutoPublishedContentType : PublishedContentType
internal class AutoPublishedContentType : PublishedContentType
{
private static readonly IPublishedPropertyType Default;
@@ -418,20 +423,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)

View File

@@ -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)

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
@@ -25,7 +26,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)

View File

@@ -139,7 +139,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 = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Auto, "da");
@@ -150,7 +150,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)

View File

@@ -140,7 +140,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 =>

View File

@@ -158,7 +158,7 @@ namespace Umbraco.Tests.Routing
var umbracoSettings = Current.Configs.Settings();
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>();
@@ -203,7 +203,7 @@ namespace Umbraco.Tests.Routing
var umbracoSettings = Current.Configs.Settings();
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>();
@@ -257,7 +257,7 @@ namespace Umbraco.Tests.Routing
var umbracoSettings = Current.Configs.Settings();
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>();

View File

@@ -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);

View File

@@ -36,7 +36,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]

View File

@@ -65,7 +65,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>();

View File

@@ -54,12 +54,12 @@ namespace Umbraco.Tests.Templates
contentUrlProvider
.Setup(x => x.GetUrl(It.IsAny<UmbracoContext>(), 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>();

View File

@@ -42,7 +42,7 @@ namespace Umbraco.Tests.TestHelpers
new DataType(new VoidEditor(Mock.Of<ILogger>())) { Id = 1 });
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] { });
var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", new PublishedPropertyType[] { });
ContentTypesCache.GetPublishedContentTypeByAlias = alias => GetPublishedContentTypeByAlias(alias) ?? type;
}

View File

@@ -41,12 +41,12 @@ namespace Umbraco.Tests.TestHelpers.Entities
};
var contentCollection = new PropertyTypeCollection(true);
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -87 });
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.RichtextEditor });
var metaCollection = new PropertyTypeCollection(true);
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -89 });
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.Textarea });
contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 });
contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Meta", SortOrder = 2 });

View File

@@ -86,7 +86,7 @@ namespace Umbraco.Tests.Testing.TestingTests
var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, Enumerable.Empty<IMediaUrlProvider>(), umbracoContext.VariationContextAccessor);
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 = Mock.Of<IPublishedContent>();
Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType);

View File

@@ -147,7 +147,9 @@
<Compile Include="Persistence\Mappers\MapperTestBase.cs" />
<Compile Include="Persistence\Repositories\DocumentRepositoryTest.cs" />
<Compile Include="Persistence\Repositories\EntityRepositoryTest.cs" />
<Compile Include="PropertyEditors\BlockListPropertyValueConverterTests.cs" />
<Compile Include="PropertyEditors\DataValueReferenceFactoryCollectionTests.cs" />
<Compile Include="PropertyEditors\NestedContentPropertyComponentTests.cs" />
<Compile Include="PublishedContent\NuCacheChildrenTests.cs" />
<Compile Include="PublishedContent\PublishedContentLanguageVariantTests.cs" />
<Compile Include="PublishedContent\PublishedContentSnapshotTestBase.cs" />
@@ -265,7 +267,7 @@
<Compile Include="Web\HttpCookieExtensionsTests.cs" />
<Compile Include="Templates\HtmlImageSourceParserTests.cs" />
<Compile Include="Web\Mvc\HtmlStringUtilitiesTests.cs" />
<Compile Include="Web\ModelStateExtensionsTests.cs" />
<Compile Include="Web\Validation\ModelStateExtensionsTests.cs" />
<Compile Include="Web\Mvc\RenderIndexActionSelectorAttributeTests.cs" />
<Compile Include="Persistence\NPocoTests\PetaPocoCachesTest.cs" />
<Compile Include="Persistence\Repositories\AuditRepositoryTest.cs" />
@@ -512,6 +514,7 @@
<Compile Include="CoreThings\VersionExtensionTests.cs" />
<Compile Include="Web\TemplateUtilitiesTests.cs" />
<Compile Include="Web\UrlHelperExtensionTests.cs" />
<Compile Include="Web\Validation\ContentModelValidatorTests.cs" />
<Compile Include="Web\WebExtensionMethodTests.cs" />
<Compile Include="CoreThings\XmlExtensionsTests.cs" />
<Compile Include="Misc\XmlHelperTests.cs" />

View File

@@ -0,0 +1,369 @@
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Services;
using Umbraco.Core.Logging;
using Umbraco.Web;
using Umbraco.Web.Editors.Filters;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Core.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Editors.Binders;
using Umbraco.Core;
using Umbraco.Tests.Testing;
using Umbraco.Core.Mapping;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Composing;
using System.Web.Http.ModelBinding;
using Umbraco.Web.PropertyEditors;
using System.ComponentModel.DataAnnotations;
using Umbraco.Tests.TestHelpers;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Web.PropertyEditors.Validation;
namespace Umbraco.Tests.Web.Validation
{
[UmbracoTest(Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)]
[TestFixture]
public class ContentModelValidatorTests : UmbracoTestBase
{
private const int ComplexDataTypeId = 9999;
private const string ContentTypeAlias = "textPage";
private ContentType _contentType;
public override void SetUp()
{
base.SetUp();
_contentType = MockedContentTypes.CreateTextPageContentType(ContentTypeAlias);
// add complex editor
_contentType.AddPropertyType(
new PropertyType("complexTest", ValueStorageType.Ntext) { Alias = "complex", Name = "Complex", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = ComplexDataTypeId },
"Content");
// make them all validate with a regex rule that will not pass
foreach (var prop in _contentType.PropertyTypes)
{
prop.ValidationRegExp = "^donotmatch$";
prop.ValidationRegExpMessage = "Does not match!";
}
}
protected override void Compose()
{
base.Compose();
var complexEditorConfig = new NestedContentConfiguration
{
ContentTypes = new[]
{
new NestedContentConfiguration.ContentType { Alias = "feature" }
}
};
var dataTypeService = new Mock<IDataTypeService>();
dataTypeService.Setup(x => x.GetDataType(It.IsAny<int>()))
.Returns((int id) => id == ComplexDataTypeId
? Mock.Of<IDataType>(x => x.Configuration == complexEditorConfig)
: Mock.Of<IDataType>());
var contentTypeService = new Mock<IContentTypeService>();
contentTypeService.Setup(x => x.GetAll(It.IsAny<int[]>()))
.Returns(() => new List<IContentType>
{
_contentType
});
var textService = new Mock<ILocalizedTextService>();
textService.Setup(x => x.Localize("validation/invalidPattern", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns(() => "invalidPattern");
textService.Setup(x => x.Localize("validation/invalidNull", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidNull");
textService.Setup(x => x.Localize("validation/invalidEmpty", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidEmpty");
Composition.RegisterUnique(x => Mock.Of<IDataTypeService>(x => x.GetDataType(It.IsAny<int>()) == Mock.Of<IDataType>()));
Composition.RegisterUnique(x => dataTypeService.Object);
Composition.RegisterUnique(x => contentTypeService.Object);
Composition.RegisterUnique(x => textService.Object);
Composition.WithCollectionBuilder<DataEditorCollectionBuilder>()
.Add<TestEditor>()
.Add<ComplexTestEditor>();
}
[Test]
public void Test_Serializer()
{
var nestedLevel2 = new ComplexEditorValidationResult();
var id1 = Guid.NewGuid();
var addressInfoElementTypeResult = new ComplexEditorElementTypeValidationResult("addressInfo", id1);
var cityPropertyTypeResult = new ComplexEditorPropertyTypeValidationResult("city");
cityPropertyTypeResult.AddValidationResult(new ValidationResult("City is invalid"));
cityPropertyTypeResult.AddValidationResult(new ValidationResult("City cannot be empty"));
cityPropertyTypeResult.AddValidationResult(new ValidationResult("City is not in Australia", new[] { "country" }));
cityPropertyTypeResult.AddValidationResult(new ValidationResult("Not a capital city", new[] { "capital" }));
addressInfoElementTypeResult.ValidationResults.Add(cityPropertyTypeResult);
nestedLevel2.ValidationResults.Add(addressInfoElementTypeResult);
var nestedLevel1 = new ComplexEditorValidationResult();
var id2 = Guid.NewGuid();
var addressBookElementTypeResult = new ComplexEditorElementTypeValidationResult("addressBook", id2);
var addressesPropertyTypeResult = new ComplexEditorPropertyTypeValidationResult("addresses");
addressesPropertyTypeResult.AddValidationResult(new ValidationResult("Must have at least 3 addresses", new[] { "counter" }));
addressesPropertyTypeResult.AddValidationResult(nestedLevel2); // This is a nested result within the level 1
addressBookElementTypeResult.ValidationResults.Add(addressesPropertyTypeResult);
var bookNamePropertyTypeResult = new ComplexEditorPropertyTypeValidationResult("bookName");
bookNamePropertyTypeResult.AddValidationResult(new ValidationResult("Invalid address book name", new[] { "book" }));
addressBookElementTypeResult.ValidationResults.Add(bookNamePropertyTypeResult);
nestedLevel1.ValidationResults.Add(addressBookElementTypeResult);
var id3 = Guid.NewGuid();
var addressBookElementTypeResult2 = new ComplexEditorElementTypeValidationResult("addressBook", id3);
var addressesPropertyTypeResult2 = new ComplexEditorPropertyTypeValidationResult("addresses");
addressesPropertyTypeResult2.AddValidationResult(new ValidationResult("Must have at least 2 addresses", new[] { "counter" }));
addressBookElementTypeResult2.ValidationResults.Add(addressesPropertyTypeResult);
var bookNamePropertyTypeResult2 = new ComplexEditorPropertyTypeValidationResult("bookName");
bookNamePropertyTypeResult2.AddValidationResult(new ValidationResult("Name is too long"));
addressBookElementTypeResult2.ValidationResults.Add(bookNamePropertyTypeResult2);
nestedLevel1.ValidationResults.Add(addressBookElementTypeResult2);
// books is the outer most validation result and doesn't have it's own direct ValidationResult errors
var outerError = new ComplexEditorValidationResult();
var id4 = Guid.NewGuid();
var addressBookCollectionElementTypeResult = new ComplexEditorElementTypeValidationResult("addressBookCollection", id4);
var booksPropertyTypeResult= new ComplexEditorPropertyTypeValidationResult("books");
booksPropertyTypeResult.AddValidationResult(nestedLevel1); // books is the outer most validation result
addressBookCollectionElementTypeResult.ValidationResults.Add(booksPropertyTypeResult);
outerError.ValidationResults.Add(addressBookCollectionElementTypeResult);
var serialized = JsonConvert.SerializeObject(outerError, Formatting.Indented, new ValidationResultConverter());
Console.WriteLine(serialized);
var jsonError = JsonConvert.DeserializeObject<JArray>(serialized);
Assert.IsNotNull(jsonError.SelectToken("$[0]"));
Assert.AreEqual(id4.ToString(), jsonError.SelectToken("$[0].$id").Value<string>());
Assert.AreEqual("addressBookCollection", jsonError.SelectToken("$[0].$elementTypeAlias").Value<string>());
Assert.AreEqual(string.Empty, jsonError.SelectToken("$[0].ModelState['_Properties.books.invariant.null'][0]").Value<string>());
var error0 = jsonError.SelectToken("$[0].books") as JArray;
Assert.IsNotNull(error0);
Assert.AreEqual(id2.ToString(), error0.SelectToken("$[0].$id").Value<string>());
Assert.AreEqual("addressBook", error0.SelectToken("$[0].$elementTypeAlias").Value<string>());
Assert.IsNotNull(error0.SelectToken("$[0].ModelState"));
Assert.AreEqual(string.Empty, error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null'][0]").Value<string>());
var error1 = error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
Assert.IsNotNull(error1);
Assert.AreEqual(1, error1.Count);
var error2 = error0.SelectToken("$[0].ModelState['_Properties.bookName.invariant.null.book']") as JArray;
Assert.IsNotNull(error2);
Assert.AreEqual(1, error2.Count);
Assert.AreEqual(id3.ToString(), error0.SelectToken("$[1].$id").Value<string>());
Assert.AreEqual("addressBook", error0.SelectToken("$[1].$elementTypeAlias").Value<string>());
Assert.IsNotNull(error0.SelectToken("$[1].ModelState"));
Assert.AreEqual(string.Empty, error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null'][0]").Value<string>());
var error6 = error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null.counter']") as JArray;
Assert.IsNotNull(error6);
Assert.AreEqual(1, error6.Count);
var error7 = error0.SelectToken("$[1].ModelState['_Properties.bookName.invariant.null']") as JArray;
Assert.IsNotNull(error7);
Assert.AreEqual(1, error7.Count);
Assert.IsNotNull(error0.SelectToken("$[0].addresses"));
Assert.AreEqual(id1.ToString(), error0.SelectToken("$[0].addresses[0].$id").Value<string>());
Assert.AreEqual("addressInfo", error0.SelectToken("$[0].addresses[0].$elementTypeAlias").Value<string>());
Assert.IsNotNull(error0.SelectToken("$[0].addresses[0].ModelState"));
var error3 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.country']") as JArray;
Assert.IsNotNull(error3);
Assert.AreEqual(1, error3.Count);
var error4 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.capital']") as JArray;
Assert.IsNotNull(error4);
Assert.AreEqual(1, error4.Count);
var error5 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null']") as JArray;
Assert.IsNotNull(error5);
Assert.AreEqual(2, error5.Count);
}
[Test]
public void Validating_ContentItemSave()
{
var validator = new ContentSaveModelValidator(
Factory.GetInstance<ILogger>(),
Mock.Of<IUmbracoContextAccessor>(),
Factory.GetInstance<ILocalizedTextService>());
var content = MockedContent.CreateTextpageContent(_contentType, "test", -1);
var id1 = new Guid("c8df5136-d606-41f0-9134-dea6ae0c2fd9");
var id2 = new Guid("f916104a-4082-48b2-a515-5c4bf2230f38");
var id3 = new Guid("77E15DE9-1C79-47B2-BC60-4913BC4D4C6A");
// TODO: Ok now test with a 4th level complex nested editor
var complexValue = @"[{
""key"": """ + id1.ToString() + @""",
""name"": ""Hello world"",
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
""title"": ""Hello world"",
""bodyText"": ""The world is round""
}, {
""key"": """ + id2.ToString() + @""",
""name"": ""Super nested"",
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
""title"": ""Hi there!"",
""bodyText"": ""Well hello there"",
""complex"" : [{
""key"": """ + id3.ToString() + @""",
""name"": ""I am a sub nested content"",
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
""title"": ""Hello up there :)"",
""bodyText"": ""Hello way up there on a different level""
}]
}
]";
content.SetValue("complex", complexValue);
// map the persisted properties to a model representing properties to save
//var saveProperties = content.Properties.Select(x => Mapper.Map<ContentPropertyBasic>(x)).ToList();
var saveProperties = content.Properties.Select(x =>
{
return new ContentPropertyBasic
{
Alias = x.Alias,
Id = x.Id,
Value = x.GetValue()
};
}).ToList();
var saveVariants = new List<ContentVariantSave>
{
new ContentVariantSave
{
Culture = string.Empty,
Segment = string.Empty,
Name = content.Name,
Save = true,
Properties = saveProperties
}
};
var save = new ContentItemSave
{
Id = content.Id,
Action = ContentSaveAction.Save,
ContentTypeAlias = _contentType.Alias,
ParentId = -1,
PersistedContent = content,
TemplateAlias = null,
Variants = saveVariants
};
// This will map the ContentItemSave.Variants.PropertyCollectionDto and then map the values in the saved model
// back onto the persisted IContent model.
ContentItemBinder.BindModel(save, content);
var modelState = new ModelStateDictionary();
var isValid = validator.ValidatePropertiesData(save, saveVariants[0], saveVariants[0].PropertyCollectionDto, modelState);
// list results for debugging
foreach (var state in modelState)
{
Console.WriteLine(state.Key);
foreach (var error in state.Value.Errors)
{
Console.WriteLine("\t" + error.ErrorMessage);
}
}
// assert
Assert.IsFalse(isValid);
Assert.AreEqual(11, modelState.Keys.Count);
const string complexPropertyKey = "_Properties.complex.invariant.null";
Assert.IsTrue(modelState.Keys.Contains(complexPropertyKey));
foreach (var state in modelState.Where(x => x.Key != complexPropertyKey))
{
foreach (var error in state.Value.Errors)
{
Assert.IsFalse(error.ErrorMessage.DetectIsJson()); // non complex is just an error message
}
}
var complexEditorErrors = modelState.Single(x => x.Key == complexPropertyKey).Value.Errors;
Assert.AreEqual(1, complexEditorErrors.Count);
var nestedError = complexEditorErrors[0];
var jsonError = JsonConvert.DeserializeObject<JArray>(nestedError.ErrorMessage);
var modelStateKeys = new[] { "_Properties.title.invariant.null.innerFieldId", "_Properties.title.invariant.null.value", "_Properties.bodyText.invariant.null.innerFieldId", "_Properties.bodyText.invariant.null.value" };
AssertNestedValidation(jsonError, 0, id1, modelStateKeys);
AssertNestedValidation(jsonError, 1, id2, modelStateKeys.Concat(new[] { "_Properties.complex.invariant.null.innerFieldId", "_Properties.complex.invariant.null.value" }).ToArray());
var nestedJsonError = jsonError.SelectToken("$[1].complex") as JArray;
Assert.IsNotNull(nestedJsonError);
AssertNestedValidation(nestedJsonError, 0, id3, modelStateKeys);
}
private void AssertNestedValidation(JArray jsonError, int index, Guid id, string[] modelStateKeys)
{
Assert.IsNotNull(jsonError.SelectToken("$[" + index + "]"));
Assert.AreEqual(id.ToString(), jsonError.SelectToken("$[" + index + "].$id").Value<string>());
Assert.AreEqual("textPage", jsonError.SelectToken("$[" + index + "].$elementTypeAlias").Value<string>());
Assert.IsNotNull(jsonError.SelectToken("$[" + index + "].ModelState"));
foreach (var key in modelStateKeys)
{
var error = jsonError.SelectToken("$[" + index + "].ModelState['" + key + "']") as JArray;
Assert.IsNotNull(error);
Assert.AreEqual(1, error.Count);
}
}
[HideFromTypeFinder]
[DataEditor("complexTest", "test", "test")]
public class ComplexTestEditor : NestedContentPropertyEditor
{
public ComplexTestEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base(logger, propertyEditors, dataTypeService, contentTypeService)
{
}
protected override IDataValueEditor CreateValueEditor()
{
var editor = base.CreateValueEditor();
editor.Validators.Add(new NeverValidateValidator());
return editor;
}
}
[HideFromTypeFinder]
[DataEditor("test", "test", "test")] // This alias aligns with the prop editor alias for all properties created from MockedContentTypes.CreateTextPageContentType
public class TestEditor : DataEditor
{
public TestEditor(ILogger logger)
: base(logger)
{
}
protected override IDataValueEditor CreateValueEditor() => new TestValueEditor(Attribute);
private class TestValueEditor : DataValueEditor
{
public TestValueEditor(DataEditorAttribute attribute)
: base(attribute)
{
Validators.Add(new NeverValidateValidator());
}
}
}
public class NeverValidateValidator : IValueValidator
{
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
{
yield return new ValidationResult("WRONG!", new[] { "innerFieldId" });
}
}
}
}

View File

@@ -6,7 +6,7 @@ using NUnit.Framework;
using Umbraco.Core.Services;
using Umbraco.Web;
namespace Umbraco.Tests.Web
namespace Umbraco.Tests.Web.Validation
{
[TestFixture]
public class ModelStateExtensionsTests

View File

@@ -31,6 +31,6 @@ exports.build = series(parallel(dependencies, js, less, views), testUnit);
exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views), watchTask);
exports.watch = series(watchTask);
//
exports.runTests = series(setTestMode, parallel(js, testUnit));
exports.runUnit = series(setTestMode, parallel(js, runUnitTestServer), watchTask);
exports.runTests = series(setTestMode, series(js, testUnit));
exports.runUnit = series(setTestMode, series(js, runUnitTestServer), watchTask);
exports.testE2e = series(setTestMode, parallel(testE2e));

View File

@@ -69,6 +69,18 @@
};
}
if (!String.prototype.trimStartSpecial) {
/** trimSpecial extension method for string */
// Removes all non printable chars from beginning of a string
String.prototype.trimStartSpecial = function () {
var index = 0;
while (this.charCodeAt(index) <= 46) {
index++;
}
return this.substr(index);
};
}
if (!String.prototype.startsWith) {
/** startsWith extension method for string */
String.prototype.startsWith = function (str) {

16253
src/Umbraco.Web.UI.Client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"private": true,
"scripts": {
"test": "gulp runTests",
"unit": "gulp testUnit",
"unit": "gulp runUnit",
"e2e": "gulp testE2e",
"build": "gulp build",
"dev": "gulp dev",
@@ -73,6 +73,7 @@
"gulp-wrap-js": "0.4.1",
"jasmine-core": "3.5.0",
"karma": "4.4.1",
"karma-chrome-launcher": "^3.1.0",
"karma-jasmine": "2.0.1",
"karma-junit-reporter": "2.0.1",
"karma-phantomjs-launcher": "1.0.4",

View File

@@ -5,14 +5,14 @@
@scope
@description
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
You can easily add you own tours to the Help-drawer or show and start tours from
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
You can easily add you own tours to the Help-drawer or show and start tours from
anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install <a href="https://our.umbraco.com/projects/starter-kits/the-starter-kit/">The Starter Kit</a> in Umbraco 7.8
<h1><b>Extending the help drawer with custom tours</b></h1>
The easiest way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
picked up by Umbraco and shown in the Help-drawer.
<h3><b>The tour object</b></h3>
@@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list
"groupOrder": 200 // Control the order of tour groups
"allowDisable": // Adds a "Don't" show this tour again"-button to the intro step
"culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
"steps": [] // tour steps - see next example
}
</pre>
@@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list
"backdropOpacity": 0.4 // the backdrop opacity
"view": "" // add a custom view
"customProperties" : {} // add any custom properties needed for the custom view
"skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step
}
</pre>
<h1><b>Adding tours to other parts of the Umbraco backoffice</b></h1>
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.
<h1><b>Using the tour service</b></h1>
@@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um
"element": "[data-element='my-tour-button']",
"title": "Click the button",
"content": "Click the button",
"event": "click"
"event": "click",
"skipStepIfVisible": "[data-element='my-other-tour-button']"
}
]
};
@@ -257,9 +259,26 @@ In the following example you see how to run some custom logic before a step goes
// make sure we don't go too far
if (scope.model.currentStepIndex !== scope.model.steps.length) {
var upcomingStep = scope.model.steps[scope.model.currentStepIndex];
// If the currentStep JSON object has 'skipStepIfVisible'
// It's a DOM selector - if we find it then we ship over this step
if (upcomingStep.skipStepIfVisible) {
let tryFindDomEl = document.querySelector(upcomingStep.skipStepIfVisible);
if (tryFindDomEl) {
// check if element is visible:
if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) {
// if it was visible then we skip the step.
nextStep();
return;
}
}
}
startStep();
// tour completed - final step
} else {
// tour completed - final step
scope.loadingStep = true;
waitForPendingRerequests().then(function () {

View File

@@ -4,7 +4,8 @@
function ContentEditController($rootScope, $scope, $routeParams, $q, $window,
appState, contentResource, entityResource, navigationService, notificationsService,
serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper,
editorState, $http, eventsService, overlayService, $location, localStorageService, treeService) {
editorState, $http, eventsService, overlayService, $location, localStorageService, treeService,
$exceptionHandler) {
var evts = [];
var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode;
@@ -521,6 +522,12 @@
}
}
function handleHttpException(err) {
if (err && !err.status) {
$exceptionHandler(err);
}
}
/** Just shows a simple notification that there are client side validation issues to be fixed */
function showValidationNotification() {
//TODO: We need to make the validation UI much better, there's a lot of inconsistencies in v8 including colors, issues with the property groups and validation errors between variants
@@ -561,6 +568,7 @@
view: "views/content/overlays/unpublish.html",
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
includeUnpublished: false,
submitButtonLabelKey: "content_unpublish",
submitButtonStyle: "warning",
submit: function (model) {
@@ -581,6 +589,7 @@
overlayService.close();
}, function (err) {
$scope.page.buttonGroupState = 'error';
handleHttpException(err);
});
@@ -626,8 +635,7 @@
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
//don't reject, we've handled the error
return $q.when(err);
handleHttpException(err);
});
},
close: function () {
@@ -648,8 +656,9 @@
action: "sendToPublish"
}).then(function () {
$scope.page.buttonGroupState = "success";
}, function () {
}, function (err) {
$scope.page.buttonGroupState = "error";
handleHttpException(err);
});;
}
};
@@ -685,8 +694,7 @@
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
//don't reject, we've handled the error
return $q.when(err);
handleHttpException(err);
});
},
close: function () {
@@ -709,8 +717,9 @@
action: "publish"
}).then(function () {
$scope.page.buttonGroupState = "success";
}, function () {
}, function (err) {
$scope.page.buttonGroupState = "error";
handleHttpException(err);
});
}
};
@@ -748,8 +757,7 @@
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
//don't reject, we've handled the error
return $q.when(err);
handleHttpException(err);
});
},
close: function (oldModel) {
@@ -772,8 +780,9 @@
action: "save"
}).then(function () {
$scope.page.saveButtonState = "success";
}, function () {
}, function (err) {
$scope.page.saveButtonState = "error";
handleHttpException(err);
});
}
@@ -826,8 +835,7 @@
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = Utilities.copy($scope.content.variants);
//don't reject, we've handled the error
return $q.when(err);
handleHttpException(err);
});
},
@@ -886,8 +894,7 @@
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
//don't reject, we've handled the error
return $q.when(err);
handleHttpException(err);
});
},

View File

@@ -38,6 +38,8 @@
vm.selectAppAnchor = selectAppAnchor;
vm.requestSplitView = requestSplitView;
vm.getScope = getScope;// used by property editors to get a scope that is the root of split view, content apps etc.
//Used to track how many content views there are (for split view there will be 2, it could support more in theory)
vm.editors = [];
@@ -244,6 +246,10 @@
vm.onSelectAppAnchor({"app": app, "anchor": anchor});
}
}
function getScope() {
return $scope;
}
}
angular.module('umbraco.directives').component('umbVariantContentEditors', umbVariantContentEditors);

View File

@@ -2,10 +2,10 @@
'use strict';
function EditorContentHeader(serverValidationManager, localizationService, editorState) {
function link(scope) {
function link(scope) {
var unsubscribe = [];
if (!scope.serverValidationNameField) {
scope.serverValidationNameField = "Name";
}
@@ -46,55 +46,55 @@
scope.vm.variantsWithError = [];
scope.vm.defaultVariant = null;
scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors.
function updateVaraintErrors() {
scope.content.variants.forEach( function (variant) {
scope.content.variants.forEach(function (variant) {
variant.hasError = scope.variantHasError(variant);
});
checkErrorsOnOtherVariants();
}
function checkErrorsOnOtherVariants() {
var check = false;
scope.content.variants.forEach( function (variant) {
scope.content.variants.forEach(function (variant) {
if (variant.active !== true && variant.hasError) {
check = true;
}
});
scope.vm.errorsOnOtherVariants = check;
}
function onVariantValidation(valid, errors, allErrors, culture, segment) {
// only want to react to property errors:
if(errors.findIndex(error => {return error.propertyAlias !== null;}) === -1) {
if (errors.findIndex(error => { return error.propertyAlias !== null; }) === -1) {
// we dont have any errors for properties, meaning we will back out.
return;
}
// If error coming back is invariant, we will assign the error to the default variant by picking the defaultVariant language.
if(culture === "invariant") {
if (culture === "invariant" && scope.vm.defaultVariant) {
culture = scope.vm.defaultVariant.language.culture;
}
var index = scope.vm.variantsWithError.findIndex((item) => item.culture === culture && item.segment === segment)
if(valid === true) {
if (valid === true) {
if (index !== -1) {
scope.vm.variantsWithError.splice(index, 1);
}
} else {
if (index === -1) {
scope.vm.variantsWithError.push({"culture": culture, "segment": segment});
scope.vm.variantsWithError.push({ "culture": culture, "segment": segment });
}
}
scope.$evalAsync(updateVaraintErrors);
}
function onInit() {
// find default + check if we have variants.
scope.content.variants.forEach( function (variant) {
scope.content.variants.forEach(function (variant) {
if (variant.language !== null && variant.language.isDefault) {
scope.vm.defaultVariant = variant;
}
@@ -113,41 +113,41 @@
scope.vm.variantMenu = [];
if (scope.vm.hasCulture) {
scope.content.variants.forEach( (v) => {
scope.content.variants.forEach((v) => {
if (v.language !== null && v.segment === null) {
var variantMenuEntry = {
key: String.CreateGuid(),
open: v.language && v.language.culture === scope.editor.culture,
variant: v,
subVariants: scope.content.variants.filter( (subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null)
subVariants: scope.content.variants.filter((subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null)
};
scope.vm.variantMenu.push(variantMenuEntry);
}
});
} else {
scope.content.variants.forEach( (v) => {
scope.content.variants.forEach((v) => {
scope.vm.variantMenu.push({
key: String.CreateGuid(),
variant: v
});
});
}
scope.editor.variantApps.forEach( (app) => {
scope.editor.variantApps.forEach((app) => {
if (app.alias === "umbContent") {
app.anchors = scope.editor.content.tabs;
}
});
scope.content.variants.forEach( function (variant) {
scope.content.variants.forEach(function (variant) {
// if we are looking for the variant with default language then we also want to check for invariant variant.
if (variant.language && variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) {
if (variant.language && scope.vm.defaultVariant && variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) {
unsubscribe.push(serverValidationManager.subscribe(null, "invariant", null, onVariantValidation, null));
}
unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, null, onVariantValidation, variant.segment));
});
}
scope.goBack = function () {
@@ -164,15 +164,15 @@
}
};
scope.selectNavigationItem = function(item) {
if(scope.onSelectNavigationItem) {
scope.onSelectNavigationItem({"item": item});
scope.selectNavigationItem = function (item) {
if (scope.onSelectNavigationItem) {
scope.onSelectNavigationItem({ "item": item });
}
}
scope.selectAnchorItem = function(item, anchor) {
if(scope.onSelectAnchorItem) {
scope.onSelectAnchorItem({"item": item, "anchor": anchor});
scope.selectAnchorItem = function (item, anchor) {
if (scope.onSelectAnchorItem) {
scope.onSelectAnchorItem({ "item": item, "anchor": anchor });
}
}
@@ -188,20 +188,20 @@
scope.onOpenInSplitView({ "variant": variant });
}
};
/**
* Check whether a variant has a error, used to display errors in variant switcher.
* @param {any} culture
*/
scope.variantHasError = function(variant) {
if(scope.vm.variantsWithError.find((item) => (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment) !== undefined) {
scope.variantHasError = function (variant) {
if (scope.vm.variantsWithError.find((item) => (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment) !== undefined) {
return true;
}
return false;
}
onInit();
scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();

View File

@@ -11,13 +11,13 @@
var sectionId = '#leftcolumn';
var isLeftColumnAbove = false;
scope.editors = [];
function addEditor(editor) {
editor.inFront = true;
editor.moveRight = true;
editor.level = 0;
editor.styleIndex = 0;
// push the new editor to the dom
scope.editors.push(editor);
@@ -32,20 +32,20 @@
$timeout(() => {
editor.moveRight = false;
})
editor.animating = true;
setTimeout(revealEditorContent.bind(this, editor), 400);
updateEditors();
}
function removeEditor(editor) {
editor.moveRight = true;
editor.animating = true;
setTimeout(removeEditorFromDOM.bind(this, editor), 400);
updateEditors(-1);
if(scope.editors.length === 1){
@@ -56,17 +56,17 @@
isLeftColumnAbove = false;
}
}
function revealEditorContent(editor) {
editor.animating = false;
scope.$digest();
}
function removeEditorFromDOM(editor) {
// push the new editor to the dom
var index = scope.editors.indexOf(editor);
if (index !== -1) {
@@ -74,42 +74,42 @@
}
updateEditors();
scope.$digest();
}
/** update layer positions. With ability to offset positions, needed for when an item is moving out, then we dont want it to influence positions */
function updateEditors(offset) {
offset = offset || 0;// fallback value.
var len = scope.editors.length;
var calcLen = len + offset;
var ceiling = Math.min(calcLen, allowedNumberOfVisibleEditors);
var origin = Math.max(calcLen-1, 0)-ceiling;
var origin = Math.max(calcLen - 1, 0) - ceiling;
var i = 0;
while(i<len) {
while (i < len) {
var iEditor = scope.editors[i];
iEditor.styleIndex = Math.min(i+1, allowedNumberOfVisibleEditors);
iEditor.level = Math.max(i-origin, -1);
iEditor.styleIndex = Math.min(i + 1, allowedNumberOfVisibleEditors);
iEditor.level = Math.max(i - origin, -1);
iEditor.inFront = iEditor.level >= ceiling;
i++;
}
}
evts.push(eventsService.on("appState.editors.open", function (name, args) {
addEditor(args.editor);
}));
evts.push(eventsService.on("appState.editors.close", function (name, args) {
// remove the closed editor
if(args && args.editor) {
if (args && args.editor) {
removeEditor(args.editor);
}
// close all editors
if(args && !args.editor && args.editors.length === 0) {
if (args && !args.editor && args.editors.length === 0) {
scope.editors = [];
}
}));
@@ -134,6 +134,74 @@
}
// This directive allows for us to run a custom $compile for the view within the repeater which allows
// us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the
// infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model.
function EditorRepeaterDirective($http, $templateCache, $compile, angularHelper) {
function link(scope, el, attr, ctrl) {
var editor = scope && scope.$parent ? scope.$parent.model : null;
if (!editor) {
return;
}
var unsubscribe = [];
//if a custom parent scope is defined then we need to manually compile the view
if (editor.$parentScope) {
var element = el.find(".scoped-view");
$http.get(editor.view, { cache: $templateCache })
.then(function (response) {
var templateScope = editor.$parentScope.$new();
unsubscribe.push(function () {
templateScope.$destroy();
});
// NOTE: the 'model' name here directly affects the naming convention used in infinite editors, this why you access the model
// like $scope.model.If this is changed, everything breaks.This is because we are entirely reliant upon ng-include and inheriting $scopes.
// by default without a $parentScope used for infinite editing the 'model' propety will be set because the view creates the scopes in
// ng-repeat by ng-repeat="model in editors"
templateScope.model = editor;
element.show();
// if a parentForm is supplied then we can link them but to do that we need to inject a top level form
if (editor.$parentForm) {
element.html("<ng-form name='infiniteEditorForm'>" + response.data + "</ng-form>");
}
$compile(element)(templateScope);
// if a parentForm is supplied then we can link them
if (editor.$parentForm) {
editor.$parentForm.$addControl(templateScope.infiniteEditorForm);
}
});
}
scope.$on('$destroy', function () {
for (var i = 0; i < unsubscribe.length; i++) {
unsubscribe[i]();
}
});
}
var directive = {
restrict: 'E',
replace: true,
transclude: true,
scope: {
editors: "="
},
template: "<div ng-transclude></div>",
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('umbEditors', EditorsDirective);
angular.module('umbraco.directives').directive('umbEditorRepeater', EditorRepeaterDirective);
})();

View File

@@ -2,13 +2,20 @@ angular.module("umbraco.directives").directive('focusWhen', function ($timeout)
return {
restrict: 'A',
link: function (scope, elm, attrs, ctrl) {
var delayTimer;
attrs.$observe("focusWhen", function (newValue) {
if (newValue === "true") {
$timeout(function () {
elm.trigger("focus");
});
if (newValue === "true" && document.activeelement !== elm[0]) {
delayTimer = $timeout(function () {
elm[0].focus();
});
}
});
scope.$on('$destroy', function() {
$timeout.cancel(delayTimer);
});
}
};
});

View File

@@ -284,7 +284,7 @@ Opens an overlay to show a custom YSOD. </br>
templateScope.model = scope.model;
element.html(response.data);
element.show();
$compile(element.contents())(templateScope);
$compile(element)(templateScope);
});
}
}
@@ -473,7 +473,7 @@ Opens an overlay to show a custom YSOD. </br>
scope.submitForm = function (model) {
if (scope.model.submit) {
if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation })) {
if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation, keepServerValidation: true })) {
if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) {
//wrap in a when since we don't know if this is a promise or not

View File

@@ -3,46 +3,75 @@
* @name umbraco.directives.directive:umbProperty
* @restrict E
**/
angular.module("umbraco.directives")
.directive('umbProperty', function (userService) {
return {
scope: {
(function () {
'use strict';
angular
.module("umbraco.directives")
.component('umbProperty', {
templateUrl: 'views/components/property/umb-property.html',
controller: UmbPropertyController,
controllerAs: 'vm',
transclude: true,
require: {
parentUmbProperty: '?^^umbProperty',
parentForm: '?^^form'
},
bindings: {
property: "=",
elementKey: "@",
// optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/)
propertyAlias: "@",
showInherit: "<",
inheritsFrom: "<"
},
transclude: true,
restrict: 'E',
replace: true,
templateUrl: 'views/components/property/umb-property.html',
link: function (scope) {
scope.controlLabelTitle = null;
if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) {
userService.getCurrentUser().then(function (u) {
if(u.allowedSections.indexOf("settings") !== -1 ? true : false) {
scope.controlLabelTitle = scope.property.alias;
}
});
}
},
//Define a controller for this directive to expose APIs to other directives
controller: function ($scope) {
var self = this;
//set the API properties/methods
self.property = $scope.property;
self.setPropertyError = function (errorMsg) {
$scope.property.propertyErrorMessage = errorMsg;
};
$scope.propertyActions = [];
self.setPropertyActions = function(actions) {
$scope.propertyActions = actions;
};
}
});
function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) {
const vm = this;
vm.$onInit = onInit;
vm.setPropertyError = function (errorMsg) {
vm.property.propertyErrorMessage = errorMsg;
};
});
vm.propertyActions = [];
vm.setPropertyActions = function (actions) {
vm.propertyActions = actions;
};
// returns the validation path for the property to be used as the validation key for server side validation logic
vm.getValidationPath = function () {
var parentValidationPath = vm.parentUmbProperty ? vm.parentUmbProperty.getValidationPath() : null;
var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias;
// the elementKey will be empty when this is not a nested property
var valPath = vm.elementKey ? vm.elementKey + "/" + propAlias : propAlias;
return serverValidationManager.createPropertyValidationKey(valPath, parentValidationPath);
}
function onInit() {
vm.controlLabelTitle = null;
if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) {
userService.getCurrentUser().then(function (u) {
if (u.allowedSections.indexOf("settings") !== -1 ? true : false) {
vm.controlLabelTitle = vm.property.alias;
}
});
}
if (!vm.parentUmbProperty) {
// not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope
// inheritance is (i.e.infinite editing)
var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "UmbPropertyController");
vm.parentUmbProperty = found ? found.vm : null;
}
}
}
})();

View File

@@ -9,13 +9,14 @@ function treeSearchResults() {
return {
scope: {
results: "=",
selectResultCallback: "="
selectResultCallback: "=",
emptySearchResultPosition: '@'
},
restrict: "E", // restrict to an element
replace: true, // replace the html element with the template
templateUrl: 'views/components/tree/umb-tree-search-results.html',
link: function (scope, element, attrs, ctrl) {
scope.emptySearchResultPosition = scope.emptySearchResultPosition || "center";
}
};
}

View File

@@ -0,0 +1,59 @@
(function () {
'use strict';
function umbTextarea($document) {
function autogrow(scope, element, attributes) {
if (!element.hasClass("autogrow")) {
// no autogrow for you today
return;
}
// get possible minimum height style
var minHeight = parseInt(window.getComputedStyle(element[0]).getPropertyValue("min-height")) || 0;
// prevent newlines in textbox
element.on("keydown", function (evt) {
if (evt.which === 13) {
//evt.preventDefault();
}
});
element.on("input", function (evt) {
element.css({
height: 'auto',
minHeight: 0
});
var contentHeight = this.scrollHeight;
var borderHeight = 1;
var paddingHeight = 4;
element.css({
minHeight: null, // remove property
height: contentHeight + borderHeight + paddingHeight + "px" // because we're using border-box
});
});
// watch model changes from the outside to adjust height
scope.$watch(attributes.ngModel, trigger);
// set initial size
trigger();
function trigger() {
setTimeout(element.triggerHandler.bind(element, "input"), 1);
}
}
var directive = {
restrict: 'E',
link: autogrow
};
return directive;
}
angular.module('umbraco.directives').directive('textarea', umbTextarea);
})();

View File

@@ -12,48 +12,88 @@
* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will
* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly.
**/
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService) {
function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) {
var SHOW_VALIDATION_CLASS_NAME = "show-validation";
var SAVING_EVENT_NAME = "formSubmitting";
var SAVED_EVENT_NAME = "formSubmitted";
function notify(scope) {
scope.$broadcast("valStatusChanged", { form: scope.formCtrl });
}
function ValFormManagerController($scope) {
//This exposes an API for direct use with this directive
// We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and
// because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is
// an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager
// in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing)
$scope.valFormManager = this;
var unsubscribe = [];
var self = this;
//This is basically the same as a directive subscribing to an event but maybe a little
// nicer since the other directive can use this directive's API instead of a magical event
this.onValidationStatusChanged = function (cb) {
unsubscribe.push($scope.$on("valStatusChanged", function (evt, args) {
cb.apply(self, [evt, args]);
}));
};
this.isShowingValidation = () => $scope.showValidation === true;
this.notify = function () {
notify($scope);
}
this.isValid = function () {
return !$scope.formCtrl.$invalid;
}
//Ensure to remove the event handlers when this instance is destroyted
$scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
}
});
}
/**
* Find's the valFormManager in the scope/DOM hierarchy
* @param {any} scope
* @param {any} ctrls
* @param {any} index
*/
function getAncestorValFormManager(scope, ctrls, index) {
// first check the normal directive inheritance which relies on DOM inheritance
var found = ctrls[index];
if (found) {
return found;
}
// not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope
// inheritance is (i.e.infinite editing)
var found = angularHelper.traverseScopeChain(scope, s => s && s.valFormManager && s.valFormManager.constructor.name === "ValFormManagerController");
return found ? found.valFormManager : null;
}
return {
require: ["form", "^^?valFormManager", "^^?valSubView"],
restrict: "A",
controller: function($scope) {
//This exposes an API for direct use with this directive
var unsubscribe = [];
var self = this;
//This is basically the same as a directive subscribing to an event but maybe a little
// nicer since the other directive can use this directive's API instead of a magical event
this.onValidationStatusChanged = function (cb) {
unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) {
cb.apply(self, [evt, args]);
}));
};
this.showValidation = $scope.showValidation === true;
//Ensure to remove the event handlers when this instance is destroyted
$scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
}
});
},
controller: ValFormManagerController,
link: function (scope, element, attr, ctrls) {
function notifySubView() {
if (subView){
if (subView) {
subView.valStatusChanged({ form: formCtrl, showValidation: scope.showValidation });
}
}
var formCtrl = ctrls[0];
var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null;
var formCtrl = scope.formCtrl = ctrls[0];
var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1);
var subView = ctrls.length > 1 ? ctrls[2] : null;
var labels = {};
@@ -72,45 +112,22 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
});
//watch the list of validation errors to notify the application of any validation changes
scope.$watch(function () {
//the validators are in the $error collection: https://docs.angularjs.org/api/ng/type/form.FormController#$error
//since each key is the validator name (i.e. 'required') we can't just watch the number of keys, we need to watch
//the sum of the items inside of each key
scope.$watch(() => angularHelper.countAllFormErrors(formCtrl),
function (e) {
notify(scope);
notifySubView();
//find all invalid elements' .control-group's and apply the error class
var inError = element.find(".control-group .ng-invalid").closest(".control-group");
inError.addClass("error");
//find all control group's that have no error and ensure the class is removed
var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError);
noInError.removeClass("error");
//get the lengths of each array for each key in the $error collection
var validatorLengths = _.map(formCtrl.$error, function (val, key) {
// if there are child ng-forms, include the $error collections in those as well
var innerErrorCount = _.reduce(
_.map(val, v =>
_.reduce(
_.map(v.$error, e => e.length),
(m, n) => m + n
)
),
(memo, num) => memo + num
);
return val.length + innerErrorCount;
});
//sum up all numbers in the resulting array
var sum = _.reduce(validatorLengths, function (memo, num) {
return memo + num;
}, 0);
//this is the value we watch to notify of any validation changes on the form
return sum;
}, function (e) {
scope.$broadcast("valStatusChanged", { form: formCtrl });
notifySubView();
//find all invalid elements' .control-group's and apply the error class
var inError = element.find(".control-group .ng-invalid").closest(".control-group");
inError.addClass("error");
//find all control group's that have no error and ensure the class is removed
var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError);
noInError.removeClass("error");
});
//This tracks if the user is currently saving a new item, we use this to determine
// if we should display the warning dialog that they are leaving the page - if a new item
@@ -119,7 +136,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
var isSavingNewItem = false;
//we should show validation if there are any msgs in the server validation collection
if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.showValidation)) {
if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.isShowingValidation())) {
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
notifySubView();
@@ -128,7 +145,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
var unsubscribe = [];
//listen for the forms saving event
unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function(ev, args) {
unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) {
element.addClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = true;
notifySubView();
@@ -137,7 +154,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
}));
//listen for the forms saved event
unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function(ev, args) {
unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) {
//remove validation class
element.removeClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = false;
@@ -151,7 +168,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
//This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but
// the form has pending changes
var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) {
var locationEvent = $rootScope.$on('$locationChangeStart', function (event, nextLocation, currentLocation) {
var infiniteEditors = editorService.getEditors();
@@ -178,10 +195,10 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
"disableEscKey": true,
"submitButtonLabel": labels.stayButton,
"closeButtonLabel": labels.discardChangesButton,
submit: function() {
submit: function () {
overlayService.close();
},
close: function() {
close: function () {
// close all editors
editorService.closeAll();
// allow redirection
@@ -215,13 +232,13 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
unsubscribe.push(locationEvent);
//Ensure to remove the event handler when this instance is destroyted
scope.$on('$destroy', function() {
scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
}
});
$timeout(function(){
$timeout(function () {
formCtrl.$setPristine();
}, 1000);

View File

@@ -8,24 +8,26 @@
* We will listen for server side validation changes
* and when an error is detected for this property we'll show the error message.
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
* We don't set the validity of this validator to false when client side validation fails, only when server side
* validation fails however we do respond to the client side validation changes to display error and adjust UI state.
**/
function valPropertyMsg(serverValidationManager, localizationService) {
function valPropertyMsg(serverValidationManager, localizationService, angularHelper) {
return {
require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'],
require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'],
replace: true,
restrict: "E",
template: "<div ng-show=\"errorMsg != ''\" class='alert alert-error property-error' >{{errorMsg}}</div>",
scope: {},
link: function (scope, element, attrs, ctrl) {
var unsubscribe = [];
var watcher = null;
var hasError = false;
var hasError = false; // tracks if there is a child error or an explicit error
//create properties on our custom scope so we can use it in our template
scope.errorMsg = "";
scope.errorMsg = "";
//the property form controller api
var formCtrl = ctrl[0];
//the valFormManager controller api
@@ -33,21 +35,19 @@ function valPropertyMsg(serverValidationManager, localizationService) {
//the property controller api
var umbPropCtrl = ctrl[2];
//the variants controller api
var umbVariantCtrl = ctrl[3];
var umbVariantCtrl = ctrl[3];
var currentProperty = umbPropCtrl.property;
scope.currentProperty = currentProperty;
var currentCulture = currentProperty.culture;
var currentSegment = currentProperty.segment;
var currentSegment = currentProperty.segment;
// validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type)
var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined;
var labels = {};
localizationService.localize("errors_propertyHasErrors").then(function (data) {
labels.propertyHasErrors = data;
});
var showValidation = false;
if (umbVariantCtrl) {
//if we are inside of an umbVariantContent directive
@@ -61,17 +61,16 @@ function valPropertyMsg(serverValidationManager, localizationService) {
return;
}
}
// if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
currentCulture = currentCulture || "invariant";
// Gets the error message to display
function getErrorMsg() {
//this can be null if no property was assigned
if (scope.currentProperty) {
//first try to get the error msg from the server collection
var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, "", null);
var err = serverValidationManager.getPropertyError(umbPropCtrl.getValidationPath(), null, "", null);
//if there's an error message use it
if (err && err.errorMsg) {
return err.errorMsg;
@@ -84,43 +83,108 @@ function valPropertyMsg(serverValidationManager, localizationService) {
return labels.propertyHasErrors;
}
// We need to subscribe to any changes to our model (based on user input)
// This is required because when we have a server error we actually invalidate
// the form which means it cannot be resubmitted.
// So once a field is changed that has a server error assigned to it
// we need to re-validate it for the server side validator so the user can resubmit
// the form. Of course normal client-side validators will continue to execute.
// check the current errors in the form (and recursive sub forms), if there is 1 or 2 errors
// we can check if those are valPropertyMsg or valServer and can clear our error in those cases.
function checkAndClearError() {
var errCount = angularHelper.countAllFormErrors(formCtrl);
if (errCount === 0) {
return true;
}
if (errCount > 2) {
return false;
}
var hasValServer = Utilities.isArray(formCtrl.$error.valServer);
if (errCount === 1 && hasValServer) {
return true;
}
var hasOwnErr = hasExplicitError();
if ((errCount === 1 && hasOwnErr) || (errCount === 2 && hasOwnErr && hasValServer)) {
var propertyValidationPath = umbPropCtrl.getValidationPath();
// check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves
if (isLastServerError(propertyValidationPath)) {
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment);
return true;
}
return false;
}
return false;
}
// returns true if there is an explicit valPropertyMsg validation error on the form
function hasExplicitError() {
return Utilities.isArray(formCtrl.$error.valPropertyMsg);
}
// returns true if there is only a single server validation error for this property validation key in it's validation path
function isLastServerError(propertyValidationPath) {
var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath(
propertyValidationPath,
currentCulture,
currentSegment,
{ matchType: "prefix" });
if (nestedErrs.length === 0 || (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationPath)) {
return true;
}
return false;
}
// a custom $validator function called on when each child ngModelController changes a value.
function resetServerValidityValidator(fieldController) {
const storedFieldController = fieldController; // pin a reference to this
return (modelValue, viewValue) => {
// if the ngModelController value has changed, then we can check and clear the error
if (storedFieldController.$dirty) {
if (checkAndClearError()) {
resetError();
}
}
return true; // this validator is always 'valid'
};
}
// collect all ng-model controllers recursively within the umbProperty form
// until it reaches the next nested umbProperty form
function collectAllNgModelControllersRecursively(controls, ngModels) {
controls.forEach(ctrl => {
if (angularHelper.isForm(ctrl)) {
// if it's not another umbProperty form then recurse
if (ctrl.$name !== formCtrl.$name) {
collectAllNgModelControllersRecursively(ctrl.$getControls(), ngModels);
}
}
else if (ctrl.hasOwnProperty('$modelValue') && Utilities.isObject(ctrl.$validators)) {
ngModels.push(ctrl);
}
});
}
// We start the watch when there's server validation errors detected.
// We watch on the current form's properties and on first watch or if they are dynamically changed
// we find all ngModel controls recursively on this form (but stop recursing before we get to the next)
// umbProperty form). Then for each ngModelController we assign a new $validator. This $validator
// will execute whenever the value is changed which allows us to check and reset the server validator
function startWatch() {
//if there's not already a watch
if (!watcher) {
watcher = scope.$watch("currentProperty.value",
function (newValue, oldValue) {
if (angular.equals(newValue, oldValue)) {
return;
}
var errCount = 0;
for (var e in formCtrl.$error) {
if (Utilities.isArray(formCtrl.$error[e])) {
errCount++;
watcher = scope.$watchCollection(
() => formCtrl,
function (updatedFormController) {
var ngModels = [];
collectAllNgModelControllersRecursively(updatedFormController.$getControls(), ngModels);
ngModels.forEach(x => {
if (!x.$validators.serverValidityResetter) {
x.$validators.serverValidityResetter = resetServerValidityValidator(x);
}
}
//we are explicitly checking for valServer errors here, since we shouldn't auto clear
// based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
// is the only one, then we'll clear.
if (errCount === 0
|| (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg))
|| (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) {
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
} else if (showValidation && scope.errorMsg === "") {
formCtrl.$setValidity('valPropertyMsg', false);
scope.errorMsg = getErrorMsg();
}
}, true);
});
});
}
}
@@ -132,22 +196,31 @@ function valPropertyMsg(serverValidationManager, localizationService) {
}
}
function resetError() {
stopWatch();
hasError = false;
formCtrl.$setValidity('valPropertyMsg', true);
scope.errorMsg = "";
}
// This deals with client side validation changes and is executed anytime validators change on the containing
// valFormManager. This allows us to know when to display or clear the property error data for non-server side errors.
function checkValidationStatus() {
if (formCtrl.$invalid) {
//first we need to check if the valPropertyMsg validity is invalid
if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) {
//since we already have an error we'll just return since this means we've already set the
// hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe
//hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe
return;
}
//if there are any errors in the current property form that are not valPropertyMsg
else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) {
// errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared
if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) {
hasError = false;
showValidation = false;
scope.errorMsg = "";
resetError();
// if there's no value, the controls can be reset, which clears the error state on formCtrl
for (let control of formCtrl.$getControls()) {
@@ -164,99 +237,107 @@ function valPropertyMsg(serverValidationManager, localizationService) {
}
}
else {
hasError = false;
scope.errorMsg = "";
resetError();
}
}
else {
hasError = false;
scope.errorMsg = "";
resetError();
}
}
//if there's any remaining errors in the server validation service then we should show them.
var showValidation = serverValidationManager.items.length > 0;
if (!showValidation) {
//We can either get the form submitted status by the parent directive valFormManager (if we add a property to it)
//or we can just check upwards in the DOM for the css class (easier for now).
//The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot
//reset the status.
showValidation = element.closest(".show-validation").length > 0;
}
function onInit() {
localizationService.localize("errors_propertyHasErrors").then(function (data) {
//listen for form validation changes.
//The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then
// subscribing to this single watch.
valFormManager.onValidationStatusChanged(function (evt, args) {
checkValidationStatus();
});
labels.propertyHasErrors = data;
//listen for the forms saving event
unsubscribe.push(scope.$on("formSubmitting", function (ev, args) {
showValidation = true;
if (hasError && scope.errorMsg === "") {
scope.errorMsg = getErrorMsg();
startWatch();
}
else if (!hasError) {
scope.errorMsg = "";
stopWatch();
}
}));
//listen for the forms saved event
unsubscribe.push(scope.$on("formSubmitted", function (ev, args) {
showValidation = false;
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
}));
//listen for server validation changes
// NOTE: we pass in "" in order to listen for all validation changes to the content property, not for
// validation changes to fields in the property this is because some server side validators may not
// return the field name for which the error belongs too, just the property for which it belongs.
// It's important to note that we need to subscribe to server validation changes here because we always must
// indicate that a content property is invalid at the property level since developers may not actually implement
// the correct field validation in their property editors.
if (scope.currentProperty) { //this can be null if no property was assigned
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
hasError = !isValid;
if (hasError) {
//set the error message to the server message
scope.errorMsg = propertyErrors[0].errorMsg;
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
startWatch();
//if there's any remaining errors in the server validation service then we should show them.
showValidation = serverValidationManager.items.length > 0;
if (!showValidation) {
//We can either get the form submitted status by the parent directive valFormManager (if we add a property to it)
//or we can just check upwards in the DOM for the css class (easier for now).
//The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot
//reset the status.
showValidation = element.closest(".show-validation").length > 0;
}
else {
scope.errorMsg = "";
//flag that the current validator is valid
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
//listen for form validation changes.
//The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then
// subscribing to this single watch.
valFormManager.onValidationStatusChanged(function (evt, args) {
checkValidationStatus();
});
//listen for the forms saving event
unsubscribe.push(scope.$on("formSubmitting", function (ev, args) {
showValidation = true;
if (hasError && scope.errorMsg === "") {
scope.errorMsg = getErrorMsg();
startWatch();
}
else if (!hasError) {
resetError();
}
}));
//listen for the forms saved event
unsubscribe.push(scope.$on("formSubmitted", function (ev, args) {
showValidation = false;
resetError();
}));
if (scope.currentProperty) { //this can be null if no property was assigned
// listen for server validation changes for property validation path prefix.
// We pass in "" in order to listen for all validation changes to the content property, not for
// validation changes to fields in the property this is because some server side validators may not
// return the field name for which the error belongs too, just the property for which it belongs.
// It's important to note that we need to subscribe to server validation changes here because we always must
// indicate that a content property is invalid at the property level since developers may not actually implement
// the correct field validation in their property editors.
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
var hadError = hasError;
hasError = !isValid;
if (hasError) {
//set the error message to the server message
scope.errorMsg = propertyErrors.length > 1 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors;
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
startWatch();
if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) {
var propertyValidationPath = umbPropCtrl.getValidationPath();
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment);
resetError();
}
}
else {
resetError();
}
}
unsubscribe.push(serverValidationManager.subscribe(
umbPropCtrl.getValidationPath(),
currentCulture,
"",
serverValidationManagerCallback,
currentSegment,
{ matchType: "prefix" } // match property validation path prefix
));
}
}
unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias,
currentCulture,
"",
serverValidationManagerCallback,
currentSegment
)
);
});
}
//when the scope is disposed we need to unsubscribe
scope.$on('$destroy', function () {
stopWatch();
for (var u in unsubscribe) {
unsubscribe[u]();
}
unsubscribe.forEach(u => u());
});
onInit();
}

View File

@@ -13,14 +13,14 @@ function valServer(serverValidationManager) {
link: function (scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null;
var umbPropCtrl = ctrls[1];
if (!umbPropCtrl) {
//we cannot proceed, this validator will be disabled
return;
}
// optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages.
var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null;
var umbVariantCtrl = ctrls[2];
var currentProperty = umbPropCtrl.property;
var currentCulture = currentProperty.culture;
@@ -55,8 +55,11 @@ function valServer(serverValidationManager) {
}
}
//Need to watch the value model for it to change, previously we had subscribed to
//modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that
// Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works
var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : currentProperty.alias;
// Need to watch the value model for it to change, previously we had subscribed to
// modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that
// doesn't specifically have a 2 way ng binding. This is required because when we
// have a server error we actually invalidate the form which means it cannot be
// resubmitted. So once a field is changed that has a server error assigned to it
@@ -75,8 +78,10 @@ function valServer(serverValidationManager) {
if (modelCtrl.$invalid) {
modelCtrl.$setValidity('valServer', true);
//clear the server validation entry
serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName, currentSegment);
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment);
stopWatch();
}
}, true);
@@ -105,7 +110,9 @@ function valServer(serverValidationManager) {
stopWatch();
}
}
unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias,
unsubscribe.push(serverValidationManager.subscribe(
propertyValidationPath,
currentCulture,
fieldName,
serverValidationManagerCallback,
@@ -114,9 +121,7 @@ function valServer(serverValidationManager) {
scope.$on('$destroy', function () {
stopWatch();
for (var u in unsubscribe) {
unsubscribe[u]();
}
unsubscribe.forEach(u => u());
});
}
};

View File

@@ -0,0 +1,123 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:valServerMatch
* @restrict A
* @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data
* contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against
* a property validation key. The attribute value can be in multiple value types:
* - STRING = The property validation key to have an exact match on. If matched, then the form will have a valServerMatch validator applied.
* - OBJECT = A dictionary where the key is the match type: "contains", "prefix", "suffix" and the value is either:
* - ARRAY = A list of property validation keys to match on. If any are matched then the form will have a valServerMatch validator applied.
* - OBJECT = A dictionary where the key is the validator error name applied to the form and the value is the STRING of the property validation key to match on
**/
function valServerMatch(serverValidationManager) {
return {
require: ['form', '^^umbProperty', '?^^umbVariantContent'],
restrict: "A",
scope: {
valServerMatch: "="
},
link: function (scope, element, attr, ctrls) {
var formCtrl = ctrls[0];
var umbPropCtrl = ctrls[1];
if (!umbPropCtrl) {
//we cannot proceed, this validator will be disabled
return;
}
// optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages.
var umbVariantCtrl = ctrls[2];
var currentProperty = umbPropCtrl.property;
var currentCulture = currentProperty.culture;
var currentSegment = currentProperty.segment;
if (umbVariantCtrl) {
//if we are inside of an umbVariantContent directive
var currentVariant = umbVariantCtrl.editor.content;
// Lets check if we have variants and we are on the default language then ...
if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) {
//This property is locked cause its a invariant property shown on a non-default language.
//Therefor do not validate this field.
return;
}
}
// if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language.
currentCulture = currentCulture || "invariant";
var unsubscribe = [];
function bindCallback(validationKey, matchVal, matchType) {
if (Utilities.isString(matchVal)) {
matchVal = [matchVal]; // normalize to an array since the value can also natively be an array
}
// match for each string in the array
matchVal.forEach(m => {
unsubscribe.push(serverValidationManager.subscribe(
m,
currentCulture,
"",
// the callback
function (isValid, propertyErrors, allErrors) {
if (!isValid) {
formCtrl.$setValidity(validationKey, false);
}
else {
formCtrl.$setValidity(validationKey, true);
}
},
currentSegment,
matchType ? { matchType: matchType } : null // specify the match type
));
});
}
if (Utilities.isObject(scope.valServerMatch)) {
var allowedKeys = ["contains", "prefix", "suffix"];
Object.keys(scope.valServerMatch).forEach(matchType => {
if (allowedKeys.indexOf(matchType) === -1) {
throw "valServerMatch dictionary keys must be one of " + allowedKeys.join();
}
var matchVal = scope.valServerMatch[matchType];
if (Utilities.isObject(matchVal)) {
// as an object, the key will be the validation error instead of the default "valServerMatch"
Object.keys(matchVal).forEach(valKey => {
// matchVal[valKey] can be an ARRAY or a STRING
bindCallback(valKey, matchVal[valKey], matchType);
});
}
else {
// matchVal can be an ARRAY or a STRING
bindCallback("valServerMatch", matchVal, matchType);
}
});
}
else if (Utilities.isString(scope.valServerMatch)) {
// a STRING match which will be an exact match on the string supplied as the property validation key
bindCallback("valServerMatch", scope.valServerMatch, null);
}
else {
throw "valServerMatch value must be a string or a dictionary";
}
scope.$on('$destroy', function () {
unsubscribe.forEach(u => u());
});
}
};
}
angular.module('umbraco.directives.validation').directive("valServerMatch", valServerMatch);

View File

@@ -1,36 +1,49 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:valSubView
* @restrict A
* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data.
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
* @ngdoc directive
* @name umbraco.directives.directive:valSubView
* @restrict A
* @description Used to show validation warnings for a editor sub view (used in conjunction with:
* umb-editor-sub-view or umb-editor-sub-views) to indicate that the section content has validation errors in its data.
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
* When applied to
**/
(function () {
'use strict';
// Since this is a directive applied as an attribute, the value of that attribtue is the 'model' object property
// of the current inherited scope that the hasError/errorClass properties will apply to.
// This directive cannot have it's own scope because it's an attribute applied to another scoped directive.
// Due to backwards compatibility we can't really change this, ideally this would have it's own scope/properties.
function valSubViewDirective() {
function controller($scope, $element) {
function controller($scope, $element, $attrs) {
var model = $scope.model; // this is the default and required for backwards compat
if ($attrs && $attrs.valSubView) {
// get the property to use
model = $scope[$attrs.valSubView];
}
//expose api
return {
valStatusChanged: function (args) {
// TODO: Verify this is correct, does $scope.model ever exist?
if ($scope.model) {
if (model) {
if (!args.form.$valid) {
var subViewContent = $element.find(".ng-invalid");
if (subViewContent.length > 0) {
$scope.model.hasError = true;
$scope.model.errorClass = args.showValidation ? 'show-validation' : null;
model.hasError = true;
model.errorClass = args.showValidation ? 'show-validation' : null;
} else {
$scope.model.hasError = false;
$scope.model.errorClass = null;
model.hasError = false;
model.errorClass = null;
}
}
else {
$scope.model.hasError = false;
$scope.model.errorClass = null;
model.hasError = false;
model.errorClass = null;
}
}
}
@@ -40,12 +53,18 @@
function link(scope, el, attr, ctrl) {
//if there are no containing form or valFormManager controllers, then we do nothing
if (!ctrl || !Utilities.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) {
if (!ctrl[1]) {
return;
}
var model = scope.model; // this is the default and required for backwards compat
if (attr && attr.valSubView) {
// get the property to use
model = scope[attr.valSubView];
}
var valFormManager = ctrl[1];
scope.model.hasError = false;
model.hasError = false;
//listen for form validation changes
valFormManager.onValidationStatusChanged(function (evt, args) {
@@ -54,14 +73,14 @@
var subViewContent = el.find(".ng-invalid");
if (subViewContent.length > 0) {
scope.model.hasError = true;
model.hasError = true;
} else {
scope.model.hasError = false;
model.hasError = false;
}
}
else {
scope.model.hasError = false;
model.hasError = false;
}
});

View File

@@ -1,5 +1,5 @@
angular.module('umbraco.mocks').
factory('mocksUtils', ['$cookies', function ($cookies) {
factory('mocksUtils', ['$cookies', 'udiService', function ($cookies, udiService) {
'use strict';
//by default we will perform authorization
@@ -40,13 +40,17 @@ angular.module('umbraco.mocks').
},
/** Creats a mock content object */
getMockContent: function(id) {
getMockContent: function (id, key, udi) {
key = key || String.CreateGuid();
var udi = udi || udiService.build("content", key);
var node = {
name: "My content with id: " + id,
updateDate: new Date().toIsoDateTimeString(),
publishDate: new Date().toIsoDateTimeString(),
createDate: new Date().toIsoDateTimeString(),
id: id,
key: key,
udi: udi,
parentId: 1234,
icon: "icon-umb-content",
owner: { name: "Administrator", id: 0 },
@@ -280,6 +284,182 @@ angular.module('umbraco.mocks').
return node;
},
/** Creats a mock variant content object */
getMockVariantContent: function(id, key, udi) {
key = key || String.CreateGuid();
var udi = udi || udiService.build("content", key);
var node = {
name: "My content with id: " + id,
updateDate: new Date().toIsoDateTimeString(),
publishDate: new Date().toIsoDateTimeString(),
createDate: new Date().toIsoDateTimeString(),
id: id,
key: key,
udi: udi,
parentId: 1234,
icon: "icon-umb-content",
owner: { name: "Administrator", id: 0 },
updater: { name: "Per Ploug Krogslund", id: 1 },
path: "-1,1234,2455",
allowedActions: ["U", "H", "A"],
contentTypeAlias: "testAlias",
contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB",
apps: [],
variants: [
{
name: "",
language: null,
segment: null,
state: "NotCreated",
updateDate: "0001-01-01 00:00:00",
createDate: "0001-01-01 00:00:00",
publishDate: null,
releaseDate: null,
expireDate: null,
notifications: [],
tabs: [
{
label: "Content",
id: 2,
properties: [
{ alias: "testproperty", label: "Test property", view: "textbox", value: "asdfghjk" },
{ alias: "valTest", label: "Validation test", view: "validationtest", value: "asdfasdf" },
{ alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
{ alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
{ alias: "media", label: "Media picker", view: "mediapicker", value: "1234,23242,23232,23231", config: {multiPicker: 1} }
]
},
{
label: "Sample Editor",
id: 3,
properties: [
{ alias: "datepicker", label: "Datepicker", view: "datepicker", config: { pickTime: false, format: "yyyy-MM-dd" } },
{ alias: "tags", label: "Tags", view: "tags", value: "" }
]
},
{
label: "This",
id: 4,
properties: [
{ alias: "valTest4", label: "Validation test", view: "validationtest", value: "asdfasdf" },
{ alias: "bodyText4", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
{ alias: "textarea4", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
{ alias: "content4", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" }
]
},
{
label: "Is",
id: 5,
properties: [
{ alias: "valTest5", label: "Validation test", view: "validationtest", value: "asdfasdf" },
{ alias: "bodyText5", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
{ alias: "textarea5", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
{ alias: "content5", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" }
]
},
{
label: "Overflown",
id: 6,
properties: [
{ alias: "valTest6", label: "Validation test", view: "validationtest", value: "asdfasdf" },
{ alias: "bodyText6", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>", config: {} },
{ alias: "textarea6", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
{ alias: "content6", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" }
]
},
{
label: "Generic Properties",
id: 0,
properties: [
{
label: 'Id',
value: 1234,
view: "readonlyvalue",
alias: "_umb_id"
},
{
label: 'Created by',
description: 'Original author',
value: "Administrator",
view: "readonlyvalue",
alias: "_umb_createdby"
},
{
label: 'Created',
description: 'Date/time this document was created',
value: new Date().toIsoDateTimeString(),
view: "readonlyvalue",
alias: "_umb_createdate"
},
{
label: 'Updated',
description: 'Date/time this document was created',
value: new Date().toIsoDateTimeString(),
view: "readonlyvalue",
alias: "_umb_updatedate"
},
{
label: 'Document Type',
value: "Home page",
view: "readonlyvalue",
alias: "_umb_doctype"
},
{
label: 'Publish at',
description: 'Date/time to publish this document',
value: new Date().toIsoDateTimeString(),
view: "datepicker",
alias: "_umb_releasedate"
},
{
label: 'Unpublish at',
description: 'Date/time to un-publish this document',
value: new Date().toIsoDateTimeString(),
view: "datepicker",
alias: "_umb_expiredate"
},
{
label: 'Template',
value: "myTemplate",
view: "dropdown",
alias: "_umb_template",
config: {
items: {
"" : "-- Choose template --",
"myTemplate" : "My Templates",
"home" : "Home Page",
"news" : "News Page"
}
}
},
{
label: 'Link to document',
value: ["/testing" + id, "http://localhost/testing" + id, "http://mydomain.com/testing" + id].join(),
view: "urllist",
alias: "_umb_urllist"
},
{
alias: "test", label: "Stuff", view: "test", value: "",
config: {
fields: [
{ alias: "embedded", label: "Embbeded", view: "textstring", value: "" },
{ alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" },
{ alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" },
{ alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" }
]
}
}
]
}
]
}
]
};
return node;
},
getMockEntity : function(id){
return {name: "hello", id: id, icon: "icon-file"};
},

View File

@@ -0,0 +1,56 @@
angular.module('umbraco.mocks').
factory('variantContentMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) {
'use strict';
function returnEmptyVariantNode(status, data, headers) {
if (!mocksUtils.checkAuth()) {
return [401, null, null];
}
var response = returnVariantNodebyId(200, "", null);
var node = response[1];
var parentId = mocksUtils.getParameterByName(data, "parentId") || 1234;
node.name = "";
node.id = 0;
node.parentId = parentId;
node.tabs.forEach(function(tab) {
tab.properties.forEach(function(property) {
property.value = "";
});
});
return response;
}
function returnVariantNodebyId(status, data, headers) {
if (!mocksUtils.checkAuth()) {
return [401, null, null];
}
var id = mocksUtils.getParameterByName(data, "id") || "1234";
id = parseInt(id, 10);
var node = mocksUtils.getMockVariantContent(id);
return [200, node, null];
}
return {
register: function () {
$httpBackend
.whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Content/GetById?'))
.respond(returnVariantNodebyId);
$httpBackend
.whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Content/GetEmpty'))
.respond(returnEmptyVariantNode);
}
};
}]);

View File

@@ -388,7 +388,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetBlueprintById",
[{ id: id }])),
{ id: id })),
'Failed to retrieve data for content id ' + id)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
@@ -401,7 +401,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetNotificationOptions",
[{ contentId: id }])),
{ contentId: id })),
'Failed to retrieve data for content id ' + id);
},
@@ -498,12 +498,57 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmpty",
[{ contentTypeAlias: alias }, { parentId: parentId }])),
{ contentTypeAlias: alias, parentId: parentId })),
'Failed to retrieve data for empty content item type ' + alias)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
});
},
/**
* @ngdoc method
* @name umbraco.resources.contentResource#getScaffoldByKey
* @methodOf umbraco.resources.contentResource
*
* @description
* Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias.
*
* - Parent Id must be provided so umbraco knows where to store the content
* - Content Type Id must be provided so umbraco knows which properties to put on the content scaffold
*
* The scaffold is used to build editors for content that has not yet been populated with data.
*
* ##usage
* <pre>
* contentResource.getScaffoldByKey(1234, '...')
* .then(function(scaffold) {
* var myDoc = scaffold;
* myDoc.name = "My new document";
*
* contentResource.publish(myDoc, true)
* .then(function(content){
* alert("Retrieved, updated and published again");
* });
* });
* </pre>
*
* @param {Int} parentId id of content item to return
* @param {String} contentTypeGuid contenttype guid to base the scaffold on
* @returns {Promise} resourcePromise object containing the content scaffold.
*
*/
getScaffoldByKey: function (parentId, contentTypeKey) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmptyByKey",
{ contentTypeKey: contentTypeKey, parentId: parentId })),
'Failed to retrieve data for empty content item id ' + contentTypeKey)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));
});
},
getBlueprintScaffold: function (parentId, blueprintId) {
@@ -512,7 +557,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmpty",
[{ blueprintId: blueprintId }, { parentId: parentId }])),
{ blueprintId: blueprintId, parentId: parentId })),
'Failed to retrieve blueprint for id ' + blueprintId)
.then(function (result) {
return $q.when(umbDataFormatter.formatContentGetData(result));

Some files were not shown because too many files have changed in this diff Show More