diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 380c3ac5238c..768e6313590b 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -301,7 +301,7 @@ public void SetCultureName(string? name, string? culture) } // set - else + else if (GetCultureName(culture) != name) { this.SetCultureInfo(culture!, name, DateTime.Now); } @@ -455,10 +455,12 @@ public void SetValue(string propertyTypeAlias, object? value, string? culture = $"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); } - property?.SetValue(value, culture, segment); - - // bump the culture to be flagged for updating - this.TouchCulture(culture); + var updated = property.SetValue(value, culture, segment); + if (updated) + { + // bump the culture to be flagged for updating + this.TouchCulture(culture); + } } /// diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs index 18bc98485335..7a9764703ee9 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs @@ -162,7 +162,8 @@ protected void SetPropertyValueAndDetectChanges(T? value, ref T? valueRef, st /// The property name. /// A comparer to compare property values. /// A value indicating whether we know values have changed and no comparison is required. - protected void DetectChanges(T value, T orig, string propertyName, IEqualityComparer comparer, bool changed) + /// True if a change was detected, false otherwise. + protected bool DetectChanges(T value, T orig, string propertyName, IEqualityComparer comparer, bool changed) { // compare values changed = _withChanges && (changed || !comparer.Equals(orig, value)); @@ -172,6 +173,8 @@ protected void DetectChanges(T value, T orig, string propertyName, IEqualityC { OnPropertyChanged(propertyName); } + + return changed; } #endregion diff --git a/src/Umbraco.Core/Models/IProperty.cs b/src/Umbraco.Core/Models/IProperty.cs index d9a57e255844..a70ccb5888f0 100644 --- a/src/Umbraco.Core/Models/IProperty.cs +++ b/src/Umbraco.Core/Models/IProperty.cs @@ -32,7 +32,12 @@ public interface IProperty : IEntity, IRememberBeingDirty /// /// Sets a value. /// - void SetValue(object? value, string? culture = null, string? segment = null); + /// true if the value was set (updated), false otherwise. + /// + /// A false return value does not indicate failure, but rather that the property value was not changed + /// (i.e. the value passed in was equal to the current property value). + /// + bool SetValue(object? value, string? culture = null, string? segment = null); void PublishValues(string? culture = "*", string segment = "*"); diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index b756b143ad1a..05b93f2f5776 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -257,10 +257,8 @@ public void UnpublishValues(string? culture = "*", string? segment = "*") } } - /// - /// Sets a value. - /// - public void SetValue(object? value, string? culture = null, string? segment = null) + /// + public bool SetValue(object? value, string? culture = null, string? segment = null) { culture = culture?.NullOrWhiteSpaceAsNull(); segment = segment?.NullOrWhiteSpaceAsNull(); @@ -273,6 +271,7 @@ public void SetValue(object? value, string? culture = null, string? segment = nu (IPropertyValue? pvalue, var change) = GetPValue(culture, segment, true); + var changed = false; if (pvalue is not null) { var origValue = pvalue.EditedValue; @@ -280,8 +279,10 @@ public void SetValue(object? value, string? culture = null, string? segment = nu pvalue.EditedValue = setValue; - DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); + changed = DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); } + + return changed; } public object? ConvertAssignedValue(object? value) => diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.Publish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.Publish.cs index 12500a88308d..d843f3755876 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.Publish.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentPublishingServiceTests.Publish.cs @@ -347,4 +347,45 @@ public async Task Can_Publish_Culture_With_Unpublished_Parent_Culture() Constants.Security.SuperUserKey); Assert.IsTrue(publishAttempt.Success); } + + [Test] + public async Task Republishing_Single_Culture_Does_Not_Change_Publish_Or_Update_Date_For_Other_Cultures() + { + var (langEn, langDa, langBe, contentType) = await SetupVariantDoctypeAsync(); + var setupData = await CreateVariantContentAsync(langEn, langDa, langBe, contentType); + + var publishAttempt = await ContentPublishingService.PublishAsync( + setupData.Key, + [ + new() { Culture = langEn.IsoCode }, + new() { Culture = langDa.IsoCode }, + new() { Culture = langBe.IsoCode }, + ], + Constants.Security.SuperUserKey); + Assert.IsTrue(publishAttempt.Success); + + var content = ContentService.GetById(setupData.Key)!; + var firstPublishDateEn = content.GetPublishDate(langEn.IsoCode) + ?? throw new InvalidOperationException("Expected a publish date for EN"); + var firstPublishDateDa = content.GetPublishDate(langDa.IsoCode) + ?? throw new InvalidOperationException("Expected a publish date for DA"); + var firstPublishDateBe = content.GetPublishDate(langBe.IsoCode) + ?? throw new InvalidOperationException("Expected a publish date for BE"); + + Thread.Sleep(100); + + publishAttempt = await ContentPublishingService.PublishAsync( + content.Key, + [new() { Culture = langEn.IsoCode }], + Constants.Security.SuperUserKey); + Assert.IsTrue(publishAttempt.Success); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(firstPublishDateDa, content.GetPublishDate(langDa.IsoCode)); + Assert.AreEqual(firstPublishDateBe, content.GetPublishDate(langBe.IsoCode)); + + var lastPublishDateEn = content.GetPublishDate(langEn.IsoCode) + ?? throw new InvalidOperationException("Expected a publish date for EN"); + Assert.Greater(lastPublishDateEn, firstPublishDateEn); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs index a834b5f08647..b760d94c2968 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs @@ -567,4 +567,115 @@ public async Task Cannot_Update_Variant_Readonly_Property_Value() Assert.AreEqual("The initial Danish label value", content.GetValue("variantLabel", "da-DK")); }); } + + [Test] + public async Task Updating_Single_Variant_Name_Does_Not_Change_Update_Dates_Of_Other_Vaiants() + { + var contentType = await CreateVariantContentType(variantTitleAsMandatory: false); + + var createModel = new ContentCreateModel + { + ContentTypeKey = contentType.Key, + ParentKey = Constants.System.RootKey, + Properties = [], + Variants = + [ + new VariantModel { Culture = "en-US", Name = "Initial English Name" }, + new VariantModel { Culture = "da-DK", Name = "Initial Danish Name" } + ], + }; + + var createResult = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(createResult.Success); + + var firstUpdateDateEn = createResult.Result.Content!.GetUpdateDate("en-US")!; + var firstUpdateDateDa = createResult.Result.Content!.GetUpdateDate("da-DK")!; + + Thread.Sleep(100); + + var updateModel = new ContentUpdateModel + { + Properties = [], + Variants = + [ + new VariantModel { Culture = "en-US", Name = "Updated English Name" }, + new VariantModel { Culture = "da-DK", Name = "Initial Danish Name" } + ] + }; + + var updateResult = await ContentEditingService.UpdateAsync(createResult.Result.Content.Key, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(updateResult.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, updateResult.Status); + VerifyUpdate(updateResult.Result.Content); + + // re-get and re-test + VerifyUpdate(await ContentEditingService.GetAsync(updateResult.Result.Content!.Key)); + + void VerifyUpdate(IContent? updatedContent) + { + Assert.IsNotNull(updatedContent); + Assert.AreEqual(firstUpdateDateDa, updatedContent.GetUpdateDate("da-DK")); + + var lastUpdateDateEn = updatedContent.GetUpdateDate("en-US") + ?? throw new InvalidOperationException("Expected a publish date for EN"); + Assert.Greater(lastUpdateDateEn, firstUpdateDateEn); + } + } + + [Test] + public async Task Updating_Single_Variant_Property_Does_Not_Change_Update_Dates_Of_Other_Variants() + { + var content = await CreateCultureVariantContent(); + var firstUpdateDateEn = content.GetUpdateDate("en-US") + ?? throw new InvalidOperationException("Expected an update date for EN"); + var firstUpdateDateDa = content.GetUpdateDate("da-DK") + ?? throw new InvalidOperationException("Expected an update date for DA"); + + var updateModel = new ContentUpdateModel + { + Properties = + [ + new PropertyValueModel + { + Alias = "invariantTitle", + Value = "The invariant title" + }, + new PropertyValueModel + { + Culture = "en-US", + Alias = "variantTitle", + Value = content.GetValue("variantTitle", "en-US")! + }, + new PropertyValueModel + { + Culture = "da-DK", + Alias = "variantTitle", + Value = "The updated Danish title" + } + ], + Variants = + [ + new VariantModel { Culture = "en-US", Name = content.GetCultureName("en-US")! }, + new VariantModel { Culture = "da-DK", Name = content.GetCultureName("da-DK")! } + ] + }; + + var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + VerifyUpdate(result.Result.Content); + + // re-get and re-test + VerifyUpdate(await ContentEditingService.GetAsync(content.Key)); + + void VerifyUpdate(IContent? updatedContent) + { + Assert.IsNotNull(updatedContent); + Assert.AreEqual(firstUpdateDateEn, updatedContent.GetUpdateDate("en-US")); + + var lastUpdateDateDa = updatedContent.GetUpdateDate("da-DK") + ?? throw new InvalidOperationException("Expected an update date for DA"); + Assert.Greater(lastUpdateDateDa, firstUpdateDateDa); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs index 001a281a8839..8687f5490b90 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs @@ -74,7 +74,7 @@ protected IContentType CreateInvariantContentType(params ITemplate[] templates) return contentType; } - protected async Task CreateVariantContentType(ContentVariation variation = ContentVariation.Culture) + protected async Task CreateVariantContentType(ContentVariation variation = ContentVariation.Culture, bool variantTitleAsMandatory = true) { var language = new LanguageBuilder() .WithCultureInfo("da-DK") @@ -88,7 +88,7 @@ protected async Task CreateVariantContentType(ContentVariation var .AddPropertyType() .WithAlias("variantTitle") .WithName("Variant Title") - .WithMandatory(true) + .WithMandatory(variantTitleAsMandatory) .WithVariations(variation) .Done() .AddPropertyType() diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs index d7a5e372a2e3..c7bb0b07c5fb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs @@ -29,8 +29,9 @@ public class ContentTests private readonly PropertyEditorCollection _propertyEditorCollection = new (new DataEditorCollection(() => [])); - [Test] - public void Variant_Culture_Names_Track_Dirty_Changes() + [TestCase("name-fr", false)] + [TestCase("name-fr-updated", true)] + public void Variant_Culture_Names_Track_Dirty_Changes(string newName, bool expectedDirty) { var contentType = new ContentTypeBuilder() .WithAlias("contentType") @@ -58,14 +59,17 @@ public void Variant_Culture_Names_Track_Dirty_Changes() Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); content.ResetDirtyProperties(); + frCultureName.ResetDirtyProperties(); Assert.IsFalse(content.IsPropertyDirty("CultureInfos")); // it's been reset Assert.IsTrue(content.WasPropertyDirty("CultureInfos")); Thread.Sleep(500); // The "Date" wont be dirty if the test runs too fast since it will be the same date - content.SetCultureName("name-fr", langFr); - Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); - Assert.IsTrue(content.IsPropertyDirty("CultureInfos")); // it's true now since we've updated a name + content.SetCultureName(newName, langFr); + + // dirty is only true if we updated the name + Assert.AreEqual(expectedDirty, frCultureName.IsPropertyDirty("Date")); + Assert.AreEqual(expectedDirty, content.IsPropertyDirty("CultureInfos")); } [Test] @@ -97,6 +101,7 @@ public void Variant_Published_Culture_Names_Track_Dirty_Changes() Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); content.ResetDirtyProperties(); + frCultureName.ResetDirtyProperties(); Assert.IsFalse(content.IsPropertyDirty("PublishCultureInfos")); // it's been reset Assert.IsTrue(content.WasPropertyDirty("PublishCultureInfos"));