diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index e476bb2e54..80f5a14662 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -159,15 +159,16 @@ namespace Umbraco.Core /// protected virtual ServiceContext CreateServiceContext(DatabaseContext dbContext, IDatabaseFactory dbFactory) { + //default transient factory + var msgFactory = new TransientMessagesFactory(); return new ServiceContext( new RepositoryFactory(ApplicationCache, ProfilingLogger.Logger, dbContext.SqlSyntax, UmbracoConfig.For.UmbracoSettings()), new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), - new PublishingStrategy(), + new PublishingStrategy(msgFactory, ProfilingLogger.Logger), ApplicationCache, ProfilingLogger.Logger, - //default transient factory - new TransientMessagesFactory()); + msgFactory); } /// diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs index 4edee846cf..1025066bcc 100644 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs @@ -4,12 +4,56 @@ namespace Umbraco.Core.Events { public class DeleteEventArgs : CancellableObjectEventArgs> { - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, bool canCancel) : base(eventObject, canCancel) + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) : base(eventObject, canCancel, eventMessages) + { + MediaFilesToDelete = new List(); + } + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) + { + MediaFilesToDelete = new List(); + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) + { + MediaFilesToDelete = new List(); + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) + { + MediaFilesToDelete = new List(); + } + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, bool canCancel) : base(eventObject, canCancel) { MediaFilesToDelete = new List(); } @@ -60,7 +104,13 @@ namespace Umbraco.Core.Events public class DeleteEventArgs : CancellableEventArgs { - public DeleteEventArgs(int id, bool canCancel) + public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) + : base(canCancel, eventMessages) + { + Id = id; + } + + public DeleteEventArgs(int id, bool canCancel) : base(canCancel) { Id = id; diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs index c551dc626e..0f0a5183a9 100644 --- a/src/Umbraco.Core/Events/MoveEventArgs.cs +++ b/src/Umbraco.Core/Events/MoveEventArgs.cs @@ -6,6 +6,49 @@ namespace Umbraco.Core.Events { public class MoveEventArgs : CancellableObjectEventArgs { + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// + /// A colleciton of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(bool canCancel, EventMessages eventMessages, params MoveEventInfo[] moveInfo) + : base(default(TEntity), canCancel, eventMessages) + { + if (moveInfo.FirstOrDefault() == null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + //assign the legacy props + EventObject = moveInfo.First().Entity; + ParentId = moveInfo.First().NewParentId; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// A colleciton of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(EventMessages eventMessages, params MoveEventInfo[] moveInfo) + : base(default(TEntity), eventMessages) + { + if (moveInfo.FirstOrDefault() == null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + //assign the legacy props + EventObject = moveInfo.First().Entity; + ParentId = moveInfo.First().NewParentId; + } + /// /// Constructor accepting a collection of MoveEventInfo objects /// diff --git a/src/Umbraco.Core/Events/PublishEventArgs.cs b/src/Umbraco.Core/Events/PublishEventArgs.cs index 2355e250ef..a791781617 100644 --- a/src/Umbraco.Core/Events/PublishEventArgs.cs +++ b/src/Umbraco.Core/Events/PublishEventArgs.cs @@ -4,6 +4,52 @@ namespace Umbraco.Core.Events { public class PublishEventArgs : CancellableObjectEventArgs> { + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, bool canCancel, bool isAllPublished, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + IsAllRepublished = isAllPublished; + } + + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public PublishEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + /// + public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) + { + IsAllRepublished = isAllPublished; + } + /// /// Constructor accepting multiple entities that are used in the publish operation /// diff --git a/src/Umbraco.Core/Publishing/PublishStatus.cs b/src/Umbraco.Core/Publishing/PublishStatus.cs index 519046f5f9..ebb505730a 100644 --- a/src/Umbraco.Core/Publishing/PublishStatus.cs +++ b/src/Umbraco.Core/Publishing/PublishStatus.cs @@ -1,24 +1,17 @@ using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Services; namespace Umbraco.Core.Publishing { /// /// The result of publishing a content item /// - public class PublishStatus + public class PublishStatus : OperationStatus { - public PublishStatus() - { - //initialize - InvalidProperties = new List(); - } - public PublishStatus(IContent content, PublishStatusType statusType) - : this() - { - ContentItem = content; - StatusType = statusType; + : base(content, statusType) + { } /// @@ -29,8 +22,10 @@ namespace Umbraco.Core.Publishing { } - public IContent ContentItem { get; private set; } - public PublishStatusType StatusType { get; internal set; } + public IContent ContentItem + { + get { return Entity; } + } /// /// Gets sets the invalid properties if the status failed due to validation. diff --git a/src/Umbraco.Core/Publishing/PublishingStrategy.cs b/src/Umbraco.Core/Publishing/PublishingStrategy.cs index 4489a23e7e..258b4874c4 100644 --- a/src/Umbraco.Core/Publishing/PublishingStrategy.cs +++ b/src/Umbraco.Core/Publishing/PublishingStrategy.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core; +using Umbraco.Core.Services; namespace Umbraco.Core.Publishing { @@ -14,6 +15,16 @@ namespace Umbraco.Core.Publishing /// public class PublishingStrategy : BasePublishingStrategy { + private readonly IEventMessagesFactory _eventMessagesFactory; + private readonly ILogger _logger; + + public PublishingStrategy(IEventMessagesFactory eventMessagesFactory, ILogger logger) + { + if (eventMessagesFactory == null) throw new ArgumentNullException("eventMessagesFactory"); + if (logger == null) throw new ArgumentNullException("logger"); + _eventMessagesFactory = eventMessagesFactory; + _logger = logger; + } /// /// Publishes a single piece of Content @@ -22,18 +33,20 @@ namespace Umbraco.Core.Publishing /// Id of the User issueing the publish operation internal Attempt PublishInternal(IContent content, int userId) { - if (Publishing.IsRaisedEventCancelled(new PublishEventArgs(content), this)) + + if (Publishing.IsRaisedEventCancelled( + _eventMessagesFactory.Get(), + messages => new PublishEventArgs(content), this)) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be published, the event was cancelled.", content.Name, content.Id)); return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent)); } - //Check if the Content is Expired to verify that it can in fact be published if (content.Status == ContentStatus.Expired) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' has expired and could not be published.", content.Name, content.Id)); return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedHasExpired)); @@ -42,7 +55,7 @@ namespace Umbraco.Core.Publishing //Check if the Content is Awaiting Release to verify that it can in fact be published if (content.Status == ContentStatus.AwaitingRelease) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' is awaiting release and could not be published.", content.Name, content.Id)); return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedAwaitingRelease)); @@ -51,7 +64,7 @@ namespace Umbraco.Core.Publishing //Check if the Content is Trashed to verify that it can in fact be published if (content.Status == ContentStatus.Trashed) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' is trashed and could not be published.", content.Name, content.Id)); return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedIsTrashed)); @@ -59,7 +72,7 @@ namespace Umbraco.Core.Publishing content.ChangePublishedState(PublishedState.Published); - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' has been published.", content.Name, content.Id)); @@ -122,7 +135,7 @@ namespace Umbraco.Core.Publishing // much difference because we iterate over them all anyways?? Morten? // Because we're grouping I think this will execute all the queries anyways so need to fetch it all first. var fetchedContent = content.ToArray(); - + //We're going to populate the statuses with all content that is already published because below we are only going to iterate over // content that is not published. We'll set the status to "AlreadyPublished" statuses.AddRange(fetchedContent.Where(x => x.Published) @@ -138,7 +151,7 @@ namespace Umbraco.Core.Publishing //be published regardless of the rules mentioned in the remarks. if (!firstLevel.HasValue) { - firstLevel = level.Key; + firstLevel = level.Key; } /* Only update content thats not already been published - we want to loop through @@ -149,7 +162,7 @@ namespace Umbraco.Core.Publishing //Check if this item should be excluded because it's parent's publishing has failed/cancelled if (parentsIdsCancelled.Contains(item.ParentId)) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be published because it's parent's publishing action failed or was cancelled.", item.Name, item.Id)); //if this cannot be published, ensure that it's children can definitely not either! parentsIdsCancelled.Add(item.Id); @@ -165,23 +178,25 @@ namespace Umbraco.Core.Publishing } //Fire Publishing event - if (Publishing.IsRaisedEventCancelled(new PublishEventArgs(item), this)) + if (Publishing.IsRaisedEventCancelled( + _eventMessagesFactory.Get(), + messages => new PublishEventArgs(item, messages), this)) { //the publishing has been cancelled. - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be published, the event was cancelled.", item.Name, item.Id)); statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedCancelledByEvent))); //Does this document apply to our rule to cancel it's children being published? CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); - + continue; } //Check if the content is valid if the flag is set to check if (!item.IsValid()) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be published because some of it's content is not passing validation rules.", item.Name, item.Id)); statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedContentInvalid))); @@ -195,21 +210,21 @@ namespace Umbraco.Core.Publishing //Check if the Content is Expired to verify that it can in fact be published if (item.Status == ContentStatus.Expired) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' has expired and could not be published.", item.Name, item.Id)); statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedHasExpired))); - + //Does this document apply to our rule to cancel it's children being published? CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); - + continue; } //Check if the Content is Awaiting Release to verify that it can in fact be published if (item.Status == ContentStatus.AwaitingRelease) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' is awaiting release and could not be published.", item.Name, item.Id)); statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedAwaitingRelease))); @@ -223,7 +238,7 @@ namespace Umbraco.Core.Publishing //Check if the Content is Trashed to verify that it can in fact be published if (item.Status == ContentStatus.Trashed) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' is trashed and could not be published.", item.Name, item.Id)); statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedIsTrashed))); @@ -236,13 +251,13 @@ namespace Umbraco.Core.Publishing item.ChangePublishedState(PublishedState.Published); - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' has been published.", item.Name, item.Id)); statuses.Add(Attempt.Succeed(new PublishStatus(item))); } - + } return statuses; @@ -263,7 +278,7 @@ namespace Umbraco.Core.Publishing //TODO: We're going back to the service layer here... not sure how to avoid this? And this will add extra overhead to // any document that fails to publish... var hasPublishedVersion = ApplicationContext.Current.Services.ContentService.HasPublishedVersion(content.Id); - + if (hasPublishedVersion && !includeUnpublishedDocuments) { //it has a published version but our flag tells us to not include un-published documents and therefore we should @@ -273,7 +288,7 @@ namespace Umbraco.Core.Publishing else if (!hasPublishedVersion) { //it doesn't have a published version so we certainly cannot publish it's children. - parentsIdsCancelled.Add(content.Id); + parentsIdsCancelled.Add(content.Id); } } @@ -327,9 +342,11 @@ namespace Umbraco.Core.Publishing // at the moment it's done by the content service //Fire UnPublishing event - if (UnPublishing.IsRaisedEventCancelled(new PublishEventArgs(content), this)) + if (UnPublishing.IsRaisedEventCancelled( + _eventMessagesFactory.Get(), + messages => new PublishEventArgs(content, messages), this)) { - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' will not be unpublished, the event was cancelled.", content.Name, content.Id)); return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent)); } @@ -340,7 +357,7 @@ namespace Umbraco.Core.Publishing { content.ReleaseDate = null; - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' had its release date removed, because it was unpublished.", content.Name, content.Id)); } @@ -349,7 +366,7 @@ namespace Umbraco.Core.Publishing if (content.Published) content.ChangePublishedState(PublishedState.Unpublished); - LogHelper.Info( + _logger.Info( string.Format("Content '{0}' with Id '{1}' has been unpublished.", content.Name, content.Id)); @@ -385,7 +402,9 @@ namespace Umbraco.Core.Publishing /// thats being published public override void PublishingFinalized(IContent content) { - Published.RaiseEvent(new PublishEventArgs(content, false, false), this); + Published.RaiseEvent( + _eventMessagesFactory.Get(), + messages => new PublishEventArgs(content, false, false, messages), this); } /// @@ -395,7 +414,9 @@ namespace Umbraco.Core.Publishing /// Boolean indicating whether its all content that is republished public override void PublishingFinalized(IEnumerable content, bool isAllRepublished) { - Published.RaiseEvent(new PublishEventArgs(content, false, isAllRepublished), this); + Published.RaiseEvent( + _eventMessagesFactory.Get(), + messages => new PublishEventArgs(content, false, isAllRepublished, messages), this); } @@ -405,7 +426,9 @@ namespace Umbraco.Core.Publishing /// thats being unpublished public override void UnPublishingFinalized(IContent content) { - UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + UnPublished.RaiseEvent( + _eventMessagesFactory.Get(), + messages => new PublishEventArgs(content, false, false, messages), this); } /// @@ -414,7 +437,9 @@ namespace Umbraco.Core.Publishing /// An enumerable list of thats being unpublished public override void UnPublishingFinalized(IEnumerable content) { - UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + UnPublished.RaiseEvent( + _eventMessagesFactory.Get(), + messages => new PublishEventArgs(content, false, false, messages), this); } /// diff --git a/src/Umbraco.Core/Publishing/UnPublishStatus.cs b/src/Umbraco.Core/Publishing/UnPublishStatus.cs new file mode 100644 index 0000000000..d979a07361 --- /dev/null +++ b/src/Umbraco.Core/Publishing/UnPublishStatus.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Publishing +{ + /// + /// The result of unpublishing a content item + /// + public class UnPublishStatus : OperationStatus + { + public UnPublishStatus(IContent content, UnPublishedStatusType statusType) + : base(content, statusType) + { + } + + /// + /// Creates a successful unpublish status + /// + public UnPublishStatus(IContent content) + : this(content, UnPublishedStatusType.Success) + { + } + + public IContent ContentItem + { + get { return Entity; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Publishing/UnPublishedStatusType.cs b/src/Umbraco.Core/Publishing/UnPublishedStatusType.cs new file mode 100644 index 0000000000..515e98daf4 --- /dev/null +++ b/src/Umbraco.Core/Publishing/UnPublishedStatusType.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Core.Publishing +{ + /// + /// A status type of the result of unpublishing a content item + /// + /// + /// Anything less than 10 = Success! + /// + public enum UnPublishedStatusType + { + /// + /// The unpublishing was successful. + /// + Success = 0, + + /// + /// The item was already unpublished + /// + SuccessAlreadyUnPublished = 1, + + /// + /// The publish action has been cancelled by an event handler + /// + FailedCancelledByEvent = 14, + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 24885e98d2..5fa4303968 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Services /// /// Represents the Content Service, which is an easy access to operations involving /// - public class ContentService : RepositoryService, IContentService + public class ContentService : RepositoryService, IContentService, IContentServiceOperations { private readonly IPublishingStrategy _publishingStrategy; private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); @@ -38,18 +38,18 @@ namespace Umbraco.Core.Services private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); public ContentService( - IDatabaseUnitOfWorkProvider provider, - RepositoryFactory repositoryFactory, + IDatabaseUnitOfWorkProvider provider, + RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IPublishingStrategy publishingStrategy, - IDataTypeService dataTypeService, + IPublishingStrategy publishingStrategy, + IDataTypeService dataTypeService, IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { 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; @@ -747,7 +747,7 @@ namespace Umbraco.Core.Services } } - + /// /// Checks whether an item has any children @@ -857,6 +857,109 @@ namespace Umbraco.Core.Services return result.Success; } + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) + { + return PublishWithChildrenDo(content, userId, includeUnpublished); + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) + { + return SaveAndPublishDo(content, userId, raiseEvents); + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) + { + using (new WriteLock(Locker)) + { + var originalPath = content.Path; + + if (Trashing.IsRaisedEventCancelled( + EventMessagesFactory.Get(), + messages => new MoveEventArgs(messages, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), + this)) + { + return Attempt.Fail(OperationStatus.Cancelled); + } + + var moveInfo = new List> + { + new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) + }; + + //Make sure that published content is unpublished before being moved to the Recycle Bin + if (HasPublishedVersion(content.Id)) + { + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(content, userId); + } + + //Unpublish descendents of the content item that is being moved to trash + var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); + foreach (var descendant in descendants) + { + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(descendant, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + content.ChangeTrashedState(true); + repository.AddOrUpdate(content); + + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId + foreach (var descendant in descendants) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.WriterId = userId; + descendant.ChangeTrashedState(true, descendant.ParentId); + repository.AddOrUpdate(descendant); + } + + uow.Commit(); + } + + Trashed.RaiseEvent(EventMessagesFactory.Get(), messages => new MoveEventArgs(false, messages, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + + return Attempt.Succeed(OperationStatus.Success); + } + } + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt IContentServiceOperations.UnPublish(IContent content, int userId) + { + return UnPublishDo(content, false, userId); + } + /// /// Publishes a single object /// @@ -865,7 +968,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public Attempt PublishWithStatus(IContent content, int userId = 0) { - return SaveAndPublishDo(content, userId); + return ((IContentServiceOperations)this).Publish(content, userId); } /// @@ -881,7 +984,7 @@ namespace Umbraco.Core.Services //This used to just return false only when the parent content failed, otherwise would always return true so we'll // do the same thing for the moment - if (!result.Any(x => x.Result.ContentItem.Id == content.Id)) + if (result.All(x => x.Result.ContentItem.Id != content.Id)) return false; return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; @@ -896,7 +999,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) { - return PublishWithChildrenDo(content, userId, includeUnpublished); + return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); } /// @@ -907,7 +1010,7 @@ namespace Umbraco.Core.Services /// True if unpublishing succeeded, otherwise False public bool UnPublish(IContent content, int userId = 0) { - return UnPublishDo(content, false, userId); + return ((IContentServiceOperations) this).UnPublish(content, userId).Success; } /// @@ -933,7 +1036,7 @@ namespace Umbraco.Core.Services /// True if publishing succeeded, otherwise False public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) { - return SaveAndPublishDo(content, userId, raiseEvents); + return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); } /// @@ -942,9 +1045,9 @@ 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 Attempt SaveWithStatus(IContent content, int userId = 0, bool raiseEvents = true) + public void Save(IContent content, int userId = 0, bool raiseEvents = true) { - return Save(content, true, userId, raiseEvents); + ((IContentServiceOperations)this).Save(content, userId, raiseEvents); } /// @@ -953,7 +1056,7 @@ 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. - public Attempt SaveWithStatus(IEnumerable contents, int userId = 0, bool raiseEvents = true) + Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) { var asArray = contents.ToArray(); @@ -1012,15 +1115,80 @@ namespace Umbraco.Core.Services } } + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.Delete(IContent content, int userId) + { + using (new WriteLock(Locker)) + { + if (Deleting.IsRaisedEventCancelled( + EventMessagesFactory.Get(), + messages => new DeleteEventArgs(content, messages), + this)) + { + return Attempt.Fail(OperationStatus.Cancelled); + } + + //Make sure that published content is unpublished before being deleted + if (HasPublishedVersion(content.Id)) + { + UnPublish(content, userId); + } + + //Delete children before deleting the 'possible parent' + var children = GetChildren(content.Id); + foreach (var child in children) + { + Delete(child, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.Delete(content); + uow.Commit(); + + var evtMsgs = EventMessagesFactory.Get(); + var args = new DeleteEventArgs(content, false, evtMsgs); + Deleted.RaiseEvent(evtMsgs, messages => args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } + + Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); + + return Attempt.Succeed(OperationStatus.Success); + } + } + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt IContentServiceOperations.Publish(IContent content, int userId) + { + return SaveAndPublishDo(content, userId); + } + /// /// 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) + Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) { - SaveWithStatus(content, userId, raiseEvents); + return Save(content, true, userId, raiseEvents); } /// @@ -1035,7 +1203,7 @@ namespace Umbraco.Core.Services /// Optional boolean indicating whether or not to raise events. public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) { - SaveWithStatus(contents, userId, raiseEvents); + ((IContentServiceOperations)this).Save(contents, userId, raiseEvents); } /// @@ -1093,39 +1261,7 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting the Content public void Delete(IContent content, int userId = 0) { - using (new WriteLock(Locker)) - { - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content), this)) - return; - - //Make sure that published content is unpublished before being deleted - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } - - //Delete children before deleting the 'possible parent' - var children = GetChildren(content.Id); - foreach (var child in children) - { - Delete(child, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.Delete(content); - uow.Commit(); - - var args = new DeleteEventArgs(content, false); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - } + ((IContentServiceOperations)this).Delete(content, userId); } /// @@ -1194,59 +1330,7 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting the Content public void MoveToRecycleBin(IContent content, int userId = 0) { - using (new WriteLock(Locker)) - { - var originalPath = content.Path; - - if (Trashing.IsRaisedEventCancelled( - new MoveEventArgs( - new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) - { - return; - } - - var moveInfo = new List> - { - new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) - }; - - //Make sure that published content is unpublished before being moved to the Recycle Bin - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } - - //Unpublish descendents of the content item that is being moved to trash - var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); - foreach (var descendant in descendants) - { - UnPublish(descendant, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - content.ChangeTrashedState(true); - repository.AddOrUpdate(content); - - //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId - foreach (var descendant in descendants) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.WriterId = userId; - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - } - - uow.Commit(); - } - - Trashed.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - } + ((IContentServiceOperations) this).MoveToRecycleBin(content, userId); } /// @@ -1414,7 +1498,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); - + return true; } @@ -1504,6 +1588,7 @@ namespace Umbraco.Core.Services if (content.Published) { + //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! var published = _publishingStrategy.Publish(content, userId); shouldBePublished.Add(content); } @@ -1529,7 +1614,11 @@ namespace Umbraco.Core.Services Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); if (shouldBePublished.Any()) + { + //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! _publishingStrategy.PublishingFinalized(shouldBePublished, false); + } + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); @@ -1707,9 +1796,9 @@ namespace Umbraco.Core.Services result.Add( Attempt.Fail( new PublishStatus(content, PublishStatusType.FailedContentInvalid) - { - InvalidProperties = ((ContentBase)content).LastInvalidProperties - })); + { + InvalidProperties = ((ContentBase)content).LastInvalidProperties + })); return result; } @@ -1763,38 +1852,39 @@ namespace Umbraco.Core.Services /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache. /// Optional Id of the User issueing the publishing /// True if unpublishing succeeded, otherwise False - private bool UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) + private Attempt UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) { var newest = GetById(content.Id); // ensure we have the newest version if (content.Version != newest.Version) // but use the original object if it's already the newest version content = newest; var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version if (published == null) - return false; // already unpublished - - var unpublished = _publishingStrategy.UnPublish(content, userId); - if (unpublished) { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - repository.AddOrUpdate(content); - // is published is not newest, reset the published flag on published version - if (published.Version != content.Version) - repository.ClearPublished(published); - repository.DeleteContentXml(content); - - uow.Commit(); - } - //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache - if (omitCacheRefresh == false) - _publishingStrategy.UnPublishingFinalized(content); - - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished)); // already unpublished } + + var unpublished = _publishingStrategy.UnPublish(content, userId); + if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent)); - return unpublished; + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + repository.AddOrUpdate(content); + // is published is not newest, reset the published flag on published version + if (published.Version != content.Version) + repository.ClearPublished(published); + repository.DeleteContentXml(content); + + uow.Commit(); + } + //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache + if (omitCacheRefresh == false) + _publishingStrategy.UnPublishingFinalized(content); + + Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + + return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success)); } /// @@ -1903,13 +1993,13 @@ namespace Umbraco.Core.Services { if (raiseEvents) { - if (Saving.IsRaisedEventCancelled( - EventMessagesFactory.Get(), - messages => new SaveEventArgs(content, messages), - this)) + if (Saving.IsRaisedEventCancelled( + EventMessagesFactory.Get(), + messages => new SaveEventArgs(content, messages), + this)) { return Attempt.Fail(OperationStatus.Cancelled); - } + } } using (new WriteLock(Locker)) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 23c67edd1c..44bf88d975 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -8,6 +8,84 @@ using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { + /// + /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented + /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. + /// + public interface IContentServiceOperations + { + //TODO: Remove this class in v8 + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt Save(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 Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + Attempt Delete(IContent content, int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt Publish(IContent content, int userId = 0); + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> PublishWithChildren(IContent content, int userId = 0, bool includeUnpublished = false); + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + Attempt MoveToRecycleBin(IContent content, int userId = 0); + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt UnPublish(IContent content, int userId = 0); + } + /// /// Defines the ContentService, which is an easy access to operations involving /// @@ -188,31 +266,13 @@ namespace Umbraco.Core.Services /// /// An Enumerable list of objects IEnumerable GetContentInRecycleBin(); - + /// /// Saves a single object /// /// 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); /// @@ -221,8 +281,6 @@ 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); /// @@ -407,7 +465,7 @@ namespace Umbraco.Core.Services /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); - + /// /// Permanently deletes an object. /// @@ -416,7 +474,7 @@ namespace Umbraco.Core.Services /// /// Please note that this method will completely remove the Content from the database /// The to delete - /// Optional Id of the User deleting the Content + /// Optional Id of the User deleting the Content void Delete(IContent content, int userId = 0); /// diff --git a/src/Umbraco.Core/Services/OperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus.cs index 8aeaafd27a..827ddfd7bd 100644 --- a/src/Umbraco.Core/Services/OperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus.cs @@ -1,26 +1,54 @@ +using Umbraco.Core.Models; + namespace Umbraco.Core.Services { /// /// The status returned by many of the service methods /// - public class OperationStatus + public class OperationStatus : OperationStatus + where TStatus : struct { - //TODO: This is a pretty simple class atm, but is 'future' proofed in case we need to add more detail here + public OperationStatus(TEntity entity, TStatus statusType) : base(statusType) + { + Entity = entity; + } + public TEntity Entity { get; private set; } + + } + + public class OperationStatus + where TStatus : struct + { + public OperationStatus(TStatus statusType) + { + StatusType = statusType; + } + + public TStatus StatusType { get; internal set; } + } + + /// + /// The default operation status + /// + public class OperationStatus : OperationStatus + { + public OperationStatus(OperationStatusType statusType) : base(statusType) + { + } + + + #region Static Helper methods internal static OperationStatus Cancelled { - get { return new OperationStatus(OperationStatusType.FailedCancelledByEvent);} + 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; } + } + #endregion } + } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/OperationStatusType.cs b/src/Umbraco.Core/Services/OperationStatusType.cs index a54d125214..7398572810 100644 --- a/src/Umbraco.Core/Services/OperationStatusType.cs +++ b/src/Umbraco.Core/Services/OperationStatusType.cs @@ -17,5 +17,7 @@ namespace Umbraco.Core.Services /// The saving has been cancelled by a 3rd party add-in /// FailedCancelledByEvent = 14 + + //TODO: In the future, we might need to add more operations statuses, potentially like 'FailedByPermissions', etc... } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 98fee9f3bd..5088f8ff01 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -12,6 +12,21 @@ using Umbraco.Core.Events; namespace Umbraco.Core.Services { + /// + /// These are used currently to return the temporary 'operation' interfaces for services + /// which are used to return a status from operational methods so we can determine if things are + /// cancelled, etc... + /// + /// These will be obsoleted in v8 since all real services methods will be changed to have the correct result. + /// + public static class ServiceWithResultExtensions + { + public static IContentServiceOperations WithResult(this IContentService contentService) + { + return (IContentServiceOperations)contentService; + } + } + /// /// The Umbraco ServiceContext, which provides access to the following services: /// , , , diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 9efa5ec981..c51732fb6f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -445,6 +445,8 @@ + + @@ -1246,9 +1248,6 @@ - - - diff --git a/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs b/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs index 19da8c68c1..1da8237582 100644 --- a/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs +++ b/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs @@ -56,11 +56,12 @@ namespace Umbraco.Tests.Persistence var repositoryFactory = new RepositoryFactory(cacheHelper, _logger, SqlSyntaxProvider, SettingsForTests.GenerateMockSettings()); + var evtMsgs = new TransientMessagesFactory(); ApplicationContext.Current = new ApplicationContext( //assign the db context dbContext, //assign the service context - new ServiceContext(repositoryFactory, new PetaPocoUnitOfWorkProvider(_logger), new FileUnitOfWorkProvider(), new PublishingStrategy(), cacheHelper, _logger, new TransientMessagesFactory()), + new ServiceContext(repositoryFactory, new PetaPocoUnitOfWorkProvider(_logger), new FileUnitOfWorkProvider(), new PublishingStrategy(evtMsgs, _logger), cacheHelper, _logger, evtMsgs), cacheHelper, new ProfilingLogger(_logger, Mock.Of())) { diff --git a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs index a2ebdf450d..63ba1afb81 100644 --- a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs +++ b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs @@ -64,7 +64,7 @@ namespace Umbraco.Tests.Publishing ServiceContext.ContentTypeService.GetContentType("umbTextpage"), "Sub Sub Sub", mandatorContent.Id); ServiceContext.ContentService.Save(subContent, 0); - var strategy = new PublishingStrategy(); + var strategy = new PublishingStrategy(new TransientMessagesFactory(), Logger); //publish root and nodes at it's children level var listToPublish = ServiceContext.ContentService.GetDescendants(_homePage.Id).Concat(new[] { _homePage }); @@ -91,8 +91,8 @@ namespace Umbraco.Tests.Publishing { CreateTestData(); - var strategy = new PublishingStrategy(); - + var strategy = new PublishingStrategy(new TransientMessagesFactory(), Logger); + PublishingStrategy.Publishing +=PublishingStrategyPublishing; @@ -118,8 +118,8 @@ namespace Umbraco.Tests.Publishing { CreateTestData(); - var strategy = new PublishingStrategy(); - + var strategy = new PublishingStrategy(new TransientMessagesFactory(), Logger); + //publish root and nodes at it's children level var result1 = strategy.Publish(_homePage, 0); Assert.IsTrue(result1); @@ -145,7 +145,7 @@ namespace Umbraco.Tests.Publishing { CreateTestData(); - var strategy = new PublishingStrategy(); + var strategy = new PublishingStrategy(new TransientMessagesFactory(), Logger); //publish root and nodes at it's children level var result1 = strategy.Publish(_homePage, 0); diff --git a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs index c59b075fc4..ed87493c01 100644 --- a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs +++ b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs @@ -50,14 +50,15 @@ namespace Umbraco.Tests.Services //we need a new Database object for each thread. var repositoryFactory = new RepositoryFactory(cacheHelper, Logger, SqlSyntax, SettingsForTests.GenerateMockSettings()); _uowProvider = new PerThreadPetaPocoUnitOfWorkProvider(_dbFactory); - ApplicationContext.Services = new ServiceContext( + var evtMsgs = new TransientMessagesFactory(); + ApplicationContext.Services = new ServiceContext( repositoryFactory, _uowProvider, new FileUnitOfWorkProvider(), - new PublishingStrategy(), + new PublishingStrategy(evtMsgs, Logger), cacheHelper, Logger, - new TransientMessagesFactory()); + evtMsgs); CreateTestData(); } diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index b2de63ebff..a9e0c0464c 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -76,11 +76,12 @@ namespace Umbraco.Tests.TestHelpers var repositoryFactory = new RepositoryFactory(cacheHelper, Logger, SqlSyntax, SettingsForTests.GenerateMockSettings()); + var evtMsgs = new TransientMessagesFactory(); _appContext = new ApplicationContext( //assign the db context new DatabaseContext(dbFactory, Logger, SqlSyntax, "System.Data.SqlServerCe.4.0"), //assign the service context - new ServiceContext(repositoryFactory, new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), new PublishingStrategy(), cacheHelper, Logger, new TransientMessagesFactory()), + new ServiceContext(repositoryFactory, new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), new PublishingStrategy(evtMsgs, Logger), cacheHelper, Logger, evtMsgs), cacheHelper, ProfilingLogger) { diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 06ee2c6a22..abf1edd3a5 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -153,12 +153,13 @@ namespace Umbraco.Tests.TestHelpers var sqlSyntax = new SqlCeSyntaxProvider(); var repoFactory = new RepositoryFactory(CacheHelper.CreateDisabledCacheHelper(), Logger, sqlSyntax, SettingsForTests.GenerateMockSettings()); + var evtMsgs = new TransientMessagesFactory(); ApplicationContext.Current = new ApplicationContext( //assign the db context new DatabaseContext(new DefaultDatabaseFactory(Core.Configuration.GlobalSettings.UmbracoConnectionName, Logger), Logger, sqlSyntax, "System.Data.SqlServerCe.4.0"), //assign the service context - new ServiceContext(repoFactory, new PetaPocoUnitOfWorkProvider(Logger), new FileUnitOfWorkProvider(), new PublishingStrategy(), CacheHelper, Logger, new TransientMessagesFactory()), + new ServiceContext(repoFactory, new PetaPocoUnitOfWorkProvider(Logger), new FileUnitOfWorkProvider(), new PublishingStrategy(evtMsgs, Logger), CacheHelper, Logger, evtMsgs), CacheHelper, ProfilingLogger) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 62f8230791..9341942aa9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -91,6 +91,27 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific } }); + function showNotificationsAndReset(err, reload) { + + //check if response is ysod + if(err.status && err.status >= 500) { + dialogService.ysodDialog(err); + } + + $scope.bulkStatus = ""; + $scope.actionInProgress = false; + + if (reload === true) { + $scope.reloadView($scope.contentId); + } + + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + } + $scope.isSortDirection = function (col, direction) { return $scope.options.orderBy.toUpperCase() == col.toUpperCase() && $scope.options.orderDirection == direction; } @@ -276,12 +297,13 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific $scope.bulkStatus = "Deleted item " + current + " out of " + total + " item" + pluralSuffix; deleteItemCallback(getIdCallback(selected[i])).then(function (data) { if (current === total) { + //TODO: Should probably add notifications on the server side notificationsService.success("Bulk action", "Deleted " + total + " item" + pluralSuffix); - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; + showNotificationsAndReset(data, true); } current++; + }, function (err) { + showNotificationsAndReset(err, false); }); } } @@ -310,26 +332,12 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific .then(function(content) { if (current == total) { notificationsService.success("Bulk action", "Published " + total + " document" + pluralSuffix); - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; + showNotificationsAndReset(content, true); } current++; }, function(err) { - - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; - - //if there are validation errors for publishing then we need to show them - if (err.status === 400 && err.data && err.data.Message) { - notificationsService.error("Publish error", err.data.Message); - } - else { - dialogService.ysodDialog(err); - } + showNotificationsAndReset(err, false); }); - } }; @@ -356,12 +364,12 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific if (current == total) { notificationsService.success("Bulk action", "Unpublished " + total + " document" + pluralSuffix); - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; + showNotificationsAndReset(content, true); } current++; + }, function(err) { + showNotificationsAndReset(err, false); }); } }; diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 76ab46ee22..573439f4e4 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -30,7 +30,9 @@ using Umbraco.Core.Dynamics; using umbraco.BusinessLogic.Actions; using umbraco.cms.businesslogic.web; using umbraco.presentation.preview; +using Umbraco.Web.UI; using Constants = Umbraco.Core.Constants; +using Notification = Umbraco.Web.Models.ContentEditing.Notification; namespace Umbraco.Web.Editors { @@ -255,7 +257,8 @@ namespace Umbraco.Web.Editors if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) { //save the item - var saveResult = Services.ContentService.SaveWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + var saveResult = Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; } else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) @@ -337,30 +340,12 @@ namespace Umbraco.Web.Editors return HandleContentNotFound(id, false); } - var publishResult = Services.ContentService.PublishWithStatus(foundContent, UmbracoUser.Id); + var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.GetUserId()); if (publishResult.Success == false) { - switch (publishResult.Result.StatusType) - { - case PublishStatusType.FailedPathNotPublished: - return Request.CreateValidationErrorResponse( - 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( - Services.TextService.Localize("speechBubbles/contentPublishedFailedByEvent")); - case PublishStatusType.FailedHasExpired: - case PublishStatusType.FailedAwaitingRelease: - case PublishStatusType.FailedIsTrashed: - case PublishStatusType.FailedContentInvalid: - return Request.CreateValidationErrorResponse( - 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)) - })); - } + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult.Result, notificationModel); + return Request.CreateValidationErrorResponse(notificationModel); } //return ok @@ -392,11 +377,23 @@ namespace Umbraco.Web.Editors //if the current item is in the recycle bin if (foundContent.IsInRecycleBin() == false) { - Services.ContentService.MoveToRecycleBin(foundContent, UmbracoUser.Id); + var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.GetUserId()); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } } else { - Services.ContentService.Delete(foundContent, UmbracoUser.Id); + var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.GetUserId()); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } } return Request.CreateResponse(HttpStatusCode.OK); @@ -505,12 +502,20 @@ namespace Umbraco.Web.Editors if (foundContent == null) HandleContentNotFound(id); - Services.ContentService.UnPublish(foundContent, Security.CurrentUser.Id); + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); + var content = Mapper.Map(foundContent); - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); - - return content; + if (unpublishResult == false) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; + } } /// @@ -619,7 +624,7 @@ namespace Umbraco.Web.Editors return toMove; } - private void ShowMessageForPublishStatus(PublishStatus status, ContentItemDisplay display) + private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display) { switch (status.StatusType) { diff --git a/src/Umbraco.Web/Models/ContentEditing/Notification.cs b/src/Umbraco.Web/Models/ContentEditing/Notification.cs index cc9ffb5010..0f042b6cbc 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Notification.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Notification.cs @@ -6,6 +6,18 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "notification", Namespace = "")] public class Notification { + public Notification() + { + + } + + public Notification(string header, string message, SpeechBubbleIcon notificationType) + { + Header = header; + Message = message; + NotificationType = notificationType; + } + [DataMember(Name = "header")] public string Header { get; set; } [DataMember(Name = "message")] diff --git a/src/Umbraco.Web/Models/ContentEditing/SimpleNotificationModel.cs b/src/Umbraco.Web/Models/ContentEditing/SimpleNotificationModel.cs new file mode 100644 index 0000000000..9c4ce6e9a4 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/SimpleNotificationModel.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "notificationModel", Namespace = "")] + public class SimpleNotificationModel : INotificationModel + { + public SimpleNotificationModel() + { + Notifications = new List(); + } + + public SimpleNotificationModel(params Notification[] notifications) + { + Notifications = new List(notifications); + } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d432e9ad91..95c318b31a 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -305,6 +305,7 @@ + @@ -743,7 +744,6 @@ - @@ -755,10 +755,6 @@ - - - - @@ -813,7 +809,6 @@ - True True diff --git a/src/Umbraco.Web/WebApi/Filters/AppendCurrentEventMessagesAttribute.cs b/src/Umbraco.Web/WebApi/Filters/AppendCurrentEventMessagesAttribute.cs index fc248dc1d4..8cf70826fe 100644 --- a/src/Umbraco.Web/WebApi/Filters/AppendCurrentEventMessagesAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/AppendCurrentEventMessagesAttribute.cs @@ -17,7 +17,6 @@ namespace Umbraco.Web.WebApi.Filters 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; diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index 97448b9976..ad29726d4e 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.WebApi /// /// Returns the currently logged in Umbraco User /// - [Obsolete("This should no longer be used since it returns the legacy user object, use The Security.CurrentUser instead to return the proper user object")] + [Obsolete("This should no longer be used since it returns the legacy user object, use The Security.CurrentUser instead to return the proper user object, or Security.GetUserId() if you want to just get the user id")] protected User UmbracoUser { get diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 5b40a61c76..baac1109e4 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -84,15 +84,16 @@ namespace Umbraco.Web /// protected override ServiceContext CreateServiceContext(DatabaseContext dbContext, IDatabaseFactory dbFactory) { + //use a request based messaging factory + var evtMsgs = new RequestLifespanMessagesFactory(new SingletonUmbracoContextAccessor()); return new ServiceContext( new RepositoryFactory(ApplicationCache, ProfilingLogger.Logger, dbContext.SqlSyntax, UmbracoConfig.For.UmbracoSettings()), new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), - new PublishingStrategy(), + new PublishingStrategy(evtMsgs, ProfilingLogger.Logger), ApplicationCache, ProfilingLogger.Logger, - //use a request based messaging factory - new RequestLifespanMessagesFactory(new SingletonUmbracoContextAccessor())); + evtMsgs); } ///