diff --git a/src/Umbraco.Core/Auditing/Audit.cs b/src/Umbraco.Core/Auditing/Audit.cs index 92ea763461..fe1ec955b8 100644 --- a/src/Umbraco.Core/Auditing/Audit.cs +++ b/src/Umbraco.Core/Auditing/Audit.cs @@ -1,7 +1,10 @@ namespace Umbraco.Core.Auditing { - public class Audit + public static class Audit { - public IAuditWriteProvider WriteProvider { get; set; } + public static void Add(AuditTypes type, string comment, int userId, int objectId) + { + AuditTrail.Current.AddEntry(type, comment, userId, objectId); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Auditing/AuditTrail.cs b/src/Umbraco.Core/Auditing/AuditTrail.cs new file mode 100644 index 0000000000..321ecb40aa --- /dev/null +++ b/src/Umbraco.Core/Auditing/AuditTrail.cs @@ -0,0 +1,30 @@ +using System; + +namespace Umbraco.Core.Auditing +{ + /// + /// Represents the Audit implementation + /// + public class AuditTrail + { + #region Singleton + + private static readonly Lazy lazy = new Lazy(() => new AuditTrail()); + + public static AuditTrail Current { get { return lazy.Value; } } + + private AuditTrail() + { + WriteProvider = new DataAuditWriteProvider(); + } + + #endregion + + private IAuditWriteProvider WriteProvider { get; set; } + + public void AddEntry(AuditTypes type, string comment, int userId, int objectId) + { + WriteProvider.WriteEntry(objectId, userId, DateTime.UtcNow, type.ToString(), comment); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Auditing/AuditTypes.cs b/src/Umbraco.Core/Auditing/AuditTypes.cs new file mode 100644 index 0000000000..4407c698b3 --- /dev/null +++ b/src/Umbraco.Core/Auditing/AuditTypes.cs @@ -0,0 +1,85 @@ +namespace Umbraco.Core.Auditing +{ + /// + /// Enums for vailable types of auditing + /// + public enum AuditTypes + { + /// + /// Used when new nodes are added + /// + New, + /// + /// Used when nodes are saved + /// + Save, + /// + /// Used when nodes are opened + /// + Open, + /// + /// Used when nodes are deleted + /// + Delete, + /// + /// Used when nodes are published + /// + Publish, + /// + /// Used when nodes are send to publishing + /// + SendToPublish, + /// + /// Used when nodes are unpublished + /// + UnPublish, + /// + /// Used when nodes are moved + /// + Move, + /// + /// Used when nodes are copied + /// + Copy, + /// + /// Used when nodes are assígned a domain + /// + AssignDomain, + /// + /// Used when public access are changed for a node + /// + PublicAccess, + /// + /// Used when nodes are sorted + /// + Sort, + /// + /// Used when a notification are send to a user + /// + Notify, + /// + /// General system notification + /// + System, + /// + /// Used when a node's content is rolled back to a previous version + /// + RollBack, + /// + /// Used when a package is installed + /// + PackagerInstall, + /// + /// Used when a package is uninstalled + /// + PackagerUninstall, + /// + /// Used when a node is send to translation + /// + SendToTranslate, + /// + /// Use this log action for custom log messages that should be shown in the audit trail + /// + Custom + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Auditing/DataAuditWriteProvider.cs b/src/Umbraco.Core/Auditing/DataAuditWriteProvider.cs new file mode 100644 index 0000000000..e29f6e93b8 --- /dev/null +++ b/src/Umbraco.Core/Auditing/DataAuditWriteProvider.cs @@ -0,0 +1,29 @@ +using System; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Auditing +{ + public class DataAuditWriteProvider : IAuditWriteProvider + { + /// + /// Writes an audit entry to the underlaying datastore. + /// + /// Id of the object (Content, ContentType, Media, etc.) + /// Id of the user + /// Datestamp + /// Audit header + /// Audit comment + public void WriteEntry(int objectId, int userId, DateTime date, string header, string comment) + { + DatabaseFactory.Current.Database.Insert(new LogDto + { + Comment = comment, + Datestamp = date, + Header = header, + NodeId = objectId, + UserId = userId + }); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs b/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs index 84150ad66f..ab4abceb7f 100644 --- a/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs +++ b/src/Umbraco.Core/Auditing/IAuditWriteProvider.cs @@ -1,7 +1,17 @@ -namespace Umbraco.Core.Auditing +using System; + +namespace Umbraco.Core.Auditing { public interface IAuditWriteProvider { - + /// + /// Writes an audit entry to the underlaying datastore. + /// + /// Id of the object (Content, ContentType, Media, etc.) + /// Id of the user + /// Datestamp + /// Audit header + /// Audit comment + void WriteEntry(int objectId, int userId, DateTime date, string header, string comment); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 118cd5f538..308c803f73 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Models { @@ -16,7 +15,7 @@ namespace Umbraco.Core.Models public abstract class ContentBase : Entity, IContentBase { protected IContentTypeComposition ContentTypeBase; - private int _parentId; + private Lazy _parentId; private string _name; private int _sortOrder; private int _level; @@ -28,11 +27,13 @@ namespace Umbraco.Core.Models protected ContentBase(int parentId, IContentTypeComposition contentType, PropertyCollection properties) { - Mandate.ParameterCondition(parentId != 0, "parentId"); + //Mandate.ParameterCondition(parentId != 0, "parentId"); Mandate.ParameterNotNull(contentType, "contentType"); Mandate.ParameterNotNull(properties, "properties"); - _parentId = parentId; + //_parentId = parentId; + _parentId = new Lazy(() => parentId); + _contentTypeId = int.Parse(contentType.Id.ToString(CultureInfo.InvariantCulture)); ContentTypeBase = contentType; _properties = properties; @@ -62,10 +63,10 @@ namespace Umbraco.Core.Models [DataMember] public virtual int ParentId { - get { return _parentId; } + get { return _parentId.Value; } set { - _parentId = value; + _parentId = new Lazy(() => value); OnPropertyChanged(ParentIdSelector); } } diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index 17d320a9d6..f492518661 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Models.EntityBase { private bool _hasIdentity; private int? _hash; - private int _id; + private Lazy _id; private Guid _key; /// @@ -27,15 +27,15 @@ namespace Umbraco.Core.Models.EntityBase { get { - return _id; + return _id == null ? default(int) : _id.Value; } set { - _id = value; + _id = new Lazy(() => value); HasIdentity = true; } } - + /// /// Guid based Id /// @@ -47,7 +47,7 @@ namespace Umbraco.Core.Models.EntityBase get { if (_key == Guid.Empty) - return Id.ToGuid(); + return _id.Value.ToGuid(); return _key; } @@ -88,7 +88,7 @@ namespace Umbraco.Core.Models.EntityBase protected void ResetIdentity() { _hasIdentity = false; - _id = 0; + _id = new Lazy(() => default(int)); } /// diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 384aa325d8..48fe6019fb 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Models { diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index a5d1b48bac..ca8fa099fe 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -429,19 +429,57 @@ namespace Umbraco.Core.Services } /// - /// Saves a collection of objects + /// Saves a collection of objects. /// + /// + /// If the collection of content contains new objects that references eachother by Id or ParentId, + /// then use the overload Save method with a collection of Lazy . + /// /// Collection of to save /// Optional Id of the User saving the Content public void Save(IEnumerable contents, int userId = -1) + { + var repository = RepositoryResolver.ResolveByType(_unitOfWork); + var containsNew = contents.Any(x => x.HasIdentity == false); + + if (containsNew) + { + foreach (var content in contents) + { + SetWriter(content, userId); + repository.AddOrUpdate(content); + _unitOfWork.Commit(); + } + } + else + { + foreach (var content in contents) + { + SetWriter(content, userId); + repository.AddOrUpdate(content); + } + _unitOfWork.Commit(); + } + } + + /// + /// Saves a collection of lazy loaded objects. + /// + /// + /// This method ensures that Content is saved lazily, so a new graph of + /// objects can be saved in bulk. But not that objects are saved one at a time to ensure Ids. + /// + /// Collection of Lazy to save + /// Optional Id of the User saving the Content + public void Save(IEnumerable> contents, int userId = -1) { var repository = RepositoryResolver.ResolveByType(_unitOfWork); foreach (var content in contents) { - SetWriter(content, userId); - repository.AddOrUpdate(content); + SetWriter(content.Value, userId); + repository.AddOrUpdate(content.Value); + _unitOfWork.Commit(); } - _unitOfWork.Commit(); } /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 9e64851fa8..9549bde49b 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -130,12 +130,27 @@ namespace Umbraco.Core.Services void Save(IContent content, int userId = -1); /// - /// Saves a collection of objects + /// Saves a collection of objects. /// + /// + /// If the collection of content contains new objects that references eachother by Id or ParentId, + /// then use the overload Save method with a collection of Lazy . + /// /// Collection of to save /// Optional Id of the User saving the Content void Save(IEnumerable contents, int userId = -1); + /// + /// Saves a collection of lazy loaded objects. + /// + /// + /// This method ensures that Content is saved lazily, so a new graph of + /// objects can be saved in bulk. But not that objects are saved one at a time to ensure Ids. + /// + /// Collection of Lazy to save + /// Optional Id of the User saving the Content + void Save(IEnumerable> contents, int userId = -1); + /// /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e0d39c12d6..5f4bde8b72 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -60,7 +60,10 @@ + + + diff --git a/src/Umbraco.Tests/Auditing/AuditTests.cs b/src/Umbraco.Tests/Auditing/AuditTests.cs new file mode 100644 index 0000000000..ff5e6bb164 --- /dev/null +++ b/src/Umbraco.Tests/Auditing/AuditTests.cs @@ -0,0 +1,35 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Auditing; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Auditing +{ + [TestFixture] + public class AuditTests : BaseDatabaseFactoryTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } + + [Test] + public void Can_Add_Audit_Entry() + { + Audit.Add(AuditTypes.System, "This is a System audit trail", 0, -1); + + var dtos = DatabaseContext.Database.Fetch("WHERE id > -1"); + + Assert.That(dtos.Any(), Is.True); + Assert.That(dtos.First().Comment, Is.EqualTo("This is a System audit trail")); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 502b65da7f..48d6630488 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -4,6 +4,9 @@ using System.Linq; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -466,6 +469,20 @@ namespace Umbraco.Tests.Services Assert.That(list.Any(x => !x.HasIdentity), Is.False); } + [Test] + public void Can_Bulk_Save_New_Hierarchy_Content() + { + // Arrange + var contentService = ServiceContext.ContentService; + var hierarchy = CreateContentHierarchy(); + + // Act + contentService.Save(hierarchy, 0); + + Assert.That(hierarchy.Any(), Is.True); + Assert.That(hierarchy.Any(x => x.Value.HasIdentity == false), Is.False); + } + [Test] public void Can_Delete_Content_Of_Specific_ContentType() { @@ -561,6 +578,7 @@ namespace Umbraco.Tests.Services Assert.AreNotEqual(content.Name, copy.Name); } + [Test, NUnit.Framework.Ignore] public void Can_Send_To_Publication() { } @@ -585,6 +603,34 @@ namespace Umbraco.Tests.Services Assert.AreEqual(subpage2.Name, rollback.Name); } + [Test] + public void Can_Save_Lazy_Content() + { + var unitOfWork = new PetaPocoUnitOfWork(); + var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage"); + var root = ServiceContext.ContentService.GetById(1046); + + var c = new Lazy(() => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Page", root.Id)); + var c2 = new Lazy(() => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Subpage", c.Value.Id)); + var list = new List> {c, c2}; + + var repository = RepositoryResolver.ResolveByType(unitOfWork); + foreach (var content in list) + { + repository.AddOrUpdate(content.Value); + unitOfWork.Commit(); + } + + Assert.That(c.Value.HasIdentity, Is.True); + Assert.That(c2.Value.HasIdentity, Is.True); + + Assert.That(c.Value.Id > 0, Is.True); + Assert.That(c2.Value.Id > 0, Is.True); + + Assert.That(c.Value.ParentId > 0, Is.True); + Assert.That(c2.Value.ParentId > 0, Is.True); + } + [TearDown] public override void TearDown() { @@ -620,5 +666,38 @@ namespace Umbraco.Tests.Services trashed.Trashed = true; ServiceContext.ContentService.Save(trashed, 0); } + + private IEnumerable> CreateContentHierarchy() + { + var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage"); + var root = ServiceContext.ContentService.GetById(1046); + + var list = new List>(); + + for (int i = 0; i < 10; i++) + { + var content = new Lazy( + () => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Page " + i, root.Id)); + list.Add(content); + list.AddRange(CreateChildrenOf(contentType, content, 4)); + + Console.WriteLine("Created: 'Hierarchy Simple Text Page {0}'", i); + } + + return list; + } + + private IEnumerable> CreateChildrenOf(IContentType contentType, Lazy content, int depth) + { + var list = new List>(); + for (int i = 0; i < depth; i++) + { + var c = new Lazy(() => MockedContent.CreateSimpleContent(contentType, "Hierarchy Simple Text Subpage " + i, content.Value.Id)); + list.Add(c); + + Console.WriteLine("Created: 'Hierarchy Simple Text Subpage {0}' - Depth: {1}", i, depth); + } + return list; + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs index 7ced5269a3..79ce23d63b 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; namespace Umbraco.Tests.TestHelpers.Entities { @@ -8,7 +8,7 @@ namespace Umbraco.Tests.TestHelpers.Entities { public static Content CreateSimpleContent(IContentType contentType) { - var content = new Content(-1, contentType) { Name = "Home", Language = "en-US", Level = 1, ParentId = -1, SortOrder = 1, CreatorId = 0, WriterId = 0 }; + var content = new Content(-1, contentType) { Name = "Home", Language = "en-US", Level = 1, SortOrder = 1, CreatorId = 0, WriterId = 0 }; object obj = new { @@ -24,7 +24,7 @@ namespace Umbraco.Tests.TestHelpers.Entities public static Content CreateSimpleContent(IContentType contentType, string name, int parentId) { - var content = new Content(parentId, contentType) { Name = name, Language = "en-US", ParentId = parentId, CreatorId = 0, WriterId = 0 }; + var content = new Content(parentId, contentType) { Name = name, Language = "en-US", CreatorId = 0, WriterId = 0 }; object obj = new { @@ -40,7 +40,7 @@ namespace Umbraco.Tests.TestHelpers.Entities public static Content CreateTextpageContent(IContentType contentType, string name, int parentId) { - var content = new Content(parentId, contentType) { Name = name, Language = "en-US", ParentId = parentId, CreatorId = 0, WriterId = 0}; + var content = new Content(parentId, contentType) { Name = name, Language = "en-US", CreatorId = 0, WriterId = 0}; object obj = new { @@ -62,7 +62,7 @@ namespace Umbraco.Tests.TestHelpers.Entities for (int i = 0; i < amount; i++) { var name = "Textpage No-" + i; - var content = new Content(parentId, contentType) { Name = name, Language = "en-US", ParentId = parentId, CreatorId = 0, WriterId = 0 }; + var content = new Content(parentId, contentType) { Name = name, Language = "en-US", CreatorId = 0, WriterId = 0 }; object obj = new { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c828581d34..a59f45a0d3 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -83,6 +83,7 @@ +