diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 4a1f0a09b5..64099606d6 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -57,7 +57,7 @@ namespace Umbraco.Core /// internal static IReadOnlyList GetCulturesUnpublishing(this IContent content) { - if (!content.ContentType.VariesByCulture() && !content.IsPropertyDirty("PublishCultureInfos")) + if (!content.ContentType.VariesByCulture() && !content.IsPropertyDirty("PublishCultureInfos") && !content.Published) return Array.Empty(); var culturesChanging = content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key); diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index 150390bb00..5f0a09285f 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -147,11 +147,9 @@ namespace Umbraco.Core.Models /// /// /// - public IEnumerable GetPending(ContentScheduleChange changeType, DateTime date) + public IReadOnlyList GetPending(ContentScheduleChange changeType, DateTime date) { - if (_schedule.TryGetValue(string.Empty, out var changes)) - return changes.Values.Where(x => x.Date <= date); - return Enumerable.Empty(); + return _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 5b377fb8bc..9e31936a62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1083,9 +1083,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //load in the schedule if (schedule.TryGetValue(temp.Content.Id, out var s)) temp.Content.ContentSchedule = s; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); } } @@ -1100,6 +1097,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetVariations(temp.Content, contentVariations, documentVariations); } + foreach(var c in content) + c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) + return content; } diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs index c424496a38..2eb3e39ed1 100644 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -31,7 +31,7 @@ namespace Umbraco.Core.Publishing /// public int CheckPendingAndProcess() { - var results = _contentService.PerformScheduledPublish(); + var results = _contentService.PerformScheduledPublish(DateTime.Now); return results.Count(x => x.Success); } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index f573a829f3..b22490dd1d 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -423,7 +423,7 @@ namespace Umbraco.Core.Services /// /// Publishes and unpublishes scheduled documents. /// - IEnumerable PerformScheduledPublish(); + IEnumerable PerformScheduledPublish(DateTime date); #endregion diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index a3f932d147..557cef8d06 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1174,7 +1174,7 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable PerformScheduledPublish() + public IEnumerable PerformScheduledPublish(DateTime date) { var evtMsgs = EventMessagesFactory.Get(); @@ -1182,7 +1182,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); - var now = DateTime.Now; + var now = date; foreach (var d in GetContentForRelease(now)) { @@ -1192,7 +1192,8 @@ namespace Umbraco.Core.Services.Implement //find which cultures have pending schedules var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleChange.Start, now) .Select(x => x.Culture) - .Distinct(); + .Distinct() + .ToList(); foreach (var c in pendingCultures) { @@ -1202,10 +1203,13 @@ namespace Umbraco.Core.Services.Implement d.PublishCulture(c); } - result = SavePublishing(d, d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - + if (pendingCultures.Count > 0) + { + result = SavePublishing(d, d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; + } } else { @@ -1214,9 +1218,8 @@ namespace Umbraco.Core.Services.Implement result = SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; } - - yield return result; } foreach (var d in GetContentForExpiration(now)) @@ -1227,7 +1230,8 @@ namespace Umbraco.Core.Services.Implement //find which cultures have pending schedules var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleChange.End, now) .Select(x => x.Culture) - .Distinct(); + .Distinct() + .ToList(); foreach (var c in pendingCultures) { @@ -1237,10 +1241,13 @@ namespace Umbraco.Core.Services.Implement d.UnpublishCulture(c); } - result = SavePublishing(d, d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - + if (pendingCultures.Count > 0) + { + result = SavePublishing(d, d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; + } } else { @@ -1249,9 +1256,10 @@ namespace Umbraco.Core.Services.Implement result = Unpublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + yield return result; } - yield return result; + } scope.Complete(); @@ -2292,14 +2300,14 @@ namespace Umbraco.Core.Services.Implement if (variesByCulture) { var publishedCultures = content.PublishedCultures.ToList(); - var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published - if (!cannotBePublished) - { - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); - cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published - } - if (cannotBePublished) + if (publishedCultures.Count == 0) // no published cultures = cannot be published + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryMissing = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published + + if (mandatoryMissing) return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); //track which cultures are being published @@ -2312,7 +2320,7 @@ namespace Umbraco.Core.Services.Implement if (((Content) content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) { Logger.Info("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document does not have published values"); - return new PublishResult(PublishResultType.FailedPublishNoPublishedValues, evtMsgs, content); + return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } //loop over each culture publishing - or string.Empty for invariant diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 4d2c45b10f..0dc679ba79 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -97,9 +97,9 @@ FailedPublishContentInvalid = FailedPublish | 8, /// - /// Cannot republish a document that hasn't been published. + /// Cannot publish a document that has no publishing flags or values /// - FailedPublishNoPublishedValues = FailedPublish | 9, // in ContentService.StrategyCanPublish - fixme weird + FailedPublishNothingToPublish = FailedPublish | 9, // in ContentService.StrategyCanPublish - fixme weird /// /// Some mandatory cultures are missing. diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 0f0f75edb5..92f0adf9d1 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -250,6 +250,90 @@ namespace Umbraco.Tests.Services Assert.That(hierarchy.All(c => c.Path.StartsWith("-1,-20") == false), Is.True); } + [Test] + public void Perform_Scheduled_Publishing() + { + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var ctInvariant = MockedContentTypes.CreateBasicContentType("invariantPage"); + ServiceContext.ContentTypeService.Save(ctInvariant); + + var ctVariant = MockedContentTypes.CreateBasicContentType("variantPage"); + ctVariant.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(ctVariant); + + var now = DateTime.Now; + + //10x invariant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds + var invariant = new List(); + for (var i = 0; i < 10; i++) + { + var c = MockedContent.CreateBasicContent(ctInvariant); + c.Name = "name" + i; + if (i % 2 == 0) + { + c.ContentSchedule.Add(now.AddSeconds(5), null); //release in 5 seconds + var r = ServiceContext.ContentService.Save(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + else + { + c.ContentSchedule.Add(null, now.AddSeconds(5)); //expire in 5 seconds + var r = ServiceContext.ContentService.SaveAndPublish(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + invariant.Add(c); + } + + //10x variant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds + var variant = new List(); + var alternatingCulture = langFr.IsoCode; + for (var i = 0; i < 10; i++) + { + var c = MockedContent.CreateBasicContent(ctVariant); + c.SetCultureName("name-uk" + i, langUk.IsoCode); + c.SetCultureName("name-fr" + i, langFr.IsoCode); + + if (i % 2 == 0) + { + c.ContentSchedule.Add(alternatingCulture, now.AddSeconds(5), null); //release in 5 seconds + var r = ServiceContext.ContentService.Save(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + + alternatingCulture = alternatingCulture == langFr.IsoCode ? langUk.IsoCode : langFr.IsoCode; + } + else + { + c.ContentSchedule.Add(alternatingCulture, null, now.AddSeconds(5)); //expire in 5 seconds + var r = ServiceContext.ContentService.SaveAndPublish(c); + Assert.IsTrue(r.Success, r.Result.ToString()); + } + variant.Add(c); + } + + + var runSched = ServiceContext.ContentService.PerformScheduledPublish( + now.AddMinutes(1)).ToList(); //lets go way later just to be safe, NOTE: This is NOT based on actual timer so it's safe + + //this is 21 because the test data installed before this test runs has a scheduled item! + Assert.AreEqual(21, runSched.Count); + Assert.AreEqual(20, runSched.Count(x => x.Success), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessPublish), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessUnpublish), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessPublishCulture), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + Assert.AreEqual(5, runSched.Count(x => x.Result == PublishResultType.SuccessUnpublishCulture), + string.Join(Environment.NewLine, runSched.Select(x => $"{x.Entity.Name} - {x.Result}"))); + } + [Test] public void Remove_Scheduled_Publishing_Date() {