diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs
index eff7202201..75afb33a84 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;
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 443b1dcbff..540b98b737 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(source, 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