Skip to content

Commit

Permalink
ContentVersion cleanup backoffice UI (#11637)
Browse files Browse the repository at this point in the history
* init rollback ui prototype

* add busy state to button, deselect version, add pagination status

* add localisation

* style current version

* disable rollback button when nothing is selected

* stop click event

* Endpoints for paginated content versions.
Light on tests, tight on time.

* Endpoints to "pin" content versions

* camel case json output.
Not sure why json formatter not set for controller, bit risky to add it now

* wire up paging

* wire up pin/unpin

* rename getPagedRollbackVersions to getPagedContentVersions

* prevent selection of current version and current draft

* add current draft and current version to UI

* remove pointer if the row is not selectable

* Improve warning for globally disabled cleanup feature.

* Fix current loses prevent cleanup state on publish.

* Added umbracoLog audit entries for "pin" / "unpin"

* Match v9 defaults for keepVersions settings

* Fix - losing preventCleanup on save current with content changes

* update pin/unpin button labels

* fix pagination bug

* add missing "

* always send culture when a doc type can vary

Co-authored-by: Mads Rasmussen <[email protected]>
  • Loading branch information
Paul Johnson and madsrasmussen authored Nov 16, 2021
1 parent 1fbf02d commit d89725b
Show file tree
Hide file tree
Showing 28 changed files with 805 additions and 175 deletions.
12 changes: 11 additions & 1 deletion src/Umbraco.Core/Models/AuditType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ public enum AuditType
/// <summary>
/// Custom audit message.
/// </summary>
Custom
Custom,

/// <summary>
/// Content version preventCleanup set to true
/// </summary>
ContentVersionPreventCleanup,

/// <summary>
/// Content version preventCleanup set to false
/// </summary>
ContentVersionEnableCleanup
}
}
45 changes: 45 additions & 0 deletions src/Umbraco.Core/Models/ContentVersionMeta.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;

namespace Umbraco.Core.Models
{
public class ContentVersionMeta
{
public int ContentId { get; }
public int ContentTypeId { get; }
public int VersionId { get; }
public int UserId { get; }

public DateTime VersionDate { get; }
public bool CurrentPublishedVersion { get; }
public bool CurrentDraftVersion { get; }
public bool PreventCleanup { get; }
public string Username { get; }

public ContentVersionMeta() { }

public ContentVersionMeta(
int versionId,
int contentId,
int contentTypeId,
int userId,
DateTime versionDate,
bool currentPublishedVersion,
bool currentDraftVersion,
bool preventCleanup,
string username)
{
VersionId = versionId;
ContentId = contentId;
ContentTypeId = contentTypeId;

UserId = userId;
VersionDate = versionDate;
CurrentPublishedVersion = currentPublishedVersion;
CurrentDraftVersion = currentDraftVersion;
PreventCleanup = preventCleanup;
Username = username;
}

public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}";
}
}
24 changes: 0 additions & 24 deletions src/Umbraco.Core/Models/HistoricContentVersionMeta.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,31 @@ public interface IDocumentVersionRepository : IRepository
/// <summary>
/// Gets a list of all historic content versions.
/// </summary>
public IReadOnlyCollection<HistoricContentVersionMeta> GetDocumentVersionsEligibleForCleanup();
public IReadOnlyCollection<ContentVersionMeta> GetDocumentVersionsEligibleForCleanup();

/// <summary>
/// Gets cleanup policy override settings per content type.
/// </summary>
public IReadOnlyCollection<ContentVersionCleanupPolicySettings> GetCleanupPolicies();

/// <summary>
/// Gets paginated content versions for given content id paginated.
/// </summary>
public IEnumerable<ContentVersionMeta> GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null);

/// <summary>
/// Deletes multiple content versions by ID.
/// </summary>
void DeleteVersions(IEnumerable<int> versionIds);

/// <summary>
/// Updates the prevent cleanup flag on a content version.
/// </summary>
void SetPreventCleanup(int versionId, bool preventCleanup);

/// <summary>
/// Gets the content version metadata for a specific version.
/// </summary>
ContentVersionMeta Get(int versionId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,10 @@ protected override void PersistUpdatedItem(IContent entity)
documentVersionDto.Published = true; // now published
contentVersionDto.Current = false; // no more current
}

// Ensure existing version retains current preventCleanup flag (both saving and publishing).
contentVersionDto.PreventCleanup = version.PreventCleanup;

Database.Update(contentVersionDto);
Database.Update(documentVersionDto);

Expand All @@ -606,6 +610,7 @@ protected override void PersistUpdatedItem(IContent entity)
contentVersionDto.Id = 0; // want a new id
contentVersionDto.Current = true; // current version
contentVersionDto.Text = entity.Name;
contentVersionDto.PreventCleanup = false; // new draft version disregards prevent cleanup flag
Database.Insert(contentVersionDto);
entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,33 @@ public DocumentVersionRepository(IScopeAccessor scopeAccessor)
/// Never includes current published version.<br/>
/// Never includes versions marked as "preventCleanup".<br/>
/// </remarks>
public IReadOnlyCollection<HistoricContentVersionMeta> GetDocumentVersionsEligibleForCleanup()
public IReadOnlyCollection<ContentVersionMeta> GetDocumentVersionsEligibleForCleanup()
{
var query = _scopeAccessor.AmbientScope.SqlContext.Sql();

query.Select(@"umbracoDocument.nodeId as contentId,
umbracoContent.contentTypeId as contentTypeId,
umbracoContentVersion.id as versionId,
umbracoContentVersion.versionDate as versionDate")
umbracoContentVersion.userId as userId,
umbracoContentVersion.versionDate as versionDate,
umbracoDocumentVersion.published as currentPublishedVersion,
umbracoContentVersion.[current] as currentDraftVersion,
umbracoContentVersion.preventCleanup as preventCleanup,
umbracoUser.userName as username")
.From<DocumentDto>()
.InnerJoin<ContentDto>()
.On<DocumentDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<ContentVersionDto>()
.On<ContentDto, ContentVersionDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>(left => left.Id, right => right.Id)
.LeftJoin<UserDto>()
.On<UserDto, ContentVersionDto>(left => left.Id, right => right.UserId)
.Where<ContentVersionDto>(x => !x.Current) // Never delete current draft version
.Where<ContentVersionDto>(x => !x.PreventCleanup) // Never delete "pinned" versions
.Where<DocumentVersionDto>(x => !x.Published); // Never delete published version

return _scopeAccessor.AmbientScope.Database.Fetch<HistoricContentVersionMeta>(query);
return _scopeAccessor.AmbientScope.Database.Fetch<ContentVersionMeta>(query);
}

/// <inheritdoc />
Expand All @@ -55,6 +62,47 @@ public IReadOnlyCollection<ContentVersionCleanupPolicySettings> GetCleanupPolici
return _scopeAccessor.AmbientScope.Database.Fetch<ContentVersionCleanupPolicySettings>(query);
}

/// <inheritdoc />
public IEnumerable<ContentVersionMeta> GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null)
{
var query = _scopeAccessor.AmbientScope.SqlContext.Sql();

query.Select(@"umbracoDocument.nodeId as contentId,
umbracoContent.contentTypeId as contentTypeId,
umbracoContentVersion.id as versionId,
umbracoContentVersion.userId as userId,
umbracoContentVersion.versionDate as versionDate,
umbracoDocumentVersion.published as currentPublishedVersion,
umbracoContentVersion.[current] as currentDraftVersion,
umbracoContentVersion.preventCleanup as preventCleanup,
umbracoUser.userName as username")
.From<DocumentDto>()
.InnerJoin<ContentDto>()
.On<DocumentDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<ContentVersionDto>()
.On<ContentDto, ContentVersionDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>(left => left.Id, right => right.Id)
.LeftJoin<UserDto>()
.On<UserDto, ContentVersionDto>(left => left.Id, right => right.UserId)
.LeftJoin<ContentVersionCultureVariationDto>()
.On<ContentVersionCultureVariationDto, ContentVersionDto>(left => left.VersionId, right => right.Id)
.Where<ContentVersionDto>(x => x.NodeId == contentId);

// TODO: If there's not a better way to write this then we need a better way to write this.
query = languageId.HasValue
? query.Where<ContentVersionCultureVariationDto>(x => x.LanguageId == languageId.Value)
: query.Where("umbracoContentVersionCultureVariation.languageId is null");

query = query.OrderByDescending<ContentVersionDto>(x => x.Id);

var page = _scopeAccessor.AmbientScope.Database.Page<ContentVersionMeta>(pageIndex + 1, pageSize, query);

totalRecords = page.TotalItems;

return page.Items;
}

/// <inheritdoc />
/// <remarks>
/// Deletes in batches of <see cref="Constants.Sql.MaxParameterCount"/>
Expand Down Expand Up @@ -90,5 +138,45 @@ public void DeleteVersions(IEnumerable<int> versionIds)
_scopeAccessor.AmbientScope.Database.Execute(query);
}
}

/// <inheritdoc />
public void SetPreventCleanup(int versionId, bool preventCleanup)
{
var query = _scopeAccessor.AmbientScope.SqlContext.Sql()
.Update<ContentVersionDto>(x => x.Set(y => y.PreventCleanup, preventCleanup))
.Where<ContentVersionDto>(x => x.Id == versionId);

_scopeAccessor.AmbientScope.Database.Execute(query);
}

/// <inheritdoc />
public ContentVersionMeta Get(int versionId)
{
var query = _scopeAccessor.AmbientScope.SqlContext.Sql();

query.Select(@"umbracoDocument.nodeId as contentId,
umbracoContent.contentTypeId as contentTypeId,
umbracoContentVersion.id as versionId,
umbracoContentVersion.userId as userId,
umbracoContentVersion.versionDate as versionDate,
umbracoDocumentVersion.published as currentPublishedVersion,
umbracoContentVersion.[current] as currentDraftVersion,
umbracoContentVersion.preventCleanup as preventCleanup,
umbracoUser.userName as username")
.From<DocumentDto>()
.InnerJoin<ContentDto>()
.On<DocumentDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<ContentVersionDto>()
.On<ContentDto, ContentVersionDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>(left => left.Id, right => right.Id)
.LeftJoin<UserDto>()
.On<UserDto, ContentVersionDto>(left => left.Id, right => right.UserId)
.LeftJoin<ContentVersionCultureVariationDto>()
.On<ContentVersionCultureVariationDto, ContentVersionDto>(left => left.VersionId, right => right.Id)
.Where<ContentVersionDto>(x => x.Id == versionId);

return _scopeAccessor.AmbientScope.Database.Single<ContentVersionMeta>(query);
}
}
}
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ public interface IContentVersionCleanupPolicy
/// <summary>
/// Filters a set of candidates historic content versions for cleanup according to policy settings.
/// </summary>
IEnumerable<HistoricContentVersionMeta> Apply(DateTime asAtDate, IEnumerable<HistoricContentVersionMeta> items);
IEnumerable<ContentVersionMeta> Apply(DateTime asAtDate, IEnumerable<ContentVersionMeta> items);
}
}
13 changes: 12 additions & 1 deletion src/Umbraco.Core/Services/IContentVersionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ public interface IContentVersionService
/// <summary>
/// Removes historic content versions according to a policy.
/// </summary>
IReadOnlyCollection<HistoricContentVersionMeta> PerformContentVersionCleanup(DateTime asAtDate);
IReadOnlyCollection<ContentVersionMeta> PerformContentVersionCleanup(DateTime asAtDate);

/// <summary>
/// Gets paginated content versions for given content id paginated.
/// </summary>
/// <exception cref="ArgumentException">Thrown when <paramref name="culture"/> is invalid.</exception>
IEnumerable<ContentVersionMeta> GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string culture = null);

/// <summary>
/// Updates preventCleanup value for given content version.
/// </summary>
void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1);
}
}
49 changes: 44 additions & 5 deletions src/Umbraco.Core/Services/Implement/ContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3227,25 +3227,64 @@ public OperationResult Rollback(int id, int versionId, string culture = "*", int
/// In v9 this can live in another class as we publish the notifications via IEventAggregator.
/// But for v8 must be here for access to the static events.
/// </remarks>
public IReadOnlyCollection<HistoricContentVersionMeta> PerformContentVersionCleanup(DateTime asAtDate)
public IReadOnlyCollection<ContentVersionMeta> PerformContentVersionCleanup(DateTime asAtDate)
{
return CleanupDocumentVersions(asAtDate);
// Media - ignored
// Members - ignored
}

/// <inheritdoc />
public IEnumerable<ContentVersionMeta> GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string culture = null)
{
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));

// NOTE: v9 - don't service locate
var documentVersionRepository = Composing.Current.Factory.GetInstance<IDocumentVersionRepository>();

using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
var languageId = _languageRepository.GetIdByIsoCode(culture, throwOnNotFound: true);
scope.ReadLock(Constants.Locks.ContentTree);
return documentVersionRepository.GetPagedItemsByContentId(contentId, pageIndex, pageSize, out totalRecords, languageId);
}
}

/// <inheritdoc />
public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1)
{
// NOTE: v9 - don't service locate
var documentVersionRepository = Composing.Current.Factory.GetInstance<IDocumentVersionRepository>();

using (ScopeProvider.CreateScope(autoComplete: true))
{
documentVersionRepository.SetPreventCleanup(versionId, preventCleanup);

var version = documentVersionRepository.Get(versionId);

var auditType = preventCleanup
? AuditType.ContentVersionPreventCleanup
: AuditType.ContentVersionEnableCleanup;

var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";

Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
}
}

/// <remarks>
/// v9 - move to another class
/// </remarks>
private IReadOnlyCollection<HistoricContentVersionMeta> CleanupDocumentVersions(DateTime asAtDate)
private IReadOnlyCollection<ContentVersionMeta> CleanupDocumentVersions(DateTime asAtDate)
{
// NOTE: v9 - don't service locate
var documentVersionRepository = Composing.Current.Factory.GetInstance<IDocumentVersionRepository>();

// NOTE: v9 - don't service locate
var cleanupPolicy = Composing.Current.Factory.GetInstance<IContentVersionCleanupPolicy>();

List<HistoricContentVersionMeta> versionsToDelete;
List<ContentVersionMeta> versionsToDelete;

/* Why so many scopes?
*
Expand Down Expand Up @@ -3278,7 +3317,7 @@ private IReadOnlyCollection<HistoricContentVersionMeta> CleanupDocumentVersions(
var allHistoricVersions = documentVersionRepository.GetDocumentVersionsEligibleForCleanup();

Logger.Debug<ContentService>("Discovered {count} candidate(s) for ContentVersion cleanup.", allHistoricVersions.Count);
versionsToDelete = new List<HistoricContentVersionMeta>(allHistoricVersions.Count);
versionsToDelete = new List<ContentVersionMeta>(allHistoricVersions.Count);

var filteredContentVersions = cleanupPolicy.Apply(asAtDate, allHistoricVersions);

Expand All @@ -3299,7 +3338,7 @@ private IReadOnlyCollection<HistoricContentVersionMeta> CleanupDocumentVersions(
if (!versionsToDelete.Any())
{
Logger.Debug<ContentService>("No remaining ContentVersions for cleanup.", versionsToDelete.Count);
return Array.Empty<HistoricContentVersionMeta>();
return Array.Empty<ContentVersionMeta>();
}

Logger.Debug<ContentService>("Removing {count} ContentVersion(s).", versionsToDelete.Count);
Expand Down
Loading

0 comments on commit d89725b

Please sign in to comment.