From b9ea85d29dcb618398ab22feb1819754c99f3be9 Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Wed, 20 Mar 2013 20:31:44 -0100 Subject: [PATCH] Updating Template to have a lazy master template id. Adding a few new methods to the DataTypeService, which were needed for the import of DataTypes. Adding bulk saving of templates. Adding TopologicalSorter for sorting dependencies. Implementing import of DataTypes and Templates. Renaming the import test fixture for consistency after having creating the PackagingService. --- src/Umbraco.Core/Models/Template.cs | 2 +- .../Persistence/Factories/TemplateFactory.cs | 32 ++-- .../Repositories/TemplateRepository.cs | 14 +- src/Umbraco.Core/Services/DataTypeService.cs | 25 ++- src/Umbraco.Core/Services/FileService.cs | 25 +++ src/Umbraco.Core/Services/IDataTypeService.cs | 7 + src/Umbraco.Core/Services/IFileService.cs | 7 + src/Umbraco.Core/Services/PackagingService.cs | 115 +++++++++++- src/Umbraco.Core/TopologicalSorter.cs | 173 ++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + ...ntImportTests.cs => PackageImportTests.cs} | 25 ++- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- 12 files changed, 399 insertions(+), 29 deletions(-) create mode 100644 src/Umbraco.Core/TopologicalSorter.cs rename src/Umbraco.Tests/Services/Importing/{ContentImportTests.cs => PackageImportTests.cs} (71%) diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index 9621ed5791..fca0d420c8 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -52,7 +52,7 @@ namespace Umbraco.Core.Models internal string NodePath { get; set; } [DataMember] - internal int MasterTemplateId { get; set; } + internal Lazy MasterTemplateId { get; set; } [DataMember] internal string MasterTemplateAlias { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs index b2b1fea52f..c96d5090ea 100644 --- a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs @@ -28,17 +28,22 @@ namespace Umbraco.Core.Persistence.Factories public Template BuildEntity(TemplateDto dto) { - return new Template(string.Empty, dto.NodeDto.Text, dto.Alias) - { - CreateDate = dto.NodeDto.CreateDate, - Id = dto.NodeId, - Key = dto.NodeDto.UniqueId.Value, - CreatorId = dto.NodeDto.UserId.Value, - Level = dto.NodeDto.Level, - ParentId = dto.NodeDto.ParentId, - SortOrder = dto.NodeDto.SortOrder, - NodePath = dto.NodeDto.Path - }; + var template = new Template(string.Empty, dto.NodeDto.Text, dto.Alias) + { + CreateDate = dto.NodeDto.CreateDate, + Id = dto.NodeId, + Key = dto.NodeDto.UniqueId.Value, + CreatorId = dto.NodeDto.UserId.Value, + Level = dto.NodeDto.Level, + ParentId = dto.NodeDto.ParentId, + SortOrder = dto.NodeDto.SortOrder, + NodePath = dto.NodeDto.Path + }; + + if(dto.Master.HasValue) + template.MasterTemplateId = new Lazy(() => dto.Master.Value); + + return template; } public TemplateDto BuildDto(Template entity) @@ -50,6 +55,11 @@ namespace Umbraco.Core.Persistence.Factories NodeDto = BuildNodeDto(entity) }; + if (entity.MasterTemplateId != null && entity.MasterTemplateId.Value != default(int)) + { + dto.Master = entity.MasterTemplateId.Value; + } + if (entity.HasIdentity) { dto.NodeId = entity.Id; diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index a83bdca872..818ab6f147 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -71,7 +71,7 @@ namespace Umbraco.Core.Persistence.Repositories { var masterTemplate = Get(dto.Master.Value); template.MasterTemplateAlias = masterTemplate.Alias; - template.MasterTemplateId = dto.Master.Value; + template.MasterTemplateId = new Lazy(() => dto.Master.Value); } if (_viewsFileSystem.FileExists(csViewName)) @@ -172,11 +172,13 @@ namespace Umbraco.Core.Persistence.Repositories { if (entity.GetTypeOfRenderingEngine() == RenderingEngine.Mvc) { - _viewsFileSystem.AddFile(entity.Name, stream, true); + string viewName = string.Concat(entity.Alias, ".cshtml"); + _viewsFileSystem.AddFile(viewName, stream, true); } else { - _masterpagesFileSystem.AddFile(entity.Name, stream, true); + string masterpageName = string.Concat(entity.Alias, ".master"); + _masterpagesFileSystem.AddFile(masterpageName, stream, true); } } @@ -223,11 +225,13 @@ namespace Umbraco.Core.Persistence.Repositories { if (entity.GetTypeOfRenderingEngine() == RenderingEngine.Mvc) { - _viewsFileSystem.AddFile(entity.Name, stream, true); + string viewName = string.Concat(entity.Alias, ".cshtml"); + _viewsFileSystem.AddFile(viewName, stream, true); } else { - _masterpagesFileSystem.AddFile(entity.Name, stream, true); + string masterpageName = string.Concat(entity.Alias, ".master"); + _masterpagesFileSystem.AddFile(masterpageName, stream, true); } } diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 2142c5ca70..0b22d74070 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -107,11 +107,26 @@ namespace Umbraco.Core.Services /// An enumerable list of string values public IEnumerable GetPreValuesByDataTypeId(int id) { - var uow = _uowProvider.GetUnitOfWork(); - var dtos = uow.Database.Fetch("WHERE datatypeNodeId = @Id", new {Id = id}); - var list = dtos.Select(x => x.Value).ToList(); - uow.Dispose(); - return list; + using (var uow = _uowProvider.GetUnitOfWork()) + { + var dtos = uow.Database.Fetch("WHERE datatypeNodeId = @Id", new {Id = id}); + var list = dtos.Select(x => x.Value).ToList(); + return list; + } + } + + /// + /// Gets a specific PreValue by its Id + /// + /// Id of the PreValue to retrieve the value from + /// PreValue as a string + public string GetPreValueAsString(int id) + { + using (var uow = _uowProvider.GetUnitOfWork()) + { + var dto = uow.Database.FirstOrDefault("WHERE id = @Id", new { Id = id }); + return dto != null ? dto.Value : string.Empty; + } } /// diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index ace5d1d612..53ac74b3a2 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -255,6 +255,31 @@ namespace Umbraco.Core.Services Audit.Add(AuditTypes.Save, string.Format("Save Template performed by user"), userId, template.Id); } + /// + /// Saves a collection of objects + /// + /// List of to save + /// Optional id of the user + public void SaveTemplate(IEnumerable templates, int userId = 0) + { + if (SavingTemplate.IsRaisedEventCancelled(new SaveEventArgs(templates), this)) + return; + + var uow = _dataUowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateTemplateRepository(uow)) + { + foreach (var template in templates) + { + repository.AddOrUpdate(template); + } + uow.Commit(); + + SavedTemplate.RaiseEvent(new SaveEventArgs(templates, false), this); + } + + Audit.Add(AuditTypes.Save, string.Format("Save Template performed by user"), userId, -1); + } + /// /// Deletes a template by its alias /// diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index 4bef5430ae..5fac95a25c 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -89,5 +89,12 @@ namespace Umbraco.Core.Services /// Id of the DataTypeDefinition to save PreValues for /// List of string values to save void SavePreValues(int id, IEnumerable values); + + /// + /// Gets a specific PreValue by its Id + /// + /// Id of the PreValue to retrieve the value from + /// PreValue as a string + string GetPreValueAsString(int id); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index d4ee7fd71e..baf2126b20 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -116,5 +116,12 @@ namespace Umbraco.Core.Services /// to validate /// True if Script is valid, otherwise false bool ValidateTemplate(ITemplate template); + + /// + /// Saves a collection of objects + /// + /// List of to save + /// Optional id of the user + void SaveTemplate(IEnumerable templates, int userId = 0); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index b25ff04d67..571a883b2a 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Xml.Linq; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; @@ -178,6 +180,7 @@ namespace Umbraco.Core.Services { throw new NotImplementedException(); } + #endregion #region ContentTypes @@ -191,13 +194,15 @@ namespace Umbraco.Core.Services public IEnumerable ImportContentTypes(XElement element, int userId = 0) { var name = element.Name.LocalName; - if (name.Equals("DocumentTypes") == false) + if (name.Equals("DocumentTypes") == false && name.Equals("DocumentType")) { - throw new ArgumentException("The passed in XElement is not valid! It does not contain a root element called 'DocumentTypes'."); + throw new ArgumentException("The passed in XElement is not valid! It does not contain a root element called 'DocumentTypes' for multiple imports or 'DocumentType' for a single import."); } _importedContentTypes = new Dictionary(); - var documentTypes = (from doc in element.Elements("DocumentType") select doc).ToList(); + var documentTypes = name.Equals("DocumentTypes") + ? (from doc in element.Elements("DocumentType") select doc).ToList() + : new List {element.Element("DocumentType")}; //NOTE it might be an idea to sort the doctype XElements based on dependencies //before creating the doc types - should also allow for a better structure/inheritance support. foreach (var documentType in documentTypes) @@ -438,6 +443,7 @@ namespace Umbraco.Core.Services return contentType; } } + #endregion #region DataTypes @@ -495,5 +501,108 @@ namespace Umbraco.Core.Services } } #endregion + + #region Dictionary Items + #endregion + + #region Files + #endregion + + #region Languages + #endregion + + #region Macros + #endregion + + #region Media + #endregion + + #region MediaTypes + #endregion + + #region Package Manifest + #endregion + + #region Templates + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional user id + /// An enumrable list of generated Templates + public IEnumerable ImportTemplates(XElement element, int userId = 0) + { + var name = element.Name.LocalName; + if (name.Equals("Templates") == false && name.Equals("Template")) + { + throw new ArgumentException("The passed in XElement is not valid! It does not contain a root element called 'Templates' for multiple imports or 'Template' for a single import."); + } + + var templates = new List(); + var templateElements = name.Equals("Templates") + ? (from doc in element.Elements("Template") select doc).ToList() + : new List { element.Element("Template") }; + + var fields = new List>(); + foreach (XElement tempElement in templateElements) + { + var dependencies = new List(); + if(tempElement.Element("Master") != null && string.IsNullOrEmpty(tempElement.Element("Master").Value) == false) + dependencies.Add(tempElement.Element("Master").Value); + + var field = new TopologicalSorter.DependencyField + { + Alias = tempElement.Element("Alias").Value, + Item = new Lazy(() => tempElement), + DependsOn = dependencies.ToArray() + }; + + fields.Add(field); + } + //Sort templates by dependencies to a potential master template + var sortedElements = TopologicalSorter.RetrieveMappedItems(fields); + foreach (var templateElement in sortedElements) + { + var templateName = templateElement.Element("Name").Value; + var alias = templateElement.Element("Alias").Value; + var design = templateElement.Element("Design").Value; + var masterElement = templateElement.Element("Master"); + + var isMasterPage = IsMasterPageSyntax(design); + var path = isMasterPage ? MasterpagePath(alias) : ViewPath(alias); + + var template = new Template(path, templateName, alias) { Content = design }; + if (masterElement != null && string.IsNullOrEmpty(masterElement.Value) == false) + { + template.MasterTemplateAlias = masterElement.Value; + var masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); + if(masterTemplate != null) + template.MasterTemplateId = new Lazy(() => masterTemplate.Id); + } + templates.Add(template); + } + + _fileService.SaveTemplate(templates, userId); + return templates; + } + + private bool IsMasterPageSyntax(string code) + { + return Regex.IsMatch(code, @"<%@\s*Master", RegexOptions.IgnoreCase) || + code.InvariantContains(" 0) // while vertices remain, + { + // get a vertex with no successors, or -1 + int currentVertex = NoSuccessors(); + if (currentVertex == -1) // must be a cycle + throw new Exception("Graph has cycles"); + + // insert vertex label in sorted array (start at end) + _sortedArray[_numVerts - 1] = _vertices[currentVertex]; + + DeleteVertex(currentVertex); // delete vertex + } + + // vertices all gone; return sortedArray + return _sortedArray; + } + + #endregion + + #region Private Helper Methods + + // returns vert with no successors (or -1 if no such verts) + private int NoSuccessors() + { + for (int row = 0; row < _numVerts; row++) + { + bool isEdge = false; // edge from row to column in adjMat + for (int col = 0; col < _numVerts; col++) + { + if (_matrix[row, col] > 0) // if edge to another, + { + isEdge = true; + break; // this vertex has a successor try another + } + } + if (!isEdge) // if no edges, has no successors + return row; + } + return -1; // no + } + + private void DeleteVertex(int delVert) + { + // if not last vertex, delete from vertexList + if (delVert != _numVerts - 1) + { + for (int j = delVert; j < _numVerts - 1; j++) + _vertices[j] = _vertices[j + 1]; + + for (int row = delVert; row < _numVerts - 1; row++) + MoveRowUp(row, _numVerts); + + for (int col = delVert; col < _numVerts - 1; col++) + MoveColLeft(col, _numVerts - 1); + } + _numVerts--; // one less vertex + } + + private void MoveRowUp(int row, int length) + { + for (int col = 0; col < length; col++) + _matrix[row, col] = _matrix[row + 1, col]; + } + + private void MoveColLeft(int col, int length) + { + for (int row = 0; row < length; row++) + _matrix[row, col] = _matrix[row, col + 1]; + } + + #endregion + + #region Internal Staic methods + internal static IEnumerable RetrieveMappedItems(List> fields) where T : class + { + int[] sortOrder = GetTopologicalSortOrder(fields); + var list = new List(); + for (int i = 0; i < sortOrder.Length; i++) + { + var field = fields[sortOrder[i]]; + list.Add(field.Item.Value); + } + list.Reverse(); + return list; + } + + internal static int[] GetTopologicalSortOrder(List> fields) where T : class + { + var g = new TopologicalSorter(fields.Count()); + var indexes = new Dictionary(); + + //add vertices + for (int i = 0; i < fields.Count(); i++) + { + indexes[fields[i].Alias.ToLower()] = g.AddVertex(i); + } + + //add edges + for (int i = 0; i < fields.Count; i++) + { + if (fields[i].DependsOn != null) + { + for (int j = 0; j < fields[i].DependsOn.Length; j++) + { + g.AddEdge(i, + indexes[fields[i].DependsOn[j].ToLower()]); + } + } + } + + int[] result = g.Sort(); + return result; + } + #endregion + + internal class DependencyField where T : class + { + public DependencyField() + { + } + + public DependencyField(string @alias, string[] dependsOn, Lazy item) + { + Alias = alias; + DependsOn = dependsOn; + Item = item; + } + + public string Alias { get; set; } + public string[] DependsOn { get; set; } + public Lazy Item { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 52951c8642..11fd35fb97 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -657,6 +657,7 @@ + diff --git a/src/Umbraco.Tests/Services/Importing/ContentImportTests.cs b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs similarity index 71% rename from src/Umbraco.Tests/Services/Importing/ContentImportTests.cs rename to src/Umbraco.Tests/Services/Importing/PackageImportTests.cs index 590d221bc9..564bfc6d3e 100644 --- a/src/Umbraco.Tests/Services/Importing/ContentImportTests.cs +++ b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs @@ -5,7 +5,7 @@ using NUnit.Framework; namespace Umbraco.Tests.Services.Importing { [TestFixture, RequiresSTA] - public class ContentImportTests : BaseServiceTest + public class PackageImportTests : BaseServiceTest { [SetUp] public override void Initialize() @@ -20,7 +20,26 @@ namespace Umbraco.Tests.Services.Importing } [Test] - public void ContentTypeService_Can_Import_Package_Xml() + public void PackagingService_Can_Import_Template_Package_Xml() + { + // Arrange + string strXml = ImportResources.package; + var xml = XElement.Parse(strXml); + var element = xml.Descendants("Templates").First(); + var packagingService = ServiceContext.PackagingService; + + // Act + var templates = packagingService.ImportTemplates(element); + var numberOfTemplates = (from doc in element.Elements("Template") select doc).Count(); + + // Assert + Assert.That(templates, Is.Not.Null); + Assert.That(templates.Any(), Is.True); + Assert.That(templates.Count(), Is.EqualTo(numberOfTemplates)); + } + + [Test] + public void PackagingService_Can_Import_ContentType_Package_Xml() { // Arrange string strXml = ImportResources.package; @@ -44,7 +63,7 @@ namespace Umbraco.Tests.Services.Importing } [Test] - public void ContentService_Can_Import_Package_Xml() + public void PackagingService_Can_Import_Content_Package_Xml() { // Arrange string strXml = ImportResources.package; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 1c1c41e517..d42783d63e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -262,7 +262,7 @@ - + True True