Adding content type filter to the Recycle Bin

This commit is contained in:
Benedek Farkas
2026-01-07 19:35:39 +01:00
parent 165505b1db
commit 8f3b3d451c
5 changed files with 163 additions and 52 deletions

View File

@@ -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
}
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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; }
}
}
}

View File

@@ -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>