Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;

[ApiVersion("1.0")]
public class AllowedParentsDocumentTypeController : DocumentTypeControllerBase
{
private readonly IContentTypeService _contentTypeService;

public AllowedParentsDocumentTypeController(IContentTypeService contentTypeService)
{
_contentTypeService = contentTypeService;
}

[HttpGet("{id:guid}/allowed-parents")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentTypeAllowedParentsResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> AllowedParentsByKey(
CancellationToken cancellationToken,
Guid id)
{
Attempt<IEnumerable<Guid>?, ContentTypeOperationStatus> attempt = await _contentTypeService.GetAllowedParentsAsync(id, UmbracoObjectTypes.DocumentType);
if (attempt.Success is false)
{
return OperationStatusResult(attempt.Status);
}

if (attempt.Result == null || !attempt.Result.Any())
{
return Ok(new DocumentTypeAllowedParentsResponseModel
{
AllowedParentsKeys = [],
});
}

var model = new DocumentTypeAllowedParentsResponseModel
{
AllowedParentsKeys = attempt.Result,
};

return Ok(model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Management.Controllers.MediaType;

[ApiVersion("1.0")]
public class AllowedParentsMediaTypeController : MediaTypeControllerBase
{
private readonly IMediaTypeService _mediaTypeService;

public AllowedParentsMediaTypeController(IMediaTypeService mediaTypeService)
{
_mediaTypeService = mediaTypeService;
}

[HttpGet("{id:guid}/allowed-parents")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(MediaTypeAllowedParentsResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> AllowedParentsByKey(
CancellationToken cancellationToken,
Guid id)
{
Attempt<IEnumerable<Guid>?, ContentTypeOperationStatus> attempt = await _mediaTypeService.GetAllowedParentsAsync(id, UmbracoObjectTypes.MediaType);
if (attempt.Success is false)
{
return OperationStatusResult(attempt.Status);
}

if (attempt.Result == null || !attempt.Result.Any())
{
return Ok(new MediaTypeAllowedParentsResponseModel
{
AllowedParentsKeys = [],
});
}

var model = new MediaTypeAllowedParentsResponseModel
{
AllowedParentsKeys = attempt.Result,
};

return Ok(model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;

public class DocumentTypeAllowedParentsResponseModel
{
public required IEnumerable<Guid> AllowedParentsKeys { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;

public class MediaTypeAllowedParentsResponseModel
{
public required IEnumerable<Guid> AllowedParentsKeys { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ public interface IContentTypeRepositoryBase<TItem> : IReadWriteQueryRepository<i
/// Returns true or false depending on whether content nodes have been created based on the provided content type id.
/// </summary>
bool HasContentNodes(int id);

/// <summary>
/// Gets the allowed parent keys for a child content type.
/// </summary>
/// <param name="key">The child content type.</param>
/// <param name="umbracoObjectType">The object type.</param>
/// <returns>An IEnumerable of the allowed parent keys.</returns>
IEnumerable<Guid> GetAllowedParentKeys(Guid key, UmbracoObjectTypes umbracoObjectType);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Globalization;

Check notice on line 1 in src/Umbraco.Core/Services/ContentTypeServiceBase{TRepository,TItem}.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Primitive Obsession

The ratio of primitive types in function arguments decreases from 57.76% to 57.63%, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.
using System.Runtime.InteropServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -1201,6 +1201,21 @@
return Attempt.SucceedWithStatus<PagedModel<TItem>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result);
}

public async Task<Attempt<IEnumerable<Guid>?, ContentTypeOperationStatus>> GetAllowedParentsAsync(Guid key, UmbracoObjectTypes objectType)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);

if (objectType == UmbracoObjectTypes.Member || objectType == UmbracoObjectTypes.MemberType)
{
// Return an empty array if it's a member or member type.
return Attempt.SucceedWithStatus<IEnumerable<Guid>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, []);
}

IEnumerable<Guid> allowedParentKeys = Repository.GetAllowedParentKeys(key, objectType);

return Attempt.SucceedWithStatus<IEnumerable<Guid>?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, allowedParentKeys);
}

#endregion

#region Containers
Expand Down
1 change: 1 addition & 0 deletions src/Umbraco.Core/Services/IContentTypeBaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,5 @@ Task<Attempt<ContentTypeOperationStatus>> UpdateAsync(TItem item, Guid performin
Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, Guid? parentContentKey, int skip, int take)
=> GetAllowedChildrenAsync(key, skip, take);

Task<Attempt<IEnumerable<Guid>?, ContentTypeOperationStatus>> GetAllowedParentsAsync(Guid key, UmbracoObjectTypes objectType);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Data;

Check notice on line 1 in src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 5.74 to 5.63, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.
using System.Globalization;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -1706,6 +1706,31 @@
?? Array.Empty<(TEntity, int)>();
}

public IEnumerable<Guid> GetAllowedParentKeys(Guid key, UmbracoObjectTypes objectType)
{
Attempt<int> childNodeIdAttempt = _idKeyMap.GetIdForKey(key, objectType);

if (childNodeIdAttempt.Success is false)
{
return [];
}

Sql<ISqlContext> sql = Sql()
.Select<ContentTypeAllowedContentTypeDto>(x => x.Id)
.From<ContentTypeAllowedContentTypeDto>()
.Where<ContentTypeAllowedContentTypeDto>(x => x.AllowedId == childNodeIdAttempt.Result);

IEnumerable<int> allowedIds = Database.Fetch<int>(sql);

List<Guid> allowedKeys = [];
allowedKeys
.AddRange(
from id in allowedIds select _idKeyMap.GetKeyForId(id, objectType)
into keyAttempt where keyAttempt.Success select keyAttempt.Result);

return allowedKeys;
}

private sealed class NameCompareDto
{
public int NodeId { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2403,6 +2403,80 @@ public async Task CreateTemplateAsync_WithIsDefault_SetsAsDefaultTemplate()
Assert.That(updatedContentType.DefaultTemplate!.Key, Is.EqualTo(result.Result));
}

[Test]
public async Task GetAllowedParentsAsync_ReturnsEmptyCollection_WhenNoParentsAllowChildType()
{
// Arrange
var childContentType = ContentTypeBuilder.CreateBasicContentType("child", "Child");
ContentTypeService.Save(childContentType);

var parentContentType = ContentTypeBuilder.CreateBasicContentType("parent", "Parent");

// Parent does not allow child as a child type
ContentTypeService.Save(parentContentType);

// Act
var result = await ContentTypeService.GetAllowedParentsAsync(childContentType.Key, UmbracoObjectTypes.DocumentType);

// Assert
Assert.That(result.Success, Is.True);
Assert.That(result.Result, Is.Not.Null);
Assert.That(result.Result, Is.Empty);
}

[Test]
public async Task GetAllowedParentsAsync_ReturnsParentKeys_WhenParentsAllowChildType()
{
// Arrange
var childContentType = ContentTypeBuilder.CreateBasicContentType("child", "Child");
ContentTypeService.Save(childContentType);

var parentContentType1 = ContentTypeBuilder.CreateBasicContentType("parent1", "Parent1");
parentContentType1.AllowedContentTypes =
[
new ContentTypeSort(childContentType.Key, 0, childContentType.Alias)
];
ContentTypeService.Save(parentContentType1);

var parentContentType2 = ContentTypeBuilder.CreateBasicContentType("parent2", "Parent2");
parentContentType2.AllowedContentTypes =
[
new ContentTypeSort(childContentType.Key, 0, childContentType.Alias)
];
ContentTypeService.Save(parentContentType2);

// A parent that does NOT allow the child type
var unrelatedParentContentType = ContentTypeBuilder.CreateBasicContentType("unrelated", "Unrelated");
ContentTypeService.Save(unrelatedParentContentType);

// Act
var result = await ContentTypeService.GetAllowedParentsAsync(childContentType.Key, UmbracoObjectTypes.DocumentType);

// Assert
Assert.That(result.Success, Is.True);
Assert.That(result.Result, Is.Not.Null);
var parentKeys = result.Result!.ToList();
Assert.That(parentKeys.Count, Is.EqualTo(2));
Assert.That(parentKeys, Does.Contain(parentContentType1.Key));
Assert.That(parentKeys, Does.Contain(parentContentType2.Key));
Assert.That(parentKeys, Does.Not.Contain(unrelatedParentContentType.Key));
}

[Test]
public async Task GetAllowedParentsAsync_ReturnsEmptyCollection_WhenContentTypeDoesNotExist()
{
// Arrange
var nonExistentKey = Guid.NewGuid();

// Act
var result = await ContentTypeService.GetAllowedParentsAsync(nonExistentKey, UmbracoObjectTypes.DocumentType);

// Assert
Assert.That(result.Success, Is.True);
Assert.That(result.Result, Is.Not.Null);
Assert.That(result.Result, Is.Empty);
}

private ContentType CreateComponent()
{
var component = new ContentType(ShortStringHelper, -1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,80 @@ public void Cannot_Change_Alias_Of_System_Media_Type(string mediaTypeAlias)
Assert.Throws<InvalidOperationException>(() => mediaType.Alias += "_updated");
}

[Test]
public async Task GetAllowedParentsAsync_ReturnsEmptyCollection_WhenNoParentsAllowChildType()
{
// Arrange
var childMediaType = MediaTypeBuilder.CreateSimpleMediaType("child", "Child");
MediaTypeService.Save(childMediaType);

var parentMediaType = MediaTypeBuilder.CreateSimpleMediaType("parent", "Parent");

// Parent does not allow child as a child type
MediaTypeService.Save(parentMediaType);

// Act
var result = await MediaTypeService.GetAllowedParentsAsync(childMediaType.Key, UmbracoObjectTypes.MediaType);

// Assert
Assert.That(result.Success, Is.True);
Assert.That(result.Result, Is.Not.Null);
Assert.That(result.Result, Is.Empty);
}

[Test]
public async Task GetAllowedParentsAsync_ReturnsParentKeys_WhenParentsAllowChildType()
{
// Arrange
var childMediaType = MediaTypeBuilder.CreateSimpleMediaType("child", "Child");
MediaTypeService.Save(childMediaType);

var parentMediaType1 = MediaTypeBuilder.CreateSimpleMediaType("parent1", "Parent1");
parentMediaType1.AllowedContentTypes = new[]
{
new ContentTypeSort(childMediaType.Key, 0, childMediaType.Alias)
};
MediaTypeService.Save(parentMediaType1);

var parentMediaType2 = MediaTypeBuilder.CreateSimpleMediaType("parent2", "Parent2", null, true);
parentMediaType2.AllowedContentTypes = new[]
{
new ContentTypeSort(childMediaType.Key, 0, childMediaType.Alias)
};
MediaTypeService.Save(parentMediaType2);

// A parent that does NOT allow the child type
var unrelatedParentMediaType = MediaTypeBuilder.CreateSimpleMediaType("unrelated", "Unrelated", null, true);
MediaTypeService.Save(unrelatedParentMediaType);

// Act
var result = await MediaTypeService.GetAllowedParentsAsync(childMediaType.Key, UmbracoObjectTypes.MediaType);

// Assert
Assert.That(result.Success, Is.True);
Assert.That(result.Result, Is.Not.Null);
var parentKeys = result.Result!.ToList();
Assert.That(parentKeys.Count, Is.EqualTo(2));
Assert.That(parentKeys, Does.Contain(parentMediaType1.Key));
Assert.That(parentKeys, Does.Contain(parentMediaType2.Key));
Assert.That(parentKeys, Does.Not.Contain(unrelatedParentMediaType.Key));
}

[Test]
public async Task GetAllowedParentsAsync_ReturnsEmptyCollection_WhenMediaTypeDoesNotExist()
{
// Arrange
var nonExistentKey = Guid.NewGuid();

// Act
var result = await MediaTypeService.GetAllowedParentsAsync(nonExistentKey, UmbracoObjectTypes.MediaType);

// Assert
Assert.That(result.Success, Is.True);
Assert.That(result.Result, Is.Not.Null);
Assert.That(result.Result, Is.Empty);
}

internal sealed class ContentNotificationHandler :
INotificationHandler<MediaMovedToRecycleBinNotification>
{
Expand Down
Loading