Merge pull request #1984 from umbraco/temp-content-blueprints

Content blueprints
This commit is contained in:
Shannon Deminick
2017-06-05 21:26:35 +02:00
committed by GitHub
54 changed files with 1558 additions and 442 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# Usage: on line 2 put the release version, on line 3 put the version comment (example: beta)
7.6.3
7.6.4
+2 -2
View File
@@ -11,5 +11,5 @@ using System.Resources;
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("7.6.3")]
[assembly: AssemblyInformationalVersion("7.6.3")]
[assembly: AssemblyFileVersion("7.6.4")]
[assembly: AssemblyInformationalVersion("7.6.4")]
+6 -1
View File
@@ -56,7 +56,12 @@
/// <summary>
/// alias for the content tree.
/// </summary>
public const string Content = "content";
public const string Content = "content";
/// <summary>
/// alias for the content blueprint tree.
/// </summary>
public const string ContentBlueprints = "contentBlueprints";
/// <summary>
/// alias for the member tree.
+11 -1
View File
@@ -68,12 +68,22 @@ namespace Umbraco.Core
/// Guid for a Document object.
/// </summary>
public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972";
/// <summary>
/// Guid for a Document object.
/// </summary>
public static readonly Guid DocumentGuid = new Guid(Document);
/// <summary>
/// Guid for a Document Blueprint object.
/// </summary>
public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA";
/// <summary>
/// Guid for a Document object.
/// </summary>
public static readonly Guid DocumentBlueprintGuid = new Guid(DocumentBlueprint);
/// <summary>
/// Guid for a Document Type object.
/// </summary>
@@ -38,6 +38,14 @@ namespace Umbraco.Core.Models
[UmbracoUdiType(Constants.UdiEntityType.Document)]
Document,
/// <summary>
/// Document
/// </summary>
[UmbracoObjectType(Constants.ObjectTypes.DocumentBlueprint, typeof(IContent))]
[FriendlyName("DocumentBlueprint")]
[UmbracoUdiType(Constants.UdiEntityType.DocumentBluePrint)]
DocumentBlueprint,
/// <summary>
/// Media
/// </summary>
@@ -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" });
}
@@ -20,6 +20,28 @@ using Umbraco.Core.Persistence.UnitOfWork;
namespace Umbraco.Core.Persistence.Repositories
{
/// <summary>
/// Override the base content repository so we can change the node object type
/// </summary>
/// <remarks>
/// 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
/// </remarks>
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; }
}
}
/// <summary>
/// Represents a repository for doing CRUD operations for <see cref="IContent"/>
/// </summary>
@@ -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<int>(sqlAllIds);
var docObjectType = Guid.Parse(Constants.ObjectTypes.Document);
var docObjectType = NodeObjectTypeId;
var xmlIdsQuery = new Sql()
.Select("DISTINCT cmsContentXml.nodeId")
.From<ContentXmlDto>(SqlSyntax)
@@ -742,7 +764,19 @@ namespace Umbraco.Core.Persistence.Repositories
return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true);
}
public IEnumerable<IContent> GetBlueprints(IQuery<IContent> query)
{
Func<SqlTranslator<IContent>, Sql> translate = t => t.Translate();
var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple);
var translatorFull = new SqlTranslator<IContent>(sqlFull, query);
var sqlIds = GetBaseQuery(BaseQueryType.Ids);
var translatorIds = new SqlTranslator<IContent>(sqlIds, query);
return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true);
}
/// <summary>
/// This builds the Xml document used for the XML cache
/// </summary>
@@ -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<dynamic>(sql, new { type = new Guid(Constants.ObjectTypes.Document) }))
foreach (var row in Database.Query<dynamic>(sql, new { type = NodeObjectTypeId }))
{
string parentId = ((int)row.parentID).ToInvariantString();
string xml = row.xml;
@@ -110,6 +110,19 @@ namespace Umbraco.Core.Persistence.Repositories
return Database.Fetch<string>(sql);
}
public IEnumerable<int> GetAllContentTypeIds(string[] aliases)
{
if (aliases.Length == 0) return Enumerable.Empty<int>();
var sql = new Sql().Select("cmsContentType.nodeId")
.From<ContentTypeDto>(SqlSyntax)
.InnerJoin<NodeDto>(SqlSyntax)
.On<ContentTypeDto, NodeDto>(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId)
.Where<ContentTypeDto>(dto => aliases.Contains(dto.Alias), SqlSyntax);
return Database.Fetch<int>(sql);
}
protected override Sql GetBaseQuery(bool isCount)
{
var sql = new Sql();
@@ -48,8 +48,8 @@ namespace Umbraco.Core.Persistence.Repositories
public IEnumerable<IUmbracoEntity> GetPagedResultsByQuery(IQuery<IUmbracoEntity> query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords,
string orderBy, Direction orderDirection, IQuery<IUmbracoEntity> 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<IUmbracoEntity> PerformGetAll(Guid objectTypeId, Action<Sql> 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<IUmbracoEntity> GetByQuery(IQuery<IUmbracoEntity> 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);
@@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
namespace Umbraco.Core.Persistence.Repositories
{
{
public interface IContentRepository : IRepositoryVersionable<int, IContent>, IRecycleBinRepository<IContent>, IDeleteMediaFilesRepository
{
@@ -47,5 +47,7 @@ namespace Umbraco.Core.Persistence.Repositories
/// <returns>The original alias with a number appended to it, so that it is unique.</returns>
/// /// <remarks>Unique accross all content, media and member types.</remarks>
string GetUniqueAlias(string alias);
IEnumerable<int> GetAllContentTypeIds(string[] aliases);
}
}
@@ -52,25 +52,7 @@ namespace Umbraco.Core.Persistence.Repositories
/// </summary>
/// <param name="id">Id of the <see cref="TEntity"/> to retrieve versions from</param>
/// <returns>An enumerable list of the same <see cref="TEntity"/> object with different versions</returns>
public virtual IEnumerable<TEntity> GetAllVersions(int id)
{
var sql = new Sql();
sql.Select("*")
.From<ContentVersionDto>(SqlSyntax)
.InnerJoin<ContentDto>(SqlSyntax)
.On<ContentVersionDto, ContentDto>(SqlSyntax, left => left.NodeId, right => right.NodeId)
.InnerJoin<NodeDto>(SqlSyntax)
.On<ContentDto, NodeDto>(SqlSyntax, left => left.NodeId, right => right.NodeId)
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.Where<NodeDto>(x => x.NodeId == id)
.OrderByDescending<ContentVersionDto>(x => x.VersionDate, SqlSyntax);
var dtos = Database.Fetch<ContentVersionDto, ContentDto, NodeDto>(sql);
foreach (var dto in dtos)
{
yield return GetByVersion(dto.VersionId);
}
}
public abstract IEnumerable<TEntity> GetAllVersions(int id);
/// <summary>
/// Gets a list of all version Ids for the given content item ordered so latest is first
@@ -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(
+93 -4
View File
@@ -174,8 +174,7 @@ namespace Umbraco.Core.Services
content.WriterId = userId;
uow.Events.Dispatch(Created, this, new NewEventArgs<IContent>(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<IContent>(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;
}
/// <summary>
/// Saves a single <see cref="IContent"/> object
/// </summary>
@@ -1787,6 +1861,21 @@ namespace Umbraco.Core.Services
return true;
}
public IEnumerable<IContent> GetBlueprintsForContentTypes(params int[] documentTypeIds)
{
using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
{
var repository = RepositoryFactory.CreateContentBlueprintRepository(uow);
var query = new Query<IContent>();
if (documentTypeIds.Length > 0)
{
query.Where(x => documentTypeIds.Contains(x.ContentTypeId));
}
return repository.GetByQuery(query);
}
}
/// <summary>
/// Gets paged content descendants as XML by path
/// </summary>
@@ -367,6 +367,15 @@ namespace Umbraco.Core.Services
}
}
public IEnumerable<int> GetAllContentTypeIds(string[] aliases)
{
using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
{
var repository = RepositoryFactory.CreateContentTypeRepository(uow);
return repository.GetAllContentTypeIds(aliases);
}
}
/// <summary>
/// Copies a content type as a child under the specified parent if specified (otherwise to the root)
/// </summary>
+10 -4
View File
@@ -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.
/// </summary>
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...
/// <summary>
/// Saves a single <see cref="IContent"/> object
@@ -96,6 +96,12 @@ namespace Umbraco.Core.Services
/// </summary>
public interface IContentService : IContentServiceBase
{
IEnumerable<IContent> 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);
/// <summary>
/// Gets all XML entries found in the cmsContentXml table based on the given path
/// </summary>
@@ -61,6 +61,13 @@ namespace Umbraco.Core.Services
/// </param>
/// <returns></returns>
IEnumerable<string> GetAllContentTypeAliases(params Guid[] objectTypes);
/// <summary>
/// Returns all content type Ids for the aliases given
/// </summary>
/// <param name="aliases"></param>
/// <returns></returns>
IEnumerable<int> GetAllContentTypeIds(string[] aliases);
/// <summary>
/// Copies a content type as a child under the specified parent if specified (otherwise to the root)
+4
View File
@@ -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)]
@@ -301,7 +301,7 @@ AnotherContentFinder
public void Resolves_Actions()
{
var actions = _manager.ResolveActions();
Assert.AreEqual(37, actions.Count());
Assert.AreEqual(38, actions.Count());
}
[Test]
@@ -46,11 +46,112 @@ namespace Umbraco.Tests.Services
VersionableRepositoryBase<int, IContent>.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);
}
/// <summary>
/// Ensures that we don't unpublish all nodes when a node is deleted that has an invalid path of -1
@@ -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]
@@ -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);
})();
@@ -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
);
}
@@ -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;
}
@@ -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 <a target='_blank' href='http://codegarden15.com'>CodeGarden</a>",
"More than 550 people from all over the world meet each year in Denmark in June for our annual conference <a target='_blank' href='http://codegarden17.com'>CodeGarden</a>",
"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"
@@ -0,0 +1,86 @@
<div>
<umb-load-indicator ng-if="page.loading"></umb-load-indicator>
<form name="contentForm"
ng-submit="save()"
novalidate
val-form-manager>
<umb-editor-view ng-if="!page.loading" umb-tabs>
<umb-editor-header
menu="page.menu"
name="content.name"
tabs="content.tabs"
hide-icon="true"
hide-description="true"
hide-alias="true">
</umb-editor-header>
<umb-editor-container>
<umb-tabs-content class="form-horizontal" view="true">
<umb-tab id="tab{{tab.id}}" ng-repeat="tab in content.tabs" rel="{{tab.id}}">
<umb-property ng-repeat="property in tab.properties" property="property">
<umb-editor model="property"></umb-editor>
</umb-property>
</umb-tab>
</umb-tabs-content>
</umb-editor-container>
<umb-editor-footer>
<umb-editor-footer-content-left>
<umb-breadcrumbs
ng-if="ancestors && ancestors.length > 0"
ancestors="ancestors"
entity-type="content">
</umb-breadcrumbs>
</umb-editor-footer-content-left>
<umb-editor-footer-content-right>
<umb-button
ng-if="page.listViewPath"
type="link"
href="#{{page.listViewPath}}"
label="Return to list"
label-key="buttons_returnToList">
</umb-button>
<umb-button
ng-if="!page.isNew && content.allowPreview"
type="button"
button-style="info"
action="preview(content)"
label="Preview page"
label-key="buttons_showPage">
</umb-button>
<umb-button-group
ng-if="defaultButton"
default-button="defaultButton"
sub-buttons="subButtons"
state="page.buttonGroupState"
direction="up"
float="right">
</umb-button-group>
</umb-editor-footer-content-right>
</umb-editor-footer>
</umb-editor-view>
</form>
</div>
@@ -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);
angular.module("umbraco").controller("Umbraco.Editors.Content.CreateController", contentCreateController);
angular.module("umbraco").value("blueprintConfig", {
skipSelect: false,
allowBlank: true
});
@@ -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);
}());
@@ -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);
@@ -1,45 +1,53 @@
<div class="umb-dialog-body with-footer" ng-controller="Umbraco.Editors.Content.CreateController" ng-cloak>
<div class="umb-pane">
<h5><localize key="create_createUnder">Create a page under</localize> {{currentNode.name}}</h5>
<p class="abstract" ng-if="allowedTypes && allowedTypes.length === 0">
<localize key="create_noDocumentTypes" />
</p>
<div class="umb-pane">
<h5 ng-show="selectContentType"><localize key="create_createUnder">Create a page under</localize> {{currentNode.name}}</h5>
<h5 ng-show="selectBlueprint"><localize key="content_selectBlueprint">Select a blueprint</localize></h5>
<ul class="umb-actions umb-actions-child">
<p class="abstract" ng-if="allowedTypes && allowedTypes.length === 0">
<localize key="create_noDocumentTypes" />
</p>
<li ng-repeat="docType in allowedTypes | orderBy:'name':false">
<a href="#content/content/edit/{{currentNode.id}}?doctype={{docType.alias}}&create=true" ng-click="nav.hideNavigation()">
<i class="large {{docType.icon}}"></i>
<ul class="umb-actions umb-actions-child" ng-show="selectContentType">
<span class="menu-label">
{{docType.name}}
<small>
{{docType.description}}
</small>
</span>
</a>
</li>
<!--
<li class="add">
<a href="#settings/documenttype/create/{{currentNode.id}}?doctype={{docType.alias}}&create=true" ng-click="nav.hideNavigation()">
<i class="large icon-add"></i>
<span class="menu-label">
Create a new document type
<small>
Design and configure a new document type
</small>
</span>
</a>
</li> -->
</ul>
</div>
<li ng-repeat="docType in allowedTypes | orderBy:'name':false">
<a ng-click="createOrSelectBlueprintIfAny(docType)">
<i class="large {{docType.icon}}"></i>
<span class="menu-label">
{{docType.name}}
<small>
{{docType.description}}
</small>
</span>
</a>
</li>
</ul>
<ul class="umb-actions umb-actions-child" ng-show="selectBlueprint">
<li ng-repeat="(key, value) in docType.blueprints | orderBy:'name':false">
<a ng-click="createFromBlueprint(key)">
<i class="large {{docType.icon}}"></i>
<span class="menu-label">
{{value}}
</span>
</a>
</li>
<li ng-show="allowBlank">
<a ng-click="createBlank(docType)">
<i class="large {{docType.icon}}"></i>
<span class="menu-label">
<localize key="content_blankBlueprint">Blank</localize>
</span>
</a>
</li>
</ul>
</div>
</div>
<div class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<button class="btn btn-info" ng-click="nav.hideDialog(true)">
<localize key="buttons_somethingElse">Do something else</localize>
</button>
<button class="btn btn-info" ng-click="nav.hideDialog(true)">
<localize key="buttons_somethingElse">Do something else</localize>
</button>
</div>
@@ -0,0 +1,32 @@
<div ng-controller="Umbraco.Editors.Content.CreateBlueprintController">
<form name="blueprintForm"
novalidate
ng-submit="create()"
val-form-manager>
<div class="umbracoDialog umb-dialog-body with-footer"
ng-cloak>
<div class="umb-pane">
<umb-control-group label="{{label}}" hide-label="false">
<input type="text" name="name" ng-model="message.name" class="umb-textstring textstring input-block-level"
val-server-field="name"
required />
<span class="help-inline" val-msg-for="name" val-toggle-msg="required"><localize key="required" /></span>
<span class="help-inline" val-msg-for="name" val-toggle-msg="valServerField"></span>
</umb-control-group>
</div>
</div>
<div class="umb-dialog-footer btn-toolbar umb-btn-toolbar">
<button type="button" class="btn umb-button btn-info" ng-click="cancel()">
<localize key="general_cancel">Cancel</localize>
</button>
<button type="submit" class="btn umb-button btn-success">
<localize key="general_create">Create blueprint</localize>
</button>
</div>
</form>
</div>
@@ -1,86 +1,11 @@
<div ng-controller="Umbraco.Editors.Content.EditController">
<umb-load-indicator ng-if="page.loading"></umb-load-indicator>
<form name="contentForm"
ng-submit="save()"
novalidate
val-form-manager>
<umb-editor-view ng-if="!page.loading" umb-tabs>
<umb-editor-header
menu="page.menu"
name="content.name"
tabs="content.tabs"
hide-icon="true"
hide-description="true"
hide-alias="true">
</umb-editor-header>
<umb-editor-container>
<umb-tabs-content class="form-horizontal" view="true">
<umb-tab id="tab{{tab.id}}" ng-repeat="tab in content.tabs" rel="{{tab.id}}">
<umb-property ng-repeat="property in tab.properties" property="property">
<umb-editor model="property"></umb-editor>
</umb-property>
</umb-tab>
</umb-tabs-content>
</umb-editor-container>
<umb-editor-footer>
<umb-editor-footer-content-left>
<umb-breadcrumbs
ng-if="ancestors && ancestors.length > 0"
ancestors="ancestors"
entity-type="content">
</umb-breadcrumbs>
</umb-editor-footer-content-left>
<umb-editor-footer-content-right>
<umb-button
ng-if="page.listViewPath"
type="link"
href="#{{page.listViewPath}}"
label="Return to list"
label-key="buttons_returnToList">
</umb-button>
<umb-button
ng-if="!page.isNew && content.allowPreview"
type="button"
button-style="info"
action="preview(content)"
label="Preview page"
label-key="buttons_showPage">
</umb-button>
<umb-button-group
ng-if="defaultButton"
default-button="defaultButton"
sub-buttons="subButtons"
state="page.buttonGroupState"
direction="up"
float="right">
</umb-button-group>
</umb-editor-footer-content-right>
</umb-editor-footer>
</umb-editor-view>
</form>
<content-editor create-options="createOptions"
content-id="contentId"
page="page"
save-method="saveMethod"
get-method="getMethod"
get-scaffold-method="getScaffoldMethod"
tree-alias="content"
is-new="isNew">
</content-editor>
</div>
@@ -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);
@@ -0,0 +1,12 @@
<div class="umb-dialog umb-pane" ng-controller="Umbraco.Editors.ContentBlueprint.DeleteController">
<div class="umb-dialog-body">
<p class="umb-abstract">
<localize key="defaultdialogs_confirmdelete">Are you sure you want to delete</localize> <strong>{{currentNode.name}}</strong> ?
</p>
<umb-confirm on-confirm="performDelete" on-cancel="cancel">
</umb-confirm>
</div>
</div>
@@ -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);
@@ -0,0 +1,9 @@
<div ng-controller="Umbraco.Editors.ContentBlueprint.EditController">
<content-editor content-id="contentId"
save-method="saveMethod"
get-method="getMethod"
get-scaffold-method="getScaffoldMethod"
is-new="isNew"
tree-alias="contentblueprints">
</content-editor>
</div>
@@ -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',
@@ -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);
});
});
}());
@@ -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);
+2 -2
View File
@@ -2376,9 +2376,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\"
<WebProjectProperties>
<UseIIS>True</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>7630</DevelopmentServerPort>
<DevelopmentServerPort>7640</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:7630</IISUrl>
<IISUrl>http://localhost:7640</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>False</UseCustomServer>
<CustomServerUrl>
+2 -1
View File
@@ -16,6 +16,7 @@
<add application="settings" alias="languages" title="Languages" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.LanguageTreeController, umbraco" sortOrder="5" />
<add application="settings" alias="dictionary" title="Dictionary" type="umbraco.loadDictionary, umbraco" iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="6" />
<add initialize="true" sortOrder="7" alias="mediaTypes" application="settings" title="Media Types" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.MediaTypeTreeController, umbraco" />
<add initialize="true" sortOrder="8" alias="contentBlueprints" application="settings" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.ContentBlueprintTreeController, umbraco" />
<!--Developer-->
<add initialize="true" sortOrder="0" alias="packager" application="developer" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Web.Trees.PackagesTreeController, umbraco" />
<add initialize="true" sortOrder="1" alias="dataTypes" application="developer" title="Data Types" iconClosed="icon-folder" iconOpen="icon-folder" type="Umbraco.Web.Trees.DataTypeTreeController, umbraco" />
@@ -40,5 +41,5 @@
<add initialize="true" sortOrder="2" alias="datasource" application="forms" title="Datasources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.DataSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="0" alias="form" application="forms" title="Forms" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="prevaluesource" application="forms" title="Prevalue sources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.PreValueSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="formsecurity" application="users" title="Forms Security" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormSecurityTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="formsecurity" application="users" title="Forms Security" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormSecurityTreeController, Umbraco.Forms.Web" />
</trees>
@@ -40,6 +40,7 @@
<key alias="translate">Translate</key>
<key alias="update">Update</key>
<key alias="defaultValue">Default value</key>
<key alias="createblueprint">Create blueprint</key>
</area>
<area alias="assignDomain">
<key alias="permissionDenied">Permission denied.</key>
@@ -177,6 +178,12 @@
<key alias="target" version="7.0">Target</key>
<key alias="scheduledPublishServerTime">This translates to the following time on the server:</key>
<key alias="scheduledPublishDocumentation"><![CDATA[<a href="https://our.umbraco.org/documentation/Getting-Started/Data/Scheduled-Publishing/#timezones" target="_blank">What does this mean?</a>]]></key>
<key alias="createBlueprintFrom">Create a blueprint from %0%</key>
<key alias="blankBlueprint">Blank</key>
<key alias="selectBlueprint">Select a blueprint</key>
<key alias="createdBlueprintHeading">Blueprint created</key>
<key alias="createdBlueprintMessage">Blueprint was created from %0%</key>
<key alias="duplicateBlueprintMessage">Another Blueprint with the same name already exists.</key>
</area>
<area alias="media">
<key alias="clickToUpload">Click to upload</key>
@@ -1340,6 +1347,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="uploadTranslationXml">Upload translation XML</key>
</area>
<area alias="treeHeaders">
<key alias="contentBlueprints">Content Blueprints</key>
<key alias="cacheBrowser">Cache Browser</key>
<key alias="contentRecycleBin">Recycle Bin</key>
<key alias="createdPackages">Created packages</key>
@@ -1337,6 +1337,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="uploadTranslationXml">Upload translation XML</key>
</area>
<area alias="treeHeaders">
<key alias="contentBlueprints">Content Blueprints</key>
<key alias="cacheBrowser">Cache Browser</key>
<key alias="contentRecycleBin">Recycle Bin</key>
<key alias="createdPackages">Created packages</key>
+144 -7
View File
@@ -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<IContent, ContentItemDisplay>(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);
}
/// <summary>
/// Gets the content json for the content id
/// </summary>
@@ -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<ContentItemDisplay>(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;
}
/// <summary>
/// Gets the Url for a given node ID
/// </summary>
@@ -275,6 +326,69 @@ namespace Umbraco.Web.Editors
return false;
}
/// <summary>
/// Creates a blueprint from a content item
/// </summary>
/// <param name="contentId">The content id to copy</param>
/// <param name="name">The name of the blueprint</param>
/// <returns></returns>
[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));
}
}
/// <summary>
/// Saves content
/// </summary>
/// <returns></returns>
[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<OperationStatus>.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages()));
});
SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent);
return contentItemDisplay;
}
/// <summary>
/// Saves content
/// </summary>
@@ -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<IContent, Attempt<OperationStatus>> 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;
}
}
/// <summary>
/// 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);
}
/// <summary>
/// Moves an item to the recycle bin, if it is already there then it will permanently delete it
/// </summary>
@@ -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;
}
@@ -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<int, string>();
}
/// <summary>
/// Overridden to apply our own validation attributes since this is not always required for other classes
/// </summary>
@@ -105,5 +111,9 @@ namespace Umbraco.Web.Models.ContentEditing
: IOHelper.ResolveUrl("~/umbraco/images/thumbnails/" + Thumbnail);
}
}
[DataMember(Name = "blueprints")]
[ReadOnly(true)]
public IDictionary<int, string> Blueprints { get; set; }
}
}
@@ -163,11 +163,14 @@ namespace Umbraco.Web.Models.Mapping
});
config.CreateMap<IMemberType, ContentTypeBasic>()
.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<IMediaType, ContentTypeBasic>()
.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<IContentType, ContentTypeBasic>()
.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<PropertyTypeBasic, PropertyType>()
@@ -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())
@@ -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
{
/// <summary>
/// The content blueprint tree controller
/// </summary>
/// <remarks>
/// This authorizes based on access to the content section even though it exists in the settings
/// </remarks>
[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<int>();
//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<RefreshNode, ActionRefresh>(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<ActionCreateBlueprintFromContent>(Services.TextService.Localize(string.Format("actions/{0}", ActionCreateBlueprintFromContent.Instance.Alias)));
createItem.NavigateToRoute("/settings/contentBlueprints/edit/-1?create=true&doctype=" + ct.Alias);
menu.Items.Add<RefreshNode, ActionRefresh>(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
return menu;
}
menu.Items.Add<ActionDelete>(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)));
return menu;
}
}
}
@@ -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<ActionNew>(ui.Text("actions", ActionNew.Instance.Alias));
menu.Items.Add<ActionDelete>(ui.Text("actions", ActionDelete.Instance.Alias));
menu.Items.Add<ActionCreateBlueprintFromContent>(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<ActionMove>(ui.Text("actions", ActionMove.Instance.Alias), true);
menu.Items.Add<ActionCopy>(ui.Text("actions", ActionCopy.Instance.Alias));
+1
View File
@@ -465,6 +465,7 @@
<Compile Include="Models\Mapping\PropertyTypeGroupResolver.cs" />
<Compile Include="Security\Identity\PreviewAuthenticationMiddleware.cs" />
<Compile Include="SingletonHttpContextAccessor.cs" />
<Compile Include="Trees\ContentBlueprintTreeController.cs" />
<Compile Include="Trees\ContentTypeTreeController.cs" />
<Compile Include="Trees\PackagesTreeController.cs" />
<Compile Include="Trees\MediaTypeTreeController.cs" />
@@ -343,5 +343,24 @@ namespace Umbraco.Web
{
return url.SurfaceAction(action, typeof (T), additionalRouteVals);
}
/// <summary>
/// Generates a Absolute Media Item URL based on the current context
/// </summary>
/// <param name="urlHelper"></param>
/// <param name="mediaItem"></param>
/// <returns></returns>
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;
}
}
}
@@ -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";
}
}
}
+1
View File
@@ -183,6 +183,7 @@
<Compile Include="Actions\ActionBrowse.cs" />
<Compile Include="Actions\ActionChangeDocType.cs" />
<Compile Include="Actions\ActionCopy.cs" />
<Compile Include="Actions\ActionCreateBlueprintFromContent.cs" />
<Compile Include="Actions\ActionDelete.cs" />
<Compile Include="Actions\ActionDisable.cs" />
<Compile Include="Actions\ActionEmptyTranscan.cs" />