mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Adding content type filter to the Recycle Bin
This commit is contained in:
@@ -4,7 +4,6 @@ using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.AuditTrail.Helpers;
|
||||
using Orchard.AuditTrail.Services;
|
||||
using Orchard.AuditTrail.Services.Models;
|
||||
using Orchard.AuditTrail.ViewModels;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Environment.Extensions;
|
||||
@@ -28,7 +27,12 @@ namespace Orchard.AuditTrail.Controllers
|
||||
private readonly IOrchardServices _services;
|
||||
private readonly IRecycleBin _recycleBin;
|
||||
|
||||
public RecycleBinController(IAuthorizer authorizer, IContentManager contentManager, INotifier notifier, IOrchardServices services, IRecycleBin recycleBin)
|
||||
public RecycleBinController(
|
||||
IAuthorizer authorizer,
|
||||
IContentManager contentManager,
|
||||
INotifier notifier,
|
||||
IOrchardServices services,
|
||||
IRecycleBin recycleBin)
|
||||
{
|
||||
_authorizer = authorizer;
|
||||
_contentManager = contentManager;
|
||||
@@ -42,12 +46,14 @@ namespace Orchard.AuditTrail.Controllers
|
||||
public Localizer T { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public ActionResult Index(PagerParameters pagerParameters, AuditTrailOrderBy? orderBy = null)
|
||||
public ActionResult Index(PagerParameters pagerParameters, string contentTypeName = null)
|
||||
{
|
||||
if (!_authorizer.Authorize(Permissions.ViewAuditTrail))
|
||||
{
|
||||
return new HttpUnauthorizedResult();
|
||||
}
|
||||
|
||||
var viewModel = SetupViewModel(new RecycleBinViewModel(), pagerParameters);
|
||||
var viewModel = SetupViewModel(new RecycleBinViewModel(), pagerParameters, contentTypeName);
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
@@ -55,8 +61,11 @@ namespace Orchard.AuditTrail.Controllers
|
||||
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;
|
||||
@@ -104,14 +113,15 @@ namespace Orchard.AuditTrail.Controllers
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
private RecycleBinViewModel SetupViewModel(RecycleBinViewModel viewModel, PagerParameters pagerParameters)
|
||||
private RecycleBinViewModel SetupViewModel(RecycleBinViewModel viewModel, PagerParameters pagerParameters, string contentTypeName = null)
|
||||
{
|
||||
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 removedContentItems = _recycleBin.List(pager.Page, pager.PageSize, contentTypeName);
|
||||
var pagerShape = _services.New.Pager(pager).TotalItemCount(removedContentItems.TotalItemCount);
|
||||
|
||||
viewModel.FilterContentType = contentTypeName;
|
||||
viewModel.ContentItems = removedContentItems;
|
||||
viewModel.Pager = pagershape;
|
||||
viewModel.Pager = pagerShape;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
@@ -160,6 +170,5 @@ namespace Orchard.AuditTrail.Controllers
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,26 +9,27 @@ namespace Orchard.AuditTrail.Services
|
||||
/// <summary>
|
||||
/// Returns all removed content items.
|
||||
/// </summary>
|
||||
IPageOfItems<ContentItem> List(int page, int pageSize);
|
||||
IPageOfItems<ContentItem> List(int page, int pageSize, string contentTypeName = null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all removed content items.
|
||||
/// </summary>
|
||||
IPageOfItems<T> List<T>(int page, int pageSize) where T : class, IContent;
|
||||
IPageOfItems<T> List<T>(int page, int pageSize, string contentTypeName = null) where T : class, IContent;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the specified list of content items from the recycle bin.
|
||||
/// </summary>
|
||||
IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null);
|
||||
IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the specified list of content items from the recycle bin.
|
||||
/// </summary>
|
||||
IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null) where T : class, IContent;
|
||||
IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null)
|
||||
where T : class, IContent;
|
||||
|
||||
/// <summary>
|
||||
/// Restores the specified content item.
|
||||
/// </summary>
|
||||
ContentItem Restore(ContentItem contentItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ using System.Linq;
|
||||
using NHibernate;
|
||||
using Orchard.Collections;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.MetaData;
|
||||
using Orchard.Data;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Environment.Extensions;
|
||||
|
||||
namespace Orchard.AuditTrail.Services
|
||||
@@ -14,21 +16,29 @@ namespace Orchard.AuditTrail.Services
|
||||
{
|
||||
private readonly ITransactionManager _transactionManager;
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly IContentDefinitionManager _contentDefinitionManager;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
|
||||
public RecycleBin(ITransactionManager transactionManager, IContentManager contentManager)
|
||||
public RecycleBin(
|
||||
ITransactionManager transactionManager,
|
||||
IContentManager contentManager,
|
||||
IContentDefinitionManager contentDefinitionManager,
|
||||
ShellSettings shellSettings)
|
||||
{
|
||||
_transactionManager = transactionManager;
|
||||
_contentManager = contentManager;
|
||||
_contentDefinitionManager = contentDefinitionManager;
|
||||
_shellSettings = shellSettings;
|
||||
}
|
||||
|
||||
public IPageOfItems<ContentItem> List(int page, int pageSize)
|
||||
public IPageOfItems<ContentItem> List(int page, int pageSize, string contentTypeName = null)
|
||||
{
|
||||
return List<ContentItem>(page, pageSize);
|
||||
return List<ContentItem>(page, pageSize, contentTypeName);
|
||||
}
|
||||
|
||||
public IPageOfItems<T> List<T>(int page, int pageSize) where T : class, IContent
|
||||
public IPageOfItems<T> List<T>(int page, int pageSize, string contentTypeName = null) where T : class, IContent
|
||||
{
|
||||
var query = GetDeletedVersionsQuery();
|
||||
var query = GetDeletedVersionsQuery(null, contentTypeName);
|
||||
var totalCount = query.List().Count;
|
||||
|
||||
query.SetFirstResult((page - 1) * pageSize);
|
||||
@@ -44,14 +54,13 @@ namespace Orchard.AuditTrail.Services
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null)
|
||||
{
|
||||
return GetMany<ContentItem>(contentItemIds, hints);
|
||||
}
|
||||
public IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null) =>
|
||||
GetMany<ContentItem>(contentItemIds, hints, contentTypeName);
|
||||
|
||||
public IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null) where T : class, IContent
|
||||
public IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null)
|
||||
where T : class, IContent
|
||||
{
|
||||
var query = GetDeletedVersionsQuery(contentItemIds);
|
||||
var query = GetDeletedVersionsQuery(contentItemIds, contentTypeName);
|
||||
return LoadContentItems<T>(query, hints);
|
||||
}
|
||||
|
||||
@@ -60,10 +69,10 @@ namespace Orchard.AuditTrail.Services
|
||||
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));
|
||||
return lastVersion.Latest || lastVersion.Published
|
||||
? throw new InvalidOperationException(
|
||||
string.Format("Cannot restore content item with ID {0} from the recycle bin, since that item is not deleted.", contentItem.Id))
|
||||
: _contentManager.Restore(contentItem, VersionOptions.Restore(lastVersion.Number, publish: false));
|
||||
}
|
||||
|
||||
private IEnumerable<T> LoadContentItems<T>(IQuery query, QueryHints hints = null) where T : class, IContent
|
||||
@@ -73,22 +82,53 @@ namespace Orchard.AuditTrail.Services
|
||||
return _contentManager.GetManyByVersionId<T>(versionIds, hints ?? QueryHints.Empty);
|
||||
}
|
||||
|
||||
private IQuery GetDeletedVersionsQuery(IEnumerable<int> contentItemIds = null)
|
||||
private IQuery GetDeletedVersionsQuery(IEnumerable<int> contentItemIds = null, string contentTypeName = null)
|
||||
{
|
||||
var session = _transactionManager.GetSession();
|
||||
|
||||
// Select only the highest versions where both Published and Latest are false.
|
||||
var select =
|
||||
"select max(ContentItemVersionRecord.Id), ContentItemVersionRecord.ContentItemRecord.Id, max(ContentItemVersionRecord.Number) " +
|
||||
"from Orchard.ContentManagement.Records.ContentItemVersionRecord ContentItemVersionRecord ";
|
||||
var select = @"
|
||||
select
|
||||
max(contentItemVersionRecord.Id),
|
||||
max(contentItemVersionRecord.Number),
|
||||
contentItemVersionRecord.ContentItemRecord.Id
|
||||
from
|
||||
Orchard.ContentManagement.Records.ContentItemVersionRecord contentItemVersionRecord
|
||||
left join
|
||||
Orchard.Core.Common.Models.CommonPartVersionRecord commonPartVersionRecord
|
||||
on
|
||||
contentItemVersionRecord.Id = commonPartVersionRecord.Id";
|
||||
|
||||
var filter = contentItemIds != null ? "where ContentItemVersionRecord.ContentItemRecord.Id in (:ids) " : default(string);
|
||||
var filter = contentItemIds == null
|
||||
? default
|
||||
: "\nwhere contentItemVersionRecord.ContentItemRecord.Id in (:ids)";
|
||||
|
||||
var group =
|
||||
"group by ContentItemVersionRecord.ContentItemRecord.Id " +
|
||||
"having max(cast(Latest as Int32)) = 0 and max(cast(Published as Int32)) = 0 ";
|
||||
// ContentTypeName is safe to use in a query directly, because it's a technical name without special characters.
|
||||
if (contentTypeName != null
|
||||
&& _contentDefinitionManager.ListTypeDefinitions().Any(typeDefinition => typeDefinition.Name == contentTypeName))
|
||||
{
|
||||
filter += $@"
|
||||
{(filter == default ? "where" : "and")}
|
||||
contentItemVersionRecord.ContentItemRecord.ContentType.Name = '{contentTypeName}'";
|
||||
}
|
||||
|
||||
var hql = string.Concat(select, filter, group);
|
||||
var group = @"
|
||||
group by
|
||||
contentItemVersionRecord.ContentItemRecord.Id
|
||||
having
|
||||
max(cast(Latest as Int32)) = 0
|
||||
and max(cast(Published as Int32)) = 0";
|
||||
|
||||
var order = "";
|
||||
// SQL CE doesn't support ordering by an aggregate value.
|
||||
if (!string.Equals(_shellSettings.DataProvider, "SqlCe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
order = @"
|
||||
order by
|
||||
max(commonPartVersionRecord.ModifiedUtc) desc";
|
||||
}
|
||||
|
||||
var hql = string.Concat(select, filter, group, order);
|
||||
var query = session.CreateQuery(hql);
|
||||
|
||||
if (contentItemIds != null)
|
||||
@@ -99,4 +139,4 @@ namespace Orchard.AuditTrail.Services
|
||||
return query;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ namespace Orchard.AuditTrail.ViewModels
|
||||
{
|
||||
SelectedContentItems = new List<RemovedContentItemViewModel>(0);
|
||||
}
|
||||
|
||||
public string FilterContentType { get; set; }
|
||||
public RecycleBinCommand? RecycleBinCommand { get; set; }
|
||||
public IList<RemovedContentItemViewModel> SelectedContentItems { get; set; }
|
||||
public IPageOfItems<ContentItem> ContentItems { get; set; }
|
||||
public dynamic Pager { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,95 @@
|
||||
@using Orchard.AuditTrail.ViewModels
|
||||
@using Orchard.AuditTrail.ViewModels
|
||||
@using Orchard.ContentManagement
|
||||
@using Orchard.Core.Common.Models
|
||||
@using Orchard.Localization.Services
|
||||
@using Orchard.ContentManagement.MetaData
|
||||
@using Orchard.Core.Contents.Settings
|
||||
|
||||
@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;
|
||||
var dateLocalizationServices = WorkContext.Resolve<IDateLocalizationServices>();
|
||||
|
||||
var listableContentTypeDefinitions = WorkContext.Resolve<IContentDefinitionManager>()
|
||||
.ListTypeDefinitions()
|
||||
.Where(definition => definition.Settings.GetModel<ContentTypeSettings>().Listable)
|
||||
.ToList();
|
||||
var selectedContentType = Request.QueryString["contentTypeName"];
|
||||
|
||||
var routeData = new RouteValueDictionary(ViewContext.RouteData.Values);
|
||||
var queryString = ViewContext.HttpContext.Request.QueryString;
|
||||
|
||||
if (queryString != null)
|
||||
{
|
||||
foreach (string key in queryString.Keys)
|
||||
{
|
||||
if (key != null && !routeData.ContainsKey(key))
|
||||
{
|
||||
routeData[key] = queryString[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (routeData.ContainsKey("id") && !HasText(routeData["id"]))
|
||||
{
|
||||
routeData.Remove("id");
|
||||
}
|
||||
}
|
||||
|
||||
<div id="recycle-bin">
|
||||
@Html.ValidationSummary()
|
||||
@using (Html.BeginFormAntiForgeryPost()) {
|
||||
|
||||
@using (Html.BeginForm("Index", "RecycleBin", FormMethod.Get))
|
||||
{
|
||||
<fieldset class="bulk-actions">
|
||||
<label for="filterResults" class="bulk-filter">@T("Show")</label>
|
||||
<select id="filterResults" name="contentTypeName">
|
||||
@Html.SelectOption(Model.FilterContentType, "", T("any (show all)").Text)
|
||||
|
||||
@foreach (var contentTypeDefinition in listableContentTypeDefinitions)
|
||||
{
|
||||
@Html.SelectOption(Model.FilterContentType, contentTypeDefinition.Name, contentTypeDefinition.DisplayName)
|
||||
}
|
||||
</select>
|
||||
|
||||
<button type="submit">@T("Apply")</button>
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
<br />
|
||||
|
||||
@using (Html.BeginFormAntiForgeryPost())
|
||||
{
|
||||
<fieldset class="bulk-actions">
|
||||
<label>@T("Actions:")</label>
|
||||
<select name="RecycleBinCommand">
|
||||
<option></option>
|
||||
<option value="@RecycleBinCommand.Restore" data-unsafe-action="@T("Are you sure you want to restore the selected items?")" @if(Model.RecycleBinCommand == RecycleBinCommand.Restore){<text>selected="selected"</text>}>@T("Restore")</option>
|
||||
<option value="@RecycleBinCommand.Restore" data-unsafe-action="@T("Are you sure you want to restore the selected items?")" @if (Model.RecycleBinCommand == RecycleBinCommand.Restore) { <text> selected="selected" </text> }>
|
||||
@T("Restore")
|
||||
</option>
|
||||
@**TODO: Decide wether or not to allow users to permanently delete items. Commented out for now.*@
|
||||
@*<option value="@RecycleBinCommand.Destroy" data-unsafe-action="@T("WARNING: This will PERMANENTLY delete the selected content items, including related content part records, never to be seen again. Are you sure you want to do this?")" @if (Model.RecycleBinCommand == RecycleBinCommand.Destroy) { <text> selected="selected" </text> }>@T("Remove Permanently ☠")</option>*@
|
||||
@*<option value="@RecycleBinCommand.Destroy" data-unsafe-action="@T("WARNING: This will PERMANENTLY delete the selected content items, including related content part records, never to be seen again. Are you sure you want to do this?")"
|
||||
@if (Model.RecycleBinCommand == RecycleBinCommand.Destroy) { <text> selected="selected" </text> }>@T("Remove Permanently ☠")</option>*@
|
||||
</select>
|
||||
</fieldset>
|
||||
<div class="bulk-actions">
|
||||
<button type="submit" class="filter-apply-button" name="ExecuteActionButton" value="ExecuteActionButton">@T("Execute")</button>
|
||||
</div>
|
||||
|
||||
<section class="recycle-bin-list-section">
|
||||
@if (!contentItems.Any()) {
|
||||
@if (!contentItems.Any())
|
||||
{
|
||||
<p class="info">@T("There are no records to display.")</p>
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -44,17 +102,18 @@
|
||||
<tbody>
|
||||
@{
|
||||
var index = 0;
|
||||
foreach (var contentItem in contentItems) {
|
||||
foreach (var contentItem in contentItems)
|
||||
{
|
||||
var isSelected = Model.SelectedContentItems.Where(x => x.Id == contentItem.Id && x.Selected).Select(x => x.Id).Any();
|
||||
var commonPart = contentItem.As<CommonPart>();
|
||||
var removedText = commonPart != null ? dateLocalizationServices.ConvertToLocalizedString(commonPart.VersionModifiedUtc) : T("-").Text;
|
||||
var contentDisplayTextHtmlString = Html.ItemDisplayText(contentItem);
|
||||
var contentDisplayText = contentDisplayTextHtmlString != null ? contentDisplayTextHtmlString.ToString() : contentItem.ContentType;
|
||||
var contentDisplayUrl = Url.Action("Detail", "Content", new {id = contentItem.Id, version = contentItem.Version, area = "Orchard.AuditTrail"});
|
||||
var contentDisplayUrl = Url.Action("Detail", "Content", new { id = contentItem.Id, version = contentItem.Version, area = "Orchard.AuditTrail" });
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="SelectedContentItems[@index].Id" value="@contentItem.Id" />
|
||||
<input type="checkbox" name="SelectedContentItems[@index].Selected" value="true" @if(isSelected){<text>checked="checked"</text>} />
|
||||
<input type="checkbox" name="SelectedContentItems[@index].Selected" value="true" @if (isSelected) { <text> checked="checked" </text> } />
|
||||
</td>
|
||||
<td class="content-column"><a href="@contentDisplayUrl">@contentDisplayText</a></td>
|
||||
<td class="content-removed-column">@removedText</td>
|
||||
@@ -67,7 +126,7 @@
|
||||
@Html.ActionLink(T("View Audit Trail").Text, "Index", "Admin", new { content = contentItem.Id, area = "Orchard.AuditTrail" }, null)
|
||||
</li>
|
||||
<li class="action-link">
|
||||
<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>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
@@ -83,4 +142,4 @@
|
||||
@Display(Model.Pager)
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user