U4-10382 Add end point to get a paginated audit trail

This commit is contained in:
Shannon
2017-09-07 17:27:37 +10:00
parent eb1edf94d3
commit c7b9a57795
16 changed files with 302 additions and 71 deletions

View File

@@ -2,18 +2,54 @@
namespace Umbraco.Core.Models
{
public sealed class AuditItem : Entity, IAggregateRoot
public sealed class AuditItem : Entity, IAuditItem
{
/// <summary>
/// Constructor for creating an item to be created
/// </summary>
/// <param name="objectId"></param>
/// <param name="comment"></param>
/// <param name="type"></param>
/// <param name="userId"></param>
public AuditItem(int objectId, string comment, AuditType type, int userId)
{
DisableChangeTracking();
Id = objectId;
Comment = comment;
AuditType = type;
UserId = userId;
EnableChangeTracking();
}
/// <summary>
/// Constructor for creating an item that is returned from the database
/// </summary>
/// <param name="objectId"></param>
/// <param name="comment"></param>
/// <param name="type"></param>
/// <param name="userId"></param>
/// <param name="userName"></param>
/// <param name="userAvatar"></param>
public AuditItem(int objectId, string comment, AuditType type, int userId, string userName, string userAvatar)
{
DisableChangeTracking();
Id = objectId;
Comment = comment;
AuditType = type;
UserId = userId;
UserName = userName;
UserAvatar = userAvatar;
EnableChangeTracking();
}
public string Comment { get; private set; }
public AuditType AuditType { get; private set; }
public int UserId { get; private set; }
public string UserName { get; private set; }
public string UserAvatar { get; private set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using Umbraco.Core.Models.EntityBase;
namespace Umbraco.Core.Models
{
public interface IAuditItem : IAggregateRoot
{
string Comment { get; }
AuditType AuditType { get; }
int UserId { get; }
string UserName { get; }
string UserAvatar { get; }
}
}

View File

@@ -32,6 +32,6 @@ namespace Umbraco.Core.Models.Rdbms
[Column("logComment")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(4000)]
public string Comment { get; set; }
public string Comment { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
using System;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseAnnotations;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Models.Rdbms
{
/// <summary>
/// object used for returning data from the umbracoLog table
/// </summary>
[TableName("umbracoLog")]
[PrimaryKey("id")]
[ExplicitColumns]
internal class ReadOnlyLogDto
{
[Column("id")]
[PrimaryKeyColumn]
public int Id { get; set; }
[Column("userId")]
public int UserId { get; set; }
[Column("NodeId")]
public int NodeId { get; set; }
[Column("Datestamp")]
public DateTime Datestamp { get; set; }
[Column("logHeader")]
public string Header { get; set; }
[Column("logComment")]
public string Comment { get; set; }
[ResultColumn("userName")]
public string UserName { get; set; }
[ResultColumn("userAvatar")]
public string UserAvatar { get; set; }
}
}

View File

@@ -5,6 +5,7 @@ using Umbraco.Core.Models.Rdbms;
namespace Umbraco.Core.Persistence.Factories
{
internal class MacroFactory
{
public IMacro BuildEntity(MacroDto dto)

View File

@@ -0,0 +1,46 @@
using System.Collections.Concurrent;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Rdbms;
using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Persistence.Mappers
{
[MapperFor(typeof(AuditItem))]
[MapperFor(typeof(IAuditItem))]
public sealed class AuditMapper : BaseMapper
{
private static readonly ConcurrentDictionary<string, DtoMapModel> PropertyInfoCacheInstance = new ConcurrentDictionary<string, DtoMapModel>();
public AuditMapper(ISqlSyntaxProvider sqlSyntax) : base(sqlSyntax)
{
}
//NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it
// otherwise that would fail because there is no public constructor.
public AuditMapper()
{
BuildMap();
}
#region Overrides of BaseMapper
internal override ConcurrentDictionary<string, DtoMapModel> PropertyInfoCache
{
get { return PropertyInfoCacheInstance; }
}
internal override void BuildMap()
{
if (PropertyInfoCache.IsEmpty)
{
CacheMap<AuditItem, LogDto>(src => src.Id, dto => dto.NodeId);
CacheMap<AuditItem, LogDto>(src => src.CreateDate, dto => dto.Datestamp);
CacheMap<AuditItem, LogDto>(src => src.UserId, dto => dto.UserId);
CacheMap<AuditItem, LogDto>(src => src.AuditType, dto => dto.Header);
}
}
#endregion
}
}

View File

@@ -1,29 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Models.Rdbms;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Persistence.UnitOfWork;
namespace Umbraco.Core.Persistence.Repositories
{
internal class AuditRepository : PetaPocoRepositoryBase<int, AuditItem>, IAuditRepository
internal class AuditRepository : PetaPocoRepositoryBase<int, IAuditItem>, IAuditRepository
{
public AuditRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax)
: base(work, cache, logger, sqlSyntax)
{
}
protected override void PersistNewItem(AuditItem entity)
public IEnumerable<IAuditItem> GetPagedResultsByQuery(IQuery<IAuditItem> query, long pageIndex, int pageSize, out long totalRecords, Direction orderDirection, IQuery<IAuditItem> customFilter)
{
throw new NotImplementedException();
var customFilterWheres = customFilter != null ? customFilter.GetWhereClauses().ToArray() : null;
var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0;
if (hasCustomFilter)
{
var filterSql = new Sql();
foreach (var filterClause in customFilterWheres)
{
filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2);
}
}
var sql = GetBaseQuery(false);
if (orderDirection == Direction.Descending)
sql.OrderByDescending("Datestamp");
else
sql.OrderBy("Datestamp");
if (query == null) query = new Query<IAuditItem>();
var translatorIds = new SqlTranslator<IAuditItem>(sql, query);
var translatedQuery = translatorIds.Translate();
// Get page of results and total count
var pagedResult = Database.Page<ReadOnlyLogDto>(pageIndex + 1, pageSize, translatedQuery);
totalRecords = pagedResult.TotalItems;
return pagedResult.Items.Select(
dto => new AuditItem(dto.Id, dto.Comment, Enum<AuditType>.Parse(dto.Header), dto.UserId, dto.UserName, dto.UserAvatar)).ToArray();
}
#region Not Implemented - not needed
protected override void PersistUpdatedItem(AuditItem entity)
protected override void PersistUpdatedItem(IAuditItem entity)
{
Database.Insert(new LogDto
{
@@ -35,26 +64,40 @@ namespace Umbraco.Core.Persistence.Repositories
});
}
protected override AuditItem PerformGet(int id)
{
throw new NotImplementedException();
}
protected override IEnumerable<AuditItem> PerformGetAll(params int[] ids)
{
throw new NotImplementedException();
}
protected override IEnumerable<AuditItem> PerformGetByQuery(IQuery<AuditItem> query)
{
throw new NotImplementedException();
}
protected override Sql GetBaseQuery(bool isCount)
{
var sql = new Sql()
.Select(isCount ? "COUNT(*)" : "umbracoLog.id, umbracoLog.userId, umbracoLog.NodeId, umbracoLog.Datestamp, umbracoLog.logHeader, umbracoLog.logComment, umbracoUser.userName, umbracoUser.avatar as userAvatar")
.From<LogDto>(SqlSyntax);
if (isCount == false)
{
sql = sql.LeftJoin<UserDto>(SqlSyntax).On<UserDto, LogDto>(SqlSyntax, dto => dto.Id, dto => dto.UserId);
}
return sql;
}
#region Not Implemented - not needed currently
protected override void PersistNewItem(IAuditItem entity)
{
throw new NotImplementedException();
}
protected override IAuditItem PerformGet(int id)
{
throw new NotImplementedException();
}
protected override IEnumerable<IAuditItem> PerformGetAll(params int[] ids)
{
throw new NotImplementedException();
}
protected override IEnumerable<IAuditItem> PerformGetByQuery(IQuery<IAuditItem> query)
{
throw new NotImplementedException();
}
protected override string GetBaseWhereClause()
{
throw new NotImplementedException();
@@ -70,7 +113,6 @@ namespace Umbraco.Core.Persistence.Repositories
get { throw new NotImplementedException(); }
}
#endregion
}
}

View File

@@ -1,11 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using Umbraco.Core.Auditing;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
namespace Umbraco.Core.Persistence.Repositories
{
public interface IAuditRepository : IRepository<int, AuditItem>
public interface IAuditRepository : IRepository<int, IAuditItem>
{
IEnumerable<IAuditItem> GetPagedResultsByQuery(
IQuery<IAuditItem> query,
long pageIndex, int pageSize, out long totalRecords,
Direction orderDirection, IQuery<IAuditItem> customFilter);
}
}

View File

@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.UnitOfWork;
namespace Umbraco.Core.Services
@@ -22,5 +26,26 @@ namespace Umbraco.Core.Services
uow.Commit();
}
}
public IEnumerable<IAuditItem> GetPagedItems(int id, long pageIndex, int pageSize, out long totalRecords, Direction orderDirection = Direction.Descending, IQuery<IAuditItem> filter = null)
{
Mandate.ParameterCondition(pageIndex >= 0, "pageIndex");
Mandate.ParameterCondition(pageSize > 0, "pageSize");
if (id == Constants.System.Root || id <= 0)
{
totalRecords = 0;
return Enumerable.Empty<IAuditItem>();
}
using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
{
var repository = RepositoryFactory.CreateAuditRepository(uow);
var query = Query<IAuditItem>.Builder.Where(x => x.Id == id);
return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, filter);
}
}
}
}

View File

@@ -1,9 +1,29 @@
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
namespace Umbraco.Core.Services
{
public interface IAuditService : IService
{
void Add(AuditType type, string comment, int userId, int objectId);
/// <summary>
/// Returns paged items in the audit trail
/// </summary>
/// <param name="id"></param>
/// <param name="pageIndex"></param>
/// <param name="pageSize"></param>
/// <param name="totalRecords"></param>
/// <param name="orderDirection">
/// By default this will always be ordered descending (newest first)
/// </param>
/// <param name="filter">
/// Optional filter to be applied
/// </param>
/// <returns></returns>
IEnumerable<IAuditItem> GetPagedItems(int id, long pageIndex, int pageSize, out long totalRecords,
Direction orderDirection = Direction.Descending, IQuery<IAuditItem> filter = null);
}
}

View File

@@ -359,6 +359,7 @@
<Compile Include="IHttpContextAccessor.cs" />
<Compile Include="Models\EntityBase\EntityPath.cs" />
<Compile Include="Models\EntityBase\IDeletableEntity.cs" />
<Compile Include="Models\IAuditItem.cs" />
<Compile Include="Models\IUserControl.cs" />
<Compile Include="Models\Membership\ContentPermissionSet.cs" />
<Compile Include="Models\Membership\EntityPermissionCollection.cs" />
@@ -370,6 +371,7 @@
<Compile Include="Models\Membership\UserState.cs" />
<Compile Include="Models\Membership\UserType.cs" />
<Compile Include="Models\PublishedContent\PublishedContentTypeConverter.cs" />
<Compile Include="Models\Rdbms\ReadOnlyLogDto.cs" />
<Compile Include="Models\Rdbms\UserStartNodeDto.cs" />
<Compile Include="Models\UserControl.cs" />
<Compile Include="OrderedHashSet.cs" />
@@ -530,6 +532,7 @@
<Compile Include="Persistence\LockedRepository.cs" />
<Compile Include="Persistence\LockingRepository.cs" />
<Compile Include="Persistence\Mappers\AccessMapper.cs" />
<Compile Include="Persistence\Mappers\AuditMapper.cs" />
<Compile Include="Persistence\Mappers\DomainMapper.cs" />
<Compile Include="Persistence\Mappers\ExternalLoginMapper.cs" />
<Compile Include="Persistence\Mappers\MigrationEntryMapper.cs" />

View File

@@ -1,24 +1,31 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Models;
namespace Umbraco.Web.Models.ContentEditing
{
[DataContract(Name = "auditLog", Namespace = "")]
public class AuditLog
{
[DataMember(Name = "userId", IsRequired = true)]
[DataMember(Name = "userId")]
public int UserId { get; set; }
[DataMember(Name = "nodeId", IsRequired = true)]
[DataMember(Name = "userName")]
public string UserName { get; set; }
[DataMember(Name = "userAvatar")]
public string UserAvatar { get; set; }
[DataMember(Name = "nodeId")]
public int NodeId { get; set; }
[DataMember(Name = "timestamp", IsRequired = true)]
[DataMember(Name = "timestamp")]
public DateTime Timestamp { get; set; }
[DataMember(Name = "logType", IsRequired = true)]
public AuditLogType LogType { get; set; }
[DataMember(Name = "logType")]
public AuditType LogType { get; set; }
[DataMember(Name = "comment", IsRequired = true)]
[DataMember(Name = "comment")]
public string Comment { get; set; }
}
}

View File

@@ -1,13 +1,14 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// Defines audit trail log types
/// </summary>
[Obsolete("Use Umbraco.Core.Models.AuditType instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
public enum AuditLogType
{
/// <summary>

View File

@@ -13,7 +13,7 @@ namespace Umbraco.Web.Models.Mapping
/// <summary>
/// A model mapper used to map models for the various dashboards
/// </summary>
internal class DashboardModelsMapper : MapperConfiguration
internal class MiscModelsMapper : MapperConfiguration
{
public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext)
{
@@ -24,7 +24,14 @@ namespace Umbraco.Web.Models.Mapping
//for the logging controller (and assuming dashboard that is used in uaas? otherwise not sure what that controller is used for)
config.CreateMap<LogItem, AuditLog>()
.ForMember(log => log.LogType, expression => expression.MapFrom(item => Enum<AuditLogType>.Parse(item.LogType.ToString())));
.ForMember(log => log.UserAvatar, expression => expression.Ignore())
.ForMember(log => log.UserName, expression => expression.Ignore())
.ForMember(log => log.LogType, expression => expression.MapFrom(item => Enum<AuditType>.Parse(item.LogType.ToString())));
config.CreateMap<IAuditItem, AuditLog>()
.ForMember(log => log.NodeId, expression => expression.MapFrom(item => item.Id))
.ForMember(log => log.Timestamp, expression => expression.MapFrom(item => item.CreateDate))
.ForMember(log => log.LogType, expression => expression.MapFrom(item => item.AuditType));
}
}
}

View File

@@ -647,7 +647,7 @@
<Compile Include="Models\ContentEditing\StylesheetRule.cs" />
<Compile Include="Models\ContentEditing\UmbracoEntityTypes.cs" />
<Compile Include="Models\ContentEditing\AuditLogType.cs" />
<Compile Include="Models\Mapping\DashboardModelsMapper.cs" />
<Compile Include="Models\Mapping\MiscModelsMapper.cs" />
<Compile Include="Models\Mapping\MacroModelMapper.cs" />
<Compile Include="Models\Mapping\MemberModelMapper.cs" />
<Compile Include="Models\Mapping\TabModelMapper.cs" />

View File

@@ -56,17 +56,11 @@ namespace umbraco.BusinessLogic
public static Log Instance
{
get { return Singleton<Log>.Instance; }
}
}
#endregion
/// <summary>
/// Adds the specified log item to the log.
/// </summary>
/// <param name="type">The log type.</param>
/// <param name="user">The user adding the item.</param>
/// <param name="nodeId">The affected node id.</param>
/// <param name="comment">Comment.</param>
[Obsolete("Use IAuditService.Add instead")]
public static void Add(LogTypes type, User user, int nodeId, string comment)
{
if (Instance.ExternalLogger != null)
@@ -118,13 +112,7 @@ namespace umbraco.BusinessLogic
}
}
/// <summary>
/// Adds the specified log item to the Umbraco log no matter if an external logger has been defined.
/// </summary>
/// <param name="type">The log type.</param>
/// <param name="user">The user adding the item.</param>
/// <param name="nodeId">The affected node id.</param>
/// <param name="comment">Comment.</param>
[Obsolete("Use IAuditService.Add instead")]
public static void AddLocally(LogTypes type, User user, int nodeId, string comment)
{
if (comment.Length > 3999)
@@ -140,24 +128,13 @@ namespace umbraco.BusinessLogic
AddSynced(type, user == null ? 0 : user.Id, nodeId, comment);
}
/// <summary>
/// Adds the specified log item to the log without any user information attached.
/// </summary>
/// <param name="type">The log type.</param>
/// <param name="nodeId">The affected node id.</param>
/// <param name="comment">Comment.</param>
[Obsolete("Use IAuditService.Add instead")]
public static void Add(LogTypes type, int nodeId, string comment)
{
Add(type, null, nodeId, comment);
}
/// <summary>
/// Adds a log item to the log immidiately instead of Queuing it as a work item.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="userId">The user id.</param>
/// <param name="nodeId">The node id.</param>
/// <param name="comment">The comment.</param>
[Obsolete("Use IAuditService.Add instead")]
public static void AddSynced(LogTypes type, int userId, int nodeId, string comment)
{
var logTypeIsAuditType = type.GetType().GetField(type.ToString()).GetCustomAttributes(typeof(AuditTrailLogItem), true).Length != 0;
@@ -192,7 +169,8 @@ namespace umbraco.BusinessLogic
"Redirected log call (please use Umbraco.Core.Logging.LogHelper instead of umbraco.BusinessLogic.Log) | Type: {0} | User: {1} | NodeId: {2} | Comment: {3}",
() => type.ToString(), () => userId, () => nodeId.ToString(CultureInfo.InvariantCulture), () => comment);
}
[Obsolete("Use IAuditService.GetPagedItems instead")]
public List<LogItem> GetAuditLogItems(int NodeId)
{
if (UmbracoConfig.For.UmbracoSettings().Logging.ExternalLoggerEnableAuditTrail && ExternalLogger != null)
@@ -204,6 +182,7 @@ namespace umbraco.BusinessLogic
sqlHelper.CreateParameter("@id", NodeId)));
}
[Obsolete("Use IAuditService.GetPagedItems instead")]
public List<LogItem> GetLogItems(LogTypes type, DateTime sinceDate)
{
if (ExternalLogger != null)
@@ -216,6 +195,7 @@ namespace umbraco.BusinessLogic
sqlHelper.CreateParameter("@dateStamp", sinceDate)));
}
[Obsolete("Use IAuditService.GetPagedItems instead")]
public List<LogItem> GetLogItems(int nodeId)
{
if (ExternalLogger != null)
@@ -227,6 +207,7 @@ namespace umbraco.BusinessLogic
sqlHelper.CreateParameter("@id", nodeId)));
}
[Obsolete("Use IAuditService.GetPagedItems instead")]
public List<LogItem> GetLogItems(User user, DateTime sinceDate)
{
if (ExternalLogger != null)
@@ -239,6 +220,7 @@ namespace umbraco.BusinessLogic
sqlHelper.CreateParameter("@dateStamp", sinceDate)));
}
[Obsolete("Use IAuditService.GetPagedItems instead")]
public List<LogItem> GetLogItems(User user, LogTypes type, DateTime sinceDate)
{
if (ExternalLogger != null)