From 02b6f94a1b01a408f3b941de3fb7d107cff28b34 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 7 Jan 2016 19:46:22 +0100 Subject: [PATCH 1/2] moves the current logic into a re-usable ext method --- .../Services/ContentTypeService.cs | 90 +++++++++++++++++++ .../Editors/ContentTypeControllerBase.cs | 77 ++-------------- 2 files changed, 95 insertions(+), 72 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index eff7202201..9caa1a7a6e 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; using System.Threading; +using AutoMapper; using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; @@ -20,6 +21,95 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { + public static class ContentTypeServiceExtensions + { + /// + /// Returns the available composite content types for a given content type + /// + /// + public static IEnumerable GetAvailableCompositeContentTypes(this IContentTypeService ctService, + int contentTypeId, + IContentTypeComposition[] allContentTypes) + { + IContentTypeComposition source = null; + + //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + + // note: there are many sanity checks missing here and there ;-(( + // make sure once and for all + //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) + // throw new Exception("A parent does not belong to a composition."); + + // find out if any content type uses this content type + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == contentTypeId)).ToArray(); + if (isUsing.Length > 0) + { + //if already in use a composition, do not allow any composited types + return new List(); + } + + // if it is not used then composition is possible + // hashset guarantees unicity on Id + var list = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + // usable types are those that are top-level + var usableContentTypes = allContentTypes + .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + foreach (var x in usableContentTypes) + list.Add(x); + + // indirect types are those that we use, directly or indirectly + var indirectContentTypes = GetIndirect(source).ToArray(); + foreach (var x in indirectContentTypes) + list.Add(x); + + //// directContentTypes are those we use directly + //// they are already in indirectContentTypes, no need to add to the list + //var directContentTypes = source.ContentTypeComposition.ToArray(); + + //var enabled = usableContentTypes.Select(x => x.Id) // those we can use + // .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used + // .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used + // .Where(x => x != source.ParentId) // but not the parent + // .Distinct() + // .ToArray(); + + return list + .Where(x => x.Id != contentTypeId) + .OrderBy(x => x.Name) + .ToList(); + } + + private static IEnumerable GetIndirect(IContentTypeComposition ctype) + { + // hashset guarantees unicity on Id + var all = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + var stack = new Stack(); + + if (ctype != null) + { + foreach (var x in ctype.ContentTypeComposition) + stack.Push(x); + } + + while (stack.Count > 0) + { + var x = stack.Pop(); + all.Add(x); + foreach (var y in x.ContentTypeComposition) + stack.Push(y); + } + + return all; + } + + } + /// /// Represents the ContentType Service, which is an easy access to operations involving /// diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 443b1dcbff..f9ae8b6da0 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -57,7 +57,6 @@ namespace Umbraco.Web.Editors //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic IContentTypeComposition[] allContentTypes; - switch (type) { case UmbracoObjectTypes.DocumentType: @@ -89,52 +88,11 @@ namespace Umbraco.Web.Editors default: throw new ArgumentOutOfRangeException("The entity type was not a content type"); - } + } + + var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(contentTypeId, allContentTypes); - // note: there are many sanity checks missing here and there ;-(( - // make sure once and for all - //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) - // throw new Exception("A parent does not belong to a composition."); - - // find out if any content type uses this content type - var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == contentTypeId)).ToArray(); - if (isUsing.Length > 0) - { - //if already in use a composition, do not allow any composited types - return new List(); - } - - // if it is not used then composition is possible - // hashset guarantees unicity on Id - var list = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - // usable types are those that are top-level - var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); - foreach (var x in usableContentTypes) - list.Add(x); - - // indirect types are those that we use, directly or indirectly - var indirectContentTypes = GetIndirect(source).ToArray(); - foreach (var x in indirectContentTypes) - list.Add(x); - - //// directContentTypes are those we use directly - //// they are already in indirectContentTypes, no need to add to the list - //var directContentTypes = source.ContentTypeComposition.ToArray(); - - //var enabled = usableContentTypes.Select(x => x.Id) // those we can use - // .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used - // .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used - // .Where(x => x != source.ParentId) // but not the parent - // .Distinct() - // .ToArray(); - - return list - .Where(x => x.Id != contentTypeId) - .OrderBy(x => x.Name) + return filtered .Select(Mapper.Map) .Select(x => { @@ -143,32 +101,7 @@ namespace Umbraco.Web.Editors }) .ToList(); } - - private static IEnumerable GetIndirect(IContentTypeComposition ctype) - { - // hashset guarantees unicity on Id - var all = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - var stack = new Stack(); - - if (ctype != null) - { - foreach (var x in ctype.ContentTypeComposition) - stack.Push(x); - } - - while (stack.Count > 0) - { - var x = stack.Pop(); - all.Add(x); - foreach (var y in x.ContentTypeComposition) - stack.Push(y); - } - - return all; - } + /// /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors From 4cb523fb6630638970602e004b33ace0acef38fc Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Jan 2016 12:29:11 +0100 Subject: [PATCH 2/2] Added tests for ContentTypeServiceExtensions --- .../Services/ContentTypeService.cs | 89 ----------- .../Services/ContentTypeServiceExtensions.cs | 101 ++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../ContentTypeServiceExtensionsTests.cs | 148 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Editors/ContentTypeControllerBase.cs | 2 +- 6 files changed, 252 insertions(+), 90 deletions(-) create mode 100644 src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs create mode 100644 src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 9caa1a7a6e..75afb33a84 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -21,95 +21,6 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public static class ContentTypeServiceExtensions - { - /// - /// Returns the available composite content types for a given content type - /// - /// - public static IEnumerable GetAvailableCompositeContentTypes(this IContentTypeService ctService, - int contentTypeId, - IContentTypeComposition[] allContentTypes) - { - IContentTypeComposition source = null; - - //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic - - // note: there are many sanity checks missing here and there ;-(( - // make sure once and for all - //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) - // throw new Exception("A parent does not belong to a composition."); - - // find out if any content type uses this content type - var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == contentTypeId)).ToArray(); - if (isUsing.Length > 0) - { - //if already in use a composition, do not allow any composited types - return new List(); - } - - // if it is not used then composition is possible - // hashset guarantees unicity on Id - var list = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - // usable types are those that are top-level - var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); - foreach (var x in usableContentTypes) - list.Add(x); - - // indirect types are those that we use, directly or indirectly - var indirectContentTypes = GetIndirect(source).ToArray(); - foreach (var x in indirectContentTypes) - list.Add(x); - - //// directContentTypes are those we use directly - //// they are already in indirectContentTypes, no need to add to the list - //var directContentTypes = source.ContentTypeComposition.ToArray(); - - //var enabled = usableContentTypes.Select(x => x.Id) // those we can use - // .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used - // .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used - // .Where(x => x != source.ParentId) // but not the parent - // .Distinct() - // .ToArray(); - - return list - .Where(x => x.Id != contentTypeId) - .OrderBy(x => x.Name) - .ToList(); - } - - private static IEnumerable GetIndirect(IContentTypeComposition ctype) - { - // hashset guarantees unicity on Id - var all = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - var stack = new Stack(); - - if (ctype != null) - { - foreach (var x in ctype.ContentTypeComposition) - stack.Push(x); - } - - while (stack.Count > 0) - { - var x = stack.Pop(); - all.Add(x); - foreach (var y in x.ContentTypeComposition) - stack.Push(y); - } - - return all; - } - - } - /// /// Represents the ContentType Service, which is an easy access to operations involving /// diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs new file mode 100644 index 0000000000..6b440c6b2c --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + public static class ContentTypeServiceExtensions + { + /// + /// Returns the available composite content types for a given content type + /// + /// + public static IEnumerable GetAvailableCompositeContentTypes(this IContentTypeService ctService, + IContentTypeComposition source, + IContentTypeComposition[] allContentTypes) + { + + if (source == null) throw new ArgumentNullException("source"); + //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + + // note: there are many sanity checks missing here and there ;-(( + // make sure once and for all + //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) + // throw new Exception("A parent does not belong to a composition."); + + // find out if any content type uses this content type + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == source.Id)).ToArray(); + if (isUsing.Length > 0) + { + //if already in use a composition, do not allow any composited types + return new List(); + } + + // if it is not used then composition is possible + // hashset guarantees unicity on Id + var list = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + // usable types are those that are top-level + var usableContentTypes = allContentTypes + .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + foreach (var x in usableContentTypes) + list.Add(x); + + // indirect types are those that we use, directly or indirectly + var indirectContentTypes = GetDirectOrIndirect(source).ToArray(); + foreach (var x in indirectContentTypes) + list.Add(x); + + //// directContentTypes are those we use directly + //// they are already in indirectContentTypes, no need to add to the list + //var directContentTypes = source.ContentTypeComposition.ToArray(); + + //var enabled = usableContentTypes.Select(x => x.Id) // those we can use + // .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used + // .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used + // .Where(x => x != source.ParentId) // but not the parent + // .Distinct() + // .ToArray(); + + return list + .Where(x => x.Id != source.Id) + .OrderBy(x => x.Name) + .ToList(); + } + + /// + /// Get those that we use directly or indirectly + /// + /// + /// + private static IEnumerable GetDirectOrIndirect(IContentTypeComposition ctype) + { + // hashset guarantees unicity on Id + var all = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + var stack = new Stack(); + + if (ctype != null) + { + foreach (var x in ctype.ContentTypeComposition) + stack.Push(x); + } + + while (stack.Count > 0) + { + var x = stack.Pop(); + all.Add(x); + foreach (var y in x.ContentTypeComposition) + stack.Push(y); + } + + return all; + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 97e8a8988e..76cafe293a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -496,6 +496,7 @@ + diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs new file mode 100644 index 0000000000..fbdb45114e --- /dev/null +++ b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs @@ -0,0 +1,148 @@ +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + public class ContentTypeServiceExtensionsTests : BaseUmbracoApplicationTest + { + [Test] + public void GetAvailableCompositeContentTypes_Not_Itself() + { + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] {ct1, ct2, ct3}); + + Assert.AreEqual(2, availableTypes.Count()); + Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); + Assert.AreEqual(ct3.Id, availableTypes.ElementAt(1).Id); + } + + //This shows that a nested comp is not allowed + [Test] + public void GetAvailableCompositeContentTypes_No_Results_If_Already_A_Composition_By_Parent() + { + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1"); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", ct1); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3"); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3 }); + + Assert.AreEqual(0, availableTypes.Count()); + } + + //This shows that a nested comp is not allowed + [Test] + public void GetAvailableCompositeContentTypes_No_Results_If_Already_A_Composition() + { + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1"); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2"); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3"); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + + ct2.AddContentType(ct1); + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3 }); + + Assert.AreEqual(0, availableTypes.Count()); + } + + [Test] + public void GetAvailableCompositeContentTypes_Do_Not_Include_Other_Composed_Types() + { + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1"); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2"); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3"); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + + ct2.AddContentType(ct3); + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3 }); + + Assert.AreEqual(1, availableTypes.Count()); + Assert.AreEqual(ct3.Id, availableTypes.Single().Id); + } + + [Test] + public void GetAvailableCompositeContentTypes_Include_Direct_Composed_Types() + { + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1"); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2"); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3"); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + + ct1.AddContentType(ct3); + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3 }); + + Assert.AreEqual(2, availableTypes.Count()); + Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); + Assert.AreEqual(ct3.Id, availableTypes.ElementAt(1).Id); + } + + [Test] + public void GetAvailableCompositeContentTypes_Include_Indirect_Composed_Types() + { + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1"); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2"); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3"); + var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4"); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + ct4.Id = 4; + + ct1.AddContentType(ct3); //ct3 is direct to ct1 + ct3.AddContentType(ct4); //ct4 is indirect to ct1 + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3 }); + + Assert.AreEqual(3, availableTypes.Count()); + Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); + Assert.AreEqual(ct3.Id, availableTypes.ElementAt(1).Id); + Assert.AreEqual(ct4.Id, availableTypes.ElementAt(2).Id); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f3ed7d225b..ccd42301a3 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -191,6 +191,7 @@ + diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index f9ae8b6da0..540b98b737 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -90,7 +90,7 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(contentTypeId, allContentTypes); + var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes); return filtered .Select(Mapper.Map)