diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 618b5bd3bc..30ac526f4a 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.Serialization; @@ -15,6 +16,7 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] + [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] public abstract class ContentTypeBase : Entity, IContentTypeBase { private Lazy _parentId; @@ -497,7 +499,6 @@ namespace Umbraco.Core.Models /// Alias of the to remove public void RemovePropertyType(string propertyTypeAlias) { - //check if the property exist in one of our collections if (PropertyGroups.Any(group => group.PropertyTypes.Any(pt => pt.Alias == propertyTypeAlias)) || _propertyTypes.Any(x => x.Alias == propertyTypeAlias)) @@ -524,6 +525,7 @@ namespace Umbraco.Core.Models public void RemovePropertyGroup(string propertyGroupName) { PropertyGroups.RemoveItem(propertyGroupName); + OnPropertyChanged(PropertyGroupCollectionSelector); } /// diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 1706ba5ab1..c4c0362acc 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -20,7 +20,6 @@ namespace Umbraco.Core.Models private string _email; private string _rawPasswordValue; private object _providerUserKey; - private Type _userTypeKey; /// /// Constructor for creating an empty Member object @@ -114,7 +113,6 @@ namespace Umbraco.Core.Models private static readonly PropertyInfo EmailSelector = ExpressionHelper.GetPropertyInfo(x => x.Email); private static readonly PropertyInfo PasswordSelector = ExpressionHelper.GetPropertyInfo(x => x.RawPasswordValue); private static readonly PropertyInfo ProviderUserKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ProviderUserKey); - private static readonly PropertyInfo UserTypeKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ProviderUserKeyType); /// /// Gets or sets the Username @@ -502,38 +500,7 @@ namespace Umbraco.Core.Models } } - /// - /// Gets or sets the type of the provider user key. - /// - /// - /// The type of the provider user key. - /// - [IgnoreDataMember] - internal Type ProviderUserKeyType - { - get - { - return _userTypeKey; - } - private set - { - SetPropertyValueAndDetectChanges(o => - { - _userTypeKey = value; - return _userTypeKey; - }, _userTypeKey, UserTypeKeySelector); - } - } - - /// - /// Sets the type of the provider user key. - /// - /// The type. - internal void SetProviderUserKeyType(Type type) - { - ProviderUserKeyType = type; - } - + /// /// Method to call when Entity is being saved /// diff --git a/src/Umbraco.Core/Models/Membership/MembershipUserExtensions.cs b/src/Umbraco.Core/Models/Membership/MembershipUserExtensions.cs index 6008c0ae02..2289983033 100644 --- a/src/Umbraco.Core/Models/Membership/MembershipUserExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/MembershipUserExtensions.cs @@ -7,9 +7,9 @@ namespace Umbraco.Core.Models.Membership { internal static class MembershipUserExtensions { - internal static UmbracoMembershipMember AsConcreteMembershipUser(this IMembershipUser member, string providerName) + internal static UmbracoMembershipMember AsConcreteMembershipUser(this IMembershipUser member, string providerName, bool providerKeyAsGuid = false) { - var membershipMember = new UmbracoMembershipMember(member, providerName); + var membershipMember = new UmbracoMembershipMember(member, providerName, providerKeyAsGuid); return membershipMember; } diff --git a/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs b/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs index 1b8c7f5393..0f02f4f73b 100644 --- a/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs +++ b/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs @@ -1,5 +1,6 @@ using System; using System.Web.Security; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Models.Membership { @@ -25,7 +26,7 @@ namespace Umbraco.Core.Models.Membership //NOTE: We are not calling the base constructor which will validate that a provider with the specified name exists which causes issues with unit tests. The ctor // validation for that doesn't need to be there anyways (have checked the source). - public UmbracoMembershipMember(IMembershipUser member, string providerName) + public UmbracoMembershipMember(IMembershipUser member, string providerName, bool providerKeyAsGuid = false) { _member = member; //NOTE: We are copying the values here so that everything is consistent with how the underlying built-in ASP.Net membership user @@ -37,7 +38,7 @@ namespace Umbraco.Core.Models.Membership if (member.PasswordQuestion != null) _passwordQuestion = member.PasswordQuestion.Trim(); _providerName = providerName; - _providerUserKey = member.ProviderUserKey; + _providerUserKey = providerKeyAsGuid ? member.ProviderUserKey : member.Id; _comment = member.Comments; _isApproved = member.IsApproved; _isLockedOut = member.IsLockedOut; diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index ba9b89d779..c9b55bd937 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -58,7 +58,6 @@ namespace Umbraco.Core.Models.Membership private IUserType _userType; private string _name; - private Type _userTypeKey; private List _addedSections; private List _removedSections; private ObservableCollection _sectionCollection; @@ -80,7 +79,6 @@ namespace Umbraco.Core.Models.Membership private static readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); private static readonly PropertyInfo AllowedSectionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedSections); private static readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); - private static readonly PropertyInfo UserTypeKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ProviderUserKeyType); private static readonly PropertyInfo UsernameSelector = ExpressionHelper.GetPropertyInfo(x => x.Username); private static readonly PropertyInfo EmailSelector = ExpressionHelper.GetPropertyInfo(x => x.Email); @@ -101,37 +99,6 @@ namespace Umbraco.Core.Models.Membership set { throw new NotSupportedException("Cannot set the provider user key for a user"); } } - /// - /// Gets or sets the type of the provider user key. - /// - /// - /// The type of the provider user key. - /// - [IgnoreDataMember] - internal Type ProviderUserKeyType - { - get - { - return _userTypeKey; - } - private set - { - SetPropertyValueAndDetectChanges(o => - { - _userTypeKey = value; - return _userTypeKey; - }, _userTypeKey, UserTypeKeySelector); - } - } - - /// - /// Sets the type of the provider user key. - /// - /// The type. - internal void SetProviderUserKeyType(Type type) - { - ProviderUserKeyType = type; - } [DataMember] public string Username diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index 3162634ceb..399fa22be6 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -77,6 +77,10 @@ namespace Umbraco.Core.Models var exists = this.Contains(item.Id); if (exists) { + var keyExists = this.Contains(item.Name); + if(keyExists) + throw new Exception(string.Format("Naming conflict: Changing the name of PropertyGroup '{0}' would result in duplicates", item.Name)); + SetItem(IndexOfKey(item.Id), item); return; } diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index fb7e82b27e..a19b7d98c2 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -1,9 +1,9 @@ using System; +using System.Diagnostics; using System.Reflection; using System.Runtime.Serialization; using System.Text.RegularExpressions; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Strings; @@ -14,6 +14,7 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] + [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] public class PropertyType : Entity, IEquatable { private readonly bool _isExplicitDbType; diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 81a7321cbe..8c1b24329c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; @@ -297,14 +298,36 @@ AND umbracoNode.id <> @id", //Delete Tabs/Groups by excepting entries from db with entries from collections var dbPropertyGroups = Database.Fetch("WHERE contenttypeNodeId = @Id", new {Id = entity.Id}) - .Select(x => new Tuple(x.Id, x.Text)); + .Select(x => new Tuple(x.Id, x.Text)) + .ToList(); var entityPropertyGroups = entity.PropertyGroups.Select(x => new Tuple(x.Id, x.Name)).ToList(); - var tabs = dbPropertyGroups.Except(entityPropertyGroups); + var tabsToDelete = dbPropertyGroups.Select(x => x.Item1).Except(entityPropertyGroups.Select(x => x.Item1)); + var tabs = dbPropertyGroups.Where(x => tabsToDelete.Any(y => y == x.Item1)); //Update Tab name downstream to ensure renaming is done properly foreach (var propertyGroup in entityPropertyGroups) { Database.Update("SET Text = @TabName WHERE parentGroupId = @TabId", - new { TabId = propertyGroup.Item1, TabName = propertyGroup.Item2 }); + new { TabName = propertyGroup.Item2, TabId = propertyGroup.Item1 }); + + var childGroups = Database.Fetch("WHERE parentGroupId = @TabId", new { TabId = propertyGroup.Item1 }); + foreach (var childGroup in childGroups) + { + var sibling = Database.Fetch("WHERE contenttypeNodeId = @Id AND text = @Name", + new { Id = childGroup.ContentTypeNodeId, Name = propertyGroup.Item2 }) + .FirstOrDefault(x => x.ParentGroupId.HasValue == false || x.ParentGroupId.Value.Equals(propertyGroup.Item1) == false); + //If the child group doesn't have a sibling there is no chance of duplicates and we continue + if (sibling == null || (sibling.ParentGroupId.HasValue && sibling.ParentGroupId.Value.Equals(propertyGroup.Item1))) continue; + + //Since the child group has a sibling with the same name we need to point any PropertyTypes to the sibling + //as this child group is about to leave the party. + Database.Update( + "SET propertyTypeGroupId = @PropertyTypeGroupId WHERE propertyTypeGroupId = @PropertyGroupId AND ContentTypeId = @ContentTypeId", + new { PropertyTypeGroupId = sibling.Id, PropertyGroupId = childGroup.Id, ContentTypeId = childGroup.ContentTypeNodeId }); + + //Since the parent group has been renamed and we have duplicates we remove this group + //and leave our sibling in charge of the part. + Database.Delete(childGroup); + } } //Do Tab updates foreach (var tab in tabs) diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 0acbf8b693..4c1f9cd038 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Diagnostics; using System.Linq; using System.Text; using System.Xml.Linq; @@ -283,7 +284,7 @@ namespace Umbraco.Core.Services } } - private void ValidateLocked(IContentTypeComposition compo) + private void ValidateLocked(IContentTypeComposition compositionContentType) { // performs business-level validation of the composition // should ensure that it is absolutely safe to save the composition @@ -291,8 +292,8 @@ namespace Umbraco.Core.Services // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors) // but that cannot be used (conflict with descendants) - var contentType = compo as IContentType; - var mediaType = compo as IMediaType; + var contentType = compositionContentType as IContentType; + var mediaType = compositionContentType as IMediaType; IContentTypeComposition[] allContentTypes; if (contentType != null) @@ -302,52 +303,42 @@ namespace Umbraco.Core.Services else throw new Exception("Composition is neither IContentType nor IMediaType?"); - // recursively find all descendants + var compositionAliases = compositionContentType.CompositionAliases(); + var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y))); + var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).ToArray(); + var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id)); var comparer = new DelegateEqualityComparer((x, y) => x.Id == y.Id, x => x.Id); - var descendants = new HashSet(comparer); - var stack = new Stack(); - foreach (var z in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compo.Id))) - stack.Push(z); + var dependencies = new HashSet(compositions, comparer); + var stack = new Stack(); + indirectReferences.ForEach(stack.Push);//Push indirect references to a stack, so we can add recursively while (stack.Count > 0) { - var c = stack.Pop(); - descendants.Add(c); - foreach (var z in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == c.Id))) - stack.Push(z); + var indirectReference = stack.Pop(); + dependencies.Add(indirectReference); + //Get all compositions for the current indirect reference + var directReferences = indirectReference.ContentTypeComposition; + + foreach (var directReference in directReferences) + { + if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue; + dependencies.Add(directReference); + //A direct reference has compositions of its own - these also need to be taken into account + var directReferenceGraph = directReference.CompositionAliases(); + allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))).ForEach(c => dependencies.Add(c)); + } + //Recursive lookup of indirect references + allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)).ForEach(stack.Push); } - // ensure that no descendant has a property with an alias that is used by content type - var aliases = compo.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).ToArray(); - foreach (var d in descendants) + foreach (var dependency in dependencies) { - var intersect = d.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(aliases).ToArray(); + if (dependency.Id == compositionContentType.Id) continue; + var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase)); + if (contentTypeDependency == null) continue; + var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); if (intersect.Length == 0) continue; - var message = string.Format("The following property aliases conflict with descendants : {0}.", - string.Join(", ", intersect)); - throw new Exception(message); - } - - // find all ancestors - var ancestors = new HashSet(comparer); - stack.Clear(); - foreach (var z in compo.ContentTypeComposition) - stack.Push(z); - while (stack.Count > 0) - { - var c = stack.Pop(); - ancestors.Add(c); - foreach (var z in c.ContentTypeComposition) - stack.Push(z); - } - - // ensure that no ancestor has a property with an alias that is used by content type - foreach (var a in ancestors) - { - var intersect = a.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(aliases).ToArray(); - if (intersect.Length == 0) continue; - - var message = string.Format("The following property aliases conflict with ancestors : {0}.", + var message = string.Format("The following PropertyType aliases from the current ContentType conflict with existing PropertyType aliases: {0}.", string.Join(", ", intersect)); throw new Exception(message); } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs index b5f1b255c6..e2e1b91087 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs @@ -74,7 +74,8 @@ namespace Umbraco.Tests.PublishedContent { var contentTypeService = ctx.Application.Services.ContentTypeService; var baseType = new ContentType(-1) {Alias = "base", Name = "Base"}; - var inheritedType = new ContentType(baseType, baseType.Alias) {Alias = "inherited", Name = "Inherited"}; + const string contentTypeAlias = "inherited"; + var inheritedType = new ContentType(baseType, contentTypeAlias) {Alias = contentTypeAlias, Name = "Inherited"}; contentTypeService.Save(baseType); contentTypeService.Save(inheritedType); createContentTypes = false; diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index a986bb678e..f430a93e23 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.Remoting; using NUnit.Framework; using System; using System.Collections.Generic; @@ -5,6 +6,8 @@ using System.Linq; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Caching; +using Umbraco.Tests.CodeFirst.TestModels.Composition; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -178,10 +181,11 @@ namespace Umbraco.Tests.Services /*,"Navigation"*/); cts.Save(ctBase); - var ctHomePage = new ContentType(ctBase, ctBase.Alias) + const string contentTypeAlias = "HomePage"; + var ctHomePage = new ContentType(ctBase, contentTypeAlias) { Name = "Home Page", - Alias = "HomePage", + Alias = contentTypeAlias, Icon = "settingDomain.gif", Thumbnail = "folder.png", AllowedAsRoot = true @@ -191,7 +195,7 @@ namespace Umbraco.Tests.Services cts.Save(ctHomePage); // Act - var homeDoc = cs.CreateContent("Home Page", -1, "HomePage"); + var homeDoc = cs.CreateContent("Home Page", -1, contentTypeAlias); cs.SaveAndPublishWithStatus(homeDoc); // Assert @@ -243,13 +247,13 @@ namespace Umbraco.Tests.Services var global = MockedContentTypes.CreateSimpleContentType("global", "Global"); service.Save(global); - var components = MockedContentTypes.CreateSimpleContentType("components", "Components", global); + var components = MockedContentTypes.CreateSimpleContentType("components", "Components", global, true); service.Save(components); - var component = MockedContentTypes.CreateSimpleContentType("component", "Component", components); + var component = MockedContentTypes.CreateSimpleContentType("component", "Component", components, true); service.Save(component); - var category = MockedContentTypes.CreateSimpleContentType("category", "Category", global); + var category = MockedContentTypes.CreateSimpleContentType("category", "Category", global, true); service.Save(category); var success = category.AddContentType(component); @@ -465,7 +469,7 @@ namespace Umbraco.Tests.Services } [Test] - public void Cannot_Add_Duplicate_PropertyType_Alias_To__Referenced_Composition() + public void Cannot_Add_Duplicate_PropertyType_Alias_To_Referenced_Composition() { //Related the second issue in screencast from this post http://issues.umbraco.org/issue/U4-5986 @@ -489,13 +493,440 @@ namespace Umbraco.Tests.Services Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 }; var added = composition.AddPropertyType(duplicatePropertyType, "Meta"); - service.Save(composition); // Assert Assert.That(added, Is.True); + Assert.Throws(() => service.Save(composition)); Assert.DoesNotThrow(() => service.GetContentType("simpleChildPage")); } + [Test] + public void Cannot_Add_Duplicate_PropertyType_Alias_In_Composition_Graph() + { + // Arrange + var service = ServiceContext.ContentTypeService; + + var basePage = MockedContentTypes.CreateSimpleContentType("basePage", "Base Page", null, true); + service.Save(basePage); + var contentPage = MockedContentTypes.CreateSimpleContentType("contentPage", "Content Page", basePage); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateSimpleContentType("advancedPage", "Advanced Page", contentPage, true); + service.Save(advancedPage); + + var metaComposition = MockedContentTypes.CreateMetaContentType(); + service.Save(metaComposition); + var seoComposition = MockedContentTypes.CreateSeoContentType(); + service.Save(seoComposition); + + var metaAdded = contentPage.AddContentType(metaComposition); + service.Save(contentPage); + var seoAdded = advancedPage.AddContentType(seoComposition); + service.Save(advancedPage); + + // Act + var duplicatePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var addedToBasePage = basePage.AddPropertyType(duplicatePropertyType, "Content"); + var addedToAdvancedPage = advancedPage.AddPropertyType(duplicatePropertyType, "Content"); + var addedToMeta = metaComposition.AddPropertyType(duplicatePropertyType, "Meta"); + var addedToSeo = seoComposition.AddPropertyType(duplicatePropertyType, "Seo"); + + // Assert + Assert.That(metaAdded, Is.True); + Assert.That(seoAdded, Is.True); + + Assert.That(addedToBasePage, Is.True); + Assert.That(addedToAdvancedPage, Is.False); + Assert.That(addedToMeta, Is.True); + Assert.That(addedToSeo, Is.True); + + Assert.Throws(() => service.Save(basePage)); + Assert.Throws(() => service.Save(metaComposition)); + Assert.Throws(() => service.Save(seoComposition)); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + Assert.DoesNotThrow(() => service.GetContentType("meta")); + Assert.DoesNotThrow(() => service.GetContentType("seo")); + } + + [Test] + public void Cannot_Add_Duplicate_PropertyType_Alias_At_Root_Which_Conflicts_With_Third_Levels_Composition() + { + /* + * BasePage, gets 'Title' added but should not be allowed + * -- Content Page + * ---- Advanced Page -> Content Meta + * Content Meta :: Composition, has 'Title' + * + * Content Meta has 'Title' PropertyType + * Adding 'Title' to BasePage should fail + */ + + // Arrange + var service = ServiceContext.ContentTypeService; + var basePage = MockedContentTypes.CreateBasicContentType(); + service.Save(basePage); + var contentPage = MockedContentTypes.CreateBasicContentType("contentPage", "Content Page", basePage); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateBasicContentType("advancedPage", "Advanced Page", contentPage); + service.Save(advancedPage); + + var contentMetaComposition = MockedContentTypes.CreateContentMetaContentType(); + service.Save(contentMetaComposition); + + // Act + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = basePage.AddPropertyType(bodyTextPropertyType, "Content"); + service.Save(basePage); + + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorAdded = contentPage.AddPropertyType(authorPropertyType, "Content"); + service.Save(contentPage); + + var compositionAdded = advancedPage.AddContentType(contentMetaComposition); + service.Save(advancedPage); + + //NOTE: It should not be possible to Save 'BasePage' with the Title PropertyType added + var titlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var titleAdded = basePage.AddPropertyType(titlePropertyType, "Content"); + + // Assert + Assert.That(bodyTextAdded, Is.True); + Assert.That(authorAdded, Is.True); + Assert.That(titleAdded, Is.True); + Assert.That(compositionAdded, Is.True); + + Assert.Throws(() => service.Save(basePage)); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + } + + [Test] + public void Cannot_Rename_PropertyType_Alias_On_Composition_Which_Would_Cause_Conflict_In_Other_Composition() + { + /* + * Meta renames alias to 'title' + * Seo has 'Title' + * BasePage + * -- ContentPage + * ---- AdvancedPage -> Seo + * ------ MoreAdvanedPage -> Meta + */ + + // Arrange + var service = ServiceContext.ContentTypeService; + var basePage = MockedContentTypes.CreateBasicContentType(); + service.Save(basePage); + var contentPage = MockedContentTypes.CreateBasicContentType("contentPage", "Content Page", basePage); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateBasicContentType("advancedPage", "Advanced Page", contentPage); + service.Save(advancedPage); + var moreAdvancedPage = MockedContentTypes.CreateBasicContentType("moreAdvancedPage", "More Advanced Page", advancedPage); + service.Save(moreAdvancedPage); + + var seoComposition = MockedContentTypes.CreateSeoContentType(); + service.Save(seoComposition); + var metaComposition = MockedContentTypes.CreateMetaContentType(); + service.Save(metaComposition); + + // Act + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = basePage.AddPropertyType(bodyTextPropertyType, "Content"); + service.Save(basePage); + + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorAdded = contentPage.AddPropertyType(authorPropertyType, "Content"); + service.Save(contentPage); + + var subtitlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "subtitle", Name = "Subtitle", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var subtitleAdded = advancedPage.AddPropertyType(subtitlePropertyType, "Content"); + service.Save(advancedPage); + + var titlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var titleAdded = seoComposition.AddPropertyType(titlePropertyType, "Content"); + service.Save(seoComposition); + + var seoCompositionAdded = advancedPage.AddContentType(seoComposition); + var metaCompositionAdded = moreAdvancedPage.AddContentType(metaComposition); + service.Save(advancedPage); + service.Save(moreAdvancedPage); + + var keywordsPropertyType = metaComposition.PropertyTypes.First(x => x.Alias.Equals("metakeywords")); + keywordsPropertyType.Alias = "title"; + + // Assert + Assert.That(bodyTextAdded, Is.True); + Assert.That(subtitleAdded, Is.True); + Assert.That(authorAdded, Is.True); + Assert.That(titleAdded, Is.True); + Assert.That(seoCompositionAdded, Is.True); + Assert.That(metaCompositionAdded, Is.True); + + Assert.Throws(() => service.Save(metaComposition)); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + Assert.DoesNotThrow(() => service.GetContentType("moreAdvancedPage")); + } + + [Test] + public void Can_Add_Additional_Properties_On_Composition_Once_Composition_Has_Been_Saved() + { + /* + * Meta renames alias to 'title' + * Seo has 'Title' + * BasePage + * -- ContentPage + * ---- AdvancedPage -> Seo + * ------ MoreAdvancedPage -> Meta + */ + + // Arrange + var service = ServiceContext.ContentTypeService; + var basePage = MockedContentTypes.CreateBasicContentType(); + service.Save(basePage); + var contentPage = MockedContentTypes.CreateBasicContentType("contentPage", "Content Page", basePage); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateBasicContentType("advancedPage", "Advanced Page", contentPage); + service.Save(advancedPage); + var moreAdvancedPage = MockedContentTypes.CreateBasicContentType("moreAdvancedPage", "More Advanced Page", advancedPage); + service.Save(moreAdvancedPage); + + var seoComposition = MockedContentTypes.CreateSeoContentType(); + service.Save(seoComposition); + var metaComposition = MockedContentTypes.CreateMetaContentType(); + service.Save(metaComposition); + + // Act + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = basePage.AddPropertyType(bodyTextPropertyType, "Content"); + service.Save(basePage); + + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorAdded = contentPage.AddPropertyType(authorPropertyType, "Content"); + service.Save(contentPage); + + var subtitlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "subtitle", Name = "Subtitle", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var subtitleAdded = advancedPage.AddPropertyType(subtitlePropertyType, "Content"); + service.Save(advancedPage); + + var titlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var titleAdded = seoComposition.AddPropertyType(titlePropertyType, "Content"); + service.Save(seoComposition); + + var seoCompositionAdded = advancedPage.AddContentType(seoComposition); + var metaCompositionAdded = moreAdvancedPage.AddContentType(metaComposition); + service.Save(advancedPage); + service.Save(moreAdvancedPage); + + // Assert + Assert.That(bodyTextAdded, Is.True); + Assert.That(subtitleAdded, Is.True); + Assert.That(authorAdded, Is.True); + Assert.That(titleAdded, Is.True); + Assert.That(seoCompositionAdded, Is.True); + Assert.That(metaCompositionAdded, Is.True); + + var testPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "test", Name = "Test", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var testAdded = seoComposition.AddPropertyType(testPropertyType, "Content"); + service.Save(seoComposition); + + Assert.That(testAdded, Is.True); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + Assert.DoesNotThrow(() => service.GetContentType("moreAdvancedPage")); + } + + [Test] + public void Cannot_Rename_PropertyGroup_On_Child_Avoiding_Conflict_With_Parent_PropertyGroup() + { + // Arrange + var service = ServiceContext.ContentTypeService; + var page = MockedContentTypes.CreateSimpleContentType("page", "Page", null, true, "Content"); + service.Save(page); + var contentPage = MockedContentTypes.CreateSimpleContentType("contentPage", "Content Page", page, true, "Content_"); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateSimpleContentType("advancedPage", "Advanced Page", contentPage, true, "Details"); + service.Save(advancedPage); + + var contentMetaComposition = MockedContentTypes.CreateContentMetaContentType(); + service.Save(contentMetaComposition); + + // Act + var subtitlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "subtitle", Name = "Subtitle", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var subtitleAdded = contentPage.AddPropertyType(subtitlePropertyType, "Content"); + var authorAdded = contentPage.AddPropertyType(authorPropertyType, "Content"); + service.Save(contentPage); + + var compositionAdded = contentPage.AddContentType(contentMetaComposition); + service.Save(contentPage); + + //Change the name of the tab on the "root" content type 'page'. + var propertyGroup = contentPage.PropertyGroups["Content_"]; + Assert.Throws(() => contentPage.PropertyGroups.Add(new PropertyGroup + { + Id = propertyGroup.Id, + Name = "Content", + SortOrder = 0 + })); + + // Assert + Assert.That(compositionAdded, Is.True); + Assert.That(subtitleAdded, Is.True); + Assert.That(authorAdded, Is.True); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + } + + [Test] + public void Cannot_Rename_PropertyType_Alias_Causing_Conflicts_With_Parents() + { + // Arrange + var service = ServiceContext.ContentTypeService; + var basePage = MockedContentTypes.CreateBasicContentType(); + service.Save(basePage); + var contentPage = MockedContentTypes.CreateBasicContentType("contentPage", "Content Page", basePage); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateBasicContentType("advancedPage", "Advanced Page", contentPage); + service.Save(advancedPage); + + // Act + var titlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var titleAdded = basePage.AddPropertyType(titlePropertyType, "Content"); + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = contentPage.AddPropertyType(bodyTextPropertyType, "Content"); + var subtitlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "subtitle", Name = "Subtitle", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var subtitleAdded = contentPage.AddPropertyType(subtitlePropertyType, "Content"); + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorAdded = advancedPage.AddPropertyType(authorPropertyType, "Content"); + service.Save(basePage); + service.Save(contentPage); + service.Save(advancedPage); + + //Rename the PropertyType to something that already exists in the Composition - NOTE this should not be allowed and Saving should throw an exception + var authorPropertyTypeToRename = advancedPage.PropertyTypes.First(x => x.Alias.Equals("author")); + authorPropertyTypeToRename.Alias = "title"; + + // Assert + Assert.That(bodyTextAdded, Is.True); + Assert.That(authorAdded, Is.True); + Assert.That(titleAdded, Is.True); + Assert.That(subtitleAdded, Is.True); + + Assert.Throws(() => service.Save(advancedPage)); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + } + + [Test] + public void Can_Add_PropertyType_Alias_Which_Exists_In_Composition_Outside_Graph() + { + /* + * Meta (Composition) + * Content Meta (Composition) has 'Title' -> Meta + * BasePage + * -- ContentPage gets 'Title' added -> Meta + * ---- Advanced Page + */ + // Arrange + var service = ServiceContext.ContentTypeService; + + var basePage = MockedContentTypes.CreateSimpleContentType("basePage", "Base Page", null, true); + service.Save(basePage); + var contentPage = MockedContentTypes.CreateSimpleContentType("contentPage", "Content Page", basePage, true); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateSimpleContentType("advancedPage", "Advanced Page", contentPage, true); + service.Save(advancedPage); + + var metaComposition = MockedContentTypes.CreateMetaContentType(); + service.Save(metaComposition); + + var contentMetaComposition = MockedContentTypes.CreateContentMetaContentType(); + service.Save(contentMetaComposition); + + var metaAdded = contentPage.AddContentType(metaComposition); + service.Save(contentPage); + + var metaAddedToComposition = contentMetaComposition.AddContentType(metaComposition); + service.Save(contentMetaComposition); + + // Act + var propertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var addedToContentPage = contentPage.AddPropertyType(propertyType, "Content"); + + // Assert + Assert.That(metaAdded, Is.True); + Assert.That(metaAddedToComposition, Is.True); + + Assert.That(addedToContentPage, Is.True); + Assert.DoesNotThrow(() => service.Save(contentPage)); + } + [Test] public void Can_Rename_PropertyGroup_With_Inherited_PropertyGroups() { @@ -549,6 +980,254 @@ namespace Umbraco.Tests.Services Assert.That(compPropertyTypeCount, Is.EqualTo(10)); } + [Test] + public void Can_Rename_PropertyGroup_On_Parent_Without_Causing_Duplicate_PropertyGroups() + { + // Arrange + var service = ServiceContext.ContentTypeService; + var page = MockedContentTypes.CreateSimpleContentType("page", "Page", null, true, "Content_"); + service.Save(page); + var contentPage = MockedContentTypes.CreateSimpleContentType("contentPage", "Content Page", page, true, "Contentx"); + service.Save(contentPage); + var advancedPage = MockedContentTypes.CreateSimpleContentType("advancedPage", "Advanced Page", contentPage, true, "Contenty"); + service.Save(advancedPage); + + var contentMetaComposition = MockedContentTypes.CreateContentMetaContentType(); + service.Save(contentMetaComposition); + var compositionAdded = contentPage.AddContentType(contentMetaComposition); + service.Save(contentPage); + + // Act + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var subtitlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "subtitle", Name = "Subtitle", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = contentPage.AddPropertyType(bodyTextPropertyType, "Content_");//Will be added to the parent tab + var subtitleAdded = contentPage.AddPropertyType(subtitlePropertyType, "Content");//Will be added to the "Content Meta" composition + service.Save(contentPage); + + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var descriptionPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "description", Name = "Description", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var keywordsPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "keywords", Name = "Keywords", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorAdded = advancedPage.AddPropertyType(authorPropertyType, "Content_");//Will be added to an ancestor tab + var descriptionAdded = advancedPage.AddPropertyType(descriptionPropertyType, "Contentx");//Will be added to a parent tab + var keywordsAdded = advancedPage.AddPropertyType(keywordsPropertyType, "Content");//Will be added to the "Content Meta" composition + service.Save(advancedPage); + + //Change the name of the tab on the "root" content type 'page'. + var propertyGroup = page.PropertyGroups["Content_"]; + page.PropertyGroups.Add(new PropertyGroup { Id = propertyGroup.Id, Name = "Content", SortOrder = 0 }); + service.Save(page); + + // Assert + Assert.That(compositionAdded, Is.True); + Assert.That(bodyTextAdded, Is.True); + Assert.That(subtitleAdded, Is.True); + Assert.That(authorAdded, Is.True); + Assert.That(descriptionAdded, Is.True); + Assert.That(keywordsAdded, Is.True); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + + var advancedPageReloaded = service.GetContentType("advancedPage"); + var contentUnderscoreTabExists = advancedPageReloaded.CompositionPropertyGroups.Any(x => x.Name.Equals("Content_")); + Assert.That(contentUnderscoreTabExists, Is.False); + + var numberOfContentTabs = advancedPageReloaded.CompositionPropertyGroups.Count(x => x.Name.Equals("Content")); + Assert.That(numberOfContentTabs, Is.EqualTo(4)); + } + + [Test] + public void Can_Rename_PropertyGroup_On_Parent_Without_Causing_Duplicate_PropertyGroups_v2() + { + // Arrange + var service = ServiceContext.ContentTypeService; + var page = MockedContentTypes.CreateSimpleContentType("page", "Page", null, true, "Content_"); + service.Save(page); + var contentPage = MockedContentTypes.CreateSimpleContentType("contentPage", "Content Page", page, true, "Content"); + service.Save(contentPage); + + var contentMetaComposition = MockedContentTypes.CreateContentMetaContentType(); + service.Save(contentMetaComposition); + + // Act + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var subtitlePropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "subtitle", Name = "Subtitle", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = page.AddPropertyType(bodyTextPropertyType, "Content_"); + var subtitleAdded = contentPage.AddPropertyType(subtitlePropertyType, "Content"); + var authorAdded = contentPage.AddPropertyType(authorPropertyType, "Content_"); + service.Save(page); + service.Save(contentPage); + + var compositionAdded = contentPage.AddContentType(contentMetaComposition); + service.Save(contentPage); + + //Change the name of the tab on the "root" content type 'page'. + var propertyGroup = page.PropertyGroups["Content_"]; + page.PropertyGroups.Add(new PropertyGroup { Id = propertyGroup.Id, Name = "Content", SortOrder = 0 }); + service.Save(page); + + // Assert + Assert.That(compositionAdded, Is.True); + Assert.That(bodyTextAdded, Is.True); + Assert.That(subtitleAdded, Is.True); + Assert.That(authorAdded, Is.True); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + } + + [Test] + public void Can_Remove_PropertyGroup_On_Parent_Without_Causing_Duplicate_PropertyGroups() + { + // Arrange + var service = ServiceContext.ContentTypeService; + var basePage = MockedContentTypes.CreateBasicContentType(); + service.Save(basePage); + + var contentPage = MockedContentTypes.CreateBasicContentType("contentPage", "Content Page", basePage); + service.Save(contentPage); + + var advancedPage = MockedContentTypes.CreateBasicContentType("advancedPage", "Advanced Page", contentPage); + service.Save(advancedPage); + + var contentMetaComposition = MockedContentTypes.CreateContentMetaContentType(); + service.Save(contentMetaComposition); + + // Act + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = basePage.AddPropertyType(bodyTextPropertyType, "Content"); + service.Save(basePage); + + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorAdded = contentPage.AddPropertyType(authorPropertyType, "Content"); + service.Save(contentPage); + + var compositionAdded = contentPage.AddContentType(contentMetaComposition); + service.Save(contentPage); + + basePage.RemovePropertyGroup("Content"); + service.Save(basePage); + + // Assert + Assert.That(bodyTextAdded, Is.True); + Assert.That(authorAdded, Is.True); + Assert.That(compositionAdded, Is.True); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + + var contentType = service.GetContentType("contentPage"); + var propertyGroup = contentType.PropertyGroups["Content"]; + Assert.That(propertyGroup.ParentId.HasValue, Is.False); + } + + [Test] + public void Can_Add_PropertyGroup_With_Same_Name_On_Parent_and_Child() + { + /* + * BasePage + * - Content Page + * -- Advanced Page + * Content Meta :: Composition + */ + + // Arrange + var service = ServiceContext.ContentTypeService; + var basePage = MockedContentTypes.CreateBasicContentType(); + service.Save(basePage); + + var contentPage = MockedContentTypes.CreateBasicContentType("contentPage", "Content Page", basePage); + service.Save(contentPage); + + var advancedPage = MockedContentTypes.CreateBasicContentType("advancedPage", "Advanced Page", contentPage); + service.Save(advancedPage); + + var contentMetaComposition = MockedContentTypes.CreateContentMetaContentType(); + service.Save(contentMetaComposition); + + // Act + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "author", Name = "Author", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var authorAdded = contentPage.AddPropertyType(authorPropertyType, "Content"); + service.Save(contentPage); + + var bodyTextPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "bodyText", Name = "Body Text", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 + }; + var bodyTextAdded = basePage.AddPropertyType(bodyTextPropertyType, "Content"); + service.Save(basePage); + + var compositionAdded = contentPage.AddContentType(contentMetaComposition); + service.Save(contentPage); + + // Assert + Assert.That(bodyTextAdded, Is.True); + Assert.That(authorAdded, Is.True); + Assert.That(compositionAdded, Is.True); + + Assert.DoesNotThrow(() => service.GetContentType("contentPage")); + Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); + + var contentType = service.GetContentType("contentPage"); + var propertyGroup = contentType.PropertyGroups["Content"]; + Assert.That(propertyGroup.ParentId.HasValue, Is.False); + + var numberOfContentTabs = contentType.CompositionPropertyGroups.Count(x => x.Name.Equals("Content")); + Assert.That(numberOfContentTabs, Is.EqualTo(3)); + + //Ensure that adding a new PropertyType to the "Content"-tab also adds it to the right group + + var descriptionPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) + { + Alias = "description", Name = "Description", Description = "", HelpText = "", Mandatory = false, SortOrder = 1,DataTypeDefinitionId = -88 + }; + var descriptionAdded = contentType.AddPropertyType(descriptionPropertyType, "Content"); + service.Save(contentType); + Assert.That(descriptionAdded, Is.True); + + var contentPageReloaded = service.GetContentType("contentPage"); + var propertyGroupReloaded = contentPageReloaded.PropertyGroups["Content"]; + var hasDescriptionPropertyType = propertyGroupReloaded.PropertyTypes.Contains("description"); + Assert.That(hasDescriptionPropertyType, Is.True); + Assert.That(propertyGroupReloaded.ParentId.HasValue, Is.False); + + var descriptionPropertyTypeReloaded = propertyGroupReloaded.PropertyTypes["description"]; + Assert.That(descriptionPropertyTypeReloaded.PropertyGroupId.IsValueCreated, Is.False); + } + private ContentType CreateComponent() { var component = new ContentType(-1) @@ -572,9 +1251,10 @@ namespace Umbraco.Tests.Services private ContentType CreateBannerComponent(ContentType parent) { - var banner = new ContentType(parent, parent.Alias) + const string contentTypeAlias = "banner"; + var banner = new ContentType(parent, contentTypeAlias) { - Alias = "banner", + Alias = contentTypeAlias, Name = "Banner Component", Description = "ContentType used for Banner Component", Icon = ".sprTreeDoc3", @@ -620,9 +1300,10 @@ namespace Umbraco.Tests.Services private ContentType CreateHomepage(ContentType parent) { - var contentType = new ContentType(parent, parent.Alias) + const string contentTypeAlias = "homepage"; + var contentType = new ContentType(parent, contentTypeAlias) { - Alias = "homepage", + Alias = contentTypeAlias, Name = "Homepage", Description = "ContentType used for the Homepage", Icon = ".sprTreeDoc3", diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index c2ecc1e3e2..39916453dc 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -6,7 +6,25 @@ namespace Umbraco.Tests.TestHelpers.Entities { public class MockedContentTypes { - + public static ContentType CreateBasicContentType(string alias = "basePage", string name = "Base Page", + ContentType parent = null) + { + var contentType = parent == null ? new ContentType(-1) : new ContentType(parent, alias); + + contentType.Alias = alias; + contentType.Name = name; + contentType.Description = "ContentType used for basic pages"; + contentType.Icon = ".sprTreeDoc3"; + contentType.Thumbnail = "doc2.png"; + contentType.SortOrder = 1; + contentType.CreatorId = 0; + contentType.Trashed = false; + + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + + return contentType; + } public static ContentType CreateTextpageContentType(string alias = "textPage", string name = "Text Page") { @@ -65,6 +83,57 @@ namespace Umbraco.Tests.TestHelpers.Entities return contentType; } + public static ContentType CreateContentMetaContentType() + { + var contentType = new ContentType(-1) + { + Alias = "contentMeta", + Name = "Content Meta", + Description = "ContentType used for Content Meta", + Icon = ".sprTreeDoc3", + Thumbnail = "doc.png", + SortOrder = 1, + CreatorId = 0, + Trashed = false + }; + + var metaCollection = new PropertyTypeCollection(); + metaCollection.Add(new PropertyType("test", DataTypeDatabaseType.Ntext) { Alias = "title", Name = "Title", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 }); + + contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Content", SortOrder = 2 }); + + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + + return contentType; + } + + public static ContentType CreateSeoContentType() + { + var contentType = new ContentType(-1) + { + Alias = "seo", + Name = "Seo", + Description = "ContentType used for Seo", + Icon = ".sprTreeDoc3", + Thumbnail = "doc.png", + SortOrder = 1, + CreatorId = 0, + Trashed = false + }; + + var metaCollection = new PropertyTypeCollection(); + metaCollection.Add(new PropertyType("seotest", DataTypeDatabaseType.Ntext) { Alias = "seokeywords", Name = "Seo Keywords", Description = "", HelpText = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88 }); + metaCollection.Add(new PropertyType("seotest", DataTypeDatabaseType.Ntext) { Alias = "seodescription", Name = "Seo Description", Description = "", HelpText = "", Mandatory = false, SortOrder = 2, DataTypeDefinitionId = -89 }); + + contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Seo", SortOrder = 5 }); + + //ensure that nothing is marked as dirty + contentType.ResetDirtyProperties(false); + + return contentType; + } + public static ContentType CreateSimpleContentType() { var contentType = new ContentType(-1) diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml index d05908e2cf..0e13eba345 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml @@ -130,7 +130,7 @@ - +
member
/create/member.ascx @@ -324,7 +324,7 @@
Macro
- /Create/PartialView.ascx + /Create/PartialView.ascx @@ -338,4 +338,20 @@
+ +
Macro
+ /Create/PartialView.ascx + + + + +
+ +
Macro
+ /Create/PartialViewMacro.ascx + + + + +
diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml index c2a4c90d0a..1ab0f4180c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml @@ -330,14 +330,6 @@ - -
Macro
- /Create/PartialView.ascx - - - - -
Macro
/Create/PartialViewMacro.ascx @@ -346,6 +338,14 @@
+ +
Macro
+ /Create/PartialView.ascx + + + + +
Macro
/Create/PartialViewMacro.ascx diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 0faf9a61a3..f066476948 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -415,7 +415,7 @@ namespace Umbraco.Web.Security if (member != null) { var propValue = member.Properties[prop.Alias]; - if (propValue != null) + if (propValue != null && propValue.Value != null) { value = propValue.Value.ToString(); } diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index 4ad0836a12..916194b5cc 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -42,6 +42,7 @@ namespace Umbraco.Web.Security.Providers private string _defaultMemberTypeAlias = "Member"; private volatile bool _hasDefaultMember = false; private static readonly object Locker = new object(); + private bool _providerKeyAsGuid = false; public override string ProviderName { @@ -58,7 +59,7 @@ namespace Umbraco.Web.Security.Providers protected override MembershipUser ConvertToMembershipUser(IMember entity) { - return entity.AsConcreteMembershipUser(Name); + return entity.AsConcreteMembershipUser(Name, _providerKeyAsGuid); } public string LockPropertyTypeAlias { get; private set; } @@ -85,6 +86,15 @@ namespace Umbraco.Web.Security.Providers } _hasDefaultMember = true; } + + //devs can configure the provider user key to be a guid if they want, by default it is int + if (config["providerKeyType"] != null) + { + if (config["providerKeyType"] == "guid") + { + _providerKeyAsGuid = true; + } + } } public override string DefaultMemberTypeAlias diff --git a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs index 16ce523645..5d18039fb6 100644 --- a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Security.Providers protected override MembershipUser ConvertToMembershipUser(IUser entity) { //the provider user key is always the int id - return entity.AsConcreteMembershipUser(Name); + return entity.AsConcreteMembershipUser(Name, true); } public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs index a1f4762aa6..3a051939a5 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs @@ -16,6 +16,7 @@ using System.Web.UI.WebControls; using ClientDependency.Core; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Strings; @@ -308,7 +309,7 @@ namespace umbraco.controls Trace.Write("ContentTypeControlNew", "executing task"); //we need to re-set the UmbracoContext since it will be nulled and our cache handlers need it - //global::Umbraco.Web.UmbracoContext.Current = asyncState.UmbracoContext; + global::Umbraco.Web.UmbracoContext.Current = asyncState.UmbracoContext; _contentType.ContentTypeItem.Name = txtName.Text; _contentType.ContentTypeItem.Alias = txtAlias.Text; // raw, contentType.Alias takes care of it @@ -351,8 +352,16 @@ namespace umbraco.controls var compositionType = isMediaType ? Services.ContentTypeService.GetMediaType(compositionId).SafeCast() : Services.ContentTypeService.GetContentType(compositionId).SafeCast(); - var added = _contentType.ContentTypeItem.AddContentType(compositionType); - //TODO if added=false then return error message + try + { + //TODO if added=false then return error message + var added = _contentType.ContentTypeItem.AddContentType(compositionType); + } + catch (InvalidCompositionException ex) + { + state.SaveArgs.IconType = BasePage.speechBubbleIcon.error; + state.SaveArgs.Message = ex.Message; + } } // then iterate over removed = existing except checked @@ -1073,7 +1082,7 @@ jQuery(document).ready(function() {{ refreshDropDowns(); }}); private string GetHtmlForNoPropertiesMessageListItem() { - return @"
  • " + ui.Text("settings", "noPropertiesDefinedOnTab") + "
  • "; + return @"
  • " + ui.Text("settings", "noPropertiesDefinedOnTab", Security.CurrentUser) + "
  • "; } private void SavePropertyType(SaveClickEventArgs e, IContentTypeComposition contentTypeItem) @@ -1127,7 +1136,7 @@ jQuery(document).ready(function() {{ refreshDropDowns(); }}); } else { - e.Message = ui.Text("contentTypeDublicatePropertyType"); + e.Message = ui.Text("contentTypeDublicatePropertyType", Security.CurrentUser); e.IconType = BasePage.speechBubbleIcon.warning; } } @@ -1430,7 +1439,7 @@ jQuery(document).ready(function() {{ refreshDropDowns(); }}); LoadContentType(); - var ea = new SaveClickEventArgs(ui.Text("contentTypeTabCreated")); + var ea = new SaveClickEventArgs(ui.Text("contentTypeTabCreated", Security.CurrentUser)); ea.IconType = BasePage.speechBubbleIcon.success; RaiseBubbleEvent(new object(), ea); @@ -1469,7 +1478,7 @@ jQuery(document).ready(function() {{ refreshDropDowns(); }}); LoadContentType(); - var ea = new SaveClickEventArgs(ui.Text("contentTypeTabDeleted")); + var ea = new SaveClickEventArgs(ui.Text("contentTypeTabDeleted", Security.CurrentUser)); ea.IconType = BasePage.speechBubbleIcon.success; RaiseBubbleEvent(new object(), ea);