diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/RecycleBinController.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/RecycleBinController.cs index 89ac5984f..e64a38013 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/RecycleBinController.cs +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Controllers/RecycleBinController.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using System.Security.Policy; using System.Web.Mvc; using System.Web.UI; @@ -9,6 +11,8 @@ using Orchard.AuditTrail.ViewModels; using Orchard.ContentManagement; using Orchard.Core.Contents.Settings; using Orchard.Localization; +using Orchard.Logging; +using Orchard.Mvc; using Orchard.Security; using Orchard.UI.Admin; using Orchard.UI.Navigation; @@ -31,22 +35,17 @@ namespace Orchard.AuditTrail.Controllers { _services = services; _recycleBin = recycleBin; T = NullLocalizer.Instance; + Logger = NullLogger.Instance; } public Localizer T { get; set; } + public ILogger Logger { 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 - }; - + var viewModel = SetupViewModel(new RecycleBinViewModel(), pagerParameters); return View(viewModel); } @@ -63,5 +62,85 @@ namespace Orchard.AuditTrail.Controllers { return this.RedirectReturn(returnUrl, () => Url.Action("Index", "RecycleBin")); } + + [ActionName("Index")] + [HttpPost] + [FormValueRequired("ExecuteActionButton")] + public ActionResult ExecuteAction(RecycleBinViewModel viewModel, PagerParameters pagerParameters) { + if (viewModel.RecycleBinCommand == null) { + ModelState.AddModelError("RecycleBinCommand", T("Please select an action to execute.").Text); + } + + if (viewModel.SelectedContentItems == null || !viewModel.SelectedContentItems.Any()) { + ModelState.AddModelError("SelectedContentItems", T("Please select one or more content items.").Text); + } + + if (!ModelState.IsValid) { + SetupViewModel(viewModel, pagerParameters); + return View("Index", viewModel); + } + + if (ModelState.IsValid) { + switch (viewModel.RecycleBinCommand) { + case RecycleBinCommand.Restore: + RestoreContentItems(viewModel.SelectedContentItems); + break; + case RecycleBinCommand.Destroy: + DeleteContentItems(viewModel.SelectedContentItems); + break; + } + } + + return RedirectToAction("Index"); + } + + private RecycleBinViewModel SetupViewModel(RecycleBinViewModel viewModel, PagerParameters pagerParameters) { + 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); + + viewModel.ContentItems = removedContentItems; + viewModel.Pager = pagershape; + + return viewModel; + } + + private void RestoreContentItems(IEnumerable selectedContentItems) { + var contentItems = _recycleBin.GetMany(selectedContentItems); + + foreach (var contentItem in contentItems) { + var contentItemTitle = _contentManager.GetItemMetadata(contentItem).DisplayText; + if (!_authorizer.Authorize(Core.Contents.Permissions.EditContent, contentItem)) { + _notifier.Error(T("You need the EditContent permission to restore {0}.", contentItemTitle)); + continue; + } + + _recycleBin.Restore(contentItem); + _notifier.Information(T(""{0}" has been restored.", contentItemTitle)); + } + } + + private void DeleteContentItems(IEnumerable selectedContentItems) { + var contentItems = _recycleBin.GetMany(selectedContentItems); + + foreach (var contentItem in contentItems) { + var contentItemTitle = _contentManager.GetItemMetadata(contentItem).DisplayText; + if (!_authorizer.Authorize(Core.Contents.Permissions.DeleteContent, contentItem)) { + _notifier.Error(T("You need the DeleteContent permission to permanently delete {0}.", contentItemTitle)); + continue; + } + + try { + _contentManager.Destroy(contentItem); + _notifier.Information(T(""{0}" has been permanently deleted.", contentItemTitle)); + } + catch (Exception ex) { + Logger.Error(ex, "An exception occurred while trying to permanently delete content with ID {0}.", contentItem.Id); + _notifier.Error(T("An exception occurred while trying to permanently delete content with ID {0}.", contentItem.Id)); + } + + } + } + } } \ 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 e6938bf7a..0616ac9be 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Orchard.AuditTrail.csproj +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Orchard.AuditTrail.csproj @@ -85,6 +85,7 @@ + @@ -187,6 +188,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Scripts/audittrail-recyclebin.js b/src/Orchard.Web/Modules/Orchard.AuditTrail/Scripts/audittrail-recyclebin.js new file mode 100644 index 000000000..342c04873 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Scripts/audittrail-recyclebin.js @@ -0,0 +1,29 @@ +$(function () { + + var executeSelectedAction = function(e, form) { + var actionList = form.find("[name=\"RecycleBinCommand\"]"); + var selectedAction = actionList.find("option:selected"); + var prompts = selectedAction.data("unsafe-action"); + + if (prompts) { + if (!Array.isArray(prompts)) + prompts = [prompts]; + + for (var i = 0; i < prompts.length; i++) { + if (!confirm(prompts[i])) { + e.preventDefault(); + return; + } + } + } + }; + + $("#recycle-bin").on("submit", "form", function(e) { + var form = $(this); + var executeActionButton = form.find("[name=\"ExecuteActionButton\"]"); + + if (executeActionButton.val() == "ExecuteActionButton") { + executeSelectedAction(e, form); + } + }); +}); \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/IRecycleBin.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/IRecycleBin.cs index b4e3751b0..9785fb090 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/IRecycleBin.cs +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/IRecycleBin.cs @@ -1,4 +1,5 @@ -using Orchard.Collections; +using System.Collections.Generic; +using Orchard.Collections; using Orchard.ContentManagement; namespace Orchard.AuditTrail.Services { @@ -13,6 +14,16 @@ namespace Orchard.AuditTrail.Services { /// IPageOfItems List(int page, int pageSize) where T : class, IContent; + /// + /// Returns the specified list of content items from the recycle bin. + /// + IEnumerable GetMany(IEnumerable contentItemIds); + + /// + /// Returns the specified list of content items from the recycle bin. + /// + IEnumerable GetMany(IEnumerable contentItemIds) where T : class, IContent; + /// /// Restores the specified content item. /// diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/RecycleBin.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/RecycleBin.cs index 12b49b957..f9085c64d 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/RecycleBin.cs +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Services/RecycleBin.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using NHibernate; using Orchard.Collections; using Orchard.ContentManagement; @@ -26,9 +28,7 @@ namespace Orchard.AuditTrail.Services { 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); + var contentItems = LoadContentItems(query); return new PageOfItems(contentItems) { PageNumber = page, @@ -37,23 +37,53 @@ namespace Orchard.AuditTrail.Services { }; } + public IEnumerable GetMany(IEnumerable contentItemIds) { + return GetMany(contentItemIds); + } + + public IEnumerable GetMany(IEnumerable contentItemIds) where T : class, IContent { + var query = GetDeletedVersionsQuery(contentItemIds); + return LoadContentItems(query); + } + public ContentItem Restore(ContentItem contentItem) { var versions = contentItem.Record.Versions.OrderBy(x => x.Number).ToArray(); var lastVersion = versions.Last(); + + if (lastVersion.Latest || lastVersion.Published) + throw new InvalidOperationException(String.Format("Cannot restore content item with ID {0} ftom the recycle bin, since that item is not deleted", contentItem.Id)); + return _contentManager.Restore(contentItem, VersionOptions.Restore(lastVersion.Number, publish: false)); } - private IQuery GetDeletedVersionsQuery() { + private IEnumerable LoadContentItems(IQuery query) where T: class, IContent { + var rows = query.List(); + var versionIds = rows.Cast().Select(x => (int)x[0]); + return _contentManager.GetManyByVersionId(versionIds, QueryHints.Empty); + } + + private IQuery GetDeletedVersionsQuery(IEnumerable contentItemIds = null) { var session = _sessionLocator.For(typeof(ContentItemVersionRecord)); // Select only the highest versions where both Published and Latest are false. - var query = session.CreateQuery( + var select = "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 "); + "join ContentItemVersionRecord.ContentItemRecord ContentItemRecord "; + var filter = contentItemIds != null ? "WHERE ContentItemVersionRecord.ContentItemRecord.Id IN (:ids) " : default(String); + + var group = + "group by ContentItemVersionRecord.ContentItemRecord.Id " + + "having max(cast(Latest as Int32)) = 0 and max(cast(Published AS Int32)) = 0 "; + + var hql = String.Concat(select, filter, group); + var query = session.CreateQuery(hql); + + if (contentItemIds != null) { + query.SetParameterList("ids", contentItemIds); + } + return query; } } diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinCommand.cs b/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinCommand.cs new file mode 100644 index 000000000..8f2406d29 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinCommand.cs @@ -0,0 +1,6 @@ +namespace Orchard.AuditTrail.ViewModels { + public enum RecycleBinCommand { + Restore, + Destroy, + } +} \ 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 index c7a03a689..7b643f5b3 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinViewModel.cs +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/ViewModels/RecycleBinViewModel.cs @@ -4,6 +4,11 @@ using Orchard.ContentManagement; namespace Orchard.AuditTrail.ViewModels { public class RecycleBinViewModel { + public RecycleBinViewModel() { + SelectedContentItems = new List(0); + } + public RecycleBinCommand? RecycleBinCommand { get; set; } + public IList SelectedContentItems { get; set; } public IPageOfItems ContentItems { get; set; } public dynamic Pager { get; set; } } diff --git a/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/RecycleBin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/RecycleBin/Index.cshtml index 65428b4db..eab0df710 100644 --- a/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/RecycleBin/Index.cshtml +++ b/src/Orchard.Web/Modules/Orchard.AuditTrail/Views/RecycleBin/Index.cshtml @@ -1,68 +1,77 @@ -@model Orchard.AuditTrail.ViewModels.RecycleBinViewModel +@using Orchard.AuditTrail.ViewModels +@model RecycleBinViewModel @{ Style.Include("audittrail-recycle-bin.css"); Script.Require("ShapesBase"); + Script.Include(("audittrail-recyclebin.js")); 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"}); +
+ @Html.ValidationSummary() + @using (Html.BeginFormAntiForgeryPost()) { +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+ @if (!contentItems.Any()) { +

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

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