diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 44e5968a9f..1cbb4a6ea1 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -595,7 +595,6 @@ namespace Umbraco.Core return null; } - /// /// Attempts to serialize the value to an XmlString using ToXmlString /// @@ -788,5 +787,20 @@ namespace Umbraco.Core return BoolConvertCache[type] = false; } + + /// + /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. + /// + /// The nullable type. + /// The value to compare. + /// The value to compare to. + /// The value to use when any value is null. + /// Do not use outside of Sql expressions. + // see usage in ExpressionVisitorBase + public static bool NEquals(this T? value, T? other, T fallbackValue) + where T : struct + { + return (value ?? fallbackValue).Equals(other ?? fallbackValue); + } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs index 4a07b16a07..cbe4cf0cd4 100644 --- a/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/TagRelationshipDto.cs @@ -3,11 +3,13 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Persistence.Dtos { - [TableName(Constants.DatabaseSchema.Tables.TagRelationship)] + [TableName(TableName)] [PrimaryKey("nodeId", AutoIncrement = false)] [ExplicitColumns] internal class TagRelationshipDto { + public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship; + [Column("nodeId")] [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index d313d27bbc..493bbcacc7 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -653,6 +653,23 @@ namespace Umbraco.Core.Persistence.Querying else throw new NotSupportedException("Expression is not a proper lambda."); + // c# 'x == null' becomes sql 'x IS NULL' which is fine + // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, + // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, + // so for comparing nullable types, we use x.NEquals(y, fb) where fb is a fallback + // value which will be used when values are null - turning the comparison into + // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside + // of x and y range - and if that is not possible, then a manual comparison need + // to be written + //TODO support NEquals with 0 parameters, using the full syntax below + case "NEquals": + var compareTo = Visit(m.Arguments[1]); + var fallback = Visit(m.Arguments[2]); + // that would work without a fallback value but is more cumbersome + //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; + // use a fallback value + return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; + default: throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3184c69dfe..7e720ba08a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -190,8 +190,8 @@ AND umbracoNode.nodeObjectType = @objectType", SortOrder = allowedContentType.SortOrder }); } - - + + //Insert Tabs foreach (var propertyGroup in entity.PropertyGroups) { @@ -620,7 +620,7 @@ AND umbracoNode.id <> @id", var sqlDelete = Sql() .Delete() .WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), sqlSelect); - + Database.Execute(sqlDelete); } @@ -663,9 +663,11 @@ AND umbracoNode.id <> @id", { case ContentVariation.Culture: CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); + CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); break; case ContentVariation.Nothing: CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); + CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -690,7 +692,7 @@ AND umbracoNode.id <> @id", //first clear out any existing names that might already exists under the default lang //there's 2x tables to update - //clear out the versionCultureVariation table + //clear out the versionCultureVariation table var sqlSelect = Sql().Select(x => x.Id) .From() .InnerJoin().On(x => x.Id, x => x.VersionId) @@ -757,6 +759,129 @@ AND umbracoNode.id <> @id", } } + /// + private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) + { + // note: important to use NEquals for nullable types, cannot directly compare language identifiers + + // fixme - should we batch then? + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + // delete existing relations (for target language) + // do *not* delete existing tags + + var sqlTagToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + sqlTagToDelete + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlTagToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => x.LanguageId.NEquals(targetLanguageId, -1)); + + var sqlDeleteRel = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlTagToDelete); + + sqlDeleteRel.WriteToConsole(); + Database.Execute(sqlDeleteRel); + + // do *not* delete the tags - they could be used by other content types / property types + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + + // copy tags from source language to target language + + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + var sqlSelect = Sql() + .Select(x => x.Text, x => x.Group) + .Append(", " + targetLanguageIdS) + .From(); + + sqlSelect + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .LeftJoin("xtags").On((tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && tag.LanguageId.NEquals(targetLanguageId, -1), aliasRight: "xtags"); + + if (contentTypeIds != null) + sqlSelect + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId); + + sqlSelect + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .WhereNull(x => x.Id, "xtags"); // ie, not exists + + if (contentTypeIds != null) + sqlSelect + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlSelect.Where(x => x.LanguageId.NEquals(sourceLanguageId, -1)); + + var cols = Sql().Columns(x => x.Text, x => x.Group, x => x.LanguageId); + var sqlInsertTag = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelect); + + sqlInsertTag.WriteToConsole(); + Database.Execute(sqlInsertTag); + + // create relations to new tags + + var sqlFoo = Sql() + .Select(x => x.NodeId, x => x.PropertyTypeId) + .AndSelect("otag", x => x.Id) + .From() + .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) + .InnerJoin("otag").On((tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && otag.LanguageId.NEquals(targetLanguageId, -1), aliasRight: "otag") + .Where(x => x.LanguageId.NEquals(sourceLanguageId, -1)); + + var cols2 = Sql().Columns(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + var sqlInsertRel = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({cols2})").Append(sqlFoo); + + sqlInsertRel.WriteToConsole(); + Database.Execute(sqlInsertRel); + + // delete original relations - *not* the tags - all of them + // cannot really "go back" with relations, would have to do it with property values + + sqlTagToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + sqlTagToDelete + .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + + sqlTagToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => !x.LanguageId.NEquals(targetLanguageId, -1)); + + sqlDeleteRel = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlTagToDelete); + + sqlDeleteRel.WriteToConsole(); + Database.Execute(sqlDeleteRel); + + // no + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + } + /// /// Copies property data from one language to another. /// @@ -766,6 +891,8 @@ AND umbracoNode.id <> @id", /// The content type identifiers. private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { + // note: important to use NEquals for nullable types, cannot directly compare language identifiers + // // fixme - should we batch then? var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); if (whereInArgsCount > 2000) @@ -793,11 +920,7 @@ AND umbracoNode.id <> @id", sqlDelete.WhereIn(x => x.VersionId, inSql); } - // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it - if (targetLanguageId == null) - sqlDelete.Where(x => x.LanguageId == null); - else - sqlDelete.Where(x => x.LanguageId == targetLanguageId); + sqlDelete.Where(x => x.LanguageId.NEquals(targetLanguageId, -1)); sqlDelete .WhereIn(x => x.PropertyTypeId, propertyTypeIds); @@ -821,11 +944,7 @@ AND umbracoNode.id <> @id", .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); - // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it - if (sourceLanguageId == null) - sqlSelectData.Where(x => x.LanguageId == null); - else - sqlSelectData.Where(x => x.LanguageId == sourceLanguageId); + sqlSelectData.Where(x => x.LanguageId.NEquals(sourceLanguageId, -1)); sqlSelectData .WhereIn(x => x.PropertyTypeId, propertyTypeIds); diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 64e4c0adca..2c07dff63c 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -67,6 +67,24 @@ namespace Umbraco.Core.Persistence /// public ISqlContext SqlContext { get; } + #region Temp + + // work around NPoco issue https://github.com/schotime/NPoco/issues/517 while we wait for the fix + public override DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args) + { + var command = base.CreateCommand(connection, commandType, sql, args); + + if (!DatabaseType.IsSqlCe()) return command; + + foreach (DbParameter parameter in command.Parameters) + if (parameter.Value == DBNull.Value) + parameter.DbType = DbType.String; + + return command; + } + + #endregion + #region Testing, Debugging and Troubleshooting private bool _enableCount; @@ -242,7 +260,9 @@ namespace Umbraco.Core.Persistence sb.Append(" @"); sb.Append(i++); sb.Append(":"); - sb.Append(arg); + if (arg == DBNull.Value) sb.Append(""); + else if (arg == null) sb.Append(""); + else sb.Append(arg); } return sb.ToString(); diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs index 458479b293..9fbba01e94 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NPoco; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Tests.TestHelpers; @@ -11,6 +12,80 @@ namespace Umbraco.Tests.Persistence.NPocoTests [TestFixture] public class NPocoSqlExtensionsTests : BaseUsingSqlCeSyntax { + [Test] + public void WhereTest() + { + var sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == null); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] is null))", sql.SQL, sql.SQL); + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == 123); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL); + + var id = 123; + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == id); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL); + + int? nid = 123; + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId == nid); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE (([umbracoPropertyData].[languageId] = @0))", sql.SQL, sql.SQL); + + // but the above comparison fails if @0 is null + // what we want is something similar to: + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => (nid == null && x.LanguageId == null) || (nid != null && x.LanguageId == nid)); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((((@0 is null) AND ([umbracoPropertyData].[languageId] is null)) OR ((@1 is not null) AND ([umbracoPropertyData].[languageId] = @2))))", sql.SQL, sql.SQL); + + // new NEquals method does it automatically + // 'course it would be nicer if '==' could do it + // see note in ExpressionVisitorBase for NEquals + + //sql = new Sql(SqlContext) + // .Select("*") + // .From() + // .Where(x => x.LanguageId.NEquals(nid)); + //Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((((@0 is null) AND ([umbracoPropertyData].[languageId] is null)) OR ((@0 is not null) AND ([umbracoPropertyData].[languageId] = @0))))", sql.SQL, sql.SQL); + + // but, the expression above fails with SQL CE, 'specified argument for the function is not valid' in 'isnull' function + // so... compare with fallback values + + sql = new Sql(SqlContext) + .Select("*") + .From() + .Where(x => x.LanguageId.NEquals(nid, -1)); + Assert.AreEqual("SELECT *\nFROM [umbracoPropertyData]\nWHERE ((COALESCE([umbracoPropertyData].[languageId],@0) = COALESCE(@1,@0)))", sql.SQL, sql.SQL); + } + + [Test] + public void NEqualsTest() + { + int? a, b; + a = b = null; + Assert.IsTrue(a.NEquals(b, -1)); + b = 2; + Assert.IsFalse(a.NEquals(b, -1)); + a = 2; + Assert.IsTrue(a.NEquals(b, -1)); + b = null; + Assert.IsFalse(a.NEquals(b, -1)); + } + [Test] public void WhereInValueFieldTest() { diff --git a/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs b/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs index c05bde4c7c..071fa99264 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTagsTests.cs @@ -129,6 +129,133 @@ namespace Umbraco.Tests.Services Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); } + [Test] + public void TagsCanBecomeVariant() + { + var enId = ServiceContext.LocalizationService.GetLanguageIdByIsoCode("en-US").Value; + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + PropertyType propertyType; + contentType.PropertyGroups.First().PropertyTypes.Add( + propertyType = new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041 + }); + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }); + contentService.SaveAndPublish(content1); + + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + // no changes + content1 = contentService.GetById(content1.Id); + + var tags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.AreEqual(4, tags.Length); + Assert.Contains("one", tags); + Assert.AreEqual(-1, tags.IndexOf("plus")); + + var tagGroups = tagService.GetAllTags().GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags()) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == null); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + + propertyType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + // changes + content1 = contentService.GetById(content1.Id); + + // property value has been moved from invariant to en-US + tags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.IsEmpty(tags); + + tags = content1.Properties["tags"].GetTagsValue("en-US").ToArray(); + Assert.AreEqual(4, tags.Length); + Assert.Contains("one", tags); + Assert.AreEqual(-1, tags.IndexOf("plus")); + + // tags have been copied from invariant to en-US + tagGroups = tagService.GetAllTags(culture: "*").GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags("*")) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + + enTagGroup = tagGroups.FirstOrDefault(x => x.Key == enId); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + + [Test] + public void TagsCanBecomeInvariant() + { + var languageService = ServiceContext.LocalizationService; + languageService.Save(new Language("fr-FR")); // en-US is already there + + var enId = ServiceContext.LocalizationService.GetLanguageIdByIsoCode("en-US").Value; + + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + PropertyType propertyType; + contentType.PropertyGroups.First().PropertyTypes.Add( + propertyType = new PropertyType("test", ValueStorageType.Ntext, "tags") + { + DataTypeId = 1041, + Variations = ContentVariation.Culture + }); + contentType.Variations = ContentVariation.Culture; + contentTypeService.Save(contentType); + + IContent content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetCultureName("name-fr", "fr-FR"); + content1.SetCultureName("name-en", "en-US"); + content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); + content1.AssignTags("tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); + contentService.SaveAndPublish(content1); + + contentType.Variations = ContentVariation.Nothing; + contentTypeService.Save(contentType); + + // changes + content1 = contentService.GetById(content1.Id); + + // property value has been moved from en-US to invariant, fr-FR tags are gone + Assert.IsEmpty(content1.Properties["tags"].GetTagsValue("fr-FR")); + Assert.IsEmpty(content1.Properties["tags"].GetTagsValue("en-US")); + + var tags = content1.Properties["tags"].GetTagsValue().ToArray(); + Assert.AreEqual(4, tags.Length); + Assert.Contains("one", tags); + Assert.AreEqual(-1, tags.IndexOf("plus")); + + // tags have been copied from en-US to invariant, fr-FR tags are gone + var tagGroups = tagService.GetAllTags(culture: "*").GroupBy(x => x.LanguageId); + foreach (var tag in tagService.GetAllTags("*")) + Console.WriteLine($"{tag.Group}:{tag.Text} {tag.LanguageId}"); + Assert.AreEqual(1, tagGroups.Count()); + + var enTagGroup = tagGroups.FirstOrDefault(x => x.Key == null); + Assert.IsNotNull(enTagGroup); + Assert.AreEqual(4, enTagGroup.Count()); + Assert.IsTrue(enTagGroup.Any(x => x.Text == "one")); + Assert.IsFalse(enTagGroup.Any(x => x.Text == "plus")); + } + [Test] public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_One() { @@ -193,7 +320,7 @@ namespace Umbraco.Tests.Services } [Test] - [Ignore("U4-8442, will need to be fixed eventually.")] + //[Ignore("U4-8442, will need to be fixed eventually.")] public void TagsAreUpdatedWhenContentIsTrashedAndUnTrashed_Tree() { var contentService = ServiceContext.ContentService; @@ -209,12 +336,10 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "plus" }); - content1.PublishCulture(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1.Id); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.PublishCulture(); contentService.SaveAndPublish(content2); // verify @@ -298,7 +423,7 @@ namespace Umbraco.Tests.Services } [Test] - [Ignore("U4-8442, will need to be fixed eventually.")] + //[Ignore("U4-8442, will need to be fixed eventually.")] public void TagsAreUpdatedWhenContentIsUnpublishedAndRePublished_Tree() { var contentService = ServiceContext.ContentService; @@ -314,12 +439,10 @@ namespace Umbraco.Tests.Services var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); content1.AssignTags("tags", new[] { "hello", "world", "some", "tags", "bam" }); - content1.PublishCulture(); contentService.SaveAndPublish(content1); var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", content1); content2.AssignTags("tags", new[] { "hello", "world", "some", "tags" }); - content2.PublishCulture(); contentService.SaveAndPublish(content2); contentService.Unpublish(content1);