Align template API with dictionary API (#13714)

* Align the template services and API with the dictionary ones (use attempt pattern)

* A little controller clean-up

* Mimic old file service behavior, make unit tests happy

* Align CreateForContentTypeAsync return value with the rest of the TemplateService API

* Scaffold endpoint should no longer feature master templates

* Update the OpenAPI JSON
This commit is contained in:
Kenn Jacobsen
2023-01-31 12:20:46 +01:00
committed by GitHub
parent 5f8ef4cfff
commit ac8cfcf634
15 changed files with 604 additions and 197 deletions

View File

@@ -24,7 +24,7 @@ public class ByKeyTemplateController : TemplateControllerBase
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<TemplateViewModel>> ByKey(Guid key)
{
ITemplate? template = await _templateService.GetTemplateAsync(key);
ITemplate? template = await _templateService.GetAsync(key);
return template == null
? NotFound()
: Ok(_umbracoMapper.Map<TemplateViewModel>(template));

View File

@@ -1,10 +1,11 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Template;
@@ -12,32 +13,30 @@ public class CreateTemplateController : TemplateControllerBase
{
private readonly ITemplateService _templateService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly ITemplateContentParserService _templateContentParserService;
public CreateTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ITemplateContentParserService templateContentParserService)
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_templateService = templateService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_templateContentParserService = templateContentParserService;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Create(TemplateCreateModel createModel)
public async Task<IActionResult> Create(TemplateCreateModel createModel)
{
ITemplate? template = await _templateService.CreateTemplateWithIdentityAsync(
Attempt<ITemplate, TemplateOperationStatus> result = await _templateService.CreateAsync(
createModel.Name,
createModel.Alias,
createModel.Content,
CurrentUserId(_backOfficeSecurityAccessor));
return template == null
? NotFound()
: CreatedAtAction<ByKeyTemplateController>(controller => nameof(controller.ByKey), template.Key);
return result.Success
? CreatedAtAction<ByKeyTemplateController>(controller => nameof(controller.ByKey), result.Result.Key)
: TemplateOperationStatusResult(result.Status);
}
}

View File

@@ -1,7 +1,10 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Template;
@@ -19,9 +22,13 @@ public class DeleteTemplateController : TemplateControllerBase
[HttpDelete("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Delete(Guid key)
=> await _templateService.DeleteTemplateAsync(key, CurrentUserId(_backOfficeSecurityAccessor))
public async Task<IActionResult> Delete(Guid key)
{
Attempt<ITemplate?, TemplateOperationStatus> result = await _templateService.DeleteAsync(key, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: NotFound();
: TemplateOperationStatusResult(result.Status);
}
}

View File

@@ -16,11 +16,11 @@ public class ScaffoldTemplateController : TemplateControllerBase
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(TemplateScaffoldViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<TemplateScaffoldViewModel>> Scaffold(string? masterTemplateAlias = null)
public async Task<ActionResult<TemplateScaffoldViewModel>> Scaffold()
{
var scaffoldViewModel = new TemplateScaffoldViewModel
{
Content = _defaultViewContentProvider.GetDefaultFileContent(masterTemplateAlias)
Content = _defaultViewContentProvider.GetDefaultFileContent()
};
return await Task.FromResult(Ok(scaffoldViewModel));

View File

@@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Template;
@@ -10,4 +13,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template;
[ApiVersion("1.0")]
public class TemplateControllerBase : ManagementApiControllerBase
{
protected IActionResult TemplateOperationStatusResult(TemplateOperationStatus status) =>
status switch
{
TemplateOperationStatus.TemplateNotFound => NotFound("The template could not be found"),
TemplateOperationStatus.InvalidAlias => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid alias")
.WithDetail("The template alias is not valid.")
.Build()),
TemplateOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the template operation.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown template operation status")
};
}

View File

@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Template;
@@ -27,18 +29,22 @@ public class UpdateTemplateController : TemplateControllerBase
[HttpPut("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Update(Guid key, TemplateUpdateModel updateModel)
public async Task<IActionResult> Update(Guid key, TemplateUpdateModel updateModel)
{
ITemplate? template = await _templateService.GetTemplateAsync(key);
ITemplate? template = await _templateService.GetAsync(key);
if (template == null)
{
return NotFound();
}
template = _umbracoMapper.Map(updateModel, template);
await _templateService.SaveTemplateAsync(template, CurrentUserId(_backOfficeSecurityAccessor));
return Ok();
Attempt<ITemplate, TemplateOperationStatus> result = await _templateService.UpdateAsync(template, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success ?
Ok()
: TemplateOperationStatusResult(result.Status);
}
}

View File

@@ -4337,6 +4337,16 @@
"201": {
"description": "Created"
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
@@ -4410,6 +4420,16 @@
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
@@ -4451,6 +4471,16 @@
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
@@ -4519,15 +4549,6 @@
"Template"
],
"operationId": "GetTemplateScaffold",
"parameters": [
{
"name": "masterTemplateAlias",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",

View File

@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
using File = System.IO.File;
@@ -32,7 +33,7 @@ public class FileService : RepositoryService, IFileService
private readonly IScriptRepository _scriptRepository;
private readonly IStylesheetRepository _stylesheetRepository;
private readonly ITemplateService _templateService;
private readonly IShortStringHelper _shortStringHelper;
private readonly ITemplateRepository _templateRepository;
[Obsolete("Use other ctor - will be removed in Umbraco 15")]
public FileService(
@@ -77,9 +78,9 @@ public class FileService : RepositoryService, IFileService
IAuditRepository auditRepository,
IHostingEnvironment hostingEnvironment,
ITemplateService templateService,
ITemplateRepository templateRepository,
// unused dependencies but the DI forces us to have them, otherwise we'll get an "ambiguous constructor"
// exception (and [ActivatorUtilitiesConstructor] doesn't work here either)
ITemplateRepository templateRepository,
IShortStringHelper shortStringHelper,
IOptions<GlobalSettings> globalSettings)
: base(uowProvider, loggerFactory, eventMessagesFactory)
@@ -91,7 +92,7 @@ public class FileService : RepositoryService, IFileService
_auditRepository = auditRepository;
_hostingEnvironment = hostingEnvironment;
_templateService = templateService;
_shortStringHelper = shortStringHelper;
_templateRepository = templateRepository;
}
#region Stylesheets
@@ -372,7 +373,21 @@ public class FileService : RepositoryService, IFileService
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public Attempt<OperationResult<OperationResultType, ITemplate>?> CreateTemplateForContentType(
string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId)
=> _templateService.CreateTemplateForContentTypeAsync(contentTypeAlias, contentTypeName, userId).GetAwaiter().GetResult();
{
// mimic old service behavior
if (contentTypeAlias.Length > 255)
{
throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
}
Attempt<ITemplate, TemplateOperationStatus> result = _templateService.CreateForContentTypeAsync(contentTypeAlias, contentTypeName, userId).GetAwaiter().GetResult();
// mimic old service behavior
EventMessages eventMessages = EventMessagesFactory.Get();
return result.Success
? OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, result.Result)
: OperationResult.Attempt.Succeed(OperationResultType.Failed, eventMessages, result.Result);
}
/// <summary>
/// Create a new template, setting the content if a view exists in the filesystem
@@ -390,8 +405,18 @@ public class FileService : RepositoryService, IFileService
string? content,
ITemplate? masterTemplate = null,
int userId = Constants.Security.SuperUserId)
=> _templateService.CreateTemplateWithIdentityAsync(name, alias, content, userId).GetAwaiter().GetResult()
?? new Template(_shortStringHelper, name, alias);
{
// mimic old service behavior
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(alias);
if (name.Length > 255)
{
throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
}
Attempt<ITemplate, TemplateOperationStatus> result = _templateService.CreateAsync(name, alias, content, userId).GetAwaiter().GetResult();
return result.Result;
}
/// <summary>
/// Gets a list of all <see cref="ITemplate" /> objects
@@ -399,7 +424,7 @@ public class FileService : RepositoryService, IFileService
/// <returns>An enumerable list of <see cref="ITemplate" /> objects</returns>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public IEnumerable<ITemplate> GetTemplates(params string[] aliases)
=> _templateService.GetTemplatesAsync(aliases).GetAwaiter().GetResult();
=> _templateService.GetAllAsync(aliases).GetAwaiter().GetResult();
/// <summary>
/// Gets a list of all <see cref="ITemplate" /> objects
@@ -407,7 +432,7 @@ public class FileService : RepositoryService, IFileService
/// <returns>An enumerable list of <see cref="ITemplate" /> objects</returns>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public IEnumerable<ITemplate> GetTemplates(int masterTemplateId)
=> _templateService.GetTemplatesAsync(masterTemplateId).GetAwaiter().GetResult();
=> _templateService.GetChildrenAsync(masterTemplateId).GetAwaiter().GetResult();
/// <summary>
/// Gets a <see cref="ITemplate" /> object by its alias.
@@ -416,7 +441,7 @@ public class FileService : RepositoryService, IFileService
/// <returns>The <see cref="ITemplate" /> object matching the alias, or null.</returns>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public ITemplate? GetTemplate(string? alias)
=> _templateService.GetTemplateAsync(alias).GetAwaiter().GetResult();
=> _templateService.GetAsync(alias).GetAwaiter().GetResult();
/// <summary>
/// Gets a <see cref="ITemplate" /> object by its identifier.
@@ -425,7 +450,7 @@ public class FileService : RepositoryService, IFileService
/// <returns>The <see cref="ITemplate" /> object matching the identifier, or null.</returns>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public ITemplate? GetTemplate(int id)
=> _templateService.GetTemplateAsync(id).GetAwaiter().GetResult();
=> _templateService.GetAsync(id).GetAwaiter().GetResult();
/// <summary>
/// Gets a <see cref="ITemplate" /> object by its guid identifier.
@@ -434,7 +459,7 @@ public class FileService : RepositoryService, IFileService
/// <returns>The <see cref="ITemplate" /> object matching the identifier, or null.</returns>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public ITemplate? GetTemplate(Guid id)
=> _templateService.GetTemplateAsync(id).GetAwaiter().GetResult();
=> _templateService.GetAsync(id).GetAwaiter().GetResult();
/// <summary>
/// Gets the template descendants
@@ -443,7 +468,7 @@ public class FileService : RepositoryService, IFileService
/// <returns></returns>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public IEnumerable<ITemplate> GetTemplateDescendants(int masterTemplateId)
=> _templateService.GetTemplateDescendantsAsync(masterTemplateId).GetAwaiter().GetResult();
=> _templateService.GetDescendantsAsync(masterTemplateId).GetAwaiter().GetResult();
/// <summary>
/// Saves a <see cref="Template" />
@@ -452,16 +477,62 @@ public class FileService : RepositoryService, IFileService
/// <param name="userId"></param>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId)
=> _templateService.SaveTemplateAsync(template, userId).GetAwaiter().GetResult();
{
// mimic old service behavior
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255)
{
throw new InvalidOperationException(
"Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length.");
}
if (template.Id > 0)
{
_templateService.UpdateAsync(template, userId).GetAwaiter().GetResult();
}
else
{
_templateService.CreateAsync(template, userId).GetAwaiter().GetResult();
}
}
/// <summary>
/// Saves a collection of <see cref="Template" /> objects
/// </summary>
/// <param name="templates">List of <see cref="Template" /> to save</param>
/// <param name="userId">Optional id of the user</param>
// FIXME: we need to re-implement PackageDataInstallation.ImportTemplates so it imports templates in the correct order
// instead of relying on being able to save invalid templates (child templates whose master has yet to be created)
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public void SaveTemplate(IEnumerable<ITemplate> templates, int userId = Constants.Security.SuperUserId)
=> _templateService.SaveTemplateAsync(templates, userId).GetAwaiter().GetResult();
{
ITemplate[] templatesA = templates.ToArray();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new TemplateSavingNotification(templatesA, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return;
}
foreach (ITemplate template in templatesA)
{
_templateRepository.Save(template);
}
scope.Notifications.Publish(
new TemplateSavedNotification(templatesA, eventMessages).WithStateFrom(savingNotification));
Audit(AuditType.Save, userId, -1, UmbracoObjectTypes.Template.GetName());
scope.Complete();
}
}
/// <summary>
/// Deletes a template by its alias
@@ -470,22 +541,22 @@ public class FileService : RepositoryService, IFileService
/// <param name="userId"></param>
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId)
=> _templateService.DeleteTemplateAsync(alias, userId).GetAwaiter().GetResult();
=> _templateService.DeleteAsync(alias, userId).GetAwaiter().GetResult();
/// <inheritdoc />
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public Stream GetTemplateFileContentStream(string filepath)
=> _templateService.GetTemplateFileContentStreamAsync(filepath).GetAwaiter().GetResult();
=> _templateService.GetFileContentStreamAsync(filepath).GetAwaiter().GetResult();
/// <inheritdoc />
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public void SetTemplateFileContent(string filepath, Stream content)
=> _templateService.SetTemplateFileContentAsync(filepath, content).GetAwaiter().GetResult();
=> _templateService.SetFileContentAsync(filepath, content).GetAwaiter().GetResult();
/// <inheritdoc />
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public long GetTemplateFileSize(string filepath)
=> _templateService.GetTemplateFileSizeAsync(filepath).GetAwaiter().GetResult();
=> _templateService.GetFileSizeAsync(filepath).GetAwaiter().GetResult();
#endregion

View File

@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
@@ -8,49 +9,49 @@ public interface ITemplateService : IService
/// Gets a list of all <see cref="ITemplate" /> objects
/// </summary>
/// <returns>An enumerable list of <see cref="ITemplate" /> objects</returns>
Task<IEnumerable<ITemplate>> GetTemplatesAsync(params string[] aliases);
Task<IEnumerable<ITemplate>> GetAllAsync(params string[] aliases);
/// <summary>
/// Gets a list of all <see cref="ITemplate" /> objects
/// </summary>
/// <returns>An enumerable list of <see cref="ITemplate" /> objects</returns>
Task<IEnumerable<ITemplate>> GetTemplatesAsync(int masterTemplateId);
Task<IEnumerable<ITemplate>> GetChildrenAsync(int masterTemplateId);
/// <summary>
/// Gets a <see cref="ITemplate" /> object by its alias.
/// </summary>
/// <param name="alias">The alias of the template.</param>
/// <returns>The <see cref="ITemplate" /> object matching the alias, or null.</returns>
Task<ITemplate?> GetTemplateAsync(string? alias);
Task<ITemplate?> GetAsync(string? alias);
/// <summary>
/// Gets a <see cref="ITemplate" /> object by its identifier.
/// </summary>
/// <param name="id">The identifier of the template.</param>
/// <returns>The <see cref="ITemplate" /> object matching the identifier, or null.</returns>
Task<ITemplate?> GetTemplateAsync(int id);
Task<ITemplate?> GetAsync(int id);
/// <summary>
/// Gets a <see cref="ITemplate" /> object by its guid identifier.
/// </summary>
/// <param name="id">The guid identifier of the template.</param>
/// <returns>The <see cref="ITemplate" /> object matching the identifier, or null.</returns>
Task<ITemplate?> GetTemplateAsync(Guid id);
Task<ITemplate?> GetAsync(Guid id);
/// <summary>
/// Gets the template descendants
/// </summary>
/// <param name="masterTemplateId"></param>
/// <returns></returns>
Task<IEnumerable<ITemplate>> GetTemplateDescendantsAsync(int masterTemplateId);
Task<IEnumerable<ITemplate>> GetDescendantsAsync(int masterTemplateId);
/// <summary>
/// Saves a <see cref="ITemplate" />
/// Updates a <see cref="ITemplate" />
/// </summary>
/// <param name="template"><see cref="ITemplate" /> to save</param>
/// <param name="template"><see cref="ITemplate" /> to update</param>
/// <param name="userId">Optional id of the user saving the template</param>
/// <returns>True if the template was saved, false otherwise.</returns>
Task<bool> SaveTemplateAsync(ITemplate template, int userId = Constants.Security.SuperUserId);
/// <returns></returns>
Task<Attempt<ITemplate, TemplateOperationStatus>> UpdateAsync(ITemplate template, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Creates a template for a content type
@@ -61,12 +62,28 @@ public interface ITemplateService : IService
/// <returns>
/// The template created
/// </returns>
Task<Attempt<OperationResult<OperationResultType, ITemplate>?>> CreateTemplateForContentTypeAsync(
Task<Attempt<ITemplate, TemplateOperationStatus>> CreateForContentTypeAsync(
string contentTypeAlias,
string? contentTypeName,
int userId = Constants.Security.SuperUserId);
Task<ITemplate?> CreateTemplateWithIdentityAsync(string? name, string? alias, string? content, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Creates a new template
/// </summary>
/// <param name="name">Name of the new template</param>
/// <param name="alias">Alias of the template</param>
/// <param name="content">View content for the new template</param>
/// <param name="userId">Optional id of the user creating the template</param>
/// <returns></returns>
Task<Attempt<ITemplate, TemplateOperationStatus>> CreateAsync(string name, string alias, string? content, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Creates a new template
/// </summary>
/// <param name="template">The new template</param>
/// <param name="userId">Optional id of the user creating the template</param>
/// <returns></returns>
Task<Attempt<ITemplate, TemplateOperationStatus>> CreateAsync(ITemplate template, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Deletes a template by its alias
@@ -74,7 +91,7 @@ public interface ITemplateService : IService
/// <param name="alias">Alias of the <see cref="ITemplate" /> to delete</param>
/// <param name="userId">Optional id of the user deleting the template</param>
/// <returns>True if the template was deleted, false otherwise</returns>
Task<bool> DeleteTemplateAsync(string alias, int userId = Constants.Security.SuperUserId);
Task<Attempt<ITemplate?, TemplateOperationStatus>> DeleteAsync(string alias, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Deletes a template by its key
@@ -82,34 +99,26 @@ public interface ITemplateService : IService
/// <param name="key">Key of the <see cref="ITemplate" /> to delete</param>
/// <param name="userId">Optional id of the user deleting the template</param>
/// <returns>True if the template was deleted, false otherwise</returns>
Task<bool> DeleteTemplateAsync(Guid key, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Saves a collection of <see cref="Template" /> objects
/// </summary>
/// <param name="templates">List of <see cref="Template" /> to save</param>
/// <param name="userId">Optional id of the user</param>
/// <returns>True if the templates were saved, false otherwise</returns>
Task<bool> SaveTemplateAsync(IEnumerable<ITemplate> templates, int userId = Constants.Security.SuperUserId);
Task<Attempt<ITemplate?, TemplateOperationStatus>> DeleteAsync(Guid key, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Gets the content of a template as a stream.
/// </summary>
/// <param name="filepath">The filesystem path to the template.</param>
/// <returns>The content of the template.</returns>
Task<Stream> GetTemplateFileContentStreamAsync(string filepath);
Task<Stream> GetFileContentStreamAsync(string filepath);
/// <summary>
/// Sets the content of a template.
/// </summary>
/// <param name="filepath">The filesystem path to the template.</param>
/// <param name="content">The content of the template.</param>
Task SetTemplateFileContentAsync(string filepath, Stream content);
Task SetFileContentAsync(string filepath, Stream content);
/// <summary>
/// Gets the size of a template.
/// </summary>
/// <param name="filepath">The filesystem path to the template.</param>
/// <returns>The size of the template.</returns>
Task<long> GetTemplateFileSizeAsync(string filepath);
Task<long> GetFileSizeAsync(string filepath);
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum TemplateOperationStatus
{
Success,
CancelledByNotification,
InvalidAlias,
TemplateNotFound,
MasterTemplateNotFound
}

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -34,10 +35,10 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<Attempt<OperationResult<OperationResultType, ITemplate>?>> CreateTemplateForContentTypeAsync(
public async Task<Attempt<ITemplate, TemplateOperationStatus>> CreateForContentTypeAsync(
string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId)
{
var template = new Template(_shortStringHelper, contentTypeName,
ITemplate template = new Template(_shortStringHelper, contentTypeName,
// NOTE: We are NOT passing in the content type alias here, we want to use it's name since we don't
// want to save template file names as camelCase, the Template ctor will clean the alias as
@@ -45,13 +46,13 @@ public class TemplateService : RepositoryService, ITemplateService
// This fixes: http://issues.umbraco.org/issue/U4-7953
contentTypeName);
EventMessages eventMessages = EventMessagesFactory.Get();
if (contentTypeAlias != null && contentTypeAlias.Length > 255)
if (IsValidAlias(template.Alias) == false)
{
throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
return Attempt.FailWithStatus(TemplateOperationStatus.InvalidAlias, template);
}
EventMessages eventMessages = EventMessagesFactory.Get();
// check that the template hasn't been created on disk before creating the content type
// if it exists, set the new template content to the existing file content
var content = GetViewContent(contentTypeAlias);
@@ -66,56 +67,46 @@ public class TemplateService : RepositoryService, ITemplateService
if (scope.Notifications.PublishCancelable(savingEvent))
{
scope.Complete();
return OperationResult.Attempt.Fail<OperationResultType, ITemplate>(
OperationResultType.FailedCancelledByEvent, eventMessages, template);
return Attempt.FailWithStatus(TemplateOperationStatus.CancelledByNotification, template);
}
_templateRepository.Save(template);
scope.Notifications.Publish(
new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingEvent));
Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
Audit(AuditType.New, userId, template.Id, UmbracoObjectTypes.Template.GetName());
scope.Complete();
}
return await Task.FromResult(OperationResult.Attempt.Succeed<OperationResultType, ITemplate>(
OperationResultType.Success,
eventMessages,
template));
return await Task.FromResult(Attempt.SucceedWithStatus(TemplateOperationStatus.Success, template));
}
/// <inheritdoc />
public async Task<ITemplate?> CreateTemplateWithIdentityAsync(
string? name,
string? alias,
public async Task<Attempt<ITemplate, TemplateOperationStatus>> CreateAsync(
string name,
string alias,
string? content,
int userId = Constants.Security.SuperUserId)
=> await CreateAsync(new Template(_shortStringHelper, name, alias) { Content = content }, userId);
/// <inheritdoc />
public async Task<Attempt<ITemplate, TemplateOperationStatus>> CreateAsync(ITemplate template, int userId = Constants.Security.SuperUserId)
{
if (name == null)
try
{
throw new ArgumentNullException(nameof(name));
// file might already be on disk, if so grab the content to avoid overwriting
template.Content = GetViewContent(template.Alias) ?? template.Content;
return await SaveAsync(template, AuditType.New, userId);
}
if (string.IsNullOrWhiteSpace(name))
catch (PathTooLongException ex)
{
throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name));
LoggerFactory.CreateLogger<TemplateService>().LogError(ex, "The template path was too long. Consider making the template alias shorter.");
return Attempt.FailWithStatus(TemplateOperationStatus.InvalidAlias, template);
}
if (name.Length > 255)
{
throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
}
// file might already be on disk, if so grab the content to avoid overwriting
var template = new Template(_shortStringHelper, name, alias) { Content = GetViewContent(alias) ?? content };
return await SaveTemplateAsync(template, userId)
? template
: null;
}
/// <inheritdoc />
public async Task<IEnumerable<ITemplate>> GetTemplatesAsync(params string[] aliases)
public async Task<IEnumerable<ITemplate>> GetAllAsync(params string[] aliases)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -124,7 +115,7 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<IEnumerable<ITemplate>> GetTemplatesAsync(int masterTemplateId)
public async Task<IEnumerable<ITemplate>> GetChildrenAsync(int masterTemplateId)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -133,7 +124,7 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<ITemplate?> GetTemplateAsync(string? alias)
public async Task<ITemplate?> GetAsync(string? alias)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -142,7 +133,7 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<ITemplate?> GetTemplateAsync(int id)
public async Task<ITemplate?> GetAsync(int id)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -151,7 +142,7 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<ITemplate?> GetTemplateAsync(Guid id)
public async Task<ITemplate?> GetAsync(Guid id)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -161,7 +152,7 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<IEnumerable<ITemplate>> GetTemplateDescendantsAsync(int masterTemplateId)
public async Task<IEnumerable<ITemplate>> GetDescendantsAsync(int masterTemplateId)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -170,22 +161,42 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<bool> SaveTemplateAsync(ITemplate template, int userId = Constants.Security.SuperUserId)
{
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
public async Task<Attempt<ITemplate, TemplateOperationStatus>> UpdateAsync(ITemplate template, int userId = Constants.Security.SuperUserId)
=> await SaveAsync(
template,
AuditType.Save,
userId,
// fail the attempt if the template does not exist within the scope
() => _templateRepository.Exists(template.Id)
? TemplateOperationStatus.Success
: TemplateOperationStatus.TemplateNotFound);
if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255)
private async Task<Attempt<ITemplate, TemplateOperationStatus>> SaveAsync(ITemplate template, AuditType auditType, int userId = Constants.Security.SuperUserId, Func<TemplateOperationStatus>? scopeValidator = null)
{
if (IsValidAlias(template.Alias) == false)
{
throw new InvalidOperationException(
"Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length.");
return Attempt.FailWithStatus(TemplateOperationStatus.InvalidAlias, template);
}
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
ITemplate? masterTemplate = await GetMasterTemplate(template.Content);
TemplateOperationStatus scopeValidatorStatus = scopeValidator?.Invoke() ?? TemplateOperationStatus.Success;
if (scopeValidatorStatus != TemplateOperationStatus.Success)
{
return Attempt.FailWithStatus(scopeValidatorStatus, template);
}
var masterTemplateAlias = _templateContentParserService.MasterTemplateAlias(template.Content);
ITemplate? masterTemplate = masterTemplateAlias.IsNullOrWhiteSpace()
? null
: await GetAsync(masterTemplateAlias);
// fail if the template content specifies a master template but said template does not exist
if (masterTemplateAlias.IsNullOrWhiteSpace() == false && masterTemplate == null)
{
return Attempt.FailWithStatus(TemplateOperationStatus.MasterTemplateNotFound, template);
}
await SetMasterTemplateAsync(template, masterTemplate);
EventMessages eventMessages = EventMessagesFactory.Get();
@@ -193,7 +204,7 @@ public class TemplateService : RepositoryService, ITemplateService
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return false;
return Attempt.FailWithStatus(TemplateOperationStatus.CancelledByNotification, template);
}
_templateRepository.Save(template);
@@ -201,50 +212,22 @@ public class TemplateService : RepositoryService, ITemplateService
scope.Notifications.Publish(
new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingNotification));
Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
Audit(auditType, userId, template.Id, UmbracoObjectTypes.Template.GetName());
scope.Complete();
return true;
return Attempt.SucceedWithStatus(TemplateOperationStatus.Success, template);
}
}
/// <inheritdoc />
public async Task<bool> SaveTemplateAsync(IEnumerable<ITemplate> templates, int userId = Constants.Security.SuperUserId)
{
ITemplate[] templatesA = templates.ToArray();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new TemplateSavingNotification(templatesA, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return false;
}
foreach (ITemplate template in templatesA)
{
_templateRepository.Save(template);
}
scope.Notifications.Publish(
new TemplateSavedNotification(templatesA, eventMessages).WithStateFrom(savingNotification));
Audit(AuditType.Save, userId, -1, UmbracoObjectTypes.Template.GetName());
scope.Complete();
return await Task.FromResult(true);
}
}
public async Task<Attempt<ITemplate?, TemplateOperationStatus>> DeleteAsync(string alias, int userId = Constants.Security.SuperUserId)
=> await DeleteAsync(() => Task.FromResult(_templateRepository.Get(alias)), userId);
/// <inheritdoc />
public async Task<bool> DeleteTemplateAsync(string alias, int userId = Constants.Security.SuperUserId)
=> await DeleteTemplateAsync(() => Task.FromResult(_templateRepository.Get(alias)), userId);
public async Task<Attempt<ITemplate?, TemplateOperationStatus>> DeleteAsync(Guid key, int userId = Constants.Security.SuperUserId)
=> await DeleteAsync(async () => await GetAsync(key), userId);
/// <inheritdoc />
public async Task<bool> DeleteTemplateAsync(Guid key, int userId = Constants.Security.SuperUserId)
=> await DeleteTemplateAsync(async () => await GetTemplateAsync(key), userId);
/// <inheritdoc />
public async Task<Stream> GetTemplateFileContentStreamAsync(string filepath)
public async Task<Stream> GetFileContentStreamAsync(string filepath)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -253,7 +236,7 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task SetTemplateFileContentAsync(string filepath, Stream content)
public async Task SetFileContentAsync(string filepath, Stream content)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
@@ -264,7 +247,7 @@ public class TemplateService : RepositoryService, ITemplateService
}
/// <inheritdoc />
public async Task<long> GetTemplateFileSizeAsync(string filepath)
public async Task<long> GetFileSizeAsync(string filepath)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
@@ -272,21 +255,6 @@ public class TemplateService : RepositoryService, ITemplateService
}
}
private async Task<ITemplate?> GetMasterTemplate(string? content)
{
var masterTemplateAlias = _templateContentParserService.MasterTemplateAlias(content);
ITemplate? masterTemplate = masterTemplateAlias.IsNullOrWhiteSpace()
? null
: await GetTemplateAsync(masterTemplateAlias);
if (masterTemplateAlias.IsNullOrWhiteSpace() == false && masterTemplate == null)
{
throw new ArgumentException($"Could not find master template with alias: {masterTemplateAlias}", content);
}
return masterTemplate;
}
/// <inheritdoc />
private async Task SetMasterTemplateAsync(ITemplate template, ITemplate? masterTemplate)
{
@@ -308,7 +276,7 @@ public class TemplateService : RepositoryService, ITemplateService
//After updating the master - ensure we update the path property if it has any children already assigned
if (template.Id > 0)
{
IEnumerable<ITemplate> templateHasChildren = await GetTemplateDescendantsAsync(template.Id);
IEnumerable<ITemplate> templateHasChildren = await GetDescendantsAsync(template.Id);
foreach (ITemplate childTemplate in templateHasChildren)
{
@@ -331,7 +299,7 @@ public class TemplateService : RepositoryService, ITemplateService
childTemplate.Path = masterTemplate.Path + "," + template.Id + "," + childTemplatePath;
//Save the children with the updated path
await SaveTemplateAsync(childTemplate);
await UpdateAsync(childTemplate);
}
}
}
@@ -366,7 +334,7 @@ public class TemplateService : RepositoryService, ITemplateService
private void Audit(AuditType type, int userId, int objectId, string? entityType) =>
_auditRepository.Save(new AuditItem(objectId, type, userId, entityType));
private async Task<bool> DeleteTemplateAsync(Func<Task<ITemplate?>> getTemplate, int userId)
private async Task<Attempt<ITemplate?, TemplateOperationStatus>> DeleteAsync(Func<Task<ITemplate?>> getTemplate, int userId)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
@@ -374,7 +342,7 @@ public class TemplateService : RepositoryService, ITemplateService
if (template == null)
{
scope.Complete();
return true;
return Attempt.FailWithStatus<ITemplate?, TemplateOperationStatus>(TemplateOperationStatus.TemplateNotFound, null);
}
EventMessages eventMessages = EventMessagesFactory.Get();
@@ -382,7 +350,7 @@ public class TemplateService : RepositoryService, ITemplateService
if (scope.Notifications.PublishCancelable(deletingNotification))
{
scope.Complete();
return false;
return Attempt.FailWithStatus<ITemplate?, TemplateOperationStatus>(TemplateOperationStatus.CancelledByNotification, template);
}
_templateRepository.Delete(template);
@@ -392,7 +360,10 @@ public class TemplateService : RepositoryService, ITemplateService
Audit(AuditType.Delete, userId, template.Id, UmbracoObjectTypes.Template.GetName());
scope.Complete();
return await Task.FromResult(true);
return Attempt.SucceedWithStatus<ITemplate?, TemplateOperationStatus>(TemplateOperationStatus.Success, template);
}
}
private static bool IsValidAlias(string alias)
=> alias.IsNullOrWhiteSpace() == false && alias.Length <= 255;
}

View File

@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
@@ -61,7 +62,7 @@ public class TemplateController : BackOfficeNotificationsController
/// <returns></returns>
public TemplateDisplay? GetByAlias(string alias)
{
ITemplate? template = _templateService.GetTemplateAsync(alias).GetAwaiter().GetResult();
ITemplate? template = _templateService.GetAsync(alias).GetAwaiter().GetResult();
return template == null ? null : _umbracoMapper.Map<ITemplate, TemplateDisplay>(template);
}
@@ -69,7 +70,7 @@ public class TemplateController : BackOfficeNotificationsController
/// Get all templates
/// </summary>
/// <returns></returns>
public IEnumerable<EntityBasic>? GetAll() => _templateService.GetTemplatesAsync().GetAwaiter().GetResult()
public IEnumerable<EntityBasic>? GetAll() => _templateService.GetAllAsync().GetAwaiter().GetResult()
?.Select(_umbracoMapper.Map<ITemplate, EntityBasic>).WhereNotNull();
/// <summary>
@@ -79,7 +80,7 @@ public class TemplateController : BackOfficeNotificationsController
/// <returns></returns>
public ActionResult<TemplateDisplay?> GetById(int id)
{
ITemplate? template = _templateService.GetTemplateAsync(id).GetAwaiter().GetResult();
ITemplate? template = _templateService.GetAsync(id).GetAwaiter().GetResult();
if (template == null)
{
return NotFound();
@@ -96,7 +97,7 @@ public class TemplateController : BackOfficeNotificationsController
/// <returns></returns>
public ActionResult<TemplateDisplay?> GetById(Guid id)
{
ITemplate? template = _templateService.GetTemplateAsync(id).GetAwaiter().GetResult();
ITemplate? template = _templateService.GetAsync(id).GetAwaiter().GetResult();
if (template == null)
{
return NotFound();
@@ -118,7 +119,7 @@ public class TemplateController : BackOfficeNotificationsController
return NotFound();
}
ITemplate? template = _templateService.GetTemplateAsync(guidUdi.Guid).GetAwaiter().GetResult();
ITemplate? template = _templateService.GetAsync(guidUdi.Guid).GetAwaiter().GetResult();
if (template == null)
{
return NotFound();
@@ -136,13 +137,13 @@ public class TemplateController : BackOfficeNotificationsController
[HttpPost]
public IActionResult DeleteById(int id)
{
ITemplate? template = _templateService.GetTemplateAsync(id).GetAwaiter().GetResult();
ITemplate? template = _templateService.GetAsync(id).GetAwaiter().GetResult();
if (template == null)
{
return NotFound();
}
_templateService.DeleteTemplateAsync(template.Alias).GetAwaiter().GetResult();
_templateService.DeleteAsync(template.Alias).GetAwaiter().GetResult();
return Ok();
}
@@ -156,7 +157,7 @@ public class TemplateController : BackOfficeNotificationsController
if (id > 0)
{
ITemplate? master = _templateService.GetTemplateAsync(id).GetAwaiter().GetResult();
ITemplate? master = _templateService.GetAsync(id).GetAwaiter().GetResult();
if (master != null)
{
dt.SetMasterTemplate(master);
@@ -190,7 +191,7 @@ public class TemplateController : BackOfficeNotificationsController
if (display.Id > 0)
{
// update
ITemplate? template = _templateService.GetTemplateAsync(display.Id).GetAwaiter().GetResult();
ITemplate? template = _templateService.GetAsync(display.Id).GetAwaiter().GetResult();
if (template == null)
{
return NotFound();
@@ -200,11 +201,11 @@ public class TemplateController : BackOfficeNotificationsController
_umbracoMapper.Map(display, template);
_templateService.SaveTemplateAsync(template).GetAwaiter().GetResult();
_templateService.UpdateAsync(template).GetAwaiter().GetResult();
if (changeAlias)
{
template = _templateService.GetTemplateAsync(template.Id).GetAwaiter().GetResult();
template = _templateService.GetAsync(template.Id).GetAwaiter().GetResult();
}
_umbracoMapper.Map(template, display);
@@ -215,7 +216,7 @@ public class TemplateController : BackOfficeNotificationsController
ITemplate? master = null;
if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false)
{
master = _templateService.GetTemplateAsync(display.MasterTemplateAlias).GetAwaiter().GetResult();
master = _templateService.GetAsync(display.MasterTemplateAlias).GetAwaiter().GetResult();
if (master == null)
{
return NotFound();
@@ -224,14 +225,14 @@ public class TemplateController : BackOfficeNotificationsController
// we need to pass the template name as alias to keep the template file casing consistent with templates created with content
// - see comment in FileService.CreateTemplateForContentType for additional details
ITemplate? template =
_templateService.CreateTemplateWithIdentityAsync(display.Name, display.Name, display.Content).GetAwaiter().GetResult();
if (template == null)
Attempt<ITemplate, TemplateOperationStatus> result =
_templateService.CreateAsync(display.Name!, display.Name!, display.Content).GetAwaiter().GetResult();
if (result.Success == false)
{
return NotFound();
}
_umbracoMapper.Map(template, display);
_umbracoMapper.Map(result.Result, display);
}
return display;

View File

@@ -197,4 +197,14 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration);
}
}
protected void DeleteAllTemplateViewFiles()
{
var fileSystems = GetRequiredService<FileSystems>();
var viewFileSystem = fileSystems.MvcViewsFileSystem!;
foreach (var file in viewFileSystem.GetFiles(string.Empty).ToArray())
{
viewFileSystem.DeleteFile(file);
}
}
}

View File

@@ -78,6 +78,12 @@ public class PackageDataInstallationTests : UmbracoIntegrationTestWithContent
private IMediaTypeService MediaTypeService => GetRequiredService<IMediaTypeService>();
public override void CreateTestData()
{
DeleteAllTemplateViewFiles();
base.CreateTestData();
}
[Test]
public void Can_Import_uBlogsy_ContentTypes_And_Verify_Structure()
{

View File

@@ -0,0 +1,279 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class TemplateServiceTests : UmbracoIntegrationTest
{
private ITemplateService TemplateService => GetRequiredService<ITemplateService>();
[SetUp]
public void SetUp() => DeleteAllTemplateViewFiles();
[Test]
public async Task Can_Create_Template_Then_Assign_Child()
{
Attempt<ITemplate, TemplateOperationStatus> result = await TemplateService.CreateAsync("Child", "child", "test");
Assert.IsTrue(result.Success);
Assert.AreEqual(TemplateOperationStatus.Success, result.Status);
var child = result.Result;
result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
Assert.AreEqual(TemplateOperationStatus.Success, result.Status);
var parent = result.Result;
child.Content = "Layout = \"Parent.cshtml\";";
result = await TemplateService.UpdateAsync(child);
Assert.IsTrue(result.Success);
Assert.AreEqual(TemplateOperationStatus.Success, result.Status);
child = await TemplateService.GetAsync(child.Key);
Assert.NotNull(child);
Assert.AreEqual(parent.Alias, child.MasterTemplateAlias);
}
[Test]
public async Task Can_Create_Template_With_Child_Then_Unassign()
{
Attempt<ITemplate, TemplateOperationStatus> result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
var parent = result.Result;
result = await TemplateService.CreateAsync("Child", "child", "Layout = \"Parent.cshtml\";");
Assert.IsTrue(result.Success);
var child = result.Result;
child = await TemplateService.GetAsync(child.Key);
Assert.NotNull(child);
Assert.AreEqual("parent", child.MasterTemplateAlias);
child.Content = "test";
result = await TemplateService.UpdateAsync(child);
Assert.IsTrue(result.Success);
child = await TemplateService.GetAsync(child.Key);
Assert.NotNull(child);
Assert.AreEqual(null, child.MasterTemplateAlias);
}
[Test]
public async Task Can_Create_Template_With_Child_Then_Reassign()
{
Attempt<ITemplate, TemplateOperationStatus> result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
result = await TemplateService.CreateAsync("Parent2", "parent2", "test");
Assert.IsTrue(result.Success);
result = await TemplateService.CreateAsync("Child", "child", "Layout = \"Parent.cshtml\";");
Assert.IsTrue(result.Success);
var child = result.Result;
child = await TemplateService.GetAsync(child.Key);
Assert.NotNull(child);
Assert.AreEqual("parent", child.MasterTemplateAlias);
child.Content = "Layout = \"Parent2.cshtml\";";
result = await TemplateService.UpdateAsync(child);
Assert.IsTrue(result.Success);
child = await TemplateService.GetAsync(child.Key);
Assert.NotNull(child);
Assert.AreEqual("parent2", child.MasterTemplateAlias);
}
[Test]
public async Task Child_Template_Paths_Are_Updated_When_Reassigning_Master()
{
Attempt<ITemplate, TemplateOperationStatus> result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
var parent = result.Result;
result = await TemplateService.CreateAsync("Parent2", "parent2", "test");
Assert.IsTrue(result.Success);
var parent2 = result.Result;
result = await TemplateService.CreateAsync("Child", "child", "Layout = \"Parent.cshtml\";");
Assert.IsTrue(result.Success);
var child = result.Result;
result = await TemplateService.CreateAsync("Child1", "child1", "Layout = \"Child.cshtml\";");
Assert.IsTrue(result.Success);
var childOfChild1 = result.Result;
result = await TemplateService.CreateAsync("Child2", "child2", "Layout = \"Child.cshtml\";");
Assert.IsTrue(result.Success);
var childOfChild2 = result.Result;
Assert.AreEqual($"child", childOfChild1.MasterTemplateAlias);
Assert.AreEqual($"{parent.Path},{child.Id},{childOfChild1.Id}", childOfChild1.Path);
Assert.AreEqual($"child", childOfChild2.MasterTemplateAlias);
Assert.AreEqual($"{parent.Path},{child.Id},{childOfChild2.Id}", childOfChild2.Path);
child.Content = "Layout = \"Parent2.cshtml\";";
result = await TemplateService.UpdateAsync(child);
Assert.IsTrue(result.Success);
childOfChild1 = await TemplateService.GetAsync(childOfChild1.Key);
Assert.NotNull(childOfChild1);
childOfChild2 = await TemplateService.GetAsync(childOfChild2.Key);
Assert.NotNull(childOfChild2);
Assert.AreEqual($"child", childOfChild1.MasterTemplateAlias);
Assert.AreEqual($"{parent2.Path},{child.Id},{childOfChild1.Id}", childOfChild1.Path);
Assert.AreEqual($"child", childOfChild2.MasterTemplateAlias);
Assert.AreEqual($"{parent2.Path},{child.Id},{childOfChild2.Id}", childOfChild2.Path);
}
[Test]
public async Task Can_Query_Template_Children()
{
Attempt<ITemplate, TemplateOperationStatus> result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
var parent = result.Result;
result = await TemplateService.CreateAsync("Child1", "child1", "Layout = \"Parent.cshtml\";");
Assert.IsTrue(result.Success);
var child1 = result.Result;
result = await TemplateService.CreateAsync("Child2", "child2", "Layout = \"Parent.cshtml\";");
Assert.IsTrue(result.Success);
var child2 = result.Result;
var children = await TemplateService.GetChildrenAsync(parent.Id);
Assert.AreEqual(2, children.Count());
Assert.NotNull(children.FirstOrDefault(t => t.Id == child1.Id));
Assert.NotNull(children.FirstOrDefault(t => t.Id == child2.Id));
}
[Test]
public async Task Can_Update_Template()
{
var result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
var parent = result.Result;
parent.Name = "Parent Updated";
result = await TemplateService.UpdateAsync(parent);
Assert.IsTrue(result.Success);
parent = await TemplateService.GetAsync(parent.Key);
Assert.IsNotNull(parent);
Assert.AreEqual("Parent Updated", parent.Name);
Assert.AreEqual("parent", parent.Alias);
}
[Test]
public async Task Can_Delete_Template()
{
var result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
var parent = result.Result;
result = await TemplateService.DeleteAsync(parent.Key);
Assert.IsTrue(result.Success);
parent = await TemplateService.GetAsync(parent.Key);
Assert.IsNull(parent);
}
[Test]
public async Task Deleting_Master_Template_Also_Deletes_Children()
{
Attempt<ITemplate, TemplateOperationStatus> result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
var parent = result.Result;
result = await TemplateService.CreateAsync("Child", "child", "Layout = \"Parent.cshtml\";");
Assert.IsTrue(result.Success);
var child = result.Result;
Assert.AreEqual("parent", child.MasterTemplateAlias);
result = await TemplateService.DeleteAsync(parent.Key);
Assert.IsTrue(result.Success);
child = await TemplateService.GetAsync(child.Key);
Assert.Null(child);
}
[Test]
public async Task Cannot_Update_Non_Existing_Template()
{
var result = await TemplateService.CreateAsync("Parent", "parent", "test");
Assert.IsTrue(result.Success);
var parent = result.Result;
result = await TemplateService.DeleteAsync("parent");
Assert.IsTrue(result.Success);
parent.Name = "Parent Updated";
result = await TemplateService.UpdateAsync(parent);
Assert.IsFalse(result.Success);
Assert.AreEqual(TemplateOperationStatus.TemplateNotFound, result.Status);
}
[Test]
public async Task Cannot_Create_Child_Template_Without_Master_Template()
{
var result = await TemplateService.CreateAsync("Child", "child", "Layout = \"Parent.cshtml\";");
Assert.IsFalse(result.Success);
Assert.AreEqual(TemplateOperationStatus.MasterTemplateNotFound, result.Status);
}
[Test]
public async Task Cannot_Update_Child_Template_Without_Master_Template()
{
var result = await TemplateService.CreateAsync("Child", "child", "test");
Assert.IsTrue(result.Success);
var child = result.Result;
child.Content = "Layout = \"Parent.cshtml\";";
result = await TemplateService.UpdateAsync(child);
Assert.IsFalse(result.Success);
Assert.AreEqual(TemplateOperationStatus.MasterTemplateNotFound, result.Status);
}
[Test]
public async Task Cannot_Create_Template_With_Invalid_Alias()
{
var invalidAlias = new string('a', 256);
var result = await TemplateService.CreateAsync("Child", invalidAlias, "test");
Assert.IsFalse(result.Success);
Assert.AreEqual(TemplateOperationStatus.InvalidAlias, result.Status);
}
[Test]
public async Task Cannot_Update_Template_With_Invalid_Alias()
{
var result = await TemplateService.CreateAsync("Child", "child", "test");
Assert.IsTrue(result.Success);
var child = result.Result;
var invalidAlias = new string('a', 256);
child.Alias = invalidAlias;
result = await TemplateService.UpdateAsync(child);
Assert.IsFalse(result.Success);
Assert.AreEqual(TemplateOperationStatus.InvalidAlias, result.Status);
}
}