diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index cfcaa921fb..bd5a3445f6 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace Umbraco.Core.Events { @@ -7,6 +8,32 @@ namespace Umbraco.Core.Events /// public static class EventExtensions { + /// + /// Raises the event and checks if a cancelation has occurred, if so it checks if any messages have been + /// added to the event messages collection and if not it adds a default cancelation message + /// + /// + /// + /// + /// + /// + /// + /// + public static bool IsRaisedEventCancelled( + this TypedEventHandler eventHandler, + EventMessages evtMsgs, + Func args, + TSender sender) + where TArgs : CancellableEventArgs + { + var evtArgs = args(evtMsgs); + + if (eventHandler != null) + eventHandler(sender, evtArgs); + + return evtArgs.Cancel; + } + /// /// Raises the event and returns a boolean value indicating if the event was cancelled /// diff --git a/src/Umbraco.Core/Events/EventMessages.cs b/src/Umbraco.Core/Events/EventMessages.cs index adea701d4a..2900f3d471 100644 --- a/src/Umbraco.Core/Events/EventMessages.cs +++ b/src/Umbraco.Core/Events/EventMessages.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; namespace Umbraco.Core.Events { /// - /// Default transient event messages collection + /// Event messages collection /// - public sealed class EventMessages + public sealed class EventMessages : DisposableObject { private readonly List _msgs = new List(); @@ -14,9 +14,19 @@ namespace Umbraco.Core.Events _msgs.Add(msg); } + public int Count + { + get { return _msgs.Count; } + } + public IEnumerable GetAll() { return _msgs; } + + protected override void DisposeResources() + { + _msgs.Clear(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IEventMessagesFactory.cs b/src/Umbraco.Core/Events/IEventMessagesFactory.cs index e85b853ee1..cb2391186d 100644 --- a/src/Umbraco.Core/Events/IEventMessagesFactory.cs +++ b/src/Umbraco.Core/Events/IEventMessagesFactory.cs @@ -2,19 +2,11 @@ using System.Collections.Generic; namespace Umbraco.Core.Events { + /// + /// Event messages factory + /// public interface IEventMessagesFactory { - EventMessages CreateMessages(); - } - - /// - /// A simple/default transient messages factory - /// - internal class TransientMessagesFactory : IEventMessagesFactory - { - public EventMessages CreateMessages() - { - return new EventMessages(); - } + EventMessages Get(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index d3ac8b22e5..b84e28285e 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -4,12 +4,55 @@ namespace Umbraco.Core.Events { public class SaveEventArgs : CancellableObjectEventArgs> { - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel) + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) + { + } + + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel) : base(eventObject, canCancel) { } diff --git a/src/Umbraco.Core/Events/TransientMessagesFactory.cs b/src/Umbraco.Core/Events/TransientMessagesFactory.cs new file mode 100644 index 0000000000..5cd291a37f --- /dev/null +++ b/src/Umbraco.Core/Events/TransientMessagesFactory.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Events +{ + /// + /// A simple/default transient messages factory + /// + internal class TransientMessagesFactory : IEventMessagesFactory + { + public EventMessages Get() + { + return new EventMessages(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 0c861ac1ef..57073731a7 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -28,7 +28,6 @@ namespace Umbraco.Core.Services /// public class ContentService : RepositoryService, IContentService { - private readonly IPublishingStrategy _publishingStrategy; private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; @@ -50,7 +49,7 @@ namespace Umbraco.Core.Services { if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - if (userService == null) throw new ArgumentNullException("userService"); + if (userService == null) throw new ArgumentNullException("userService"); _publishingStrategy = publishingStrategy; _dataTypeService = dataTypeService; _userService = userService; @@ -746,6 +745,8 @@ namespace Umbraco.Core.Services } } + + /// /// Checks whether an item has any children /// @@ -939,29 +940,30 @@ namespace Umbraco.Core.Services /// The to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. - public void Save(IContent content, int userId = 0, bool raiseEvents = true) + public Attempt SaveWithStatus(IContent content, int userId = 0, bool raiseEvents = true) { - Save(content, true, userId, raiseEvents); + return Save(content, true, userId, raiseEvents); } /// /// Saves a collection of objects. - /// - /// - /// If the collection of content contains new objects that references eachother by Id or ParentId, - /// then use the overload Save method with a collection of Lazy . - /// + /// /// Collection of to save /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) + /// Optional boolean indicating whether or not to raise events. + public Attempt SaveWithStatus(IEnumerable contents, int userId = 0, bool raiseEvents = true) { var asArray = contents.ToArray(); if (raiseEvents) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; + if (Saving.IsRaisedEventCancelled( + EventMessagesFactory.Get(), + messages => new SaveEventArgs(asArray, messages), + this)) + { + return Attempt.Fail(OperationStatus.Cancelled); + } } using (new WriteLock(Locker)) { @@ -1003,9 +1005,37 @@ namespace Umbraco.Core.Services Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + + return Attempt.Succeed(OperationStatus.Success); } } + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IContent content, int userId = 0, bool raiseEvents = true) + { + SaveWithStatus(content, userId, raiseEvents); + } + + /// + /// Saves a collection of objects. + /// + /// + /// If the collection of content contains new objects that references eachother by Id or ParentId, + /// then use the overload Save method with a collection of Lazy . + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) + { + SaveWithStatus(contents, userId, raiseEvents); + } + /// /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. /// @@ -1373,7 +1403,6 @@ namespace Umbraco.Core.Services /// True if sending publication was succesfull otherwise false public bool SendToPublication(IContent content, int userId = 0) { - if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) return false; @@ -1383,8 +1412,7 @@ namespace Umbraco.Core.Services SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); - - //TODO: will this ever be false?? + return true; } @@ -1778,7 +1806,9 @@ namespace Umbraco.Core.Services { if (raiseEvents) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + if (Saving.IsRaisedEventCancelled( + EventMessagesFactory.Get(), + messages => new SaveEventArgs(content, messages), this)) { return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent)); } @@ -1867,12 +1897,17 @@ namespace Umbraco.Core.Services /// Boolean indicating whether or not to change the Published state upon saving /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. - private void Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) + private Attempt Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) { if (raiseEvents) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - return; + if (Saving.IsRaisedEventCancelled( + EventMessagesFactory.Get(), + messages => new SaveEventArgs(content, messages), + this)) + { + return Attempt.Fail(OperationStatus.Cancelled); + } } using (new WriteLock(Locker)) @@ -1902,6 +1937,8 @@ namespace Umbraco.Core.Services Saved.RaiseEvent(new SaveEventArgs(content, false), this); Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); + + return Attempt.Succeed(OperationStatus.Success); } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index f6c2625e23..23c67edd1c 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -195,6 +195,24 @@ namespace Umbraco.Core.Services /// The to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. + Attempt SaveWithStatus(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt SaveWithStatus(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use SaveWithStatus instead, that method will provide more detailed information on the outcome")] void Save(IContent content, int userId = 0, bool raiseEvents = true); /// @@ -203,6 +221,8 @@ namespace Umbraco.Core.Services /// Collection of to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use SaveWithStatus instead, that method will provide more detailed information on the outcome")] void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); /// @@ -347,6 +367,7 @@ namespace Umbraco.Core.Services /// The to publish along with its children /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] bool PublishWithChildren(IContent content, int userId = 0); @@ -375,6 +396,7 @@ namespace Umbraco.Core.Services /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] + [EditorBrowsable(EditorBrowsableState.Never)] bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 1de23b5b0a..ed363b6279 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -181,6 +181,24 @@ namespace Umbraco.Core.Services /// The to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. + Attempt SaveWithStatus(IMedia media, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt SaveWithStatus(IEnumerable medias, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use SaveWithStatus instead, that method will provide more detailed information on the outcome")] void Save(IMedia media, int userId = 0, bool raiseEvents = true); /// @@ -189,6 +207,8 @@ namespace Umbraco.Core.Services /// Collection of to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use SaveWithStatus instead, that method will provide more detailed information on the outcome")] void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); /// diff --git a/src/Umbraco.Core/Services/IService.cs b/src/Umbraco.Core/Services/IService.cs index 448e09aac4..e80576c493 100644 --- a/src/Umbraco.Core/Services/IService.cs +++ b/src/Umbraco.Core/Services/IService.cs @@ -1,3 +1,5 @@ +using Umbraco.Core.Logging; + namespace Umbraco.Core.Services { /// @@ -5,6 +7,6 @@ namespace Umbraco.Core.Services /// public interface IService { - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index f73c1ce0f8..d324a51720 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -896,6 +896,8 @@ namespace Umbraco.Core.Services Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); } + + /// /// Permanently deletes versions from an object prior to a specific date. /// This method will never delete the latest version of a content item. @@ -955,14 +957,14 @@ namespace Umbraco.Core.Services /// Saves a single object /// /// The to save - /// Id of the User saving the Content + /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. - public void Save(IMedia media, int userId = 0, bool raiseEvents = true) + public Attempt SaveWithStatus(IMedia media, int userId = 0, bool raiseEvents = true) { if (raiseEvents) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) - return; + return Attempt.Fail(OperationStatus.Cancelled); } var uow = UowProvider.GetUnitOfWork(); @@ -984,22 +986,24 @@ namespace Umbraco.Core.Services Saved.RaiseEvent(new SaveEventArgs(media, false), this); Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); + + return Attempt.Succeed(OperationStatus.Success); } /// /// Saves a collection of objects /// /// Collection of to save - /// Id of the User saving the Content + /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) + public Attempt SaveWithStatus(IEnumerable medias, int userId = 0, bool raiseEvents = true) { var asArray = medias.ToArray(); if (raiseEvents) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; + return Attempt.Fail(OperationStatus.Cancelled); } var uow = UowProvider.GetUnitOfWork(); @@ -1025,6 +1029,30 @@ namespace Umbraco.Core.Services Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); Audit(AuditType.Save, "Save Media items performed by user", userId, -1); + + return Attempt.Succeed(OperationStatus.Success); + } + + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IMedia media, int userId = 0, bool raiseEvents = true) + { + SaveWithStatus(media, userId, raiseEvents); + } + + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) + { + SaveWithStatus(medias, userId, raiseEvents); } /// diff --git a/src/Umbraco.Core/Services/OperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus.cs new file mode 100644 index 0000000000..8aeaafd27a --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Core.Services +{ + /// + /// The status returned by many of the service methods + /// + public class OperationStatus + { + //TODO: This is a pretty simple class atm, but is 'future' proofed in case we need to add more detail here + + internal static OperationStatus Cancelled + { + get { return new OperationStatus(OperationStatusType.FailedCancelledByEvent);} + } + + internal static OperationStatus Success + { + get { return new OperationStatus(OperationStatusType.Success); } + } + + public OperationStatus(OperationStatusType statusType) + { + StatusType = statusType; + } + public OperationStatusType StatusType { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/OperationStatusType.cs b/src/Umbraco.Core/Services/OperationStatusType.cs new file mode 100644 index 0000000000..a54d125214 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatusType.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Core.Services +{ + /// + /// A status type of the result of publishing a content item + /// + /// + /// Anything less than 10 = Success! + /// + public enum OperationStatusType + { + /// + /// The saving was successful. + /// + Success = 0, + + /// + /// The saving has been cancelled by a 3rd party add-in + /// + FailedCancelledByEvent = 14 + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1bdabacc9c..040df0b8e8 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -315,7 +315,6 @@ - @@ -325,6 +324,7 @@ + @@ -474,6 +474,8 @@ + + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 80b2519bfc..17317d282d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -774,7 +774,9 @@ To manage your website, simply open the Umbraco back office and start adding con
Do not close this window during sorting]]>
- Publishing was cancelled by a 3rd party add-in + Cancelled + Operation was cancelled by a 3rd party add-in + Publishing was cancelled by a 3rd party add-in Property type already exists Property type created DataType: %1%]]> 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 fbef663e96..87e6fabdb1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -775,6 +775,8 @@ To manage your website, simply open the Umbraco back office and start adding con
Do not close this window during sorting]]>
+ Cancelled + Operation was cancelled by a 3rd party add-in Publishing was cancelled by a 3rd party add-in Property type already exists Property type created diff --git a/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs b/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs new file mode 100644 index 0000000000..f4350bc596 --- /dev/null +++ b/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs @@ -0,0 +1,21 @@ +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Editors +{ + /// + /// An abstract controller that automatically checks if any request is a non-GET and if the + /// resulting message is INotificationModel in which case it will append any Event Messages + /// currently in the request. + /// + [AppendCurrentEventMessages] + public abstract class BackOfficeNotificationsController : UmbracoAuthorizedJsonController + { + protected BackOfficeNotificationsController() + { + } + + protected BackOfficeNotificationsController(UmbracoContext umbracoContext) : base(umbracoContext) + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 046a433d3a..5c8e4b9497 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -250,22 +250,24 @@ namespace Umbraco.Web.Editors //initialize this to successful var publishStatus = Attempt.Succeed(); + var wasCancelled = false; if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) { //save the item - Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); + var saveResult = Services.ContentService.SaveWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; } else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) { - Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; } else { //publish the item and check if it worked, if not we will show a diff msg below publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); } - //return the updated model var display = Mapper.Map(contentItem.PersistedContent); @@ -278,11 +280,33 @@ namespace Umbraco.Web.Editors { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - display.AddSuccessNotification(ui.Text("speechBubbles", "editContentSavedHeader"), ui.Text("speechBubbles", "editContentSavedText")); + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles/editContentSavedText")); + } + else + { + display.AddWarningNotification( + Services.TextService.Localize("speechBubbles/operationCancelledHeader"), + Services.TextService.Localize("speechBubbles/operationCancelledText")); + } break; case ContentSaveAction.SendPublish: case ContentSaveAction.SendPublishNew: - display.AddSuccessNotification(ui.Text("speechBubbles", "editContentSendToPublish"), ui.Text("speechBubbles", "editContentSendToPublishText")); + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + else + { + display.AddWarningNotification( + Services.TextService.Localize("speechBubbles/operationCancelledHeader"), + Services.TextService.Localize("speechBubbles/operationCancelledText")); + } break; case ContentSaveAction.Publish: case ContentSaveAction.PublishNew: @@ -374,23 +398,22 @@ namespace Umbraco.Web.Editors { case PublishStatusType.FailedPathNotPublished: return Request.CreateValidationErrorResponse( - ui.Text("publish", "contentPublishedFailedByParent", - string.Format("{0} ({1})", publishResult.Result.ContentItem.Name, publishResult.Result.ContentItem.Id), - Security.CurrentUser).Trim()); + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] {string.Format("{0} ({1})", publishResult.Result.ContentItem.Name, publishResult.Result.ContentItem.Id)}).Trim()); case PublishStatusType.FailedCancelledByEvent: return Request.CreateValidationErrorResponse( - ui.Text("speechBubbles", "contentPublishedFailedByEvent")); + Services.TextService.Localize("speechBubbles/contentPublishedFailedByEvent")); case PublishStatusType.FailedHasExpired: case PublishStatusType.FailedAwaitingRelease: case PublishStatusType.FailedIsTrashed: case PublishStatusType.FailedContentInvalid: return Request.CreateValidationErrorResponse( - ui.Text("publish", "contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", publishResult.Result.ContentItem.Name, publishResult.Result.ContentItem.Id), - string.Join(",", publishResult.Result.InvalidProperties.Select(x => x.Alias)) - }, Security.CurrentUser)); + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", publishResult.Result.ContentItem.Name, publishResult.Result.ContentItem.Id), + string.Join(",", publishResult.Result.InvalidProperties.Select(x => x.Alias)) + })); } } @@ -539,7 +562,7 @@ namespace Umbraco.Web.Editors Services.ContentService.UnPublish(foundContent); var content = Mapper.Map(foundContent); - content.AddSuccessNotification(ui.Text("content", "unPublish"), ui.Text("speechBubbles", "contentUnpublished")); + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); return content; } @@ -568,7 +591,7 @@ namespace Umbraco.Web.Editors if (toMove.ContentType.AllowedAsRoot == false) { throw new HttpResponseException( - Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedAtRoot", Security.CurrentUser))); + Request.CreateValidationErrorResponse(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); } } else @@ -584,14 +607,14 @@ namespace Umbraco.Web.Editors .Any(x => x.Value == toMove.ContentType.Id) == false) { throw new HttpResponseException( - Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedByContentType", Security.CurrentUser))); + Request.CreateValidationErrorResponse(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); } // Check on paths if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { throw new HttpResponseException( - Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedByPath", Security.CurrentUser))); + Request.CreateValidationErrorResponse(Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); } } @@ -605,30 +628,25 @@ namespace Umbraco.Web.Editors case PublishStatusType.Success: case PublishStatusType.SuccessAlreadyPublished: display.AddSuccessNotification( - ui.Text("speechBubbles", "editContentPublishedHeader", UmbracoUser), - ui.Text("speechBubbles", "editContentPublishedText", UmbracoUser)); + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles/editContentPublishedText")); break; case PublishStatusType.FailedPathNotPublished: display.AddWarningNotification( - ui.Text("publish"), - ui.Text("publish", "contentPublishedFailedByParent", - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - UmbracoUser).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); break; case PublishStatusType.FailedCancelledByEvent: display.AddWarningNotification( - ui.Text("publish"), - ui.Text("speechBubbles", "contentPublishedFailedByEvent")); + Services.TextService.Localize("publish"), + Services.TextService.Localize("speechBubbles/contentPublishedFailedByEvent")); break; case PublishStatusType.FailedAwaitingRelease: display.AddWarningNotification( - ui.Text("publish"), - ui.Text("publish", "contentPublishedFailedAwaitingRelease", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) - }, - UmbracoUser).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); break; case PublishStatusType.FailedHasExpired: //TODO: We should add proper error messaging for this! @@ -636,14 +654,13 @@ namespace Umbraco.Web.Editors //TODO: We should add proper error messaging for this! case PublishStatusType.FailedContentInvalid: display.AddWarningNotification( - ui.Text("publish"), - ui.Text("publish", "contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }, - UmbracoUser).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); break; default: throw new IndexOutOfRangeException(); diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index 5f02819f4d..d5dbdad620 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -13,7 +13,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.Models; + namespace Umbraco.Web.Editors { @@ -21,7 +21,7 @@ namespace Umbraco.Web.Editors /// An abstract base controller used for media/content (and probably members) to try to reduce code replication. ///
[OutgoingDateTimeFormat] - public abstract class ContentControllerBase : UmbracoAuthorizedJsonController + public abstract class ContentControllerBase : BackOfficeNotificationsController { /// /// Constructor diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 7f9d703763..c34170ccda 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -248,7 +248,7 @@ namespace Umbraco.Web.Editors } //save the item - Services.MediaService.Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + var saveStatus = Services.MediaService.SaveWithStatus(contentItem.PersistedContent, (int)Security.CurrentUser.Id); //return the updated model var display = Mapper.Map(contentItem.PersistedContent); @@ -261,7 +261,19 @@ namespace Umbraco.Web.Editors { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - display.AddSuccessNotification(ui.Text("speechBubbles", "editMediaSaved"), ui.Text("speechBubbles", "editMediaSavedText")); + if (saveStatus.Success) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editMediaSaved"), + Services.TextService.Localize("speechBubbles/editMediaSavedText")); + } + else + { + display.AddWarningNotification( + Services.TextService.Localize("speechBubbles/operationCancelledHeader"), + Services.TextService.Localize("speechBubbles/operationCancelledText")); + } + break; } diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs index 944b85c576..b5c3d850d2 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs @@ -24,15 +24,6 @@ namespace Umbraco.Web.PublishedCache UmbracoContext = umbracoContext; } - /// - /// Informs the contextual cache that content has changed. - /// - /// The contextual cache may, although that is not mandatory, provide an immutable snapshot of - /// the content over the duration of the context. If you make changes to the content and do want to have - /// the cache update its snapshot, you have to explicitely ask it to do so by calling ContentHasChanged. - public virtual void ContentHasChanged() - { } - /// /// Gets a content identified by its unique identifier. /// diff --git a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs b/src/Umbraco.Web/RequestLifespanMessagesFactory.cs index 11842f9d45..ec3a8b2442 100644 --- a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs +++ b/src/Umbraco.Web/RequestLifespanMessagesFactory.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web _ctxAccessor = ctxAccessor; } - public EventMessages CreateMessages() + public EventMessages Get() { if (_ctxAccessor.Value.HttpContext.Items[typeof (RequestLifespanMessagesFactory).Name] == null) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 605725598b..d92c877327 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -301,6 +301,7 @@ + @@ -632,6 +633,7 @@ + diff --git a/src/Umbraco.Web/UmbracoContextExtensions.cs b/src/Umbraco.Web/UmbracoContextExtensions.cs index f75487ac66..ec85b4dad6 100644 --- a/src/Umbraco.Web/UmbracoContextExtensions.cs +++ b/src/Umbraco.Web/UmbracoContextExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Core.Events; namespace Umbraco.Web { @@ -10,20 +11,18 @@ namespace Umbraco.Web /// public static class UmbracoContextExtensions { + /// - /// Informs the context that content has changed. + /// If there are event messages in the current request this will return them , otherwise it will return null /// - /// The context. - /// - /// The contextual caches may, although that is not mandatory, provide an immutable snapshot of - /// the content over the duration of the context. If you make changes to the content and do want to have - /// the caches update their snapshot, you have to explicitely ask them to do so by calling ContentHasChanged. - /// The context informs the contextual caches that content has changed. - /// - public static void ContentHasChanged(this UmbracoContext context) + /// + /// + public static EventMessages GetCurrentEventMessages(this UmbracoContext umbracoContext) { - context.ContentCache.ContentHasChanged(); - context.MediaCache.ContentHasChanged(); + var msgs = umbracoContext.HttpContext.Items[typeof (RequestLifespanMessagesFactory).Name]; + if (msgs == null) return null; + return (EventMessages) msgs; } + } } diff --git a/src/Umbraco.Web/WebApi/Filters/AppendCurrentEventMessagesAttribute.cs b/src/Umbraco.Web/WebApi/Filters/AppendCurrentEventMessagesAttribute.cs new file mode 100644 index 0000000000..fc248dc1d4 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/AppendCurrentEventMessagesAttribute.cs @@ -0,0 +1,66 @@ +using System; +using System.Net.Http; +using System.Web.Http.Filters; +using Umbraco.Core.Events; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.UI; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Automatically checks if any request is a non-GET and if the + /// resulting message is INotificationModel in which case it will append any Event Messages + /// currently in the request. + /// + internal sealed class AppendCurrentEventMessagesAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext context) + { + if (context.Response == null) return; + if (context.Response.IsSuccessStatusCode == false) return; + if (context.Request.Method == HttpMethod.Get) return; + if (UmbracoContext.Current == null) return; + + var obj = context.Response.Content as ObjectContent; + if (obj == null) return; + + var notifications = obj.Value as INotificationModel; + if (notifications == null) return; + + var msgs = UmbracoContext.Current.GetCurrentEventMessages(); + if (msgs == null) return; + + foreach (var eventMessage in msgs.GetAll()) + { + SpeechBubbleIcon msgType; + switch (eventMessage.MessageType) + { + case EventMessageType.Default: + msgType = SpeechBubbleIcon.Save; + break; + case EventMessageType.Info: + msgType = SpeechBubbleIcon.Info; + break; + case EventMessageType.Error: + msgType = SpeechBubbleIcon.Error; + break; + case EventMessageType.Success: + msgType = SpeechBubbleIcon.Success; + break; + case EventMessageType.Warning: + msgType = SpeechBubbleIcon.Warning; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + notifications.Notifications.Add(new Notification + { + Message = eventMessage.Message, + Header = eventMessage.Category, + NotificationType = msgType + }); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs index d8e185963a..4c4d831241 100644 --- a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Net.Http.Formatting; using System.Web.Http.Controllers; -using System.Web.Http.Filters; using Umbraco.Core; namespace Umbraco.Web.WebApi.Filters @@ -10,7 +9,7 @@ namespace Umbraco.Web.WebApi.Filters /// /// Sets the json outgoing/serialized datetime format /// - internal sealed class OutgoingDateTimeFormatAttribute : Attribute, IControllerConfiguration + internal sealed class OutgoingDateTimeFormatAttribute : Attribute, IControllerConfiguration { private readonly string _format = "yyyy-MM-dd HH:mm:ss"; @@ -27,8 +26,9 @@ namespace Umbraco.Web.WebApi.Filters /// /// Will use the standard ISO format /// - public OutgoingDateTimeFormatAttribute(){ - + public OutgoingDateTimeFormatAttribute() + { + } public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)