mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Implementing Recycle Bin functionality.
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string> defaultReturnUrl = null) {
|
||||
return new RedirectResult(controller.Request.GetReturnUrl(returnUrl, defaultReturnUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string> defaultReturnUrl = null) {
|
||||
return request.IsLocalUrl(returnUrl)
|
||||
? returnUrl
|
||||
: request.UrlReferrer != null
|
||||
? request.UrlReferrer.ToString()
|
||||
: defaultReturnUrl != null
|
||||
? defaultReturnUrl()
|
||||
: "~/Admin";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NHibernate, Version=4.0.0.4000, Culture=neutral, PublicKeyToken=aa95f207798dfdb4, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\nhibernate\NHibernate.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations">
|
||||
@@ -85,6 +89,7 @@
|
||||
<Content Include="Scripts\audittrail-checkall.js" />
|
||||
<Content Include="Scripts\audittrail-disabledcontent.js" />
|
||||
<Content Include="Styles\audittrail-content-event.css" />
|
||||
<Content Include="Styles\audittrail-recycle-bin.css" />
|
||||
<Content Include="Styles\audittrail-display.css" />
|
||||
<Content Include="Styles\audittrail-disabledcontent.css" />
|
||||
<Content Include="Styles\audittrail-part.css" />
|
||||
@@ -182,8 +187,14 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
<Compile Include="Controllers\RecycleBinController.cs" />
|
||||
<Compile Include="Controllers\ContentController.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Helpers\ControllerExtensions.cs" />
|
||||
<Compile Include="Helpers\HttpRequestExtensions.cs" />
|
||||
<Compile Include="Services\IRecycleBin.cs" />
|
||||
<Compile Include="Services\RecycleBin.cs" />
|
||||
<Compile Include="ViewModels\RecycleBinViewModel.cs" />
|
||||
<Compile Include="Drivers\ClientIpAddressSettingsPartDriver.cs" />
|
||||
<Compile Include="Drivers\AuditTrailTrimmingSettingsPartDriver.cs" />
|
||||
<Compile Include="Drivers\AuditTrailSettingsPartDriver.cs" />
|
||||
@@ -308,6 +319,9 @@
|
||||
<ItemGroup>
|
||||
<Content Include="Views\AuditTrailEventActions-Content-Published.SummaryAdmin.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\RecycleBin\Index.cshtml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Orchard.Collections;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.AuditTrail.Services {
|
||||
public interface IRecycleBin : IDependency {
|
||||
/// <summary>
|
||||
/// Returns all removed content items.
|
||||
/// </summary>
|
||||
IPageOfItems<ContentItem> List(int page, int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all removed content items.
|
||||
/// </summary>
|
||||
IPageOfItems<T> List<T>(int page, int pageSize) where T : class, IContent;
|
||||
|
||||
/// <summary>
|
||||
/// Restores the specified content item.
|
||||
/// </summary>
|
||||
ContentItem Restore(ContentItem contentItem);
|
||||
}
|
||||
}
|
||||
@@ -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<ContentItem> List(int page, int pageSize) {
|
||||
return List<ContentItem>(page, pageSize);
|
||||
}
|
||||
|
||||
public IPageOfItems<T> List<T>(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<object>();
|
||||
var versionIds = rows.Cast<object[]>().Select(x => (int)x[0]);
|
||||
var contentItems = _contentManager.GetManyByVersionId<T>(versionIds, QueryHints.Empty);
|
||||
|
||||
return new PageOfItems<T>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Collections;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.AuditTrail.ViewModels {
|
||||
public class RecycleBinViewModel {
|
||||
public IPageOfItems<ContentItem> ContentItems { get; set; }
|
||||
public dynamic Pager { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
<section class="recycle-bin-filter-section">
|
||||
<div class="filter-control-group">
|
||||
<label>@T("Actions:")</label>
|
||||
<select name="SelectedItemsAction">
|
||||
<option></option>
|
||||
<option value="Restore">@T("Restore")</option>
|
||||
<option value="Destroy">@T("Destroy Permanently")</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-control-group">
|
||||
<button type="submit" class="filter-apply-button" value="yes">@T("Apply")</button>
|
||||
</div>
|
||||
<div class="filter-control-group">
|
||||
<label>@T("Sort by:")</label>
|
||||
<select name="RecycleBinSort">
|
||||
<option>@T("Deleted (latest first")</option>
|
||||
<option>@T("Title (alphabetical")</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-control-group">
|
||||
<button type="submit" class="filter-apply-button" value="yes">@T("Apply")</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="recycle-bin-list-section">
|
||||
@if (!contentItems.Any()) {
|
||||
<p class="info">@T("There are no records to display.")</p>
|
||||
}
|
||||
else {
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="content-checkbox"><input type="checkbox" class="check-all"/></th>
|
||||
<th class="content-column">@T("Content Item")</th>
|
||||
<th class="actions-column"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@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"});
|
||||
<tr>
|
||||
<td><input type="checkbox"/></td>
|
||||
<td class="content-column"><a href="@contentDisplayUrl">@contentDisplayText</a></td>
|
||||
<td class="actions-column">
|
||||
<a href="@contentDisplayUrl">@T("View")</a> @T(" | ")
|
||||
@Html.ActionLink(T("View Audit Trail").Text, "Index", "Admin", new {content = contentItem.Id, area = "Orchard.AuditTrail"}, null) @T(" | ")
|
||||
<a href="@Url.Action("Restore", "RecycleBin", new {id = contentItem.Id, area = "Orchard.AuditTrail"})" data-unsafe-url="@T("Are you sure you want to restore this item?")">@T("Restore")</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</section>
|
||||
<section class="pager">
|
||||
@Display(Model.Pager)
|
||||
</section>
|
||||
}
|
||||
Reference in New Issue
Block a user