diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index cff81ba2d8..53c13e69a1 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -54,7 +54,7 @@ - + > @@ -140,6 +140,13 @@ + + + + + + + diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index ced63f04bb..1b1404a897 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -280,6 +280,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings } } + [ConfigurationProperty("EnableInheritedDocumentTypes")] + internal InnerTextConfigurationElement EnableInheritedDocumentTypes + { + get + { + return new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], + //set the default + true); + } + } + string IContentSection.NotificationEmailAddress { get { return Notifications.NotificationEmailAddress; } @@ -414,5 +426,10 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return DefaultDocumentTypeProperty; } } + + bool IContentSection.EnableInheritedDocumentTypes + { + get { return EnableInheritedDocumentTypes; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 93e3260b44..ebdd9ae637 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -59,5 +59,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool GlobalPreviewStorageEnabled { get; } string DefaultDocumentTypeProperty { get; } + + bool EnableInheritedDocumentTypes { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs new file mode 100644 index 0000000000..b1d2b45dc5 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResult + { + public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) + { + Composition = composition; + Allowed = allowed; + } + + public IContentTypeComposition Composition { get; private set; } + public bool Allowed { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs new file mode 100644 index 0000000000..653d7a10a9 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResults + { + public ContentTypeAvailableCompositionsResults() + { + Ancestors = Enumerable.Empty(); + Results = Enumerable.Empty(); + } + + public ContentTypeAvailableCompositionsResults(IEnumerable ancestors, IEnumerable results) + { + Ancestors = ancestors; + Results = results; + } + + public IEnumerable Ancestors { get; private set; } + public IEnumerable Results { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index cba0ed07b7..4cf4a08bf1 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -23,8 +23,8 @@ namespace Umbraco.Core.Models protected ContentTypeCompositionBase(IContentTypeComposition parent) : this(parent, null) - { - } + { + } protected ContentTypeCompositionBase(IContentTypeComposition parent, string alias) : base(parent, alias) @@ -122,10 +122,10 @@ namespace Umbraco.Core.Models return false; RemovedContentTypeKeyTracker.Add(contentTypeComposition.Id); - + //If the ContentType we are removing has Compositions of its own these needs to be removed as well var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); - if(compositionIdsToRemove.Any()) + if (compositionIdsToRemove.Any()) RemovedContentTypeKeyTracker.AddRange(compositionIdsToRemove); OnPropertyChanged(ContentTypeCompositionSelector); @@ -215,8 +215,8 @@ namespace Umbraco.Core.Models return false; // get and ensure a group local to this content type - var group = PropertyGroups.Contains(propertyGroupName) - ? PropertyGroups[propertyGroupName] + var group = PropertyGroups.Contains(propertyGroupName) + ? PropertyGroups[propertyGroupName] : AddAndReturnPropertyGroup(propertyGroupName); if (group == null) return false; diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 60d7d8e7ef..85491b9f36 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -234,8 +234,15 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - + //NOTE: This is an important call, we cannot simply make a call to: + // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + // because that method is used to query 'latest' content items where in this case we don't necessarily + // want latest content items because a pulished content item might not actually be the latest. + // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, "Path", Direction.Ascending); + var xmlItems = (from descendant in descendants let xml = serializer(descendant) select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); @@ -728,7 +735,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// public void AddOrUpdateContentXml(IContent content, Func xml) - { + { _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } @@ -826,11 +833,11 @@ namespace Umbraco.Core.Persistence.Repositories var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) .ToArray(); - + var ids = dtos .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) .Select(x => x.TemplateId.Value).ToArray(); - + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); @@ -972,4 +979,4 @@ namespace Umbraco.Core.Persistence.Repositories _contentXmlRepository.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 7599001618..2edc32f367 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1977,6 +1977,10 @@ namespace Umbraco.Core.Services var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { + if (published == false) + { + content.ChangePublishedState(PublishedState.Saved); + } //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed if (content.HasIdentity == false) { @@ -2119,6 +2123,22 @@ namespace Umbraco.Core.Services content.Name, content.Id)); return PublishStatusType.FailedPathNotPublished; } + else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' has expired and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedHasExpired; + } + else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' is awaiting release and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedAwaitingRelease; + } return PublishStatusType.Success; } diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index d16e7946b9..ed04edc6bf 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -10,27 +10,51 @@ namespace Umbraco.Core.Services /// /// Returns the available composite content types for a given content type /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// /// - public static IEnumerable GetAvailableCompositeContentTypes(this IContentTypeService ctService, + internal static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, IContentTypeComposition source, - IContentTypeComposition[] allContentTypes) - { - //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + IContentTypeComposition[] allContentTypes, + string[] filterContentTypes = null, + string[] filterPropertyTypes = null) + { + filterContentTypes = filterContentTypes == null + ? new string[] { } + : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); - // 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."); + filterPropertyTypes = filterPropertyTypes == null + ? new string[] {} + : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); - if (source != null) + //create the full list of property types to use as the filter + //this is the combination of all property type aliases found in the content types passed in for the filter + //as well as the specific property types passed in for the filter + filterPropertyTypes = allContentTypes + .Where(c => filterContentTypes.InvariantContains(c.Alias)) + .SelectMany(c => c.PropertyTypes) + .Select(c => c.Alias) + .Union(filterPropertyTypes) + .ToArray(); + + var sourceId = source != null ? source.Id : 0; + + // find out if any content type uses this content type + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); + if (isUsing.Length > 0) { - // 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 already in use a composition, do not allow any composited types + return new ContentTypeAvailableCompositionsResults(); } // if it is not used then composition is possible @@ -44,48 +68,87 @@ namespace Umbraco.Core.Services .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 != null ? source.Id : 0)) + //At this point we have a list of content types that 'could' be compositions + + //now we'll filter this list based on the filters requested + var filtered = list + .Where(x => + { + //need to filter any content types that are included in this list + return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false; + }) + .Where(x => + { + //need to filter any content types that have matching property aliases that are included in this list + //ensure that we don't return if there's any overlapping property aliases from the filtered ones specified + return filterPropertyTypes.Intersect( + x.PropertyTypes.Select(p => p.Alias), + StringComparer.InvariantCultureIgnoreCase).Any() == false; + }) .OrderBy(x => x.Name) .ToList(); + + //get ancestor ids - we will filter all ancestors + var ancestors = GetAncestors(source, allContentTypes); + var ancestorIds = ancestors.Select(x => x.Id).ToArray(); + + //now we can create our result based on what is still available and the ancestors + var result = list + //not itself + .Where(x => x.Id != sourceId) + .OrderBy(x => x.Name) + .Select(composition => filtered.Contains(composition) + ? new ContentTypeAvailableCompositionsResult(composition, ancestorIds.Contains(composition.Id) == false) + : new ContentTypeAvailableCompositionsResult(composition, false)).ToList(); + + return new ContentTypeAvailableCompositionsResults(ancestors, result); + } + + private static IContentTypeComposition[] GetAncestors(IContentTypeComposition ctype, IContentTypeComposition[] allContentTypes) + { + if (ctype == null) return new IContentTypeComposition[] {}; + var ancestors = new List(); + var parentId = ctype.ParentId; + while (parentId > 0) + { + var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId); + if (parent != null) + { + ancestors.Add(parent); + parentId = parent.ParentId; + } + else + { + parentId = -1; + } + } + return ancestors.ToArray(); } /// - /// Get those that we use directly or indirectly + /// Get those that we use directly /// /// /// private static IEnumerable GetDirectOrIndirect(IContentTypeComposition ctype) { + if (ctype == null) return Enumerable.Empty(); + // 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); - } + + foreach (var x in ctype.ContentTypeComposition) + stack.Push(x); while (stack.Count > 0) { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5b404470b7..00ca6863ed 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -181,7 +181,7 @@ - + @@ -370,6 +370,8 @@ + + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 72fe8f5f36..c8ccecdb95 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -57,6 +57,58 @@ namespace Umbraco.Tests.Persistence.Repositories return repository; } + [Test] + public void Rebuild_Xml_Structures_With_Non_Latest_Version() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + contentTypeRepository.AddOrUpdate(contentType1); + + var allCreated = new List(); + + //create 100 non published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + //create 100 published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + //IMPORTANT testing note here: We need to changed the published state here so that + // it doesn't automatically think this is simply publishing again - this forces the latest + // version to be Saved and not published + allCreated[i].ChangePublishedState(PublishedState.Saved); + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + [Test] public void Rebuild_All_Xml_Structures() { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs index fbdb45114e..ed29cb2108 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs @@ -1,6 +1,9 @@ +using System; using System.Linq; using Moq; using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -10,6 +13,119 @@ namespace Umbraco.Tests.Services [TestFixture] public class ContentTypeServiceExtensionsTests : BaseUmbracoApplicationTest { + [Test] + public void GetAvailableCompositeContentTypes_No_Overlap_By_Content_Type_And_Property_Type_Alias() + { + Action addPropType = (alias, ct) => + { + var contentCollection = new PropertyTypeCollection + { + new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = alias, Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88} + }; + var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 }; + ct.PropertyGroups.Add(pg); + }; + + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null); + addPropType("title", ct2); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null); + addPropType("title", ct3); + var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null); + var ct5 = MockedContentTypes.CreateBasicContentType("ct5", "CT5", null); + addPropType("blah", ct5); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + ct4.Id = 4; + ct5.Id = 4; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3, ct4, ct5 }, + new[] { ct2.Alias }, + new[] { "blah" }) + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); + + Assert.AreEqual(1, availableTypes.Count()); + Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); + } + + [Test] + public void GetAvailableCompositeContentTypes_No_Overlap_By_Property_Type_Alias() + { + Action addPropType = ct => + { + var contentCollection = new PropertyTypeCollection + { + new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88} + }; + var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 }; + ct.PropertyGroups.Add(pg); + }; + + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null); + addPropType(ct2); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null); + addPropType(ct3); + var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + ct4.Id = 4; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3, ct4 }, + new string[] { }, + new[] { "title" }) + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); + + Assert.AreEqual(1, availableTypes.Count()); + Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); + } + + [Test] + public void GetAvailableCompositeContentTypes_No_Overlap_By_Content_Type() + { + Action addPropType = ct => + { + var contentCollection = new PropertyTypeCollection + { + new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88} + }; + var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 }; + ct.PropertyGroups.Add(pg); + }; + + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null); + addPropType(ct2); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null); + addPropType(ct3); + var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + ct4.Id = 4; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3, ct4 }, + new [] {ct2.Alias}) + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); + + Assert.AreEqual(1, availableTypes.Count()); + Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); + } + [Test] public void GetAvailableCompositeContentTypes_Not_Itself() { @@ -24,7 +140,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] {ct1, ct2, ct3}); + new[] {ct1, ct2, ct3}) + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(2, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); @@ -46,7 +163,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }).Results; Assert.AreEqual(0, availableTypes.Count()); } @@ -68,7 +185,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }).Results; Assert.AreEqual(0, availableTypes.Count()); } @@ -89,7 +206,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }) + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct3.Id, availableTypes.Single().Id); @@ -111,7 +229,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }) + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(2, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); @@ -137,7 +256,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }) + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(3, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index c8ea32110e..a0816b1379 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q) { + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout) { function link(scope, el, attr, ctrl) { @@ -116,6 +116,49 @@ } + function filterAvailableCompositions(selectedContentType, selecting) { + + //selecting = true if the user has check the item, false if the user has unchecked the item + + var selectedContentTypeAliases = selecting ? + //the user has selected the item so add to the current list + _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : + //the user has unselected the item so remove from the current list + _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { + return i === selectedContentType.alias; + }); + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function (f) { + return f !== null && f !== undefined; + }); + + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + + return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { + //reset first + current.allowed = true; + //see if this list item is found in the response (allowed) list + var found = _.find(filteredAvailableCompositeTypes, function (f) { + return current.contentType.alias === f.contentType.alias; + }); + + //allow if the item was found in the response (allowed) list - + // and ensure its set to allowed if it is currently checked, + // DO not allow if it's a locked content type. + current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && + (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false); + + }); + }); + } + function updatePropertiesSortOrder() { angular.forEach(scope.model.groups, function(group){ @@ -126,6 +169,20 @@ } + function setupAvailableContentTypesModel(result) { + scope.compositionsDialogModel.availableCompositeContentTypes = result; + //iterate each one and set it up + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { + //set the inherited flags + c.inherited = false; + if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { + c.inherited = true; + } + // convert icons for composite content types + iconHelper.formatContentTypeIcons([c.contentType]); + }); + } + /* ---------- DELETE PROMT ---------- */ scope.togglePrompt = function (object) { @@ -186,7 +243,7 @@ // submit overlay if no compositions has been removed // or the action has been confirmed } else { - + // make sure that all tabs has an init property if (scope.model.groups.length !== 0) { angular.forEach(scope.model.groups, function(group) { @@ -211,30 +268,47 @@ scope.compositionsDialogModel = null; }, - selectCompositeContentType: function(compositeContentType) { + selectCompositeContentType: function (selectedContentType) { - if (scope.model.compositeContentTypes.indexOf(compositeContentType.alias) === -1) { - //merge composition with content type + //first check if this is a new selection - we need to store this value here before any further digests/async + // because after that the scope.model.compositeContentTypes will be populated with the selected value. + var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; - if(scope.contentType === "documentType") { + if (newSelection) { + //merge composition with content type - contentTypeResource.getById(compositeContentType.id).then(function(composition){ - contentTypeHelper.mergeCompositeContentType(scope.model, composition); - }); + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; + resourceLookup(selectedContentType.id).then(function (composition) { + //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and + // double check here. + var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); + if (overlappingAliases.length > 0) { + //this will create an invalid composition, need to uncheck it + scope.compositionsDialogModel.compositeContentTypes.splice( + scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); + //dissallow this until something else is unchecked + selectedContentType.allowed = false; + } + else { + contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } - } else if(scope.contentType === "mediaType") { + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); + }); + } + else { + // split composition from content type + contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); - mediaTypeResource.getById(compositeContentType.id).then(function(composition){ - contentTypeHelper.mergeCompositeContentType(scope.model, composition); - }); - - } - - - } else { - // split composition from content type - contentTypeHelper.splitCompositeContentType(scope.model, compositeContentType); + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); } } @@ -242,21 +316,28 @@ var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount; - $q.all([ + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function(f) { + return f !== null && f !== undefined; + }); + $q.all([ //get available composite types - availableContentTypeResource(scope.model.id).then(function (result) { - scope.compositionsDialogModel.availableCompositeContentTypes = result; - // convert icons for composite content types - iconHelper.formatContentTypeIcons(scope.compositionsDialogModel.availableCompositeContentTypes); + availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { + setupAvailableContentTypesModel(result); }), //get content type count - countContentTypeResource().then(function (result) { + countContentTypeResource().then(function(result) { scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); }) - ]).then(function () { - //resolves when both other promises are done, now show it - scope.compositionsDialogModel.show = true; - }); + ]).then(function() { + //resolves when both other promises are done, now show it + scope.compositionsDialogModel.show = true; + }); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index a59bc83ccb..c3e1368efe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -16,13 +16,37 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to retrieve count'); }, - getAvailableCompositeContentTypes: function (contentTypeId) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + + var query = ""; + _.each(filterContentTypes, function (item) { + query += "filterContentTypes=" + item + "&"; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += "filterContentTypes=&"; + } + _.each(filterPropertyTypes, function (item) { + query += "filterPropertyTypes=" + item + "&"; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += "filterPropertyTypes=&"; + } + query += "contentTypeId=" + contentTypeId; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentTypeApiBaseUrl", "GetAvailableCompositeContentTypes", - [{ contentTypeId: contentTypeId }])), + query)), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index 39315c6074..17a96821fd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -16,13 +16,37 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to retrieve count'); }, - getAvailableCompositeContentTypes: function (contentTypeId) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + + var query = ""; + _.each(filterContentTypes, function (item) { + query += "filterContentTypes=" + item + "&"; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += "filterContentTypes=&"; + } + _.each(filterPropertyTypes, function (item) { + query += "filterPropertyTypes=" + item + "&"; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += "filterPropertyTypes=&"; + } + query += "contentTypeId=" + contentTypeId; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "mediaTypeApiBaseUrl", "GetAvailableCompositeMediaTypes", - [{ contentTypeId: contentTypeId }])), + query)), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index d949844a6c..0649277c54 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -7,13 +7,37 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { - getAvailableCompositeContentTypes: function (contentTypeId) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + + var query = ""; + _.each(filterContentTypes, function (item) { + query += "filterContentTypes=" + item + "&"; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += "filterContentTypes=&"; + } + _.each(filterPropertyTypes, function (item) { + query += "filterPropertyTypes=" + item + "&"; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += "filterPropertyTypes=&"; + } + query += "contentTypeId=" + contentTypeId; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "memberTypeApiBaseUrl", "GetAvailableCompositeMemberTypes", - [{ contentTypeId: contentTypeId }])), + query)), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index f9ffcd4a23..abfa58bc72 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -43,8 +43,41 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter) { return newArray; }, + validateAddingComposition: function(contentType, compositeContentType) { + + //Validate that by adding this group that we are not adding duplicate property type aliases + + var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })); + var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function(f) { + return f !== null && f !== undefined; + }); + + var intersec = _.intersection(propertiesAdding, propAliasesExisting); + if (intersec.length > 0) { + //return the overlapping property aliases + return intersec; + } + + //no overlapping property aliases + return []; + }, + mergeCompositeContentType: function(contentType, compositeContentType) { + //Validate that there are no overlapping aliases + var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); + if (overlappingAliases.length > 0) { + throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); + } + angular.forEach(compositeContentType.groups, function(compositionGroup) { // order composition groups based on sort order @@ -134,7 +167,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter) { // push id to array of merged composite content types compositionGroup.parentTabContentTypes.push(compositeContentType.id); - + // push group before placeholder tab contentType.groups.unshift(compositionGroup); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less index 6098eb07e7..f604b79d04 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less @@ -6,20 +6,27 @@ .umb-checkbox-list__item { display: flex; align-items: center; - margin-bottom: 1px; - //border-bottom: 1px solid @grayLight; + margin-bottom: 2px; } -.umb-checkbox-list__item.-selected { - background: @blue; - color: white; +.umb-checkbox-list__item:last-child { + border-bottom: none; +} + +.umb-checkbox-list__item:hover { + background-color: @grayLighter; +} + +.umb-checkbox-list__item.-selected, +.umb-checkbox-list__item.-disabled { + background-color: fade(@blueDark, 4%); + font-weight: bold; } .umb-checkbox-list__item-checkbox { display: flex; justify-content: center; align-items: center; - background: @grayLighter; flex: 0 0 30px; height: 30px; margin-right: 10px; @@ -34,7 +41,22 @@ } .umb-checkbox-list__item-text { - font-size: 14px; + font-size: 13px; + margin-bottom: 0; + flex: 1 1 auto; +} + +.umb-checkbox-list__item-text.-faded { + opacity: 0.5; +} + +.umb-checkbox-list__item.-disabled .umb-checkbox-list__item-text { + cursor: not-allowed; +} + +.umb-checkbox-list__item-caption { + font-size: 11px; + margin-left: 2px; } .umb-checkbox-list__no-data { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js new file mode 100644 index 0000000000..7a908a7260 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js @@ -0,0 +1,19 @@ + (function() { + "use strict"; + + function CompositionsOverlay($scope) { + + var vm = this; + + vm.isSelected = isSelected; + + function isSelected(alias) { + if($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { + return true; + } + } + } + + angular.module("umbraco").controller("Umbraco.Overlays.CompositionsOverlay", CompositionsOverlay); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 2cdcef5ee6..d5cc52b942 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -1,44 +1,54 @@ -
- -
+
-
- Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. -
+
+ +
- - There are no content types available to use as a composition. - - - This content type is used in a composition, and therefore cannot be composed itself. - +
+ Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. +
-
    -
  • + + There are no content types available to use as a composition. + + + This content type is used in a composition, and therefore cannot be composed itself. + -
    - -
    +
      +
    • - - {{ compositeContentType.name }} -
+ ng-repeat="compositeContentType in model.availableCompositeContentTypes | filter:searchTerm" + ng-class="{'-disabled': compositeContentType.allowed===false, '-selected': vm.isSelected(compositeContentType.contentType.alias)}"> - - \ No newline at end of file +
+ +
+ + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 5a8af019f0..118b992ae5 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -102,6 +102,7 @@ Textstring + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index d72f7c4193..296e21e823 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -169,6 +169,7 @@ Click to upload Drop your files here... + Link to media Create a new member @@ -699,6 +700,9 @@ To manage your website, simply open the Umbraco back office and start adding con %0% could not be published because the item is scheduled for release. ]]> + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 8bbdbab0b5..b7abf9fc46 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -168,6 +168,7 @@ Click to upload Drop your files here... + Link to media Create a new member @@ -699,6 +700,9 @@ To manage your website, simply open the Umbraco back office and start adding con %0% could not be published because the item is scheduled for release. ]]> + diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 997edcc123..533b9898b6 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -91,7 +91,7 @@
- + @@ -119,6 +119,14 @@ + + + + + + + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 87201f3a36..8ed4f8d02a 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -1,230 +1,231 @@ - -
-
-
-
-
-
- - -
-
-
-
- - + +
+
+
+
+
+
- - - - - - + +
+
+
+
+ + - - - - - - + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - + + - - + + - - + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - + + + + + + @@ -232,93 +233,100 @@ - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - + - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 192b49e2cf..cb6e56cb13 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -662,9 +662,17 @@ namespace Umbraco.Web.Editors new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); break; case PublishStatusType.FailedHasExpired: - //TODO: We should add proper error messaging for this! + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; case PublishStatusType.FailedIsTrashed: //TODO: We should add proper error messaging for this! + break; case PublishStatusType.FailedContentInvalid: display.AddWarningNotification( Services.TextService.Localize("publish"), diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index a540054bf7..bf0555e00a 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -95,9 +95,31 @@ namespace Umbraco.Web.Editors return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); } - public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public HttpResponseMessage GetAvailableCompositeContentTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType); + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } [UmbracoTreeAuthorize( @@ -189,7 +211,15 @@ namespace Umbraco.Web.Editors /// public ContentTypeDisplay GetEmpty(int parentId) { - var ct = new ContentType(parentId); + IContentType ct; + if (parentId != Constants.System.Root) + { + var parent = Services.ContentTypeService.GetContentType(parentId); + ct = parent != null ? new ContentType(parent, string.Empty) : new ContentType(parentId); + } + else + ct = new ContentType(parentId); + ct.Icon = "icon-document"; var dto = Mapper.Map(ct); diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index f90c2c3e6b..ac58241b4d 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -9,6 +9,7 @@ using System.Web.Http; using AutoMapper; using Newtonsoft.Json; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; @@ -49,14 +50,29 @@ namespace Umbraco.Web.Editors /// /// Returns the available composite content types for a given content type /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// /// - protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type) + protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, + UmbracoObjectTypes type, + string[] filterContentTypes, + string[] filterPropertyTypes) { IContentTypeComposition source = null; //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: @@ -90,18 +106,31 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes); + var availableCompositions = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); - return filtered - .Select(Mapper.Map) + var currCompositions = source == null ? new IContentTypeComposition[] { } : source.ContentTypeComposition.ToArray(); + var compAliases = currCompositions.Select(x => x.Alias).ToArray(); + var ancestors = availableCompositions.Ancestors.Select(x => x.Alias); + + return availableCompositions.Results + .Select(x => new Tuple(Mapper.Map(x.Composition), x.Allowed)) .Select(x => { - x.Name = TranslateItem(x.Name); + //translate the name + x.Item1.Name = TranslateItem(x.Item1.Name); + + //we need to ensure that the item is enabled if it is already selected + // but do not allow it if it is any of the ancestors + if (compAliases.Contains(x.Item1.Alias) && ancestors.Contains(x.Item1.Alias) == false) + { + //re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!) + x = new Tuple(x.Item1, true); + } + return x; }) .ToList(); } - /// /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors @@ -166,7 +195,7 @@ namespace Umbraco.Web.Editors //Validate that there's no other ct with the same name var exists = getContentTypeByAlias(contentTypeSave.Alias); - if (exists != null) + if (exists != null && exists.Id.ToInvariantString() != contentTypeSave.Id.ToString()) { ModelState.AddModelError("Alias", "A content type with this alias already exists"); } diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index a62e8d27e0..540c95edc1 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -87,9 +87,31 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public HttpResponseMessage GetAvailableCompositeMediaTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType); + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } public ContentTypeCompositionDisplay GetEmpty(int parentId) diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 44eafe1500..924ecb3e05 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -79,9 +79,31 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public HttpResponseMessage GetAvailableCompositeMemberTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType); + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } public ContentTypeCompositionDisplay GetEmpty() diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs index 9a138dd828..5f6e2c5ce5 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -44,6 +44,10 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "compositeContentTypes")] public IEnumerable CompositeContentTypes { get; set; } + //Locked compositions + [DataMember(Name = "lockedCompositeContentTypes")] + public IEnumerable LockedCompositeContentTypes { get; set; } + [DataMember(Name = "allowAsRoot")] public bool AllowAsRoot { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index e59a474afb..5ad728ccab 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -54,10 +54,11 @@ namespace Umbraco.Web.Models.Mapping { return mapping .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) - .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) .ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore()) .ForMember(dto => dto.Notifications, expression => expression.Ignore()) - .ForMember(dto => dto.Errors, expression => expression.Ignore()); + .ForMember(dto => dto.Errors, expression => expression.Ignore()) + .ForMember(dto => dto.LockedCompositeContentTypes, exp => exp.Ignore()); } public static IMappingExpression MapBaseContentTypeEntityToDisplay( @@ -81,6 +82,10 @@ namespace Umbraco.Web.Models.Mapping dto => dto.CompositeContentTypes, expression => expression.MapFrom(dto => dto.ContentTypeComposition)) + .ForMember( + dto => dto.LockedCompositeContentTypes, + expression => expression.ResolveUsing(new LockedCompositionsResolver(applicationContext))) + .ForMember( dto => dto.Groups, expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, propertyEditorResolver))); diff --git a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs new file mode 100644 index 0000000000..d3d692d10d --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models.Mapping +{ + internal class LockedCompositionsResolver : ValueResolver> + { + private readonly ApplicationContext _applicationContext; + + public LockedCompositionsResolver(ApplicationContext applicationContext) + { + _applicationContext = applicationContext; + } + + protected override IEnumerable ResolveCore(IContentTypeComposition source) + { + var aliases = new List(); + // get ancestor ids from path of parent if not root + if (source.ParentId != Constants.System.Root) + { + var parent = _applicationContext.Services.ContentTypeService.GetContentType(source.ParentId); + if (parent != null) + { + var ancestorIds = parent.Path.Split(',').Select(int.Parse); + // loop through all content types and return ordered aliases of ancestors + var allContentTypes = _applicationContext.Services.ContentTypeService.GetAllContentTypes().ToArray(); + foreach (var ancestorId in ancestorIds) + { + var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor != null) + { + aliases.Add(ancestor.Alias); + } + } + } + } + return aliases.OrderBy(x => x); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 9a71686c28..94961d9793 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -9,6 +9,7 @@ using System.Web.Routing; using AutoMapper; using umbraco; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; using Umbraco.Core.PropertyEditors; @@ -136,6 +137,37 @@ namespace Umbraco.Web.Models.Mapping } }; + var helper = new UmbracoHelper(UmbracoContext.Current); + var mediaItem = helper.TypedMedia(media.Id); + if (mediaItem != null) + { + var crops = new List(); + var autoFillProperties = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties.ToArray(); + if (autoFillProperties.Any()) + { + foreach (var field in autoFillProperties) + { + var crop = mediaItem.GetCropUrl(field.Alias, string.Empty); + if (string.IsNullOrWhiteSpace(crop) == false) + crops.Add(crop.Split('?')[0]); + } + + if (crops.Any()) + { + var link = new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("media/urls"), + // don't add the querystring, split on the "?" will also work if there is no "?" + Value = string.Join(",", crops), + View = "urllist" + }; + + genericProperties.Add(link); + } + } + } + TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties); } diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index e29f9d6be9..c45d5d0c1b 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -4,6 +4,7 @@ using System.Net.Http.Formatting; using umbraco; using umbraco.BusinessLogic.Actions; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi.Filters; @@ -62,6 +63,8 @@ namespace Umbraco.Web.Trees { var menu = new MenuItemCollection(); + var enableInheritedDocumentTypes = UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes; + if (id == Constants.System.Root.ToInvariantString()) { //set the default to create @@ -91,13 +94,35 @@ namespace Umbraco.Web.Trees if (container.HasChildren() == false) { //can delete doc type - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); } menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); } else { - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); + var ct = Services.ContentTypeService.GetContentType(int.Parse(id)); + IContentType parent = null; + parent = ct == null ? null : Services.ContentTypeService.GetContentType(ct.ParentId); + + if (enableInheritedDocumentTypes) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + //no move action if this is a child doc type + if (parent == null) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); + } + } + else + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); + //no move action if this is a child doc type + if (parent == null) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); + } + } menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionExport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity { Id = int.Parse(id), @@ -106,6 +131,8 @@ namespace Umbraco.Web.Trees Name = "" }, "documenttypes", "settings"); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); + if (enableInheritedDocumentTypes) + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); } return menu; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a88c21f9bb..fba27991a8 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -309,6 +309,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 0bee9e631a..054ee7cbe6 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -191,7 +191,7 @@ namespace umbraco // check if document *is* published, it could be unpublished by an event if (d.Published) { - var parentId = d.Level == 1 ? -1 : d.Parent.Id; + var parentId = d.Level == 1 ? -1 : d.ParentId; // fix sortOrder - see note in UpdateSortOrder var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false); diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index ce2b55152b..e71a290754 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -70,7 +70,7 @@ namespace umbraco var docParentId = -1; try { - docParentId = document.Parent.Id; + docParentId = document.ParentId; } catch (ArgumentException) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs index 49296884fa..ace78f6803 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs @@ -93,7 +93,7 @@ namespace umbraco.presentation.channels p.dateCreated = d.CreateDateTime; p.page_title = d.Text; p.page_id = d.Id; - p.page_parent_id = d.Parent.Id; + p.page_parent_id = d.ParentId; blogPostsObjects.Add(p); } @@ -150,7 +150,7 @@ namespace umbraco.presentation.channels p.dateCreated = d.CreateDateTime; p.title = d.Text; p.page_id = d.Id; - p.wp_page_parent_id = d.Parent.Id; + p.wp_page_parent_id = d.ParentId; p.wp_page_parent_title = d.Parent.Text; p.permalink = library.NiceUrl(d.Id); p.description = d.getProperty(userChannel.FieldDescriptionAlias).Value.ToString(); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index b8d37fbb65..2499c1c817 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -83,7 +83,7 @@ namespace umbraco.presentation.preview var previewNodes = new List(); - var parentId = documentObject.Level == 1 ? -1 : documentObject.Parent.Id; + var parentId = documentObject.Level == 1 ? -1 : documentObject.ParentId; while (parentId > 0 && XmlContent.GetElementById(parentId.ToString(CultureInfo.InvariantCulture)) == null) { @@ -97,7 +97,7 @@ namespace umbraco.presentation.preview foreach (var document in previewNodes) { //Inject preview xml - parentId = document.Level == 1 ? -1 : document.Parent.Id; + parentId = document.Level == 1 ? -1 : document.ParentId; var previewXml = document.ToPreviewXml(XmlContent); if (document.ContentEntity.Published == false && ApplicationContext.Current.Services.ContentService.HasPublishedVersion(document.Id)) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs index 42b9cd757b..411734ae99 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs @@ -19,13 +19,13 @@ namespace umbraco /// Media nodes as IEnumerable public static IEnumerable GetAncestorMedia(this Media media) { - var ancestor = new Media(media.Parent.Id); + var ancestor = new Media(media.ParentId); while (ancestor != null && ancestor.Id != -1) { yield return ancestor; - ancestor = new Media(ancestor.Parent.Id); + ancestor = new Media(ancestor.ParentId); } } @@ -53,7 +53,7 @@ namespace umbraco { if (media.Parent != null) { - var parentMedia = new Media(media.Parent.Id); + var parentMedia = new Media(media.ParentId); foreach (var siblingMedia in parentMedia.GetChildMedia().Where(childMedia => childMedia.Id != media.Id)) { diff --git a/src/umbraco.businesslogic/UmbracoSettings.cs b/src/umbraco.businesslogic/UmbracoSettings.cs index cc821528e2..3d489cfaa4 100644 --- a/src/umbraco.businesslogic/UmbracoSettings.cs +++ b/src/umbraco.businesslogic/UmbracoSettings.cs @@ -609,6 +609,17 @@ namespace umbraco get { return UmbracoConfig.For.UmbracoSettings().Content.DefaultDocumentTypeProperty; } } + /// + /// Enables inherited document types. + /// This feature is not recommended and therefore is not enabled by default in new installations. + /// Inherited document types will not be supported in v8. + /// + //[Obsolete("This will not be supported in v8")] + public static bool EnableInheritedDocumentTypes + { + get { return UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes; } + } + private static string _path; /// diff --git a/src/umbraco.cms/businesslogic/CMSNode.cs b/src/umbraco.cms/businesslogic/CMSNode.cs index 661e620056..3b4e9c9b31 100644 --- a/src/umbraco.cms/businesslogic/CMSNode.cs +++ b/src/umbraco.cms/businesslogic/CMSNode.cs @@ -617,6 +617,7 @@ order by level,sortOrder"; /// /// Target CMSNode id [Obsolete("Obsolete, Use Umbraco.Core.Services.ContentService.Move() or Umbraco.Core.Services.MediaService.Move()", false)] + [EditorBrowsable(EditorBrowsableState.Never)] public virtual void Move(int newParentId) { CMSNode parent = new CMSNode(newParentId); @@ -796,6 +797,7 @@ order by level,sortOrder"; internal set { _parentid = value; } } + private IUmbracoEntity _parent; /// /// Given the hierarchical tree structure a CMSNode has only one newParent but can have many children /// @@ -805,15 +807,21 @@ order by level,sortOrder"; get { if (Level == 1) throw new ArgumentException("No newParent node"); - return new CMSNode(_parentid); + if (_parent == null) + { + _parent = ApplicationContext.Current.Services.EntityService.Get(Entity.ParentId); + } + return new CMSNode(_parent); } set { _parentid = value.Id; - SqlHelper.ExecuteNonQuery("update umbracoNode set parentId = " + value.Id.ToString() + " where id = " + this.Id.ToString()); + SqlHelper.ExecuteNonQuery("update umbracoNode set parentId = " + value.Id + " where id = " + this.Id.ToString()); if (Entity != null) Entity.ParentId = value.Id; + + _parent = value.Entity; } } @@ -1183,7 +1191,7 @@ order by level,sortOrder"; x.Attributes.Append(xmlHelper.addAttribute(xd, "id", this.Id.ToString())); x.Attributes.Append(xmlHelper.addAttribute(xd, "key", this.UniqueId.ToString())); if (this.Level > 1) - x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", this.Parent.Id.ToString())); + x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", this.ParentId.ToString())); else x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", "-1")); x.Attributes.Append(xmlHelper.addAttribute(xd, "level", this.Level.ToString())); diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index 36eafb91a2..f014ddae93 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -115,6 +115,14 @@ namespace umbraco.cms.businesslogic #region Public Properties + /// + /// Get the newParent id of the node + /// + public override int ParentId + { + get { return ContentBase == null ? base.ParentId : ContentBase.ParentId; } + } + /// /// The current Content objects ContentType, which defines the Properties of the Content (data) /// @@ -411,7 +419,7 @@ namespace umbraco.cms.businesslogic x.Attributes.Append(XmlHelper.AddAttribute(xd, "key", this.UniqueId.ToString())); x.Attributes.Append(XmlHelper.AddAttribute(xd, "version", this.Version.ToString())); if (this.Level > 1) - x.Attributes.Append(XmlHelper.AddAttribute(xd, "parentID", this.Parent.Id.ToString())); + x.Attributes.Append(XmlHelper.AddAttribute(xd, "parentID", this.ParentId.ToString())); else x.Attributes.Append(XmlHelper.AddAttribute(xd, "parentID", "-1")); x.Attributes.Append(XmlHelper.AddAttribute(xd, "level", this.Level.ToString())); diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index dbecf24220..43f375671e 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -473,7 +473,7 @@ namespace umbraco.cms.businesslogic.web #endregion #region Public Properties - + public override int sortOrder { get @@ -514,10 +514,7 @@ namespace umbraco.cms.businesslogic.web public override int ParentId { - get - { - return ContentEntity == null ? base.ParentId : ContentEntity.ParentId; - } + get { return ContentEntity == null ? base.ParentId : ContentEntity.ParentId; } } public override string Path @@ -1291,7 +1288,7 @@ namespace umbraco.cms.businesslogic.web x.Attributes.Append(addAttribute(xd, "key", UniqueId.ToString())); // x.Attributes.Append(addAttribute(xd, "version", Version.ToString())); if (Level > 1) - x.Attributes.Append(addAttribute(xd, "parentID", Parent.Id.ToString())); + x.Attributes.Append(addAttribute(xd, "parentID", ParentId.ToString())); else x.Attributes.Append(addAttribute(xd, "parentID", "-1")); x.Attributes.Append(addAttribute(xd, "level", Level.ToString()));