diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index 7af4a38c14..5fb0a79ee3 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -4,8 +4,8 @@
-
- +
+
@@ -67,190 +67,6 @@ > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 6d8c0bbb05..cc273bc7b0 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -31,8 +31,8 @@ namespace Umbraco.Core.Models /// Name of the content /// Parent object /// ContentType for the current Content object - public Content(string name, IContent parent, IContentType contentType) - : this(name, parent, contentType, new PropertyCollection()) + public Content(string name, IContent parent, IContentType contentType, string culture = null) + : this(name, parent, contentType, new PropertyCollection(), culture) { } /// @@ -42,8 +42,8 @@ namespace Umbraco.Core.Models /// Parent object /// ContentType for the current Content object /// Collection of properties - public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties) - : base(name, parent, contentType, properties) + public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, string culture = null) + : base(name, parent, contentType, properties, culture) { _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _publishedState = PublishedState.Unpublished; @@ -56,8 +56,8 @@ namespace Umbraco.Core.Models /// Name of the content /// Id of the Parent content /// ContentType for the current Content object - public Content(string name, int parentId, IContentType contentType) - : this(name, parentId, contentType, new PropertyCollection()) + public Content(string name, int parentId, IContentType contentType, string culture = null) + : this(name, parentId, contentType, new PropertyCollection(), culture) { } /// @@ -67,8 +67,8 @@ namespace Umbraco.Core.Models /// Id of the Parent content /// ContentType for the current Content object /// Collection of properties - public Content(string name, int parentId, IContentType contentType, PropertyCollection properties) - : base(name, parentId, contentType, properties) + public Content(string name, int parentId, IContentType contentType, PropertyCollection properties, string culture = null) + : base(name, parentId, contentType, properties, culture) { _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _publishedState = PublishedState.Unpublished; @@ -207,20 +207,27 @@ namespace Umbraco.Core.Models { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - - if (culture == null) + + // this is the only place where we set PublishName (apart from factories etc), and we must ensure + // that we do have an invariant name, as soon as we have a variant name, else we would end up not + // being able to publish - and not being able to change the name, as PublishName is readonly. + // see also: DocumentRepository.EnsureInvariantNameValues() - which deals with Name. + // see also: U4-11286 + if (culture == null || string.IsNullOrEmpty(PublishName)) { PublishName = name; PublishDate = date; - return; + } + + if (culture != null) + { + // private method, assume that culture is valid + + if (_publishInfos == null) + _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _publishInfos[culture] = (name, date); } - - // private method, assume that culture is valid - - if (_publishInfos == null) - _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _publishInfos[culture] = (name, date); } /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 61d0ddbb9c..bbeb3f943e 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -31,8 +31,8 @@ namespace Umbraco.Core.Models /// /// Initializes a new instance of the class. /// - protected ContentBase(string name, int parentId, IContentTypeComposition contentType, PropertyCollection properties) - : this(name, contentType, properties) + protected ContentBase(string name, int parentId, IContentTypeComposition contentType, PropertyCollection properties, string culture = null) + : this(name, contentType, properties, culture) { if (parentId == 0) throw new ArgumentOutOfRangeException(nameof(parentId)); ParentId = parentId; @@ -41,22 +41,23 @@ namespace Umbraco.Core.Models /// /// Initializes a new instance of the class. /// - protected ContentBase(string name, IContentBase parent, IContentTypeComposition contentType, PropertyCollection properties) - : this(name, contentType, properties) + protected ContentBase(string name, IContentBase parent, IContentTypeComposition contentType, PropertyCollection properties, string culture = null) + : this(name, contentType, properties, culture) { if (parent == null) throw new ArgumentNullException(nameof(parent)); SetParent(parent); } - private ContentBase(string name, IContentTypeComposition contentType, PropertyCollection properties) + private ContentBase(string name, IContentTypeComposition contentType, PropertyCollection properties, string culture = null) { ContentTypeBase = contentType ?? throw new ArgumentNullException(nameof(contentType)); // initially, all new instances have Id = 0; // no identity VersionId = 0; // no versions + + SetName(culture, name); - Name = name; _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); _properties.EnsurePropertyTypes(PropertyTypes); diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index c0e4214881..0c232ff1e5 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -159,20 +159,7 @@ namespace Umbraco.Core.Models return Current.Services.MediaService.GetById(media.ParentId); } #endregion - - #region Variants - - /// - /// Returns true if the content has any property type that allows language variants - /// - public static bool HasPropertyTypeVaryingByCulture(this IContent content) - { - // fixme - what about CultureSegment? what about content.ContentType.Variations? - return content.PropertyTypes.Any(x => x.Variations.Has(ContentVariation.CultureNeutral)); - } - - #endregion - + /// /// Removes characters that are not valide XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index a9eed7edb1..e66e0f125b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -177,9 +177,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM cmsPreviewXml WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", "DELETE FROM cmsContentXml WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", @@ -229,6 +231,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IContent entity) { + //fixme - stop doing this just so we have access to AddingEntity var content = (Content) entity; content.AddingEntity(); @@ -238,7 +241,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.Template == null) entity.Template = entity.ContentType.DefaultTemplate; - // ensure unique name on the same level + // sanitize names: ensure we have an invariant name, and names are unique-ish + // (well, only invariant name is unique at the moment) + EnsureInvariantNameValues(entity, publishing); entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); // ensure that strings don't contain characters that are invalid in xml @@ -305,7 +310,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.PublishedVersionId = content.VersionId; contentVersionDto.Id = 0; contentVersionDto.Current = true; - contentVersionDto.Text = content.Name; + contentVersionDto.Text = content.PublishName; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -416,7 +421,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionId)); } - // ensure unique name on the same level + // sanitize names: ensure we have an invariant name, and names are unique-ish + // (well, only invariant name is unique at the moment) + EnsureInvariantNameValues(entity, publishing); entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); // ensure that strings don't contain characters that are invalid in xml @@ -1074,6 +1081,43 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Utilities + /// + /// Ensures that the Name/PublishName properties are filled in and validates if all names are null + /// + private void EnsureInvariantNameValues(IContent content, bool publishing) + { + // here we have to ensure we have names and publish names, + // and to try and fix the situation if we have no name + // see also: U4-11286 + + // cannot save without an invariant name + if (string.IsNullOrWhiteSpace(content.Name)) + { + // no variant name = error + if (content.Names.Count == 0) + throw new InvalidOperationException("Cannot save content with an empty name."); + + // else pick the name for the default culture, or any name + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + if (defaultCulture != null && content.Names.TryGetValue(defaultCulture, out var cultureName)) + content.Name = cultureName; + else + content.Name = content.Names.First().Value; // only option is to take the first + } + + // cannot publish without an invariant name + if (publishing && string.IsNullOrWhiteSpace(content.PublishName)) + { + // no variant name = error + if (content.PublishNames.Count == 0) + throw new InvalidOperationException("Cannot publish content with an empty name."); + + // else... we cannot deal with it here because PublishName is readonly, so in reality, PublishName + // should not be null because it should have been set when preparing the content for publish. + // see also: Content.SetPublishInfos() - it deals with PublishName + } + } + protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 09711cf9e1..0dd7a25798 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -24,9 +24,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal class EntityRepository : IEntityRepository { private readonly IScopeAccessor _scopeAccessor; - public EntityRepository(IScopeAccessor scopeAccessor) + private readonly ILanguageRepository _langRepository; + + public EntityRepository(IScopeAccessor scopeAccessor, ILanguageRepository langRepository) { _scopeAccessor = scopeAccessor; + _langRepository = langRepository; } protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database; @@ -889,17 +892,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// /// /// - private static EntitySlim BuildDocumentEntity(ContentEntityDto dto) + private EntitySlim BuildDocumentEntity(ContentEntityDto dto) { // EntitySlim does not track changes var entity = new DocumentEntitySlim(); BuildDocumentEntity(entity, dto); - var variantInfo = new Dictionary(); + var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); if (dto.VariationInfo != null) { foreach (var info in dto.VariationInfo) { - variantInfo[info.LanguageId] = info.Name; + var isoCode = _langRepository.GetIsoCodeById(info.LanguageId); + if (isoCode != null) + variantInfo[isoCode] = info.Name; } entity.AdditionalData["CultureNames"] = variantInfo; } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index fc37000e13..919ca73273 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -163,6 +163,8 @@ namespace Umbraco.Core.Services.Implement /// public IContent Create(string name, Guid parentId, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + var parent = GetById(parentId); return Create(name, parent, contentTypeAlias, userId); } @@ -181,6 +183,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent Create(string name, int parentId, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + var contentType = GetContentType(contentTypeAlias); if (contentType == null) throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); @@ -212,6 +216,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent Create(string name, IContent parent, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var scope = ScopeProvider.CreateScope()) @@ -241,6 +247,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + using (var scope = ScopeProvider.CreateScope()) { // locking the content tree secures content types too @@ -273,6 +281,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var scope = ScopeProvider.CreateScope()) @@ -864,11 +874,6 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Cancel(evtMsgs); } - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content with empty name."); - } - var isNew = content.IsNewEntity(); scope.WriteLock(Constants.Locks.ContentTree); @@ -1217,7 +1222,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(content, evtMsgs); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs, nameof(Deleting))) { scope.Complete(); return OperationResult.Cancel(evtMsgs); @@ -1229,7 +1234,7 @@ namespace Umbraco.Core.Services.Implement // but... UnPublishing event makes no sense (not going to cancel?) and no need to save // just raise the event if (content.Trashed == false && content.Published) - scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); + scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), nameof(UnPublished)); DeleteLocked(scope, content); @@ -1265,7 +1270,7 @@ namespace Umbraco.Core.Services.Implement _documentRepository.Delete(c); var args = new DeleteEventArgs(c, false); // raise event & get flagged files - scope.Events.Dispatch(Deleted, this, args); + scope.Events.Dispatch(Deleted, this, args, nameof(Deleted)); // fixme not going to work, do it differently _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files @@ -2149,7 +2154,7 @@ namespace Umbraco.Core.Services.Implement var query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA); var contents = _documentRepository.Get(query).ToArray(); - if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contents))) + if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contents), nameof(Deleting))) { scope.Complete(); return; @@ -2296,7 +2301,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); _documentBlueprintRepository.Delete(content); - scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint"); + scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), nameof(DeletedBlueprint)); scope.Complete(); } } @@ -2357,7 +2362,7 @@ namespace Umbraco.Core.Services.Implement _documentBlueprintRepository.Delete(blueprint); } - scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(blueprints), "DeletedBlueprint"); + scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(blueprints), nameof(DeletedBlueprint)); scope.Complete(); } } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index d894af47ee..04cc2857ba 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -1069,7 +1069,7 @@ namespace Umbraco.Core /// The replacement character. /// The filtered string. public static string ReplaceMany(this string text, char[] chars, char replacement) - { + { if (text == null) throw new ArgumentNullException(nameof(text)); if (chars == null) throw new ArgumentNullException(nameof(chars)); @@ -1078,7 +1078,7 @@ namespace Umbraco.Core text = text.Replace(chars[i], replacement); return text; - } + } // FORMAT STRINGS @@ -1148,6 +1148,8 @@ namespace Umbraco.Core /// The safe url segment. public static string ToUrlSegment(this string text) { + if (string.IsNullOrWhiteSpace(text)) throw new ArgumentNullOrEmptyException(nameof(text)); + return Current.ShortStringHelper.CleanStringForUrlSegment(text); } @@ -1158,8 +1160,13 @@ namespace Umbraco.Core /// The culture. /// The safe url segment. public static string ToUrlSegment(this string text, string culture) - => Current.ShortStringHelper.CleanStringForUrlSegment(text, culture); - + { + if (string.IsNullOrWhiteSpace(text)) throw new ArgumentNullOrEmptyException(nameof(text)); + if (culture == null) throw new ArgumentNullException(nameof(culture)); + + return Current.ShortStringHelper.CleanStringForUrlSegment(text, culture); + } + // the new methods to clean a string (to alias, url segment...) /// diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index caa2453aed..035c857e35 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -83,9 +83,9 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository, cacheHelper: realCache); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, cacheHelper: realCache); - var udb = (UmbracoDatabase) scope.Database; + var udb = (UmbracoDatabase)scope.Database; udb.EnableSqlCount = false; @@ -124,7 +124,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository, out DataTypeRepository _); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, out DataTypeRepository _); var versions = new List(); var hasPropertiesContentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); @@ -134,22 +134,22 @@ namespace Umbraco.Tests.Persistence.Repositories // save = create the initial version contentTypeRepository.Save(hasPropertiesContentType); - repository.Save(content1); - + repository.Save(content1); + versions.Add(content1.VersionId); // the first version // publish = new edit version content1.SetValue("title", "title"); - ((Content) content1).TryPublishValues(); - ((Content) content1).PublishedState = PublishedState.Publishing; - repository.Save(content1); - + ((Content)content1).TryPublishValues(); + ((Content)content1).PublishedState = PublishedState.Publishing; + repository.Save(content1); + versions.Add(content1.VersionId); // NEW VERSION // new edit version has been created Assert.AreNotEqual(versions[versions.Count - 2], versions[versions.Count - 1]); Assert.IsTrue(content1.Published); - Assert.AreEqual(PublishedState.Published, ((Content) content1).PublishedState); + Assert.AreEqual(PublishedState.Published, ((Content)content1).PublishedState); Assert.AreEqual(versions[versions.Count - 1], repository.Get(content1.Id).VersionId); // misc checks @@ -159,8 +159,8 @@ namespace Umbraco.Tests.Persistence.Repositories // save = update the current (draft) version content1.Name = "name-1"; content1.SetValue("title", "title-1"); - repository.Save(content1); - + repository.Save(content1); + versions.Add(content1.VersionId); // the same version // no new version has been created @@ -172,15 +172,15 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(true, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // unpublish = no impact on versions - ((Content) content1).PublishedState = PublishedState.Unpublishing; - repository.Save(content1); - + ((Content)content1).PublishedState = PublishedState.Unpublishing; + repository.Save(content1); + versions.Add(content1.VersionId); // the same version // no new version has been created Assert.AreEqual(versions[versions.Count - 2], versions[versions.Count - 1]); Assert.IsFalse(content1.Published); - Assert.AreEqual(PublishedState.Unpublished, ((Content) content1).PublishedState); + Assert.AreEqual(PublishedState.Unpublished, ((Content)content1).PublishedState); Assert.AreEqual(versions[versions.Count - 1], repository.Get(content1.Id).VersionId); // misc checks @@ -190,8 +190,8 @@ namespace Umbraco.Tests.Persistence.Repositories // save = update the current (draft) version content1.Name = "name-2"; content1.SetValue("title", "title-2"); - repository.Save(content1); - + repository.Save(content1); + versions.Add(content1.VersionId); // the same version // no new version has been created @@ -202,16 +202,16 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(false, scope.Database.ExecuteScalar($"SELECT published FROM {Constants.DatabaseSchema.Tables.Document} WHERE nodeId=@id", new { id = content1.Id })); // publish = version - ((Content) content1).TryPublishValues(); - ((Content) content1).PublishedState = PublishedState.Publishing; - repository.Save(content1); - + ((Content)content1).TryPublishValues(); + ((Content)content1).PublishedState = PublishedState.Publishing; + repository.Save(content1); + versions.Add(content1.VersionId); // NEW VERSION // new version has been created Assert.AreNotEqual(versions[versions.Count - 2], versions[versions.Count - 1]); Assert.IsTrue(content1.Published); - Assert.AreEqual(PublishedState.Published, ((Content) content1).PublishedState); + Assert.AreEqual(PublishedState.Published, ((Content)content1).PublishedState); Assert.AreEqual(versions[versions.Count - 1], repository.Get(content1.Id).VersionId); // misc checks @@ -224,8 +224,8 @@ namespace Umbraco.Tests.Persistence.Repositories //Thread.Sleep(2000); // force date change - repository.Save(content1); - + repository.Save(content1); + versions.Add(content1.VersionId); // the same version // no new version has been created @@ -238,16 +238,16 @@ namespace Umbraco.Tests.Persistence.Repositories // publish = new version content1.Name = "name-4"; content1.SetValue("title", "title-4"); - ((Content) content1).TryPublishValues(); - ((Content) content1).PublishedState = PublishedState.Publishing; - repository.Save(content1); - + ((Content)content1).TryPublishValues(); + ((Content)content1).PublishedState = PublishedState.Publishing; + repository.Save(content1); + versions.Add(content1.VersionId); // NEW VERSION // a new version has been created Assert.AreNotEqual(versions[versions.Count - 2], versions[versions.Count - 1]); Assert.IsTrue(content1.Published); - Assert.AreEqual(PublishedState.Published, ((Content) content1).PublishedState); + Assert.AreEqual(PublishedState.Published, ((Content)content1).PublishedState); Assert.AreEqual(versions[versions.Count - 1], repository.Get(content1.Id).VersionId); // misc checks @@ -261,7 +261,7 @@ namespace Umbraco.Tests.Persistence.Repositories Console.WriteLine(); foreach (var v in allVersions) { - var c = (Content) v; + var c = (Content)v; Console.WriteLine($"{c.Id} {c.VersionId} {(c.Published ? "+" : "-")}pub pk={c.VersionId} ppk={c.PublishedVersionId} name=\"{c.Name}\" pname=\"{c.PublishName}\""); } @@ -297,7 +297,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository, out DataTypeRepository _); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, out DataTypeRepository _); var emptyContentType = MockedContentTypes.CreateBasicContentType(); var hasPropertiesContentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); @@ -310,11 +310,11 @@ namespace Umbraco.Tests.Persistence.Repositories contentTypeRepository.Save(hasPropertiesContentType); repository.Save(content1); repository.Save(content2); - repository.Save(content3); - - - // this will cause the GetPropertyCollection to execute and we need to ensure that - // all of the properties and property types are all correct + repository.Save(content3); + + + // this will cause the GetPropertyCollection to execute and we need to ensure that + // all of the properties and property types are all correct var result = repository.GetMany(content1.Id, content2.Id, content3.Id).ToArray(); var n1 = result[0]; var n2 = result[1]; @@ -347,7 +347,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository, out DataTypeRepository dataTypeDefinitionRepository); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, out DataTypeRepository dataTypeDefinitionRepository); var editor = new DecimalPropertyEditor(Logger); var dtd = new DataType(editor) { Name = "test", DatabaseType = ValueStorageType.Decimal }; @@ -394,7 +394,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! IContent textpage = MockedContent.CreateSimpleContent(contentType); @@ -414,21 +414,21 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository, out TemplateRepository templateRepository); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository, out TemplateRepository templateRepository); var template = new Template("hello", "hello"); - templateRepository.Save(template); - - + templateRepository.Save(template); + + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); contentType.AllowedTemplates = Enumerable.Empty(); // because CreateSimpleContentType assigns one already contentType.SetDefaultTemplate(template); var textpage = MockedContent.CreateSimpleContent(contentType); contentTypeRepository.Save(contentType); - repository.Save(textpage); - - + repository.Save(textpage); + + var fetched = repository.Get(textpage.Id); Assert.NotNull(textpage.Template); @@ -448,20 +448,20 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! - contentTypeRepository.Save(contentType); - - + contentTypeRepository.Save(contentType); + + var textpage = MockedContent.CreateSimpleContent(contentType, "test@umbraco.org"); var anotherTextpage = MockedContent.CreateSimpleContent(contentType, "@lightgiants"); repository.Save(textpage); - repository.Save(anotherTextpage); - - - + repository.Save(anotherTextpage); + + + Assert.That(contentType.HasIdentity, Is.True); Assert.That(textpage.HasIdentity, Is.True); @@ -481,19 +481,19 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! var textpage = MockedContent.CreateSimpleContent(contentType); contentTypeRepository.Save(contentType); - repository.Save(textpage); - - + repository.Save(textpage); + + var subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - repository.Save(subpage); - - + repository.Save(subpage); + + Assert.That(contentType.HasIdentity, Is.True); Assert.That(textpage.HasIdentity, Is.True); Assert.That(subpage.HasIdentity, Is.True); @@ -509,10 +509,10 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var content = repository.Get(NodeDto.NodeIdSeed + 3); - var dirty = ((Content) content).IsDirty(); + var dirty = ((Content)content).IsDirty(); Assert.That(dirty, Is.False); } @@ -524,12 +524,12 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var content = repository.Get(NodeDto.NodeIdSeed + 2); content.Name = "About 2"; - repository.Save(content); - + repository.Save(content); + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); Assert.AreEqual(content.Id, updatedContent.Id); @@ -538,8 +538,8 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(content.GetValue("title"), "Welcome to our Home page"); content.SetValue("title", "toot"); - repository.Save(content); - + repository.Save(content); + updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); Assert.AreEqual("toot", updatedContent.GetValue("title")); @@ -553,12 +553,12 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var content = repository.Get(NodeDto.NodeIdSeed + 2); content.Template = null; - repository.Save(content); - + repository.Save(content); + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); Assert.IsNull(updatedContent.Template); @@ -572,19 +572,19 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out var contentTypeRepository); + var repository = CreateRepository((IScopeAccessor)provider, out var contentTypeRepository); var contentType = contentTypeRepository.Get(NodeDto.NodeIdSeed + 1); var content = new Content("Textpage 2 Child Node", NodeDto.NodeIdSeed + 4, contentType); content.CreatorId = 0; content.WriterId = 0; - repository.Save(content); - + repository.Save(content); + var id = content.Id; - repository.Delete(content); - - + repository.Delete(content); + + var content1 = repository.Get(id); Assert.IsNull(content1); } @@ -596,7 +596,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var content = repository.Get(NodeDto.NodeIdSeed + 4); Assert.AreEqual(NodeDto.NodeIdSeed + 4, content.Id); @@ -617,7 +617,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); var result = repository.Get(query); @@ -634,7 +634,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); result = repository.GetMany().ToArray(); // save them all @@ -642,24 +642,24 @@ namespace Umbraco.Tests.Persistence.Repositories { content.SetValue("title", content.GetValue("title") + "x"); repository.Save(content); - } - - - // publish them all + } + + + // publish them all foreach (var content in result) { content.TryPublishValues(); repository.Save(content); - } - - + } + + scope.Complete(); } // get them all again using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var result2 = repository.GetMany().ToArray(); Assert.AreEqual(result.Length, result2.Length); @@ -679,6 +679,100 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual("[table].[column2]", matches[1].Groups[1].Value); Assert.AreEqual("[alias2]", matches[1].Groups[2].Value); } + + [Test] + public void GetPagedResultsByQuery_With_Variant_Names() + { + //2x content types, one invariant, one variant + + var invariantCT = MockedContentTypes.CreateSimpleContentType("umbInvariantTextpage", "Invariant Textpage"); + invariantCT.Variations = ContentVariation.InvariantNeutral; + foreach (var p in invariantCT.PropertyTypes) p.Variations = ContentVariation.InvariantNeutral; + ServiceContext.FileService.SaveTemplate(invariantCT.DefaultTemplate); // else, FK violation on contentType! + ServiceContext.ContentTypeService.Save(invariantCT); + + var variantCT = MockedContentTypes.CreateSimpleContentType("umbVariantTextpage", "Variant Textpage"); + variantCT.Variations = ContentVariation.CultureNeutral; + var propTypes = variantCT.PropertyTypes.ToList(); + for (int i = 0; i < propTypes.Count; i++) + { + var p = propTypes[i]; + //every 2nd one is variant + p.Variations = i % 2 == 0 ? ContentVariation.CultureNeutral : ContentVariation.InvariantNeutral; + } + ServiceContext.FileService.SaveTemplate(variantCT.DefaultTemplate); // else, FK violation on contentType! + ServiceContext.ContentTypeService.Save(variantCT); + + invariantCT.AllowedContentTypes = new[] { new ContentTypeSort(invariantCT.Id, 0), new ContentTypeSort(variantCT.Id, 1) }; + ServiceContext.ContentTypeService.Save(invariantCT); + + //create content + + var root = MockedContent.CreateSimpleContent(invariantCT); + ServiceContext.ContentService.Save(root); + + for (int i = 0; i < 25; i++) + { + var isInvariant = i % 2 == 0; + var name = (isInvariant ? "INV" : "VAR") + "_" + Guid.NewGuid().ToString(); + var culture = isInvariant ? null : "en-US"; + + var child = MockedContent.CreateSimpleContent( + isInvariant ? invariantCT : variantCT, + name, root, + culture, + setPropertyValues: isInvariant); + + if (!isInvariant) + { + //manually set the property values since we have mixed variant/invariant property types + child.SetValue("title", name + " Subpage", culture: culture); + child.SetValue("bodyText", "This is a subpage", culture: null); //this one is invariant + child.SetValue("author", "John Doe", culture: culture); + } + + ServiceContext.ContentService.Save(child); + } + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository((IScopeAccessor)provider, out _); + + var query = scope.SqlContext.Query().Where(x => x.ParentId == root.Id); + + try + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + + var result = repository.GetPage(query, 0, 20, out var totalRecords, "UpdateDate", Direction.Ascending, true); + + Assert.AreEqual(25, totalRecords); + foreach (var r in result) + { + var isInvariant = r.ContentType.Alias == "umbInvariantTextpage"; + var name = isInvariant ? r.Name : r.Names["en-US"]; + var namePrefix = (isInvariant ? "INV" : "VAR"); + + //ensure the correct name (invariant vs variant) is in the result + Assert.IsTrue(name.StartsWith(namePrefix)); + + foreach (var p in r.Properties) + { + //ensure there is a value for the correct variant/invariant property + var value = p.GetValue(p.PropertyType.Variations.Has(ContentVariation.InvariantNeutral) ? null : "en-US"); + Assert.IsNotNull(value); + } + } + } + finally + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + } + } + } [Test] public void GetPagedResultsByQuery_CustomPropertySort() @@ -686,7 +780,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Name.Contains("Text")); @@ -718,7 +812,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); @@ -747,7 +841,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); var result = repository.GetPage(query, 1, 1, out var totalRecords, "Name", Direction.Ascending, true); @@ -764,7 +858,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); var result = repository.GetPage(query, 0, 2, out var totalRecords, "Name", Direction.Ascending, true); @@ -781,7 +875,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); var result = repository.GetPage(query, 0, 1, out var totalRecords, "Name", Direction.Descending, true); @@ -798,7 +892,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); @@ -817,7 +911,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); @@ -836,7 +930,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var contents = repository.GetMany(NodeDto.NodeIdSeed + 2, NodeDto.NodeIdSeed + 3); @@ -853,7 +947,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var contents = repository.GetMany(); @@ -879,7 +973,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var exists = repository.Exists(NodeDto.NodeIdSeed + 2); @@ -893,7 +987,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Level == 2); var result = repository.Count(query); @@ -908,7 +1002,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var repository = CreateRepository((IScopeAccessor) provider, out _); + var repository = CreateRepository((IScopeAccessor)provider, out _); var query = scope.SqlContext.Query().Where(x => x.Key == new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); var content = repository.Get(query).SingleOrDefault(); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs index 5e69eff83a..14d914be1d 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs @@ -35,7 +35,7 @@ namespace Umbraco.Tests.TestHelpers.Entities return content; } - public static Content CreateSimpleContent(IContentType contentType, string name, int parentId = -1) + public static Content CreateSimpleContent(IContentType contentType, string name, int parentId = -1, string culture = null, string segment = null) { var content = new Content(name, parentId, contentType) { CreatorId = 0, WriterId = 0 }; object obj = @@ -46,25 +46,29 @@ namespace Umbraco.Tests.TestHelpers.Entities author = "John Doe" }; - content.PropertyValues(obj); + content.PropertyValues(obj, culture, segment); content.ResetDirtyProperties(false); return content; } - public static Content CreateSimpleContent(IContentType contentType, string name, IContent parent) + public static Content CreateSimpleContent(IContentType contentType, string name, IContent parent, string culture = null, string segment = null, bool setPropertyValues = true) { - var content = new Content(name, parent, contentType) { CreatorId = 0, WriterId = 0 }; - object obj = + var content = new Content(name, parent, contentType, culture) { CreatorId = 0, WriterId = 0 }; + + if (setPropertyValues) + { + object obj = new { title = name + " Subpage", bodyText = "This is a subpage", author = "John Doe" - }; - - content.PropertyValues(obj); + }; + + content.PropertyValues(obj, culture, segment); + } content.ResetDirtyProperties(false); diff --git a/src/Umbraco.Tests/Testing/ContentBaseExtensions.cs b/src/Umbraco.Tests/Testing/ContentBaseExtensions.cs index 1a94b8770a..58d4dfbd7f 100644 --- a/src/Umbraco.Tests/Testing/ContentBaseExtensions.cs +++ b/src/Umbraco.Tests/Testing/ContentBaseExtensions.cs @@ -10,7 +10,7 @@ namespace Umbraco.Tests.Testing /// Set property values by alias with an annonymous object. /// /// Does not support variants. - public static void PropertyValues(this IContentBase content, object value) + public static void PropertyValues(this IContentBase content, object value, string culture = null, string segment = null) { if (value == null) throw new Exception("No properties has been passed in"); @@ -27,7 +27,7 @@ namespace Umbraco.Tests.Testing var item = content.Properties.FirstOrDefault(x => x.Alias == propertyInfo.Name); if (item != null) { - item.SetValue(propertyInfo.GetValue(value, null)); + item.SetValue(propertyInfo.GetValue(value, null), culture, segment); //Update item with newly added value content.Properties.Add(item); } @@ -35,7 +35,7 @@ namespace Umbraco.Tests.Testing { //Create new Property to add to collection var property = propertyType.CreateProperty(); - property.SetValue(propertyInfo.GetValue(value, null)); + property.SetValue(propertyInfo.GetValue(value, null), culture, segment); content.Properties.Add(property); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index 0896f986e6..23664ed842 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -38,7 +38,7 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se calculateWidth(); }); } - + function calculateWidth(){ $timeout(function(){ //total width minus room for avatar, search, and help icon @@ -119,9 +119,10 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se } else { var lastAccessed = historyService.getLastAccessedItemForSection(section.alias); - var path = lastAccessed != null ? lastAccessed.link : section.alias; - $location.path(path).search(''); - } + var path = lastAccessed != null ? lastAccessed.link : section.alias; + $location.path(path); + } + navigationService.clearSearch(); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index ffac0bf965..63bdb9c92e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -20,7 +20,7 @@ $scope.page.listViewPath = null; $scope.page.isNew = $scope.isNew ? true : false; $scope.page.buttonGroupState = "init"; - $scope.page.languageId = $scope.languageId; + $scope.page.culture = $scope.culture; $scope.allowOpen = true; // add all editors to an editors array to support split view @@ -122,14 +122,14 @@ /** * This does the content loading and initializes everything, called on load and changing variants - * @param {any} languageId + * @param {any} culture */ - function getNode(languageId) { + function getNode(culture) { $scope.page.loading = true; //we are editing so get the content item from the server - $scope.getMethod()($scope.contentId, languageId) + $scope.getMethod()($scope.contentId, culture) .then(function (data) { $scope.content = data; @@ -258,7 +258,7 @@ else { //Browse content nodes based on the selected tree language variant - $scope.page.languageId ? getNode($scope.page.languageId) : getNode(); + $scope.page.culture ? getNode($scope.page.culture) : getNode(); } @@ -527,7 +527,7 @@ saveMethod: "&", getMethod: "&", getScaffoldMethod: "&?", - languageId: "=?" + culture: "=?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 6a15b0baba..7f727882e1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -218,6 +218,7 @@ Use this directive to construct a header inside the main editor window. } function setCurrentVariant(variants) { + angular.forEach(variants, function (variant) { if(variant.current) { scope.vm.currentVariant = variant; @@ -245,15 +246,15 @@ Use this directive to construct a header inside the main editor window. }); } - scope.goBack = function() { - if(scope.onBack) { + scope.goBack = function () { + if (scope.onBack) { scope.onBack(); } }; scope.selectVariant = function (event, variant) { scope.vm.dropdownOpen = false; - $location.search({ languageId: variant.language.id }); + $location.search("cculture", variant.language.culture); }; scope.openIconPicker = function() { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 7d560d7b43..1f2a611fe5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -314,17 +314,17 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * * @param {Int} id id of content item to return - * @param {Int} languageId optional ID of the language to retrieve the item in + * @param {Int} culture optional culture to retrieve the item in * @returns {Promise} resourcePromise object containing the content item. * */ - getById: function (id, languageId) { + getById: function (id, culture) { return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentApiBaseUrl", "GetById", - { id: id, languageId: languageId })), + { id: id, culture: culture })), 'Failed to retrieve data for content id ' + id); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 36a184cd95..e8e10c5d63 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -5,7 +5,7 @@ * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. **/ -function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, localizationService, serverValidationManager, dialogService, formHelper, appState) { +function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, navigationService, localizationService, serverValidationManager, dialogService, formHelper, appState) { function isValidIdentifier(id){ //empty id <= 0 @@ -596,7 +596,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica // /belle/#/content/edit/9876 (where 9876 is the new id) //clear the query strings - $location.search(""); + navigationService.clearSearch(); //change to new path $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); @@ -617,9 +617,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * For some editors like scripts or entites that have names as ids, these names can change and we need to redirect * to their new paths, this is helper method to do that. */ - redirectToRenamedContent: function (id) { + redirectToRenamedContent: function (id) { //clear the query strings - $location.search(""); + navigationService.clearSearch(); //change to new path $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); //don't add a browser history for this diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 257d36af31..9a19304288 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -102,6 +102,25 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo }, + /** + * @ngdoc method + * @name umbraco.services.navigationService#clearSearch + * @methodOf umbraco.services.navigationService + * + * @description + * utility to clear the querystring/search params while maintaining a known list of parameters that should be maintained throughout the app + */ + clearSearch: function () { + var retainKeys = ["mculture"]; + var currentSearch = $location.search(); + $location.search(''); + _.each(retainKeys, function (k) { + if (currentSearch[k]) { + $location.search(k, currentSearch[k]); + } + }); + }, + /** * @ngdoc method * @name umbraco.services.navigationService#load diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 5ef6eae293..70004900f9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -327,17 +327,17 @@ //get the selected variant and build the additional published variants saveModel.publishVariations = []; - //if there's more than 1 variant than we need to set the language and include the variants to publish - if (displayModel.variants.length > 1) { + //if there's any variants than we need to set the language and include the variants to publish + if (displayModel.variants.length > 0) { _.each(displayModel.variants, function (d) { //set the selected variant if this is current if (d.current === true) { - saveModel.languageId = d.language.id; + saveModel.culture = d.language.culture; } if (d.publish === true) { saveModel.publishVariations.push({ - languageId: d.language.id, + culture: d.language.culture, segment: d.segment }); } diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js index b1810ca683..00458aa6b4 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js @@ -96,10 +96,12 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar appState.setMenuState("currentNode", args.node); //not legacy, lets just set the route value and clear the query string if there is one. - $location.path(n.routePath).search(""); + $location.path(n.routePath); + navigationService.clearSearch(); } else if (n.section) { - $location.path(n.section).search(""); + $location.path(n.section); + navigationService.clearSearch(); } navigationService.hideNavigation(); @@ -236,24 +238,37 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar languageResource.getAll().then(function(languages) { $scope.languages = languages; - // make the default language selected - $scope.languages.forEach(function (language) { - if (language.isDefault) { - $scope.selectedLanguage = language; + if ($scope.languages.length > 1) { + var defaultLang = _.find($scope.languages, function (l) { + return l.isDefault; + }); + if (defaultLang) { + //set the route param + $location.search("mculture", defaultLang.culture); } - }); - }); + } + init(); + }); })); - /** - * Updates the tree's query parameters - */ - function initTree() { + function init() { + //select the current language if set in the query string + var mainCulture = $location.search().mculture; + if (mainCulture && $scope.languages && $scope.languages.length > 1) { + var found = _.find($scope.languages, function (l) { + return l.culture === mainCulture; + }); + if (found) { + //set the route param + $scope.selectedLanguage = found; + } + } + //create the custom query string param for this tree var queryParams = {}; - if ($scope.selectedLanguage && $scope.selectedLanguage.id) { - queryParams["languageId"] = $scope.selectedLanguage.id; + if ($scope.selectedLanguage && $scope.selectedLanguage.culture) { + queryParams["culture"] = $scope.selectedLanguage.culture; } var queryString = $.param(queryParams); //create the query string from the params object @@ -266,6 +281,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } } + function nodeExpandedHandler(args) { //store the reference to the expanded node path if (args.node) { @@ -274,11 +290,15 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } $scope.selectLanguage = function(language) { - $scope.selectedLanguage = language; + + $location.search("mculture", language.culture); + + //$scope.selectedLanguage = language; + // close the language selector $scope.page.languageSelectorIsOpen = false; - initTree(); //this will reset the tree params and the tree directive will pick up the changes in a $watch + init(); //re-bind language to the query string and update the tree params //execute after next digest because the internal watch on the customtreeparams needs to be bound now that we've changed it $timeout(function () { diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 5f660cf212..e4f9852a11 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -104,7 +104,6 @@ app.config(function ($routeProvider) { resolve: doLogout() }) .when('/:section', { - //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. template: "
", //This controller will execute for this route, then we can execute some code in order to set the template Url diff --git a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.controller.js index c9f3cc347c..eb2b0e5930 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.controller.js @@ -1,6 +1,6 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Notifications.ConfirmRouteChangeController", - function ($scope, $location, $log, notificationsService) { + function ($scope, $location, $log, notificationsService, navigationService) { $scope.discard = function(not){ @@ -13,7 +13,7 @@ angular.module("umbraco").controller("Umbraco.Notifications.ConfirmRouteChangeCo // when no callback is added run the normal functionality of the discard button not.args.listener(); - $location.search(""); + navigationService.clearSearch(); //we need to break the path up into path and query var parts = not.args.path.split("?"); @@ -33,4 +33,4 @@ angular.module("umbraco").controller("Umbraco.Notifications.ConfirmRouteChangeCo notificationsService.remove(not); }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js index a489e9927d..6601efef89 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js @@ -1,23 +1,27 @@ (function () { "use strict"; - function PublishController($scope, $timeout) { + function PublishController($scope) { var vm = this; - vm.variants = $scope.model.variants; + var variants = $scope.model.variants; vm.changeSelection = changeSelection; + vm.loading = true; + + vm.dirtyVariants = []; + vm.pristineVariants = []; //watch this model, if it's reset, then re init - $scope.$watch(function() { - return $scope.model.variants; - }, - function(newVal, oldVal) { + $scope.$watch(function () { + return $scope.model.variants; + }, + function (newVal, oldVal) { vm.variants = newVal; if (oldVal && oldVal.length) { //re-bind the selections for (var i = 0; i < oldVal.length; i++) { - var found = _.find(vm.variants, function(v) { - return v.language.id == oldVal[i].language.id; + var found = _.find(variants, function (v) { + return v.language.id === oldVal[i].language.id; }); if (found) { found.publish = oldVal[i].publish; @@ -28,24 +32,42 @@ }); function changeSelection(variant) { - var firstSelected = _.find(vm.variants, function(v) { + var firstSelected = _.find(variants, function (v) { return v.publish; }); $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } function onInit() { - _.each(vm.variants, - function (v) { - v.compositeId = v.language.id + "_" + (v.segment ? v.segment : ""); - v.htmlId = "publish_variant_" + v.compositeId; + _.each(variants, + function (variant) { + variant.compositeId = variant.language.id + "_" + (variant.segment ? variant.segment : ""); + variant.htmlId = "publish_variant_" + variant.compositeId; + + //separate "pristine" and "dirty" variants + if (variant.isEdited === true) { + vm.dirtyVariants.push(variant); + } else if (variant.isEdited === true || + variant.isEdited === false && variant.state === "Unpublished") { + vm.dirtyVariants.push(variant); + } else { + vm.pristineVariants.push(variant); + } }); - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function(v) { - return v.current ? 0 : 1; - }); - //ensure that the current one is selected - vm.variants[0].publish = true; + + if (vm.dirtyVariants.length !== 0) { + //now sort it so that the current one is at the top + vm.dirtyVariants = _.sortBy(vm.dirtyVariants, function (v) { + return v.current ? 0 : 1; + }); + //ensure that the current one is selected + vm.dirtyVariants[0].publish = true; + } else { + //disable Publish button if we have nothing to publish + $scope.model.disableSubmitButton = true; + } + + vm.loading = false; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html index ed7f32fc25..cfb205eb25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html @@ -1,34 +1,52 @@ -
- +
+
-

What languages would you like to publish?

+

+

-
-
+
+ +
+
+
+ +
+
+

+
+ +
+
+
{{ variant.language.name }}
+
{{ variant.state }}
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js index 700b18b518..e8b1bb0f68 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -36,7 +36,7 @@ angular.module("umbraco") //perform the path change, if it is successful then the promise will resolve otherwise it will fail $scope.model.close(); - $location.path("/logout"); + $location.path("/logout").search(''); }; $scope.gotoHistory = function (link) { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index 41d44114a0..0d6cee2573 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -50,7 +50,7 @@ server-validation-field="Alias"> - + {{vm.currentVariant.language.name}}   @@ -59,7 +59,9 @@ {{variant.language.name}} - {{variant.state}} + + + {{variant.state}}