diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 21a9c9ad15..a08f2291b4 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -364,7 +364,7 @@ namespace Umbraco.Core.Services /// /// Unpublishes a document or optionally unpublishes a culture and/or segment for the document. /// - PublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0); + UnpublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0); /// /// Gets a value indicating whether a document is path-publishable. diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 0047c53982..6531e0081f 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Services.Implement private readonly IAuditRepository _auditRepository; private readonly IContentTypeRepository _contentTypeRepository; private readonly IDocumentBlueprintRepository _documentBlueprintRepository; - + private readonly ILanguageRepository _languageRepository; private readonly MediaFileSystem _mediaFileSystem; private IQuery _queryNotTrashed; @@ -35,7 +35,7 @@ namespace Umbraco.Core.Services.Implement public ContentService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, MediaFileSystem mediaFileSystem, IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository, - IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository) + IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository) : base(provider, logger, eventMessagesFactory) { _mediaFileSystem = mediaFileSystem; @@ -44,6 +44,7 @@ namespace Umbraco.Core.Services.Implement _auditRepository = auditRepository; _contentTypeRepository = contentTypeRepository; _documentBlueprintRepository = documentBlueprintRepository; + _languageRepository = languageRepository; } #endregion @@ -1025,7 +1026,7 @@ namespace Umbraco.Core.Services.Implement } /// - public PublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0) + public UnpublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); @@ -1038,7 +1039,13 @@ namespace Umbraco.Core.Services.Implement if (tryUnpublishVariation) return tryUnpublishVariation.Result; //continue the normal Unpublish operation to unpublish the entire content item - return UnpublishInternal(scope, content, evtMsgs, userId); + var result = UnpublishInternal(scope, content, evtMsgs, userId); + + //not succesful, so return it + if (!result.Success) return result; + + //else check if there was a status returned from TryUnpublishVariationInternal and if so use that + return tryUnpublishVariation.Result ?? result; } } @@ -1055,7 +1062,7 @@ namespace Umbraco.Core.Services.Implement /// A successful attempt if a variant was unpublished and it wasn't the last, else a failed result if it's an invariant document or if it's the last. /// A failed result indicates that a normal unpublish operation will need to be performed. /// - private Attempt TryUnpublishVariationInternal(IScope scope, IContent content, EventMessages evtMsgs, string culture, string segment, int userId) + private Attempt TryUnpublishVariationInternal(IScope scope, IContent content, EventMessages evtMsgs, string culture, string segment, int userId) { if (!culture.IsNullOrWhiteSpace() || !segment.IsNullOrWhiteSpace()) { @@ -1064,16 +1071,27 @@ namespace Umbraco.Core.Services.Implement { //capture before we clear the values var publishedCultureCount = content.PublishedCultures.Count(); + + //we need to unpublish everything if this is a mandatory language + var unpublishAll = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode).Contains(culture, StringComparer.InvariantCultureIgnoreCase); + //fixme - this needs to be changed when 11227 is merged! content.ClearPublishedValues(culture, segment); //now we just publish with the name cleared SaveAndPublish(content, userId); Audit(AuditType.UnPublish, $"UnPublish variation culture: {culture ?? string.Empty}, segment: {segment ?? string.Empty} performed by user", userId, content.Id); - //This is not the last one, so complete the scope and return - if (publishedCultureCount > 1) + + //We don't need to unpublish all and this is not the last one, so complete the scope and return + if (!unpublishAll && publishedCultureCount > 1) { scope.Complete(); - return Attempt.Succeed(new PublishResult(PublishResultType.SuccessVariant, evtMsgs, content)); + return Attempt.Succeed(new UnpublishResult(UnpublishResultType.SuccessVariant, evtMsgs, content)); + } + + if (unpublishAll) + { + //return a fail with a custom status here so the normal unpublish operation takes place but the result will be this flag + return Attempt.Fail(new UnpublishResult(UnpublishResultType.SuccessMandatoryCulture, evtMsgs, content)); } } @@ -1085,8 +1103,8 @@ namespace Umbraco.Core.Services.Implement } } - //This is either a non variant document or this is the last one, so return a failed result which indicates that a normal unpublish operation needs to also take place - return Attempt.Fail(); + //This is either a non variant document or this is the last one or this was a mandatory variant, so return a failed result which indicates that a normal unpublish operation needs to also take place + return Attempt.Fail(); } /// @@ -1097,7 +1115,7 @@ namespace Umbraco.Core.Services.Implement /// /// /// - private PublishResult UnpublishInternal(IScope scope, IContent content, EventMessages evtMsgs, int userId) + private UnpublishResult UnpublishInternal(IScope scope, IContent content, EventMessages evtMsgs, int userId) { var newest = GetById(content.Id); // ensure we have the newest version if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version @@ -1105,7 +1123,7 @@ namespace Umbraco.Core.Services.Implement if (content.Published == false) { scope.Complete(); - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, content); // already unpublished + return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); // already unpublished } // strategy @@ -1123,7 +1141,7 @@ namespace Umbraco.Core.Services.Implement Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); scope.Complete(); - return new PublishResult(PublishResultType.Success, evtMsgs, content); + return new UnpublishResult(evtMsgs, content); } /// @@ -2142,23 +2160,23 @@ namespace Umbraco.Core.Services.Implement } // ensures that a document can be unpublished - internal PublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) + internal UnpublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) { // raise UnPublishing event if (scope.Events.DispatchCancelable(UnPublishing, this, new PublishEventArgs(content, evtMsgs))) { Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be unpublished: unpublishing was cancelled."); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); + return new UnpublishResult(UnpublishResultType.FailedCancelledByEvent, evtMsgs, content); } - return new PublishResult(evtMsgs, content); + return new UnpublishResult(evtMsgs, content); } // unpublishes a document - internal PublishResult StrategyUnpublish(IScope scope, IContent content, bool canUnpublish, int userId, EventMessages evtMsgs) + internal UnpublishResult StrategyUnpublish(IScope scope, IContent content, bool canUnpublish, int userId, EventMessages evtMsgs) { var attempt = canUnpublish - ? new PublishResult(evtMsgs, content) // already know we can + ? new UnpublishResult(evtMsgs, content) // already know we can : StrategyCanUnpublish(scope, content, userId, evtMsgs); // else check if (attempt.Success == false) diff --git a/src/Umbraco.Core/Services/PublishResult.cs b/src/Umbraco.Core/Services/PublishResult.cs index aeb981d74b..073d7ce1cb 100644 --- a/src/Umbraco.Core/Services/PublishResult.cs +++ b/src/Umbraco.Core/Services/PublishResult.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Services { + /// /// Represents the result of publishing a document. /// diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index e3c34b8439..b4bfe078b7 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -1,7 +1,8 @@ namespace Umbraco.Core.Services { + /// - /// A value indicating the result of (un)publishing a content item. + /// A value indicating the result of publishing a content item. /// public enum PublishResultType : byte { @@ -9,20 +10,15 @@ // every failure codes as >128 - see OperationResult and OperationResultType for details. /// - /// The (un)publishing was successful. + /// The publishing was successful. /// Success = 0, /// - /// The item was already (un)published. + /// The item was already published. /// SuccessAlready = 1, - - /// - /// The specified variant was unpublished, the content item itself remains published. - /// - SuccessVariant = 2, - + /// /// The operation failed. /// diff --git a/src/Umbraco.Core/Services/UnpublishResult.cs b/src/Umbraco.Core/Services/UnpublishResult.cs new file mode 100644 index 0000000000..7cd1506e6c --- /dev/null +++ b/src/Umbraco.Core/Services/UnpublishResult.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.Events; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + /// + /// Represents the result of unpublishing a document. + /// + public class UnpublishResult : OperationResult + { + /// + /// Creates a successful result + /// + /// + /// + public UnpublishResult(EventMessages eventMessages, IContent entity) : base(UnpublishResultType.Success, eventMessages, entity) + { + } + + public UnpublishResult(UnpublishResultType result, EventMessages eventMessages, IContent entity) : base(result, eventMessages, entity) + { + } + + /// + /// Gets the document. + /// + public IContent Content => Entity; + } +} diff --git a/src/Umbraco.Core/Services/UnpublishResultType.cs b/src/Umbraco.Core/Services/UnpublishResultType.cs new file mode 100644 index 0000000000..010c37d7a5 --- /dev/null +++ b/src/Umbraco.Core/Services/UnpublishResultType.cs @@ -0,0 +1,39 @@ +namespace Umbraco.Core.Services +{ + /// + /// A value indicating the result of unpublishing a content item. + /// + public enum UnpublishResultType : byte + { + /// + /// The unpublishing was successful. + /// + Success = 0, + + /// + /// The item was already unpublished. + /// + SuccessAlready = 1, + + /// + /// The specified variant was unpublished, the content item itself remains published. + /// + SuccessVariant = 2, + + /// + /// The specified variant was a mandatory culture therefore it was unpublished and the content item itself is unpublished + /// + SuccessMandatoryCulture = 3, + + /// + /// The operation failed. + /// + /// All values above this value indicate a failure. + Failed = 128, + + /// + /// The publish action has been cancelled by an event handler. + /// + FailedCancelledByEvent = Failed | 5, + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 456d7d667a..92854ea74c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1420,6 +1420,8 @@ + + diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 91002739d1..485cce4c0a 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -161,7 +161,7 @@ namespace Umbraco.Tests.TestHelpers var userService = GetLazyService(container, c => new UserService(scopeProvider, logger, eventMessagesFactory, runtimeState, GetRepo(c), GetRepo(c),globalSettings)); var dataTypeService = GetLazyService(container, c => new DataTypeService(scopeProvider, logger, eventMessagesFactory, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); - var contentService = GetLazyService(container, c => new ContentService(scopeProvider, logger, eventMessagesFactory, mediaFileSystem, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); + var contentService = GetLazyService(container, c => new ContentService(scopeProvider, logger, eventMessagesFactory, mediaFileSystem, GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c), GetRepo(c))); var notificationService = GetLazyService(container, c => new NotificationService(scopeProvider, userService.Value, contentService.Value, logger, GetRepo(c),globalSettings)); var serverRegistrationService = GetLazyService(container, c => new ServerRegistrationService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var memberGroupService = GetLazyService(container, c => new MemberGroupService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 85cb96864e..1bf64d524c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1385,7 +1385,8 @@ To manage your website, simply open the Umbraco back office and start adding con Please make sure that you do not have 2 templates with the same alias Template saved Template saved without any errors! - Content unpublished + Content unpublished + Content variation %0% unpublished Partial view saved Partial view saved without any errors! Partial view not saved diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 4f8051640a..e7bd4a6942 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -243,10 +243,10 @@ namespace Umbraco.Web.Editors //set a custom path since the tree that renders this has the content type id as the parent content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); - content.AllowedActions = new[] {"A"}; + content.AllowedActions = new[] { "A" }; content.IsBlueprint = true; - var excludeProps = new[] {"_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"}; + var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; var propsTab = content.Tabs.Last(); propsTab.Properties = propsTab.Properties .Where(p => excludeProps.Contains(p.Alias) == false); @@ -300,7 +300,7 @@ namespace Umbraco.Web.Editors //Remove all variants except for the default since currently the default must be saved before other variants can be edited //TODO: Allow for editing all variants at once ... this will be a future task - mapped.Variants = new[] {mapped.Variants.First(x => x.IsCurrent)}; + mapped.Variants = new[] { mapped.Variants.First(x => x.IsCurrent) }; return mapped; } @@ -515,7 +515,7 @@ namespace Umbraco.Web.Editors var notificationModel = new SimpleNotificationModel(); notificationModel.AddSuccessNotification( Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[]{ content.Name}) + Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) ); return notificationModel; @@ -944,23 +944,30 @@ namespace Umbraco.Web.Editors if (foundContent == null) HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.Unpublish(foundContent, culture:culture, userId: Security.CurrentUser.Id); + + var unpublishResult = Services.ContentService.Unpublish(foundContent, culture: culture, userId: Security.CurrentUser.Id); var content = MapToDisplay(foundContent, culture); - if (unpublishResult.Success == false) + if (!unpublishResult.Success) { AddCancelMessage(content); throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); } else - { - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + { + //fixme should have a better localized method for when we have the UnpublishResultType.SuccessMandatoryCulture status + + content.AddSuccessNotification( + Services.TextService.Localize("content/unPublish"), + unpublishResult.Result == UnpublishResultType.SuccessVariant + ? Services.TextService.Localize("speechBubbles/contentVariationUnpublished", new[] { culture }) + : Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; } - } - + } + /// /// Maps the dto property values to the persisted model /// @@ -1215,7 +1222,7 @@ namespace Umbraco.Web.Editors new Dictionary { { ContextMapper.CultureKey, culture } }); return display; - } - + } + } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs index e0b2746aa2..968a2a08e3 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs @@ -35,7 +35,11 @@ namespace Umbraco.Web.Models.Mapping Mandatory = x.Mandatory, Name = source.GetName(x.IsoCode), Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? - PublishedState = (source.IsCulturePublished(x.IsoCode) ? PublishedState.Published : PublishedState.Unpublished).ToString(), + PublishedState = (source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished + ? PublishedState.Unpublished + : source.IsCulturePublished(x.IsoCode) + ? PublishedState.Published + : PublishedState.Unpublished).ToString(), IsEdited = source.IsCultureEdited(x.IsoCode) //Segment = ?? We'll need to populate this one day when we support segments }).ToList();