diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/ContentController.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/ContentController.cs index 1e15a962b..43a67cade 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/ContentController.cs +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/ContentController.cs @@ -8,6 +8,7 @@ using Orchard.Localization; using Orchard.Security; using Orchard.UI.Admin; using Orchard.UI.Notify; +using Orchard.Utility.Extensions; namespace Orchard.AuditTrail.Controllers { [Admin] @@ -44,7 +45,7 @@ namespace Orchard.AuditTrail.Controllers { [HttpPost] public ActionResult Restore(int id, int version, string returnUrl) { - var contentItem = _contentManager.Get(id); + var contentItem = _contentManager.Get(id, VersionOptions.Number(version)); if (!_authorizer.Authorize(Core.Contents.Permissions.PublishContent, contentItem)) return new HttpUnauthorizedResult(); @@ -55,13 +56,7 @@ namespace Orchard.AuditTrail.Controllers { _notifier.Information(T(""{0}" has been restored.", restoredContentItemTitle)); - returnUrl = Url.IsLocalUrl(returnUrl) - ? returnUrl - : Request.UrlReferrer != null - ? Request.UrlReferrer.ToString() - : Url.Action("Index", "Admin"); - - return Redirect(returnUrl); + return this.RedirectReturn(returnUrl, () => Url.Action("Index", "Admin")); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/RecycleBinController.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/RecycleBinController.cs new file mode 100644 index 000000000..89ac5984f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/RecycleBinController.cs @@ -0,0 +1,67 @@ +using System.Linq; +using System.Security.Policy; +using System.Web.Mvc; +using System.Web.UI; +using Orchard.AuditTrail.Helpers; +using Orchard.AuditTrail.Services; +using Orchard.AuditTrail.Services.Models; +using Orchard.AuditTrail.ViewModels; +using Orchard.ContentManagement; +using Orchard.Core.Contents.Settings; +using Orchard.Localization; +using Orchard.Security; +using Orchard.UI.Admin; +using Orchard.UI.Navigation; +using Orchard.UI.Notify; +using Orchard.Utility.Extensions; + +namespace Orchard.AuditTrail.Controllers { + [Admin] + public class RecycleBinController : Controller { + private readonly IAuthorizer _authorizer; + private readonly IContentManager _contentManager; + private readonly INotifier _notifier; + private readonly IOrchardServices _services; + private readonly IRecycleBin _recycleBin; + + public RecycleBinController(IAuthorizer authorizer, IContentManager contentManager, INotifier notifier, IOrchardServices services, IRecycleBin recycleBin) { + _authorizer = authorizer; + _contentManager = contentManager; + _notifier = notifier; + _services = services; + _recycleBin = recycleBin; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public ActionResult Index(PagerParameters pagerParameters, AuditTrailOrderBy? orderBy = null) { + if (!_authorizer.Authorize(Permissions.ViewAuditTrail)) + return new HttpUnauthorizedResult(); + + var pager = new Pager(_services.WorkContext.CurrentSite, pagerParameters); + var removedContentItems = _recycleBin.List(pager.Page, pager.PageSize); + var pagershape = _services.New.Pager(pager).TotalItemCount(removedContentItems.TotalItemCount); + var viewModel = new RecycleBinViewModel { + ContentItems = removedContentItems, + Pager = pagershape + }; + + return View(viewModel); + } + + [HttpPost] + public ActionResult Restore(int id, string returnUrl) { + var contentItem = _contentManager.Get(id, VersionOptions.AllVersions); + if (!_authorizer.Authorize(Core.Contents.Permissions.PublishContent, contentItem)) + return new HttpUnauthorizedResult(); + + var restoredContentItem = _recycleBin.Restore(contentItem); + var restoredContentItemTitle = _contentManager.GetItemMetadata(restoredContentItem).DisplayText; + + _notifier.Information(T(""{0}" has been restored.", restoredContentItemTitle)); + + return this.RedirectReturn(returnUrl, () => Url.Action("Index", "RecycleBin")); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Helpers/ControllerExtensions.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Helpers/ControllerExtensions.cs new file mode 100644 index 000000000..6105f80fc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Helpers/ControllerExtensions.cs @@ -0,0 +1,10 @@ +using System; +using System.Web.Mvc; + +namespace Orchard.AuditTrail.Helpers { + public static class ControllerExtensions { + public static RedirectResult RedirectReturn(this Controller controller, string returnUrl = null, Func defaultReturnUrl = null) { + return new RedirectResult(controller.Request.GetReturnUrl(returnUrl, defaultReturnUrl)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Helpers/HttpRequestExtensions.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Helpers/HttpRequestExtensions.cs new file mode 100644 index 000000000..e9f5f6205 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Helpers/HttpRequestExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Web; +using Orchard.Utility.Extensions; + +namespace Orchard.AuditTrail.Helpers { + public static class HttpRequestExtensions { + public static string GetReturnUrl(this HttpRequestBase request, string returnUrl = null, Func defaultReturnUrl = null) { + return request.IsLocalUrl(returnUrl) + ? returnUrl + : request.UrlReferrer != null + ? request.UrlReferrer.ToString() + : defaultReturnUrl != null + ? defaultReturnUrl() + : "~/Admin"; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Orchard.AuditTrail.csproj b/src/Orchard.Web/Modules/Orchard.AuditTrail/Orchard.AuditTrail.csproj index adc50b69e..e6938bf7a 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Orchard.AuditTrail.csproj +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Orchard.AuditTrail.csproj @@ -52,6 +52,10 @@ False ..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll + + False + ..\..\..\..\lib\nhibernate\NHibernate.dll + @@ -85,6 +89,7 @@ + @@ -182,8 +187,14 @@ + + + + + + @@ -308,6 +319,9 @@ + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Providers/Content/ContentAuditTrailEventProvider.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Providers/Content/ContentAuditTrailEventProvider.cs index ee5785aac..0bcce46dd 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Providers/Content/ContentAuditTrailEventProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Providers/Content/ContentAuditTrailEventProvider.cs @@ -36,7 +36,7 @@ namespace Orchard.AuditTrail.Providers.Content { .Event(this, Removed, T("Removed"), T("A content item was deleted."), enableByDefault: true) .Event(this, Imported, T("Imported"), T("A content item was imported."), enableByDefault: true) .Event(this, Exported, T("Exported"), T("A content item was exported."), enableByDefault: false) - .Event(this, Restored, T("Rolled Back"), T("A content item was rolled back to a previous version."), enableByDefault: true); + .Event(this, Restored, T("Restored"), T("A content item was restored to a previous version."), enableByDefault: true); context.QueryFilter(QueryFilter); context.DisplayFilter(DisplayFilter); @@ -53,7 +53,7 @@ namespace Orchard.AuditTrail.Providers.Content { private void DisplayFilter(DisplayFilterContext context) { var contentItemId = context.Filters.Get("content").ToInt32(); if (contentItemId != null) { - var contentItem = _contentManager.Get(contentItemId.Value, VersionOptions.Latest); + var contentItem = _contentManager.Get(contentItemId.Value, VersionOptions.AllVersions); var filterDisplay = context.ShapeFactory.AuditTrailFilter__ContentItem(ContentItem: contentItem); context.FilterDisplay.Add(filterDisplay); diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/IRecycleBin.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/IRecycleBin.cs new file mode 100644 index 000000000..b4e3751b0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/IRecycleBin.cs @@ -0,0 +1,21 @@ +using Orchard.Collections; +using Orchard.ContentManagement; + +namespace Orchard.AuditTrail.Services { + public interface IRecycleBin : IDependency { + /// + /// Returns all removed content items. + /// + IPageOfItems List(int page, int pageSize); + + /// + /// Returns all removed content items. + /// + IPageOfItems List(int page, int pageSize) where T : class, IContent; + + /// + /// Restores the specified content item. + /// + ContentItem Restore(ContentItem contentItem); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/RecycleBin.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/RecycleBin.cs new file mode 100644 index 000000000..12b49b957 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/RecycleBin.cs @@ -0,0 +1,60 @@ +using System.Linq; +using NHibernate; +using Orchard.Collections; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Records; +using Orchard.Data; + +namespace Orchard.AuditTrail.Services { + public class RecycleBin : IRecycleBin { + private readonly ISessionLocator _sessionLocator; + private readonly IContentManager _contentManager; + + public RecycleBin(ISessionLocator sessionLocator, IContentManager contentManager) { + _sessionLocator = sessionLocator; + _contentManager = contentManager; + } + + public IPageOfItems List(int page, int pageSize) { + return List(page, pageSize); + } + + public IPageOfItems List(int page, int pageSize) where T: class, IContent { + var query = GetDeletedVersionsQuery(); + var totalCount = query.List().Count; + + query.SetFirstResult((page - 1) * pageSize); + query.SetFetchSize(pageSize); + + var rows = query.List(); + var versionIds = rows.Cast().Select(x => (int)x[0]); + var contentItems = _contentManager.GetManyByVersionId(versionIds, QueryHints.Empty); + + return new PageOfItems(contentItems) { + PageNumber = page, + PageSize = pageSize, + TotalItemCount = totalCount + }; + } + + public ContentItem Restore(ContentItem contentItem) { + var versions = contentItem.Record.Versions.OrderBy(x => x.Number).ToArray(); + var lastVersion = versions.Last(); + return _contentManager.Restore(contentItem, VersionOptions.Restore(lastVersion.Number, publish: false)); + } + + private IQuery GetDeletedVersionsQuery() { + var session = _sessionLocator.For(typeof(ContentItemVersionRecord)); + + // Select only the highest versions where both Published and Latest are false. + var query = session.CreateQuery( + "select max(ContentItemVersionRecord.Id), ContentItemVersionRecord.ContentItemRecord.Id, max(ContentItemVersionRecord.Number) " + + "from Orchard.ContentManagement.Records.ContentItemVersionRecord ContentItemVersionRecord " + + "join ContentItemVersionRecord.ContentItemRecord ContentItemRecord " + + "group by ContentItemVersionRecord.ContentItemRecord.Id " + + "having max(cast(Latest as Int32)) = 0 and max(cast(Published AS Int32)) = 0 "); + + return query; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Styles/audittrail-recycle-bin.css b/src/Orchard.Web/Modules/Orchard.AuditTrail/Styles/audittrail-recycle-bin.css new file mode 100644 index 000000000..ab4068d9a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Styles/audittrail-recycle-bin.css @@ -0,0 +1,30 @@ +.recycle-bin-filter-section { + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + justify-content: flex-start; + align-items: flex-end; + align-content: flex-end; + margin-bottom: 1em; + border: 1px solid #eaeaea; + background: #f5f5f5; + padding: 4px 10px 3px; +} + +.filter-control-group { + margin-right: 10px; + margin-bottom: 6px; +} + +.filter-control-group label { + padding-bottom: 0; +} + +.filter-control-group select { + padding-top: 2px !important; + padding-bottom: 2px !important; +} + +.audittrail-list-section table .content-column { + white-space: nowrap; +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinViewModel.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinViewModel.cs new file mode 100644 index 000000000..c7a03a689 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinViewModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Orchard.Collections; +using Orchard.ContentManagement; + +namespace Orchard.AuditTrail.ViewModels { + public class RecycleBinViewModel { + public IPageOfItems ContentItems { get; set; } + public dynamic Pager { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/RecycleBin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/RecycleBin/Index.cshtml new file mode 100644 index 000000000..65428b4db --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/RecycleBin/Index.cshtml @@ -0,0 +1,68 @@ +@model Orchard.AuditTrail.ViewModels.RecycleBinViewModel +@{ + Style.Include("audittrail-recycle-bin.css"); + Script.Require("ShapesBase"); + Layout.Title = T("Audit Trail"); + + var contentItems = Model.ContentItems; +} +@Html.ValidationSummary() +@using (Html.BeginFormAntiForgeryPost()) { +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+ @if (!contentItems.Any()) { +

@T("There are no records to display.")

+ } + else { + + + + + + + + + + @foreach (var contentItem in contentItems) { + var contentDisplayText = Html.ItemDisplayText(contentItem).ToString(); + var contentDisplayUrl = Url.Action("Detail", "Content", new {id = contentItem.Id, version = contentItem.Version, area = "Orchard.AuditTrail"}); + + + + + + } + +
@T("Content Item")
@contentDisplayText + @T("View") @T(" | ") + @Html.ActionLink(T("View Audit Trail").Text, "Index", "Admin", new {content = contentItem.Id, area = "Orchard.AuditTrail"}, null) @T(" | ") + @T("Restore") +
+ } +
+
+ @Display(Model.Pager) +
+} \ No newline at end of file