V14: Update template controllers (#14326)

* Add alias to document item response

* Add master template key to detailed model

* Add mater template key as optiona parameter to Scaffolding

* Check for duplicate alias when creating templates directly

* Clean

* Ensure integration tests creates templates with unique aliases

* Perform mapping in presentation factory
This commit is contained in:
Mole
2023-06-06 13:45:39 +02:00
committed by GitHub
parent 9947d4a15a
commit 582b784ffe
20 changed files with 168 additions and 47 deletions

View File

@@ -1,8 +1,8 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
@@ -12,12 +12,14 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template;
public class ByKeyTemplateController : TemplateControllerBase
{
private readonly ITemplateService _templateService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly ITemplatePresentationFactory _templatePresentationFactory;
public ByKeyTemplateController(ITemplateService templateService, IUmbracoMapper umbracoMapper)
public ByKeyTemplateController(
ITemplateService templateService,
ITemplatePresentationFactory templatePresentationFactory)
{
_templateService = templateService;
_umbracoMapper = umbracoMapper;
_templatePresentationFactory = templatePresentationFactory;
}
[HttpGet("{id:guid}")]
@@ -29,6 +31,6 @@ public class ByKeyTemplateController : TemplateControllerBase
ITemplate? template = await _templateService.GetAsync(id);
return template == null
? NotFound()
: Ok(_umbracoMapper.Map<TemplateResponseModel>(template));
: Ok(await _templatePresentationFactory.CreateTemplateResponseModelAsync(template));
}
}

View File

@@ -12,13 +12,13 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template.Item;
[ApiVersion("1.0")]
public class ItemTemplateItemController : TemplateItemControllerBase
{
private readonly IEntityService _entityService;
private readonly IUmbracoMapper _mapper;
private readonly ITemplateService _templateService;
public ItemTemplateItemController(IEntityService entityService, IUmbracoMapper mapper)
public ItemTemplateItemController(IUmbracoMapper mapper, ITemplateService templateService)
{
_entityService = entityService;
_mapper = mapper;
_templateService = templateService;
}
[HttpGet("item")]
@@ -26,8 +26,10 @@ public class ItemTemplateItemController : TemplateItemControllerBase
[ProducesResponseType(typeof(IEnumerable<TemplateItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Item([FromQuery(Name = "id")] SortedSet<Guid> ids)
{
IEnumerable<IEntitySlim> templates = _entityService.GetAll(UmbracoObjectTypes.Template, ids.ToArray());
List<TemplateItemResponseModel> responseModels = _mapper.MapEnumerable<IEntitySlim, TemplateItemResponseModel>(templates);
// This is far from ideal, that we pick out the entire model, however, we must do this to get the alias.
// This is (for one) needed for when specifying master template, since alias + .cshtml
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(ids.ToArray());
List<TemplateItemResponseModel> responseModels = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(templates);
return Ok(responseModels);
}
}

View File

@@ -3,26 +3,27 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Template;
[ApiVersion("1.0")]
public class ScaffoldTemplateController : TemplateControllerBase
{
private readonly IDefaultViewContentProvider _defaultViewContentProvider;
private readonly ITemplateService _templateService;
public ScaffoldTemplateController(IDefaultViewContentProvider defaultViewContentProvider)
=> _defaultViewContentProvider = defaultViewContentProvider;
public ScaffoldTemplateController(ITemplateService templateService) => _templateService = templateService;
[HttpGet("scaffold")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(TemplateScaffoldResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<TemplateScaffoldResponseModel>> Scaffold()
public async Task<ActionResult<TemplateScaffoldResponseModel>> Scaffold([FromQuery(Name = "masterTemplateId")] Guid? masterTemplateId)
{
var scaffoldViewModel = new TemplateScaffoldResponseModel
{
Content = _defaultViewContentProvider.GetDefaultFileContent()
Content = await _templateService.GetScaffoldAsync(masterTemplateId),
};
return await Task.FromResult(Ok(scaffoldViewModel));

View File

@@ -25,6 +25,10 @@ public class TemplateControllerBase : ManagementApiControllerBase
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the template operation.")
.Build()),
TemplateOperationStatus.DuplicateAlias => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Duplicate alias")
.WithDetail("A template with that alias already exists.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown template operation status")
};
}

View File

@@ -1,4 +1,6 @@
using Umbraco.Cms.Core.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Api.Management.Mapping.Template;
using Umbraco.Cms.Core.Mapping;
@@ -9,6 +11,7 @@ internal static class TemplateBuilderExtensions
internal static IUmbracoBuilder AddTemplates(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<TemplateViewModelMapDefinition>();
builder.Services.AddTransient<ITemplatePresentationFactory, TemplatePresentationFactory>();
return builder;
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Factories;
public interface ITemplatePresentationFactory
{
Task<TemplateResponseModel> CreateTemplateResponseModelAsync(ITemplate template);
}

View File

@@ -0,0 +1,39 @@
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Factories;
public class TemplatePresentationFactory : ITemplatePresentationFactory
{
private readonly ITemplateService _templateService;
private readonly IUmbracoMapper _mapper;
public TemplatePresentationFactory(
ITemplateService templateService,
IUmbracoMapper mapper)
{
_templateService = templateService;
_mapper = mapper;
}
public async Task<TemplateResponseModel> CreateTemplateResponseModelAsync(ITemplate template)
{
TemplateResponseModel responseModel = new()
{
Id = template.Key,
Name = template.Name ?? string.Empty,
Alias = template.Alias,
Content = template.Content
};
if (template.MasterTemplateAlias is not null)
{
ITemplate? parentTemplate = await _templateService.GetAsync(template.MasterTemplateAlias);
responseModel.MasterTemplateId = parentTemplate?.Key;
}
return responseModel;
}
}

View File

@@ -28,7 +28,7 @@ public class ItemTypeMapDefinition : IMapDefinition
mapper.Define<IContentType, DocumentTypeItemResponseModel>((_, _) => new DocumentTypeItemResponseModel(), Map);
mapper.Define<IMediaType, MediaTypeItemResponseModel>((_, _) => new MediaTypeItemResponseModel(), Map);
mapper.Define<IEntitySlim, MemberGroupItemReponseModel>((_, _) => new MemberGroupItemReponseModel(), Map);
mapper.Define<IEntitySlim, TemplateItemResponseModel>((_, _) => new TemplateItemResponseModel(), Map);
mapper.Define<ITemplate, TemplateItemResponseModel>((_, _) => new TemplateItemResponseModel { Alias = string.Empty }, Map);
mapper.Define<IMemberType, MemberTypeItemResponseModel>((_, _) => new MemberTypeItemResponseModel(), Map);
mapper.Define<IRelationType, RelationTypeItemResponseModel>((_, _) => new RelationTypeItemResponseModel(), Map);
mapper.Define<IMediaEntitySlim, MediaItemResponseModel>((_, _) => new MediaItemResponseModel(), Map);
@@ -84,10 +84,11 @@ public class ItemTypeMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
private static void Map(IEntitySlim source, TemplateItemResponseModel target, MapperContext context)
private static void Map(ITemplate source, TemplateItemResponseModel target, MapperContext context)
{
target.Name = source.Name ?? string.Empty;
target.Id = source.Key;
target.Alias = source.Alias;
}
// Umbraco.Code.MapAll

View File

@@ -14,19 +14,9 @@ public class TemplateViewModelMapDefinition : IMapDefinition
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<ITemplate, TemplateResponseModel>((_, _) => new TemplateResponseModel(), Map);
mapper.Define<UpdateTemplateRequestModel, ITemplate>((source, _) => new Core.Models.Template(_shortStringHelper, source.Name, source.Alias), Map);
}
// Umbraco.Code.MapAll
private void Map(ITemplate source, TemplateResponseModel target, MapperContext context)
{
target.Id = source.Key;
target.Name = source.Name ?? string.Empty;
target.Alias = source.Alias;
target.Content = source.Content;
}
// Umbraco.Code.MapAll -Id -Key -CreateDate -UpdateDate -DeleteDate
// Umbraco.Code.MapAll -Path -VirtualPath -MasterTemplateId -IsMasterTemplate
private void Map(UpdateTemplateRequestModel source, ITemplate target, MapperContext context)

View File

@@ -4,4 +4,5 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Template.Item;
public class TemplateItemResponseModel : ItemResponseModelBase
{
public required string Alias { get; set; }
}

View File

@@ -3,4 +3,6 @@
public class TemplateResponseModel : TemplateModelBase, INamedEntityPresentationModel
{
public Guid Id { get; set; }
public Guid? MasterTemplateId { get; set; }
}

View File

@@ -8,9 +8,15 @@ public interface ITemplateService : IService
/// <summary>
/// Gets a list of all <see cref="ITemplate" /> objects
/// </summary>
/// <returns>An enumerable list of <see cref="ITemplate" /> objects</returns>
/// <returns>An enumerable list of <see cref="ITemplate" />.</returns>
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" />.</returns>
Task<IEnumerable<ITemplate>> GetAllAsync(Guid[] keys);
/// <summary>
/// Gets a list of all <see cref="ITemplate" /> objects
/// </summary>
@@ -38,6 +44,13 @@ public interface ITemplateService : IService
/// <returns>The <see cref="ITemplate" /> object matching the identifier, or null.</returns>
Task<ITemplate?> GetAsync(Guid id);
/// <summary>
/// Gets the scaffold code for a template.
/// </summary>
/// <param name="masterTemplateKey"></param>
/// <returns></returns>
Task<string> GetScaffoldAsync(Guid? masterTemplateKey);
/// <summary>
/// Gets the template descendants
/// </summary>

View File

@@ -5,6 +5,7 @@ public enum TemplateOperationStatus
Success,
CancelledByNotification,
InvalidAlias,
DuplicateAlias,
TemplateNotFound,
MasterTemplateNotFound
}

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
@@ -18,6 +19,7 @@ public class TemplateService : RepositoryService, ITemplateService
private readonly IAuditRepository _auditRepository;
private readonly ITemplateContentParserService _templateContentParserService;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly IDefaultViewContentProvider _defaultViewContentProvider;
public TemplateService(
ICoreScopeProvider provider,
@@ -27,7 +29,8 @@ public class TemplateService : RepositoryService, ITemplateService
ITemplateRepository templateRepository,
IAuditRepository auditRepository,
ITemplateContentParserService templateContentParserService,
IUserIdKeyResolver userIdKeyResolver)
IUserIdKeyResolver userIdKeyResolver,
IDefaultViewContentProvider defaultViewContentProvider)
: base(provider, loggerFactory, eventMessagesFactory)
{
_shortStringHelper = shortStringHelper;
@@ -35,6 +38,7 @@ public class TemplateService : RepositoryService, ITemplateService
_auditRepository = auditRepository;
_templateContentParserService = templateContentParserService;
_userIdKeyResolver = userIdKeyResolver;
_defaultViewContentProvider = defaultViewContentProvider;
}
/// <inheritdoc />
@@ -100,7 +104,7 @@ public class TemplateService : RepositoryService, ITemplateService
{
// 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, userKey);
return await SaveAsync(template, AuditType.New, userKey, () => ValidateCreate(template));
}
catch (PathTooLongException ex)
{
@@ -109,6 +113,17 @@ public class TemplateService : RepositoryService, ITemplateService
}
}
private TemplateOperationStatus ValidateCreate(ITemplate templateToCreate)
{
ITemplate? existingTemplate = GetAsync(templateToCreate.Alias).GetAwaiter().GetResult();
if (existingTemplate is not null)
{
return TemplateOperationStatus.DuplicateAlias;
}
return TemplateOperationStatus.Success;
}
/// <inheritdoc />
public async Task<IEnumerable<ITemplate>> GetAllAsync(params string[] aliases)
{
@@ -118,6 +133,17 @@ public class TemplateService : RepositoryService, ITemplateService
}
}
/// <inheritdoc />
public Task<IEnumerable<ITemplate>> GetAllAsync(params Guid[] keys)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<ITemplate> query = Query<ITemplate>().Where(x => keys.Contains(x.Key));
IEnumerable<ITemplate> templates = _templateRepository.Get(query).OrderBy(x => x.Name);
return Task.FromResult(templates);
}
/// <inheritdoc />
public async Task<IEnumerable<ITemplate>> GetChildrenAsync(int masterTemplateId)
{
@@ -155,6 +181,19 @@ public class TemplateService : RepositoryService, ITemplateService
}
}
/// <inheritdoc />
public async Task<string> GetScaffoldAsync(Guid? masterTemplateKey)
{
string? masterAlias = null;
if (masterTemplateKey is not null)
{
ITemplate? masterTemplate = await GetAsync(masterTemplateKey.Value);
masterAlias = masterTemplate?.Alias;
}
return _defaultViewContentProvider.GetDefaultFileContent(masterAlias);
}
/// <inheritdoc />
public async Task<IEnumerable<ITemplate>> GetDescendantsAsync(int masterTemplateId)
{
@@ -171,9 +210,23 @@ public class TemplateService : RepositoryService, ITemplateService
AuditType.Save,
userKey,
// fail the attempt if the template does not exist within the scope
() => _templateRepository.Exists(template.Id)
? TemplateOperationStatus.Success
: TemplateOperationStatus.TemplateNotFound);
() => ValidateUpdate(template));
private TemplateOperationStatus ValidateUpdate(ITemplate templateToUpdate)
{
ITemplate? existingTemplate = GetAsync(templateToUpdate.Alias).GetAwaiter().GetResult();
if (existingTemplate is not null && existingTemplate.Key != templateToUpdate.Key)
{
return TemplateOperationStatus.DuplicateAlias;
}
if (_templateRepository.Exists(templateToUpdate.Id) is false)
{
return TemplateOperationStatus.TemplateNotFound;
}
return TemplateOperationStatus.Success;
}
private async Task<Attempt<ITemplate, TemplateOperationStatus>> SaveAsync(ITemplate template, AuditType auditType, Guid userKey, Func<TemplateOperationStatus>? scopeValidator = null)
{

View File

@@ -41,14 +41,14 @@ public class TemplateController : BackOfficeNotificationsController
throw new ArgumentNullException(nameof(defaultViewContentProvider));
}
/// <summary>
/// Gets data type by alias
/// </summary>
/// <param name="alias"></param>
/// <returns></returns>
public TemplateDisplay? GetByAlias(string alias)
{
ITemplate? template = _templateService.GetAsync(alias).GetAwaiter().GetResult();
/// <summary>
/// Gets data type by alias
/// </summary>
/// <param name="alias"></param>
/// <returns></returns>
public TemplateDisplay? GetByAlias(string alias)
{
ITemplate? template = _templateService.GetAsync(alias).GetAwaiter().GetResult();
return template == null ? null : _umbracoMapper.Map<ITemplate, TemplateDisplay>(template);
}

View File

@@ -34,7 +34,7 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest
public virtual void CreateTestData()
{
// NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested.
var template = TemplateBuilder.CreateTextPageTemplate();
var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate");
FileService.SaveTemplate(template);
// Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type)

View File

@@ -547,7 +547,7 @@ public class PackageDataInstallationTests : UmbracoIntegrationTestWithContent
var fileService = FileService;
// kill default test data
fileService.DeleteTemplate("Textpage");
fileService.DeleteTemplate("defaultTemplate");
// Act
var numberOfTemplates = (from doc in templateElement.Elements("Template") select doc).Count();

View File

@@ -64,7 +64,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest
public void CreateTestData()
{
var template = TemplateBuilder.CreateTextPageTemplate();
var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate");
FileService.SaveTemplate(template);
// Create and Save ContentType "umbTextpage" -> (_contentType.Id)

View File

@@ -262,7 +262,7 @@ public class ContentServicePerformanceTest : UmbracoIntegrationTest
public void CreateTestData()
{
var template = TemplateBuilder.CreateTextPageTemplate();
var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate");
FileService.SaveTemplate(template);
// Create and Save ContentType "textpage" -> ContentType.Id

View File

@@ -886,7 +886,7 @@ public class EntityServiceTests : UmbracoIntegrationTest
{
s_isSetup = true;
var template = TemplateBuilder.CreateTextPageTemplate();
var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate");
FileService.SaveTemplate(template); // else, FK violation on contentType!
// Create and Save ContentType "umbTextpage" -> _contentType.Id