diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index e591b5dfcb..c12ac783cc 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.6.3 \ No newline at end of file +7.6.4 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index d7e4c9a204..fe3be21eb7 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.6.3")] -[assembly: AssemblyInformationalVersion("7.6.3")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.6.4")] +[assembly: AssemblyInformationalVersion("7.6.4")] \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index fd9476a8ab..d98c12aeb1 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -56,7 +56,12 @@ /// /// alias for the content tree. /// - public const string Content = "content"; + public const string Content = "content"; + + /// + /// alias for the content blueprint tree. + /// + public const string ContentBlueprints = "contentBlueprints"; /// /// alias for the member tree. diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index adc154174a..bb12ec9efd 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -68,12 +68,22 @@ namespace Umbraco.Core /// Guid for a Document object. /// public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; - + /// /// Guid for a Document object. /// public static readonly Guid DocumentGuid = new Guid(Document); + /// + /// Guid for a Document Blueprint object. + /// + public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; + + /// + /// Guid for a Document object. + /// + public static readonly Guid DocumentBlueprintGuid = new Guid(DocumentBlueprint); + /// /// Guid for a Document Type object. /// diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 0df7a21e57..6aeab13f3b 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -38,6 +38,14 @@ namespace Umbraco.Core.Models [UmbracoUdiType(Constants.UdiEntityType.Document)] Document, + /// + /// Document + /// + [UmbracoObjectType(Constants.ObjectTypes.DocumentBlueprint, typeof(IContent))] + [FriendlyName("DocumentBlueprint")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentBluePrint)] + DocumentBlueprint, + /// /// Media /// diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 002af11611..2e32adcb54 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -170,9 +170,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateUmbracoUserTypeData() { - _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 1, Alias = "admin", Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7" }); + _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 1, Alias = "admin", Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ï" }); _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 2, Alias = "writer", Name = "Writer", DefaultPermissions = "CAH:F" }); - _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5F" }); + _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 3, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5Fï" }); _database.Insert("umbracoUserType", "id", false, new UserTypeDto { Id = 4, Alias = "translator", Name = "Translator", DefaultPermissions = "AF" }); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index a34a77d3b4..dee667675b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -20,6 +20,28 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { + /// + /// Override the base content repository so we can change the node object type + /// + /// + /// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, however to do that + /// requires quite a lot of work since we'd need to re-organize the interhitance quite a lot or create a helper class to perform a lot of the underlying logic. + /// + /// TODO: Create a helper method to conain most of the underlying logic for the ContentRepository + /// + internal class ContentBlueprintRepository : ContentRepository + { + public ContentBlueprintRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) : base(work, cacheHelper, logger, syntaxProvider, contentTypeRepository, templateRepository, tagRepository, contentSection) + { + } + + protected override Guid NodeObjectTypeId + { + get { return Constants.ObjectTypes.DocumentBlueprintGuid; } + } + + } + /// /// Represents a repository for doing CRUD operations for /// @@ -201,7 +223,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Guid NodeObjectTypeId { - get { return new Guid(Constants.ObjectTypes.Document); } + get { return Constants.ObjectTypes.DocumentGuid; } } #endregion @@ -267,7 +289,7 @@ namespace Umbraco.Core.Persistence.Repositories //now delete the items that shouldn't be there var sqlAllIds = translate(0, GetBaseQuery(BaseQueryType.Ids)); var allContentIds = Database.Fetch(sqlAllIds); - var docObjectType = Guid.Parse(Constants.ObjectTypes.Document); + var docObjectType = NodeObjectTypeId; var xmlIdsQuery = new Sql() .Select("DISTINCT cmsContentXml.nodeId") .From(SqlSyntax) @@ -742,7 +764,19 @@ namespace Umbraco.Core.Persistence.Repositories return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); } - + + public IEnumerable GetBlueprints(IQuery query) + { + Func, Sql> translate = t => t.Translate(); + + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); + } + /// /// This builds the Xml document used for the XML cache /// @@ -786,7 +820,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", XmlElement last = null; //NOTE: Query creates a reader - does not load all into memory - foreach (var row in Database.Query(sql, new { type = new Guid(Constants.ObjectTypes.Document) })) + foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) { string parentId = ((int)row.parentID).ToInvariantString(); string xml = row.xml; diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 985f9446b7..96641ba715 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -110,6 +110,19 @@ namespace Umbraco.Core.Persistence.Repositories return Database.Fetch(sql); } + public IEnumerable GetAllContentTypeIds(string[] aliases) + { + if (aliases.Length == 0) return Enumerable.Empty(); + + var sql = new Sql().Select("cmsContentType.nodeId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => aliases.Contains(dto.Alias), SqlSyntax); + + return Database.Fetch(sql); + } + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 4efa8a63ac..4e6bf6ee70 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -48,8 +48,8 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, IQuery filter = null) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var factory = new UmbracoEntityFactory(); var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, sql => @@ -178,8 +178,8 @@ namespace Umbraco.Core.Persistence.Repositories public IUmbracoEntity GetByKey(Guid key, Guid objectTypeId) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sql = GetFullSqlForEntityType(key, isContent, isMedia, objectTypeId); @@ -225,8 +225,8 @@ namespace Umbraco.Core.Persistence.Repositories public virtual IUmbracoEntity Get(int id, Guid objectTypeId) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sql = GetFullSqlForEntityType(id, isContent, isMedia, objectTypeId); @@ -280,8 +280,8 @@ namespace Umbraco.Core.Persistence.Repositories private IEnumerable PerformGetAll(Guid objectTypeId, Action filter = null) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, filter); var factory = new UmbracoEntityFactory(); @@ -324,8 +324,8 @@ namespace Umbraco.Core.Persistence.Repositories public virtual IEnumerable GetByQuery(IQuery query, Guid objectTypeId) { - bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); - bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); + bool isContent = objectTypeId == Constants.ObjectTypes.DocumentGuid || objectTypeId == Constants.ObjectTypes.DocumentBlueprintGuid; + bool isMedia = objectTypeId == Constants.ObjectTypes.MediaGuid; var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, null, objectTypeId); diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index e04608e9c6..00e0868ef0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories -{ +{ public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository { diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index f118be3b76..5cb3faebbb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -47,5 +47,7 @@ namespace Umbraco.Core.Persistence.Repositories /// The original alias with a number appended to it, so that it is unique. /// /// Unique accross all content, media and member types. string GetUniqueAlias(string alias); + + IEnumerable GetAllContentTypeIds(string[] aliases); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 98743a6a63..96db4428d0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -52,25 +52,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Id of the to retrieve versions from /// An enumerable list of the same object with different versions - public virtual IEnumerable GetAllVersions(int id) - { - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.NodeId == id) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dtos = Database.Fetch(sql); - foreach (var dto in dtos) - { - yield return GetByVersion(dto.VersionId); - } - } + public abstract IEnumerable GetAllVersions(int id); /// /// Gets a list of all version Ids for the given content item ordered so latest is first diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index afb93f620f..114d3ee95f 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -145,6 +145,23 @@ namespace Umbraco.Core.Persistence }; } + public virtual IContentRepository CreateContentBlueprintRepository(IScopeUnitOfWork uow) + { + return new ContentBlueprintRepository( + uow, + _cacheHelper, + _logger, + _sqlSyntax, + CreateContentTypeRepository(uow), + CreateTemplateRepository(uow), + CreateTagRepository(uow), + _settings.Content) + { + //duplicates are allowed + EnsureUniqueNaming = false + }; + } + public virtual IContentTypeRepository CreateContentTypeRepository(IScopeUnitOfWork uow) { return new ContentTypeRepository( diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index c00b226d3e..4f0d101379 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -174,8 +174,7 @@ namespace Umbraco.Core.Services content.WriterId = userId; uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId)); - - Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); + uow.Commit(); } @@ -217,8 +216,7 @@ namespace Umbraco.Core.Services content.WriterId = userId; uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parent)); - - Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); + uow.Commit(); } @@ -1155,6 +1153,82 @@ namespace Umbraco.Core.Services return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); } + public IContent GetBlueprintById(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + return repository.Get(id); + } + } + + public void SaveBlueprint(IContent content, int userId = 0) + { + //always ensure the blueprint is at the root + content.ParentId = -1; + + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content blueprint with empty name."); + } + + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + repository.AddOrUpdate(content); + + uow.Commit(); + } + } + } + + public void DeleteBlueprint(IContent content, int userId = 0) + { + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + repository.Delete(content); + uow.Commit(); + } + } + } + + public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) + { + if (blueprint == null) throw new ArgumentNullException("blueprint"); + + var contentType = blueprint.ContentType; + var content = new Content(name, -1, contentType); + content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id); + + using (var uow = UowProvider.GetUnitOfWork()) + { + content.CreatorId = userId; + content.WriterId = userId; + + //Now we need to map all of the properties over! + foreach (var property in blueprint.Properties) + { + content.SetValue(property.Alias, property.Value); + } + + uow.Commit(); + } + + return content; + } + /// /// Saves a single object /// @@ -1787,6 +1861,21 @@ namespace Umbraco.Core.Services return true; } + public IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + + var query = new Query(); + if (documentTypeIds.Length > 0) + { + query.Where(x => documentTypeIds.Contains(x.ContentTypeId)); + } + return repository.GetByQuery(query); + } + } + /// /// Gets paged content descendants as XML by path /// diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 1b6735153d..895d97f658 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -367,6 +367,15 @@ namespace Umbraco.Core.Services } } + public IEnumerable GetAllContentTypeIds(string[] aliases) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + return repository.GetAllContentTypeIds(aliases); + } + } + /// /// Copies a content type as a child under the specified parent if specified (otherwise to the root) /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 4af87d2b58..b5416e750f 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -16,10 +16,10 @@ namespace Umbraco.Core.Services /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. /// public interface IContentServiceOperations - { - //TODO: Remove this class in v8 - - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + { + //TODO: Remove this class in v8 + + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... /// /// Saves a single object @@ -96,6 +96,12 @@ namespace Umbraco.Core.Services /// public interface IContentService : IContentServiceBase { + IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds); + IContent GetBlueprintById(int id); + void SaveBlueprint(IContent content, int userId = 0); + void DeleteBlueprint(IContent content, int userId = 0); + IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0); + /// /// Gets all XML entries found in the cmsContentXml table based on the given path /// diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 46ee8520c6..f470bbce05 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -61,6 +61,13 @@ namespace Umbraco.Core.Services /// /// IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); + + /// + /// Returns all content type Ids for the aliases given + /// + /// + /// + IEnumerable GetAllContentTypeIds(string[] aliases); /// /// Copies a content type as a child under the specified parent if specified (otherwise to the root) diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index f6b9b1e3b0..926489f02d 100644 --- a/src/Umbraco.Core/UdiEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -24,6 +24,10 @@ namespace Umbraco.Core [UdiType(UdiType.GuidUdi)] public const string Document = "document"; + + [UdiType(UdiType.GuidUdi)] + public const string DocumentBluePrint = "document-blueprint"; + [UdiType(UdiType.GuidUdi)] public const string Media = "media"; [UdiType(UdiType.GuidUdi)] diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 333970bf67..aa30c4415a 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -301,7 +301,7 @@ AnotherContentFinder public void Resolves_Actions() { var actions = _manager.ResolveActions(); - Assert.AreEqual(37, actions.Count()); + Assert.AreEqual(38, actions.Count()); } [Test] diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index c598b272d7..d521219918 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -46,11 +46,112 @@ namespace Umbraco.Tests.Services VersionableRepositoryBase.ThrowOnWarning = false; base.TearDown(); - } - - //TODO Add test to verify there is only ONE newest document/content in cmsDocument table after updating. + } + + //TODO Add test to verify there is only ONE newest document/content in cmsDocument table after updating. //TODO Add test to delete specific version (with and without deleting prior versions) and versions by date. + [Test] + public void Create_Blueprint() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentTypeService.Save(contentType); + + var blueprint = MockedContent.CreateTextpageContent(contentType, "hello", -1); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + contentService.SaveBlueprint(blueprint); + + var found = contentService.GetBlueprintsForContentTypes().ToArray(); + Assert.AreEqual(1, found.Length); + + //ensures it's not found by normal content + var contentFound = contentService.GetById(found[0].Id); + Assert.IsNull(contentFound); + } + + [Test] + public void Delete_Blueprint() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentTypeService.Save(contentType); + + var blueprint = MockedContent.CreateTextpageContent(contentType, "hello", -1); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + contentService.SaveBlueprint(blueprint); + + contentService.DeleteBlueprint(blueprint); + + var found = contentService.GetBlueprintsForContentTypes().ToArray(); + Assert.AreEqual(0, found.Length); + } + + [Test] + public void Create_Content_From_Blueprint() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentTypeService.Save(contentType); + + var blueprint = MockedContent.CreateTextpageContent(contentType, "hello", -1); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + contentService.SaveBlueprint(blueprint); + + var fromBlueprint = contentService.CreateContentFromBlueprint(blueprint, "hello world"); + contentService.Save(fromBlueprint); + + Assert.IsTrue(fromBlueprint.HasIdentity); + Assert.AreEqual("blueprint 1", fromBlueprint.Properties["title"].Value); + Assert.AreEqual("blueprint 2", fromBlueprint.Properties["bodyText"].Value); + Assert.AreEqual("blueprint 3", fromBlueprint.Properties["keywords"].Value); + Assert.AreEqual("blueprint 4", fromBlueprint.Properties["description"].Value); + } + + [Test] + public void Get_All_Blueprints() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + + var ct1 = MockedContentTypes.CreateTextpageContentType("ct1"); + contentTypeService.Save(ct1); + var ct2 = MockedContentTypes.CreateTextpageContentType("ct2"); + contentTypeService.Save(ct2); + + for (int i = 0; i < 10; i++) + { + var blueprint = MockedContent.CreateTextpageContent(i % 2 == 0 ? ct1 : ct2, "hello" + i, -1); + contentService.SaveBlueprint(blueprint); + } + + var found = contentService.GetBlueprintsForContentTypes().ToArray(); + Assert.AreEqual(10, found.Length); + + found = contentService.GetBlueprintsForContentTypes(ct1.Id).ToArray(); + Assert.AreEqual(5, found.Length); + + found = contentService.GetBlueprintsForContentTypes(ct2.Id).ToArray(); + Assert.AreEqual(5, found.Length); + } /// /// Ensures that we don't unpublish all nodes when a node is deleted that has an invalid path of -1 diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 833ed1771b..c45864b4b4 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -56,9 +56,9 @@ namespace Umbraco.Tests.Services //assert Assert.AreEqual(3, permissions.Count()); - Assert.AreEqual(17, permissions.ElementAt(0).AssignedPermissions.Count()); - Assert.AreEqual(17, permissions.ElementAt(1).AssignedPermissions.Count()); - Assert.AreEqual(17, permissions.ElementAt(2).AssignedPermissions.Count()); + Assert.AreEqual(18, permissions.ElementAt(0).AssignedPermissions.Count()); + Assert.AreEqual(18, permissions.ElementAt(1).AssignedPermissions.Count()); + Assert.AreEqual(18, permissions.ElementAt(2).AssignedPermissions.Count()); } [Test] diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js new file mode 100644 index 0000000000..db5def83d8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -0,0 +1,252 @@ +(function () { + 'use strict'; + + function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { + + //setup scope vars + $scope.defaultButton = null; + $scope.subButtons = []; + + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentNode = null; + $scope.page.menu.currentSection = appState.getSectionState("currentSection"); + $scope.page.listViewPath = null; + $scope.page.isNew = $scope.isNew ? true : false; + $scope.page.buttonGroupState = "init"; + + function init(content) { + + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $scope.page.isNew, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: $scope.unPublish + } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; + + editorState.set($scope.content); + + //We fetch all ancestors of the node to generate the footer breadcrumb navigation + if (!$scope.page.isNew) { + if (content.parentId && content.parentId !== -1) { + entityResource.getAncestors(content.id, "document") + .then(function (anc) { + $scope.ancestors = anc; + }); + } + } + } + + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { + + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + } + else if (initialLoad === true) { + + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ tree: $scope.treeAlias, path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); + + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise( + $http.get(content.treeNodeUrl), + 'Failed to retrieve data for child node ' + content.id).then(function (node) { + $scope.page.menu.currentNode = node; + }); + } + } + + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + var deferred = $q.defer(); + + $scope.page.buttonGroupState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content, + action: args.action + }).then(function (data) { + //success + init($scope.content); + syncTreeNode($scope.content, data.path); + + $scope.page.buttonGroupState = "success"; + + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + + $scope.page.buttonGroupState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; + } + + function resetLastListPageNumber(content) { + // We're using rootScope to store the page number for list views, so if returning to the list + // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children + // we should remove this so as not to confuse if navigating to a different list + if (!content.isChildOfListView && !content.isContainer) { + $rootScope.lastListViewPageViewed = null; + } + } + + if ($scope.page.isNew) { + + $scope.page.loading = true; + + //we are creating so get an empty content item + $scope.getScaffoldMethod()() + .then(function (data) { + + $scope.content = data; + + init($scope.content); + + resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + + }); + } + else { + + $scope.page.loading = true; + + //we are editing so get the content item from the server + $scope.getMethod()($scope.contentId) + .then(function (data) { + + $scope.content = data; + + if (data.isChildOfListView && data.trashed === false) { + $scope.page.listViewPath = ($routeParams.page) ? + "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : + "/content/content/edit/" + data.parentId; + } + + init($scope.content); + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + + syncTreeNode($scope.content, data.path, true); + + resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + + }); + } + + + $scope.unPublish = function () { + + if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { + + $scope.page.buttonGroupState = "busy"; + + contentResource.unPublish($scope.content.id) + .then(function (data) { + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + + init($scope.content); + + syncTreeNode($scope.content, data.path); + + $scope.page.buttonGroupState = "success"; + + }); + } + + }; + + $scope.sendToPublish = function () { + return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); + }; + + $scope.saveAndPublish = function () { + return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); + }; + + $scope.save = function () { + return performSave({ saveMethod: $scope.saveMethod(), statusMessage: "Saving...", action: "save" }); + }; + + $scope.preview = function (content) { + + + if (!$scope.busy) { + + // Chromes popup blocker will kick in if a window is opened + // outwith the initial scoped request. This trick will fix that. + // + var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); + $scope.save().then(function (data) { + // Build the correct path so both /#/ and #/ work. + var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; + previewWindow.location.href = redirect; + }); + + + } + + }; + + } + + function createDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/content/edit.html', + controller: 'Umbraco.Editors.Content.EditorDirectiveController', + scope: { + contentId: "=", + isNew: "=?", + treeAlias: "@", + page: "=?", + saveMethod: "&", + getMethod: "&", + getScaffoldMethod: "&?" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').controller('Umbraco.Editors.Content.EditorDirectiveController', ContentEditController); + angular.module('umbraco.directives').directive('contentEditor', createDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index f44401f5c5..e629bb0beb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -26,11 +26,9 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { /** internal method process the saving of data and post processing the result */ - function saveContentItem(content, action, files) { + function saveContentItem(content, action, files, restApiUrl) { return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"), + restApiUrl: restApiUrl, content: content, action: action, files: files, @@ -269,6 +267,16 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to delete item ' + id); }, + deleteBlueprint: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteBlueprint", + [{ id: id }])), + 'Failed to delete blueprint ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getById @@ -300,6 +308,16 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to retrieve data for content id ' + id); }, + getBlueprintById: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetBlueprintById", + [{ id: id }])), + 'Failed to retrieve data for content id ' + id); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getByIds @@ -381,6 +399,17 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to retrieve data for empty content item type ' + alias); }, + getBlueprintScaffold: function (blueprintId) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ blueprintId: blueprintId }])), + 'Failed to retrieve blueprint for id ' + blueprintId); + }, + /** * @ngdoc method * @name umbraco.resources.contentResource#getNiceUrl @@ -564,9 +593,18 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ save: function (content, isNew, files) { - return saveContentItem(content, "save" + (isNew ? "New" : ""), files); + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + return saveContentItem(content, "save" + (isNew ? "New" : ""), files, endpoint); }, + saveBlueprint: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSaveBlueprint"); + return saveContentItem(content, "save" + (isNew ? "New" : ""), files, endpoint); + }, /** * @ngdoc method @@ -597,7 +635,10 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ publish: function (content, isNew, files) { - return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + return saveContentItem(content, "publish" + (isNew ? "New" : ""), files, endpoint); }, @@ -628,7 +669,10 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ sendToPublish: function (content, isNew, files) { - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files, endpoint); }, /** @@ -665,6 +709,17 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { [{ id: id }])), 'Failed to publish content with id ' + id); + }, + + createBlueprintFromContent: function (contentId, name) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "CreateBlueprintFromContent", { + contentId: contentId, name: name + }) + ), + "Failed to create blueprint from content with id " + contentId + ); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index ba395becfc..2ffecaddef 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -770,9 +770,9 @@ function umbDataFormatter() { var propTemplate = _.find(genericTab.properties, function (item) { return item.alias === "_umb_template"; }); - saveModel.expireDate = propExpireDate.value; - saveModel.releaseDate = propReleaseDate.value; - saveModel.templateAlias = propTemplate.value; + saveModel.expireDate = propExpireDate ? propExpireDate.value : null; + saveModel.releaseDate = propReleaseDate ? propReleaseDate.value : null; + saveModel.templateAlias = propTemplate ? propTemplate.value : null; return saveModel; } diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 3ba655d934..b2693f344e 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -31,7 +31,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop "At least 4 people have the Umbraco logo tattooed on them", "'Umbraco' is the danish name for an allen key", "Umbraco has been around since 2005, that's a looong time in IT", - "More than 550 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", + "More than 550 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", "You can extend Umbraco without modifying the source code using either JavaScript or C#", "Umbraco was installed in more than 165 countries in 2015" diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html new file mode 100644 index 0000000000..b97676fb7e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -0,0 +1,86 @@ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 503caeb82d..ace6058b1d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -6,11 +6,67 @@ * @description * The controller for the content creation dialog */ -function contentCreateController($scope, $routeParams, contentTypeResource, iconHelper) { - - contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function(data) { - $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); +function contentCreateController($scope, + $routeParams, + contentTypeResource, + iconHelper, + $location, + navigationService, + blueprintConfig) { + + function initialize() { + contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { + $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); }); + + $scope.selectContentType = true; + $scope.selectBlueprint = false; + $scope.allowBlank = blueprintConfig.allowBlank; + } + + function close() { + navigationService.hideMenu(); + } + + function createBlank(docType) { + $location + .path("/content/content/edit/" + $scope.currentNode.id) + .search("doctype=" + docType.alias + "&create=true"); + close(); + } + + function createOrSelectBlueprintIfAny(docType) { + var blueprintIds = _.keys(docType.blueprints || {}); + $scope.docType = docType; + if (blueprintIds.length) { + if (blueprintConfig.skipSelect) { + createFromBlueprint(blueprintIds[0]); + } else { + $scope.selectContentType = false; + $scope.selectBlueprint = true; + } + } else { + createBlank(docType); + } + } + + function createFromBlueprint(blueprintId) { + $location + .path("/content/content/edit/" + $scope.currentNode.id) + .search("doctype=" + $scope.docType.alias + "&create=true&blueprintId=" + blueprintId); + close(); + } + + $scope.createBlank = createBlank; + $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; + $scope.createFromBlueprint = createFromBlueprint; + + initialize(); } -angular.module('umbraco').controller("Umbraco.Editors.Content.CreateController", contentCreateController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.Editors.Content.CreateController", contentCreateController); + +angular.module("umbraco").value("blueprintConfig", { + skipSelect: false, + allowBlank: true +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js new file mode 100644 index 0000000000..17cdfa8f61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js @@ -0,0 +1,56 @@ +(function () { + + function CreateBlueprintController( + $scope, + contentResource, + notificationsService, + navigationService, + localizationService, + formHelper, + contentEditingHelper) { + + $scope.message = { + name: $scope.currentNode.name + }; + + var successText = {}; + localizationService.localize("content_createBlueprintFrom", [$scope.message.name]).then(function (localizedVal) { + $scope.label = localizedVal; + }); + + + + $scope.cancel = function () { + navigationService.hideMenu(); + }; + + $scope.create = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.blueprintForm, + statusMessage: "Creating blueprint..." + })) { + + contentResource.createBlueprintFromContent($scope.currentNode.id, $scope.message.name) + .then(function(data) { + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + navigationService.hideMenu(); + }, + function(err) { + + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err + }); + + } + ); + } + }; + } + + angular.module("umbraco").controller("Umbraco.Editors.Content.CreateBlueprintController", CreateBlueprintController); + +}()); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index e8e729fd72..94562c6a32 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -6,227 +6,21 @@ * @description * The controller for the content editor */ -function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { +function ContentEditController($scope, $routeParams, contentResource) { - //setup scope vars - $scope.defaultButton = null; - $scope.subButtons = []; - - $scope.page = {}; - $scope.page.loading = false; - $scope.page.menu = {}; - $scope.page.menu.currentNode = null; - $scope.page.menu.currentSection = appState.getSectionState("currentSection"); - $scope.page.listViewPath = null; - $scope.page.isNew = $routeParams.create; - $scope.page.buttonGroupState = "init"; - - function init(content) { - - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: $routeParams.create, - content: content, - methods: { - saveAndPublish: $scope.saveAndPublish, - sendToPublish: $scope.sendToPublish, - save: $scope.save, - unPublish: $scope.unPublish - } - }); - $scope.defaultButton = buttons.defaultButton; - $scope.subButtons = buttons.subButtons; - - editorState.set($scope.content); - - //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (!$routeParams.create) { - if (content.parentId && content.parentId != -1) { - entityResource.getAncestors(content.id, "document") - .then(function (anc) { - $scope.ancestors = anc; - }); - } - } - } - - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(content, path, initialLoad) { - - if (!$scope.content.isChildOfListView) { - navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); - } - else if (initialLoad === true) { - - //it's a child item, just sync the ui node to the parent - navigationService.syncTree({ tree: "content", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(content.treeNodeUrl), - 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.page.menu.currentNode = node; - }); - } - } - - // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish - function performSave(args) { - var deferred = $q.defer(); - - $scope.page.buttonGroupState = "busy"; - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: args.statusMessage, - saveMethod: args.saveMethod, - scope: $scope, - content: $scope.content, - action: args.action - }).then(function (data) { - //success - init($scope.content); - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } - - $scope.page.buttonGroupState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - - function resetLastListPageNumber(content) { - // We're using rootScope to store the page number for list views, so if returning to the list - // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children - // we should remove this so as not to confuse if navigating to a different list - if (!content.isChildOfListView && !content.isContainer) { - $rootScope.lastListViewPageViewed = null; - } - } - - if ($routeParams.create) { - - $scope.page.loading = true; - - //we are creating so get an empty content item - contentResource.getScaffold($routeParams.id, $routeParams.doctype) - .then(function (data) { - - $scope.content = data; - - init($scope.content); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - else { - - $scope.page.loading = true; - - //we are editing so get the content item from the server - contentResource.getById($routeParams.id) - .then(function (data) { - - $scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) - ? "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page - : "/content/content/edit/" + data.parentId; - } - - init($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - syncTreeNode($scope.content, data.path, true); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); - } - - - $scope.unPublish = function () { - - if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { - - $scope.page.buttonGroupState = "busy"; - - contentResource.unPublish($scope.content.id) - .then(function (data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - init($scope.content); - - syncTreeNode($scope.content, data.path); - - $scope.page.buttonGroupState = "success"; - - }); - } - - }; - - $scope.sendToPublish = function () { - return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); - }; - - $scope.saveAndPublish = function () { - return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); - }; - - $scope.save = function () { - return performSave({ saveMethod: contentResource.save, statusMessage: "Saving...", action: "save" }); - }; - - $scope.preview = function (content) { - - - if (!$scope.busy) { - - // Chromes popup blocker will kick in if a window is opened - // outwith the initial scoped request. This trick will fix that. - // - var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); - $scope.save().then(function (data) { - // Build the correct path so both /#/ and #/ work. - var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; - previewWindow.location.href = redirect; - }); - - - } - - }; + function scaffoldEmpty() { + return contentResource.getScaffold($routeParams.id, $routeParams.doctype); + } + function scaffoldBlueprint() { + return contentResource.getBlueprintScaffold($routeParams.blueprintId); + } + $scope.contentId = $routeParams.id; + $scope.saveMethod = contentResource.save; + $scope.getMethod = contentResource.getById; + $scope.getScaffoldMethod = $routeParams.blueprintId ? scaffoldBlueprint : scaffoldEmpty; + $scope.page = $routeParams.page; + $scope.isNew = $routeParams.create; } angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index 73bdc5c188..995688412b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -1,45 +1,53 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/content/createblueprint.html b/src/Umbraco.Web.UI.Client/src/views/content/createblueprint.html new file mode 100644 index 0000000000..714d20db07 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/createblueprint.html @@ -0,0 +1,32 @@ +
+ +
+ + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index 0ef68fe07b..2ad4534115 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -1,86 +1,11 @@
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.controller.js new file mode 100644 index 0000000000..cc5e4b32a9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.controller.js @@ -0,0 +1,32 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.ContentBlueprint.DeleteController + * @function + * + * @description + * The controller for deleting content blueprints + */ +function ContentBlueprintDeleteController($scope, contentResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + + contentResource.deleteBlueprint($scope.currentNode.id) + .then(function() { + $scope.currentNode.loading = false; + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.ContentBlueprint.DeleteController", ContentBlueprintDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.html new file mode 100644 index 0000000000..44a31254d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/delete.html @@ -0,0 +1,12 @@ +
+
+ +

+ Are you sure you want to delete {{currentNode.name}} ? +

+ + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js new file mode 100644 index 0000000000..cb8e091e50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js @@ -0,0 +1,34 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Content.EditController + * @function + * + * @description + * The controller for the content editor + */ +function ContentBlueprintEditController($scope, $routeParams, contentResource) { + var excludedProps = ["_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"]; + + function getScaffold() { + return contentResource.getScaffold(-1, $routeParams.doctype) + .then(function (scaffold) { + var lastTab = scaffold.tabs[scaffold.tabs.length - 1]; + lastTab.properties = _.filter(lastTab.properties, + function(p) { + return excludedProps.indexOf(p.alias) === -1; + }); + scaffold.allowPreview = false; + scaffold.allowedActions = ["A", "S", "C"]; + + return scaffold; + }); + } + + $scope.contentId = $routeParams.id; + $scope.isNew = $routeParams.id === "-1"; + $scope.saveMethod = contentResource.saveBlueprint; + $scope.getMethod = contentResource.getBlueprintById; + $scope.getScaffoldMethod = getScaffold; +} + +angular.module("umbraco").controller("Umbraco.Editors.ContentBlueprint.EditController", ContentBlueprintEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html new file mode 100644 index 0000000000..5d27978072 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html @@ -0,0 +1,9 @@ +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index ede7c20538..0e06085ff4 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -32,7 +32,7 @@ module.exports = function(karma) { 'test/config/app.unit.js', 'src/common/mocks/umbraco.servervariables.js', - 'src/common/directives/*.js', + 'src/common/directives/**/*.js', 'src/common/filters/*.js', 'src/common/services/*.js', 'src/common/security/*.js', diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js new file mode 100644 index 0000000000..46f5a7bbc9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js @@ -0,0 +1,124 @@ +(function() { + + describe("create content dialog", + function() { + + var scope, + allowedTypes = [ + { id: 1, alias: "x" }, + { id: 2, alias: "y", blueprints: { "1": "a", "2": "b" } } + ], + location, + searcher, + controller, + rootScope, + contentTypeResource; + + beforeEach(module("umbraco")); + + function initialize(blueprintConfig) { + scope = rootScope.$new(); + scope.currentNode = { id: 1234 }; + var dependencies = { + $scope: scope, + contentTypeResource: contentTypeResource + }; + if (blueprintConfig) { + dependencies.blueprintConfig = blueprintConfig; + } + controller("Umbraco.Editors.Content.CreateController", + dependencies); + + scope.$digest(); + + } + + beforeEach(inject(function($controller, $rootScope, $q, $location) { + contentTypeResource = { + getAllowedTypes: function() { + var def = $q.defer(); + def.resolve(allowedTypes); + return def.promise; + } + }; + location = $location; + controller = $controller; + rootScope = $rootScope; + + searcher = { search: function() {} }; + spyOn(location, "path").andReturn(searcher); + spyOn(searcher, "search"); + + initialize(); + })); + + it("shows available child document types for the given node", + function() { + expect(scope.selectContentType).toBe(true); + expect(scope.allowedTypes).toBe(allowedTypes); + }); + + it("creates content directly when there are no blueprints", + function() { + scope.createOrSelectBlueprintIfAny(allowedTypes[0]); + + expect(location.path).toHaveBeenCalledWith("/content/content/edit/1234"); + expect(searcher.search).toHaveBeenCalledWith("doctype=x&create=true"); + }); + + it("shows list of blueprints when there are some", + function() { + scope.createOrSelectBlueprintIfAny(allowedTypes[1]); + expect(scope.selectContentType).toBe(false); + expect(scope.selectBlueprint).toBe(true); + expect(scope.docType).toBe(allowedTypes[1]); + }); + + it("creates blueprint when selected", + function() { + scope.createOrSelectBlueprintIfAny(allowedTypes[1]); + scope.createFromBlueprint("1"); + + expect(location.path).toHaveBeenCalledWith("/content/content/edit/1234"); + expect(searcher.search).toHaveBeenCalledWith("doctype=y&create=true&blueprintId=1"); + }); + + it("skips selection and creates first blueprint when configured to", + function() { + initialize({ + allowBlank: true, + skipSelect: true + }); + + scope.createOrSelectBlueprintIfAny(allowedTypes[1]); + + expect(location.path).toHaveBeenCalledWith("/content/content/edit/1234"); + expect(searcher.search).toHaveBeenCalledWith("doctype=y&create=true&blueprintId=1"); + }); + + it("allows blank to be selected", + function() { + expect(scope.allowBlank).toBe(true); + }); + + it("creates blank when selected", + function() { + scope.createBlank(allowedTypes[1]); + + expect(location.path).toHaveBeenCalledWith("/content/content/edit/1234"); + expect(searcher.search).toHaveBeenCalledWith("doctype=y&create=true"); + }); + + it("hides blank when configured to", + function() { + initialize({ + allowBlank: false, + skipSelect: false + }); + + expect(scope.allowBlank).toBe(false); + }); + + }); + +}()); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js index 0d9b8a29e2..ae3c33a19a 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js @@ -1,12 +1,16 @@ +// The content editor has been wrapped in a directive +// The current setup will have problems with loading the HTML etc. +// These tests are therefore ignored for now. + describe('edit content controller tests', function () { - var scope, controller, routeParams, httpBackend; + var scope, controller, routeParams, httpBackend, wasSaved, q; routeParams = {id: 1234, create: false}; beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils, localizationMocks) { - + beforeEach(inject(function ($rootScope, $q, $controller, $compile, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils, localizationMocks) { + q = $q; //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -20,27 +24,32 @@ describe('edit content controller tests', function () { localizationMocks.register(); //this controller requires an angular form controller applied to it - scope.contentForm = angularHelper.getNullForm("contentForm"); - - controller = $controller('Umbraco.Editors.Content.EditController', { + scope.contentForm = angularHelper.getNullForm("contentForm"); + + var deferred = $q.defer(); + wasSaved = false; + scope.saveMethod = function() { wasSaved = true; }; + scope.getMethod = function() { return function() { return deferred.promise; } }; + scope.treeAlias = "content"; + + controller = $controller('Umbraco.Editors.Content.EditorDirectiveController', { $scope: scope, $routeParams: routeParams }); //For controller tests its easiest to have the digest and flush happen here //since its intially always the same $http calls made - - //scope.$digest resolves the promise against the httpbackend - scope.$digest(); - //httpbackend.flush() resolves all request against the httpbackend - //to fake a async response, (which is what happens on a real setup) - httpBackend.flush(); + + // Resolve the get method + deferred.resolve(mocksUtils.getMockContent(1234)); + + //scope.$digest resolves the promise + scope.$digest(); })); describe('content edit controller save and publish', function () { - it('it should have an content object', function() { - + it('it should have an content object', function() { //controller should have a content object expect(scope.content).toNotBe(undefined); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index eba9c4f1f2..bd971b6d86 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2376,9 +2376,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7630 + 7640 / - http://localhost:7630 + http://localhost:7640 False False diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 13da9246b7..d32e58f093 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -16,6 +16,7 @@ + @@ -40,5 +41,5 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index d99cd8f43f..e839fbcdb8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -40,6 +40,7 @@ Translate Update Default value + Create blueprint Permission denied. @@ -177,6 +178,12 @@ Target This translates to the following time on the server: What does this mean?]]> + Create a blueprint from %0% + Blank + Select a blueprint + Blueprint created + Blueprint was created from %0% + Another Blueprint with the same name already exists. Click to upload @@ -1340,6 +1347,7 @@ To manage your website, simply open the Umbraco back office and start adding con Upload translation XML + Content Blueprints Cache Browser Recycle Bin Created packages diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 63fc12101f..8e176246f8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1337,6 +1337,7 @@ To manage your website, simply open the Umbraco back office and start adding con Upload translation XML + Content Blueprints Cache Browser Recycle Bin Created packages diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index da5e0c3a3b..bd5f685034 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -24,6 +24,7 @@ using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using umbraco.cms.businesslogic.web; using umbraco.presentation.preview; +using Umbraco.Core.Events; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors @@ -104,6 +105,36 @@ namespace Umbraco.Web.Editors return display; } + public ContentItemDisplay GetBlueprintById(int id) + { + var foundContent = Services.ContentService.GetBlueprintById(id); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = Mapper.Map(foundContent); + + SetupBlueprint(content, foundContent); + + return content; + } + + private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) + { + content.AllowPreview = false; + + //set a custom path since the tree that renders this has the content type id as the parent + content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); + + content.AllowedActions = new[] {'A'}; + + var excludeProps = new[] {"_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"}; + var propsTab = content.Tabs.Last(); + propsTab.Properties = propsTab.Properties + .Where(p => excludeProps.Contains(p.Alias) == false); + } + /// /// Gets the content json for the content id /// @@ -163,6 +194,26 @@ namespace Umbraco.Web.Editors return mapped; } + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(int blueprintId) + { + var blueprint = Services.ContentService.GetBlueprintById(blueprintId); + if (blueprint == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + blueprint.Id = 0; + blueprint.Name = string.Empty; + + var mapped = Mapper.Map(blueprint); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + /// /// Gets the Url for a given node ID /// @@ -275,6 +326,69 @@ namespace Umbraco.Web.Editors return false; } + /// + /// Creates a blueprint from a content item + /// + /// The content id to copy + /// The name of the blueprint + /// + [HttpPost] + public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); + + var content = Services.ContentService.GetById(contentId); + if (content == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + EnsureUniqueName(name, content, "name"); + + var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.GetUserId()); + + Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId()); + + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddSuccessNotification( + Services.TextService.Localize("content/createdBlueprintHeading"), + Services.TextService.Localize("content/createdBlueprintMessage", new[]{ content.Name}) + ); + + return notificationModel; + } + + private void EnsureUniqueName(string name, IContent content, string modelName) + { + var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); + if (existing.Any(x => x.Name == name && x.Id != content.Id)) + { + ModelState.AddModelError(modelName, Services.TextService.Localize("content/duplicateBlueprintMessage")); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSaveBlueprint( + [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, + content => + { + EnsureUniqueName(content.Name, content, "Name"); + + Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants + return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); + }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; + } + /// /// Saves content /// @@ -284,6 +398,12 @@ namespace Umbraco.Web.Editors public ContentItemDisplay PostSave( [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + { + return PostSaveInternal(contentItem, + content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + } + + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) { //If we've reached here it means: // * Our model has been bound @@ -291,7 +411,6 @@ namespace Umbraco.Web.Editors // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid - MapPropertyValues(contentItem); //We need to manually check the validation results here because: @@ -331,7 +450,7 @@ namespace Umbraco.Web.Editors if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) { //save the item - var saveResult = Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); + var saveResult = saveMethod(contentItem.PersistedContent); wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; } @@ -361,8 +480,8 @@ namespace Umbraco.Web.Editors if (wasCancelled == false) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize("speechBubbles/editContentSavedText")); + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles/editContentSavedText")); } else { @@ -374,8 +493,8 @@ namespace Umbraco.Web.Editors if (wasCancelled == false) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); } else { @@ -398,8 +517,10 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); } + display.PersistedContent = contentItem.PersistedContent; + return display; - } + } /// /// Publishes a document with a given ID @@ -434,6 +555,22 @@ namespace Umbraco.Web.Editors } + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteBlueprint(int id) + { + var found = Services.ContentService.GetBlueprintById(id); + + if (found == null) + { + return HandleContentNotFound(id, false); + } + + Services.ContentService.DeleteBlueprint(found); + + return Request.CreateResponse(HttpStatusCode.OK); + } + /// /// Moves an item to the recycle bin, if it is already there then it will permanently delete it /// diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 720eca184b..f0e67ac14b 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -302,7 +302,18 @@ namespace Umbraco.Web.Editors { basic.Name = localizedTextService.UmbracoDictionaryTranslate(basic.Name); basic.Description = localizedTextService.UmbracoDictionaryTranslate(basic.Description); - } + } + + //map the blueprints + var blueprints = Services.ContentService.GetBlueprintsForContentTypes(types.Select(x => x.Id).ToArray()).ToArray(); + foreach (var basic in basics) + { + var docTypeBluePrints = blueprints.Where(x => x.ContentTypeId == (int) basic.Id).ToArray(); + foreach (var blueprint in docTypeBluePrints) + { + basic.Blueprints[blueprint.Id] = blueprint.Name; + } + } return basics; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs index 8dde1f1f97..938c7de657 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; @@ -18,6 +19,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "contentType", Namespace = "")] public class ContentTypeBasic : EntityBasic { + public ContentTypeBasic() + { + Blueprints = new Dictionary(); + } + /// /// Overridden to apply our own validation attributes since this is not always required for other classes /// @@ -105,5 +111,9 @@ namespace Umbraco.Web.Models.ContentEditing : IOHelper.ResolveUrl("~/umbraco/images/thumbnails/" + Thumbnail); } } + + [DataMember(Name = "blueprints")] + [ReadOnly(true)] + public IDictionary Blueprints { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index 24620509e9..1d9b4fedda 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -163,11 +163,14 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() - .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.MemberType, content.Key))); + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.MemberType, content.Key))) + .ForMember(x => x.Blueprints, expression => expression.Ignore()); config.CreateMap() - .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.MediaType, content.Key))); + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.MediaType, content.Key))) + .ForMember(x => x.Blueprints, expression => expression.Ignore()); config.CreateMap() - .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.DocumentType, content.Key))); + .ForMember(x => x.Udi, expression => expression.MapFrom(content => Udi.Create(Constants.UdiEntityType.DocumentType, content.Key))) + .ForMember(x => x.Blueprints, expression => expression.Ignore()); config.CreateMap() diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index 274771fead..00923c8655 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -129,6 +129,7 @@ namespace Umbraco.Web.Models.Mapping return mapping .ForMember(x => x.Udi, expression => expression.ResolveUsing(new ContentTypeUdiResolver())) .ForMember(display => display.Notifications, expression => expression.Ignore()) + .ForMember(display => display.Blueprints, expression => expression.Ignore()) .ForMember(display => display.Errors, expression => expression.Ignore()) .ForMember(display => display.AllowAsRoot, expression => expression.MapFrom(type => type.AllowedAsRoot)) .ForMember(display => display.ListViewEditorName, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs new file mode 100644 index 0000000000..ff029a020a --- /dev/null +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Net.Http.Formatting; +using System.Web.Http; +using umbraco; +using umbraco.businesslogic.Actions; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Trees +{ + /// + /// The content blueprint tree controller + /// + /// + /// This authorizes based on access to the content section even though it exists in the settings + /// + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, null, sortOrder: 8)] + [PluginController("UmbracoTrees")] + [CoreTree] + public class ContentBlueprintTreeController : TreeController + { + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var nodes = new TreeNodeCollection(); + + //get all blueprints + var entities = Services.EntityService.GetChildren(Constants.System.Root, UmbracoObjectTypes.DocumentBlueprint).ToArray(); + + //check if we're rendering the root in which case we'll render the content types that have blueprints + if (id == Constants.System.Root.ToInvariantString()) + { + //get all blueprint content types + var contentTypeAliases = entities.Select(x => ((UmbracoEntity) x).ContentTypeAlias).Distinct(); + //get the ids + var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()); + //now get the entities ... it's a bit round about but still smaller queries than getting all document types + var docTypeEntities = Services.EntityService.GetAll(UmbracoObjectTypes.DocumentType, contentTypeIds.ToArray()).ToArray(); + + nodes.AddRange(docTypeEntities + .Select(entity => + { + var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprintGuid, id, queryStrings, "icon-item-arrangement", true); + treeNode.Path = string.Format("-1,{0}", entity.Id); + treeNode.NodeType = "contentType"; + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + treeNode.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + return treeNode; + })); + + return nodes; + } + else + { + var intId = id.TryConvertTo(); + //Get the content type + var ct = Services.ContentTypeService.GetContentType(intId.Result); + if (ct == null) return nodes; + + var blueprintsForDocType = entities.Where(x => ct.Alias == ((UmbracoEntity) x).ContentTypeAlias); + nodes.AddRange(blueprintsForDocType + .Select(entity => + { + var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprintGuid, id, queryStrings, "icon-blueprint", false); + treeNode.Path = string.Format("-1,{0},{1}", ct.Id, entity.Id); + return treeNode; + })); + } + + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + return menu; + } + var cte = Services.EntityService.Get(int.Parse(id), UmbracoObjectTypes.DocumentType); + //only refresh & create if it's a content type + if (cte != null) + { + var ct = Services.ContentTypeService.GetContentType(cte.Id); + var createItem = menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionCreateBlueprintFromContent.Instance.Alias))); + createItem.NavigateToRoute("/settings/contentBlueprints/edit/-1?create=true&doctype=" + ct.Alias); + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + + return menu; + } + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + + return menu; + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index a9f0bda805..320b9d5a48 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -16,6 +16,7 @@ using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; using umbraco.businesslogic; +using umbraco.businesslogic.Actions; using umbraco.cms.businesslogic.web; using umbraco.interfaces; using Constants = Umbraco.Core.Constants; @@ -225,7 +226,9 @@ namespace Umbraco.Web.Trees var menu = new MenuItemCollection(); menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); - + + menu.Items.Add(ui.Text("actions", ActionCreateBlueprintFromContent.Instance.Alias)); + //need to ensure some of these are converted to the legacy system - until we upgrade them all to be angularized. menu.Items.Add(ui.Text("actions", ActionMove.Instance.Alias), true); menu.Items.Add(ui.Text("actions", ActionCopy.Instance.Alias)); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a6b56e5019..14f169f71d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -465,6 +465,7 @@ + diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 614c815f79..a451503803 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -343,5 +343,24 @@ namespace Umbraco.Web { return url.SurfaceAction(action, typeof (T), additionalRouteVals); } + + /// + /// Generates a Absolute Media Item URL based on the current context + /// + /// + /// + /// + public static string GetAbsoluteMediaUrl(this UrlHelper urlHelper, IPublishedContent mediaItem) + { + if (urlHelper == null) throw new ArgumentNullException("urlHelper"); + if (mediaItem == null) throw new ArgumentNullException("mediaItem"); + + if (urlHelper.RequestContext.HttpContext.Request.Url != null) + { + var requestUrl = urlHelper.RequestContext.HttpContext.Request.Url.GetLeftPart(UriPartial.Authority); + return string.Format("{0}{1}", requestUrl, mediaItem.Url); + } + return null; + } } } \ No newline at end of file diff --git a/src/umbraco.cms/Actions/ActionCreateBlueprintFromContent.cs b/src/umbraco.cms/Actions/ActionCreateBlueprintFromContent.cs new file mode 100644 index 0000000000..43ce768b03 --- /dev/null +++ b/src/umbraco.cms/Actions/ActionCreateBlueprintFromContent.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using umbraco.interfaces; + +namespace umbraco.businesslogic.Actions +{ + public class ActionCreateBlueprintFromContent : IAction + { + private static readonly ActionCreateBlueprintFromContent instance = new ActionCreateBlueprintFromContent(); + + public static ActionCreateBlueprintFromContent Instance + { + get { return instance; } + } + + public char Letter { get; private set; } + public bool ShowInNotifier { get; private set; } + public bool CanBePermissionAssigned { get; private set; } + public string Icon { get; private set; } + public string Alias { get; private set; } + public string JsFunctionName { get; private set; } + public string JsSource { get; private set; } + + public ActionCreateBlueprintFromContent() + { + Letter = 'ï'; + CanBePermissionAssigned = true; + Icon = "blueprint"; + Alias = "createblueprint"; + } + } +} diff --git a/src/umbraco.cms/umbraco.cms.csproj b/src/umbraco.cms/umbraco.cms.csproj index 921466498c..0972200b96 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -183,6 +183,7 @@ +