Skip to content

Commit

Permalink
Enable validation of specific cultures only for document updates (#17087
Browse files Browse the repository at this point in the history
)

* Enable validation of specific cultures only for document updates

* Only validate explicitly sent cultures in the create validation endpoint

* Fix backwards compat (obsolete old method)

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
  • Loading branch information
kjac and madsrasmussen authored Sep 25, 2024
1 parent 474cffb commit 548b5e4
Show file tree
Hide file tree
Showing 15 changed files with 383 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Umbraco.Cms.Api.Management.Controllers.Document;

[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
{
private readonly IContentEditingService _contentEditingService;
Expand All @@ -32,10 +33,35 @@ public ValidateUpdateDocumentController(
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[Obsolete("Please use version 1.1 of this API. Will be removed in V16.")]
public async Task<IActionResult> Validate(CancellationToken cancellationToken, Guid id, UpdateDocumentRequestModel requestModel)
=> await HandleRequest(id, requestModel, async () =>
{
ContentUpdateModel model = _documentEditingPresentationFactory.MapUpdateModel(requestModel);
var validateUpdateDocumentRequestModel = new ValidateUpdateDocumentRequestModel
{
Values = requestModel.Values,
Variants = requestModel.Variants,
Template = requestModel.Template,
Cultures = null
};
ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(validateUpdateDocumentRequestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateUpdateAsync(id, model);
return result.Success
? Ok()
: DocumentEditingOperationStatusResult(result.Status, requestModel, result.Result);
});

[HttpPut("{id:guid}/validate")]
[MapToApiVersion("1.1")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ValidateV1_1(CancellationToken cancellationToken, Guid id, ValidateUpdateDocumentRequestModel requestModel)
=> await HandleRequest(id, requestModel, async () =>
{
ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateUpdateAsync(id, model);
return result.Success
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@ public ContentCreateModel MapCreateModel(CreateDocumentRequestModel requestModel
}

public ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel)
=> MapUpdateContentModel<ContentUpdateModel>(requestModel);

public ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel)
{
ValidateContentUpdateModel model = MapUpdateContentModel<ValidateContentUpdateModel>(requestModel);
model.Cultures = requestModel.Cultures;

return model;
}

private TUpdateModel MapUpdateContentModel<TUpdateModel>(UpdateDocumentRequestModel requestModel)
where TUpdateModel : ContentUpdateModel, new()
{
ContentUpdateModel model = MapContentEditingModel<ContentUpdateModel>(requestModel);
TUpdateModel model = MapContentEditingModel<TUpdateModel>(requestModel);
model.TemplateKey = requestModel.Template?.Id;

return model;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public interface IDocumentEditingPresentationFactory
ContentCreateModel MapCreateModel(CreateDocumentRequestModel requestModel);

ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel);

ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel);
}
189 changes: 189 additions & 0 deletions src/Umbraco.Cms.Api.Management/OpenApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9372,6 +9372,149 @@
}
}
},
"deprecated": true,
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1.1/document/{id}/validate": {
"put": {
"tags": [
"Document"
],
"operationId": "PutUmbracoManagementApiV1.1DocumentByIdValidate1.1",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel"
}
]
}
},
"text/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel"
}
]
}
},
"application/*+json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel"
}
]
}
}
}
},
"responses": {
"200": {
"description": "OK",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
},
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"404": {
"description": "Not Found",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user do not have access to this resource",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
}
},
"security": [
{
"Backoffice User": [ ]
Expand Down Expand Up @@ -45526,6 +45669,52 @@
},
"additionalProperties": false
},
"ValidateUpdateDocumentRequestModel": {
"required": [
"values",
"variants"
],
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentVariantRequestModel"
}
]
}
},
"template": {
"oneOf": [
{
"$ref": "#/components/schemas/ReferenceByIdModel"
}
],
"nullable": true
},
"cultures": {
"uniqueItems": true,
"type": "array",
"items": {
"type": "string"
},
"nullable": true
}
},
"additionalProperties": false
},
"VariantItemResponseModel": {
"required": [
"name"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;

public class ValidateUpdateDocumentRequestModel : UpdateDocumentRequestModel
{
public ISet<string>? Cultures { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;

public class ValidateContentUpdateModel : ContentUpdateModel
{
public ISet<string>? Cultures { get; set; }
}
16 changes: 13 additions & 3 deletions src/Umbraco.Core/Services/ContentEditingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public ContentEditingService(
return await Task.FromResult(content);
}

[Obsolete("Please use the validate update method that is not obsoleted. Will be removed in V16.")]
public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateUpdateAsync(Guid key, ContentUpdateModel updateModel)
{
IContent? content = ContentService.GetById(key);
Expand All @@ -44,8 +45,16 @@ public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus
: Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult());
}

public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateUpdateAsync(Guid key, ValidateContentUpdateModel updateModel)
{
IContent? content = ContentService.GetById(key);
return content is not null
? await ValidateCulturesAndPropertiesAsync(updateModel, content.ContentType.Key, updateModel.Cultures)
: Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult());
}

public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateCreateAsync(ContentCreateModel createModel)
=> await ValidateCulturesAndPropertiesAsync(createModel, createModel.ContentTypeKey);
=> await ValidateCulturesAndPropertiesAsync(createModel, createModel.ContentTypeKey, createModel.Variants.Select(variant => variant.Culture));

public async Task<Attempt<ContentCreateResult, ContentEditingOperationStatus>> CreateAsync(ContentCreateModel createModel, Guid userKey)
{
Expand Down Expand Up @@ -137,10 +146,11 @@ public async Task<ContentEditingOperationStatus> SortAsync(Guid? parentKey, IEnu

private async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateCulturesAndPropertiesAsync(
ContentEditingModelBase contentEditingModelBase,
Guid contentTypeKey)
Guid contentTypeKey,
IEnumerable<string?>? culturesToValidate = null)
=> await ValidateCulturesAsync(contentEditingModelBase) is false
? Attempt.FailWithStatus(ContentEditingOperationStatus.InvalidCulture, new ContentValidationResult())
: await ValidatePropertiesAsync(contentEditingModelBase, contentTypeKey);
: await ValidatePropertiesAsync(contentEditingModelBase, contentTypeKey, culturesToValidate);

private async Task<ContentEditingOperationStatus> UpdateTemplateAsync(IContent content, Guid? templateKey)
{
Expand Down
10 changes: 6 additions & 4 deletions src/Umbraco.Core/Services/ContentEditingServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,24 @@ protected async Task<bool> ValidateCulturesAsync(ContentEditingModelBase content

protected async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidatePropertiesAsync(
ContentEditingModelBase contentEditingModelBase,
Guid contentTypeKey)
Guid contentTypeKey,
IEnumerable<string?>? culturesToValidate = null)
{
TContentType? contentType = await ContentTypeService.GetAsync(contentTypeKey);
if (contentType is null)
{
return Attempt.FailWithStatus(ContentEditingOperationStatus.ContentTypeNotFound, new ContentValidationResult());
}

return await ValidatePropertiesAsync(contentEditingModelBase, contentType);
return await ValidatePropertiesAsync(contentEditingModelBase, contentType, culturesToValidate);
}

private async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidatePropertiesAsync(
ContentEditingModelBase contentEditingModelBase,
TContentType contentType)
TContentType contentType,
IEnumerable<string?>? culturesToValidate = null)
{
ContentValidationResult result = await _validationService.ValidatePropertiesAsync(contentEditingModelBase, contentType);
ContentValidationResult result = await _validationService.ValidatePropertiesAsync(contentEditingModelBase, contentType, culturesToValidate);
return result.ValidationErrors.Any() is false
? Attempt.SucceedWithStatus(ContentEditingOperationStatus.Success, result)
: Attempt.FailWithStatus(ContentEditingOperationStatus.PropertyValidationError, result);
Expand Down
5 changes: 3 additions & 2 deletions src/Umbraco.Core/Services/ContentValidationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public ContentValidationService(IPropertyValidationService propertyValidationSer

public async Task<ContentValidationResult> ValidatePropertiesAsync(
ContentEditingModelBase contentEditingModelBase,
IContentType contentType)
=> await HandlePropertiesValidationAsync(contentEditingModelBase, contentType);
IContentType contentType,
IEnumerable<string?>? culturesToValidate = null)
=> await HandlePropertiesValidationAsync(contentEditingModelBase, contentType, culturesToValidate);
}
5 changes: 3 additions & 2 deletions src/Umbraco.Core/Services/ContentValidationServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ protected ContentValidationServiceBase(

protected async Task<ContentValidationResult> HandlePropertiesValidationAsync(
ContentEditingModelBase contentEditingModelBase,
TContentType contentType)
TContentType contentType,
IEnumerable<string?>? culturesToValidate = null)
{
var validationErrors = new List<PropertyValidationError>();

Expand All @@ -43,7 +44,7 @@ protected async Task<ContentValidationResult> HandlePropertiesValidationAsync(
return new ContentValidationResult { ValidationErrors = validationErrors };
}

var cultures = await GetCultureCodes();
var cultures = culturesToValidate?.ToArray() ?? await GetCultureCodes();
// we don't have any managed segments, so we have to make do with the ones passed in the model
var segments = contentEditingModelBase.Variants.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment).ToArray();

Expand Down
Loading

0 comments on commit 548b5e4

Please sign in to comment.