Only prevent the unpublish or delete of a related item when configured to do so if it is related as a child, not as a parent (#18886)

* Only prevent the unpubkish or delete of a related item when configured to do so if it is related as a child, not as a parent.

* Fixed incorect parameter names.

* Fixed failing integration tests.

* Use using variable instead to reduce nesting

* Applied suggestions from code review.

* Used simple using statement throughout RelationService for consistency.

* Applied XML header comments consistently.

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Andy Butland
2025-04-01 15:49:49 +02:00
committed by GitHub
parent bf89eae07f
commit 8e0912cbf1
11 changed files with 343 additions and 289 deletions

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Definition of relation directions used as a filter when requesting if a given item has relations.
/// </summary>
[Flags]
public enum RelationDirectionFilter
{
Parent = 1,
Child = 2,
Any = Parent | Child
}

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
@@ -198,7 +198,7 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
return await Task.FromResult(Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(status, content));
}
if (disabledWhenReferenced && _relationService.IsRelated(content.Id))
if (disabledWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child))
{
return Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(referenceFailStatus, content);
}

View File

@@ -311,7 +311,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
return Attempt.Fail(ContentPublishingOperationStatus.ContentNotFound);
}
if (_contentSettings.DisableUnpublishWhenReferenced && _relationService.IsRelated(content.Id))
if (_contentSettings.DisableUnpublishWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child))
{
scope.Complete();
return Attempt<ContentPublishingOperationStatus>.Fail(ContentPublishingOperationStatus.CannotUnpublishWhenReferenced);

View File

@@ -2767,7 +2767,7 @@ public class ContentService : RepositoryService, IContentService
{
foreach (IContent content in contents)
{
if (_contentSettings.DisableDeleteWhenReferenced && _relationService.IsRelated(content.Id))
if (_contentSettings.DisableDeleteWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child))
{
continue;
}

View File

@@ -297,8 +297,20 @@ public interface IRelationService : IService
/// </summary>
/// <param name="id">Id of an object to check relations for</param>
/// <returns>Returns <c>True</c> if any relations exists with the given Id, otherwise <c>False</c></returns>
[Obsolete("Please use the overload taking a RelationDirectionFilter parameter. Scheduled for removal in Umbraco 17.")]
bool IsRelated(int id);
/// <summary>
/// Checks whether any relations exists for the passed in Id and direction.
/// </summary>
/// <param name="id">Id of an object to check relations for</param>
/// <param name="directionFilter">Indicates whether to check for relations as parent, child or in either direction.</param>
/// <returns>Returns <c>True</c> if any relations exists with the given Id, otherwise <c>False</c></returns>
bool IsRelated(int id, RelationDirectionFilter directionFilter)
#pragma warning disable CS0618 // Type or member is obsolete
=> IsRelated(id);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Checks whether two items are related
/// </summary>

View File

@@ -63,28 +63,22 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IRelation? GetById(int id)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _relationRepository.Get(id);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _relationRepository.Get(id);
}
/// <inheritdoc />
public IRelationType? GetRelationTypeById(int id)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _relationTypeRepository.Get(id);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _relationTypeRepository.Get(id);
}
/// <inheritdoc />
public IRelationType? GetRelationTypeById(Guid id)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _relationTypeRepository.Get(id);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _relationTypeRepository.Get(id);
}
/// <inheritdoc />
@@ -93,10 +87,8 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IEnumerable<IRelation> GetAllRelations(params int[] ids)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _relationRepository.GetMany(ids);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _relationRepository.GetMany(ids);
}
/// <inheritdoc />
@@ -106,20 +98,16 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IEnumerable<IRelation> GetAllRelationsByRelationType(int relationTypeId)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationTypeId);
return _relationRepository.Get(query);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationTypeId);
return _relationRepository.Get(query);
}
/// <inheritdoc />
public IEnumerable<IRelationType> GetAllRelationTypes(params int[] ids)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _relationTypeRepository.GetMany(ids);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _relationTypeRepository.GetMany(ids);
}
/// <summary>
@@ -144,10 +132,8 @@ public class RelationService : RepositoryService, IRelationService
.Take(take));
}
public int CountRelationTypes()
{
return _relationTypeRepository.Count(null);
}
/// <inheritdoc />
public int CountRelationTypes() => _relationTypeRepository.Count(null);
/// <inheritdoc />
public IEnumerable<IRelation> GetByParentId(int id) => GetByParentId(id, null);
@@ -155,24 +141,22 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IEnumerable<IRelation> GetByParentId(int id, string? relationTypeAlias)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
if (relationTypeAlias.IsNullOrWhiteSpace())
{
if (relationTypeAlias.IsNullOrWhiteSpace())
{
IQuery<IRelation> qry1 = Query<IRelation>().Where(x => x.ParentId == id);
return _relationRepository.Get(qry1);
}
IRelationType? relationType = GetRelationType(relationTypeAlias!);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
IQuery<IRelation> qry2 =
Query<IRelation>().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(qry2);
IQuery<IRelation> qry1 = Query<IRelation>().Where(x => x.ParentId == id);
return _relationRepository.Get(qry1);
}
IRelationType? relationType = GetRelationType(relationTypeAlias!);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
IQuery<IRelation> qry2 =
Query<IRelation>().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(qry2);
}
/// <inheritdoc />
@@ -188,24 +172,22 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IEnumerable<IRelation> GetByChildId(int id, string? relationTypeAlias)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
if (relationTypeAlias.IsNullOrWhiteSpace())
{
if (relationTypeAlias.IsNullOrWhiteSpace())
{
IQuery<IRelation> qry1 = Query<IRelation>().Where(x => x.ChildId == id);
return _relationRepository.Get(qry1);
}
IRelationType? relationType = GetRelationType(relationTypeAlias!);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
IQuery<IRelation> qry2 =
Query<IRelation>().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(qry2);
IQuery<IRelation> qry1 = Query<IRelation>().Where(x => x.ChildId == id);
return _relationRepository.Get(qry1);
}
IRelationType? relationType = GetRelationType(relationTypeAlias!);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
IQuery<IRelation> qry2 =
Query<IRelation>().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(qry2);
}
/// <inheritdoc />
@@ -218,39 +200,34 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IEnumerable<IRelation> GetByParentOrChildId(int id)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.ChildId == id || x.ParentId == id);
return _relationRepository.Get(query);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.ChildId == id || x.ParentId == id);
return _relationRepository.Get(query);
}
/// <inheritdoc />
public IEnumerable<IRelation> GetByParentOrChildId(int id, string relationTypeAlias)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IRelationType? relationType = GetRelationType(relationTypeAlias);
if (relationType == null)
{
IRelationType? relationType = GetRelationType(relationTypeAlias);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
IQuery<IRelation> query = Query<IRelation>().Where(x =>
(x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query);
return Enumerable.Empty<IRelation>();
}
IQuery<IRelation> query = Query<IRelation>().Where(x =>
(x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query);
}
/// <inheritdoc />
public IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.ParentId == parentId &&
x.ChildId == childId &&
x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query).FirstOrDefault();
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.ParentId == parentId &&
x.ChildId == childId &&
x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query).FirstOrDefault();
}
/// <inheritdoc />
@@ -283,47 +260,41 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IEnumerable<IRelation> GetByRelationTypeId(int relationTypeId)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationTypeId);
return _relationRepository.Get(query);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationTypeId);
return _relationRepository.Get(query);
}
/// <inheritdoc />
public IEnumerable<IRelation> GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation>? query = Query<IRelation>().Where(x => x.RelationTypeId == relationTypeId);
return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation>? query = Query<IRelation>().Where(x => x.RelationTypeId == relationTypeId);
return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering);
}
/// <inheritdoc />
public async Task<PagedModel<IRelation>> GetPagedByChildKeyAsync(Guid childKey, int skip, int take, string? relationTypeAlias)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return await _relationRepository.GetPagedByChildKeyAsync(childKey, skip, take, relationTypeAlias);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return await _relationRepository.GetPagedByChildKeyAsync(childKey, skip, take, relationTypeAlias);
}
/// <inheritdoc />
public async Task<Attempt<PagedModel<IRelation>, RelationOperationStatus>> GetPagedByRelationTypeKeyAsync(Guid key, int skip, int take, Ordering? ordering = null)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IRelationType? relationType = _relationTypeRepository.Get(key);
if (relationType is null)
{
IRelationType? relationType = _relationTypeRepository.Get(key);
if (relationType is null)
{
return await Task.FromResult(Attempt.FailWithStatus<PagedModel<IRelation>, RelationOperationStatus>(RelationOperationStatus.RelationTypeNotFound, null!));
}
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationType.Id);
IEnumerable<IRelation> relations = _relationRepository.GetPagedRelationsByQuery(query, pageNumber, pageSize, out var totalRecords, ordering);
return await Task.FromResult(Attempt.SucceedWithStatus(RelationOperationStatus.Success, new PagedModel<IRelation>(totalRecords, relations)));
return await Task.FromResult(Attempt.FailWithStatus<PagedModel<IRelation>, RelationOperationStatus>(RelationOperationStatus.RelationTypeNotFound, null!));
}
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationType.Id);
IEnumerable<IRelation> relations = _relationRepository.GetPagedRelationsByQuery(query, pageNumber, pageSize, out var totalRecords, ordering);
return await Task.FromResult(Attempt.SucceedWithStatus(RelationOperationStatus.Success, new PagedModel<IRelation>(totalRecords, relations)));
}
/// <inheritdoc />
@@ -394,19 +365,15 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public IEnumerable<IUmbracoEntity> GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray());
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray());
}
/// <inheritdoc />
public IEnumerable<IUmbracoEntity> GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray());
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray());
}
/// <inheritdoc />
@@ -440,22 +407,20 @@ public class RelationService : RepositoryService, IRelationService
// TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok?
var relation = new Relation(parentId, childId, relationType);
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new RelationSavingNotification(relation, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new RelationSavingNotification(relation, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return relation; // TODO: returning sth that does not exist here?!
}
_relationRepository.Save(relation);
scope.Notifications.Publish(
new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification));
scope.Complete();
return relation;
return relation; // TODO: returning sth that does not exist here?!
}
_relationRepository.Save(relation);
scope.Notifications.Publish(
new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification));
scope.Complete();
return relation;
}
/// <inheritdoc />
@@ -491,31 +456,37 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public bool HasRelations(IRelationType relationType)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query).Any();
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query).Any();
}
/// <inheritdoc />
public bool IsRelated(int id)
public bool IsRelated(int id) => IsRelated(id, RelationDirectionFilter.Any);
/// <inheritdoc />
public bool IsRelated(int id, RelationDirectionFilter directionFilter)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>();
query = directionFilter switch
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.ParentId == id || x.ChildId == id);
return _relationRepository.Get(query).Any();
}
RelationDirectionFilter.Parent => query.Where(x => x.ParentId == id),
RelationDirectionFilter.Child => query.Where(x => x.ChildId == id),
RelationDirectionFilter.Any => query.Where(x => x.ParentId == id || x.ChildId == id),
_ => throw new ArgumentOutOfRangeException(nameof(directionFilter)),
};
return _relationRepository.Get(query).Any();
}
/// <inheritdoc />
public bool AreRelated(int parentId, int childId)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.ParentId == parentId && x.ChildId == childId);
return _relationRepository.Get(query).Any();
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>().Where(x => x.ParentId == parentId && x.ChildId == childId);
return _relationRepository.Get(query).Any();
}
/// <inheritdoc />
@@ -540,65 +511,61 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public void Save(IRelation relation)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new RelationSavingNotification(relation, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new RelationSavingNotification(relation, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return;
}
_relationRepository.Save(relation);
scope.Complete();
scope.Notifications.Publish(
new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification));
return;
}
_relationRepository.Save(relation);
scope.Complete();
scope.Notifications.Publish(
new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification));
}
/// <inheritdoc />
public void Save(IEnumerable<IRelation> relations)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
IRelation[] relationsA = relations.ToArray();
EventMessages messages = EventMessagesFactory.Get();
var savingNotification = new RelationSavingNotification(relationsA, messages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
IRelation[] relationsA = relations.ToArray();
EventMessages messages = EventMessagesFactory.Get();
var savingNotification = new RelationSavingNotification(relationsA, messages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return;
}
_relationRepository.Save(relationsA);
scope.Complete();
scope.Notifications.Publish(
new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification));
return;
}
_relationRepository.Save(relationsA);
scope.Complete();
scope.Notifications.Publish(
new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification));
}
/// <inheritdoc />
public void Save(IRelationType relationType)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new RelationTypeSavingNotification(relationType, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
EventMessages eventMessages = EventMessagesFactory.Get();
var savingNotification = new RelationTypeSavingNotification(relationType, eventMessages);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return;
}
_relationTypeRepository.Save(relationType);
Audit(AuditType.Save, Constants.Security.SuperUserId, relationType.Id, $"Saved relation type: {relationType.Name}");
scope.Complete();
scope.Notifications.Publish(
new RelationTypeSavedNotification(relationType, eventMessages).WithStateFrom(savingNotification));
return;
}
_relationTypeRepository.Save(relationType);
Audit(AuditType.Save, Constants.Security.SuperUserId, relationType.Id, $"Saved relation type: {relationType.Name}");
scope.Complete();
scope.Notifications.Publish(
new RelationTypeSavedNotification(relationType, eventMessages).WithStateFrom(savingNotification));
}
/// <inheritdoc />
public async Task<Attempt<IRelationType, RelationTypeOperationStatus>> CreateAsync(IRelationType relationType, Guid userKey)
{
if (relationType.Id != 0)
@@ -614,6 +581,7 @@ public class RelationService : RepositoryService, IRelationService
userKey);
}
/// <inheritdoc />
public async Task<Attempt<IRelationType, RelationTypeOperationStatus>> UpdateAsync(IRelationType relationType, Guid userKey) =>
await SaveAsync(
relationType,
@@ -669,105 +637,97 @@ public class RelationService : RepositoryService, IRelationService
/// <inheritdoc />
public void Delete(IRelation relation)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
EventMessages eventMessages = EventMessagesFactory.Get();
var deletingNotification = new RelationDeletingNotification(relation, eventMessages);
if (scope.Notifications.PublishCancelable(deletingNotification))
{
EventMessages eventMessages = EventMessagesFactory.Get();
var deletingNotification = new RelationDeletingNotification(relation, eventMessages);
if (scope.Notifications.PublishCancelable(deletingNotification))
{
scope.Complete();
return;
}
_relationRepository.Delete(relation);
scope.Complete();
scope.Notifications.Publish(
new RelationDeletedNotification(relation, eventMessages).WithStateFrom(deletingNotification));
return;
}
_relationRepository.Delete(relation);
scope.Complete();
scope.Notifications.Publish(
new RelationDeletedNotification(relation, eventMessages).WithStateFrom(deletingNotification));
}
/// <inheritdoc />
public void Delete(IRelationType relationType)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
EventMessages eventMessages = EventMessagesFactory.Get();
var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages);
if (scope.Notifications.PublishCancelable(deletingNotification))
{
EventMessages eventMessages = EventMessagesFactory.Get();
var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages);
if (scope.Notifications.PublishCancelable(deletingNotification))
{
scope.Complete();
return;
}
_relationTypeRepository.Delete(relationType);
scope.Complete();
scope.Notifications.Publish(
new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification));
return;
}
_relationTypeRepository.Delete(relationType);
scope.Complete();
scope.Notifications.Publish(
new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification));
}
/// <inheritdoc />
public async Task<Attempt<IRelationType?, RelationTypeOperationStatus>> DeleteAsync(Guid key, Guid userKey)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
IRelationType? relationType = _relationTypeRepository.Get(key);
if (relationType is null)
{
IRelationType? relationType = _relationTypeRepository.Get(key);
if (relationType is null)
{
return Attempt.FailWithStatus<IRelationType?, RelationTypeOperationStatus>(RelationTypeOperationStatus.NotFound, null);
}
EventMessages eventMessages = EventMessagesFactory.Get();
var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages);
if (scope.Notifications.PublishCancelable(deletingNotification))
{
scope.Complete();
return Attempt.FailWithStatus<IRelationType?, RelationTypeOperationStatus>(RelationTypeOperationStatus.CancelledByNotification, null);
}
_relationTypeRepository.Delete(relationType);
var currentUser = await _userIdKeyResolver.GetAsync(userKey);
Audit(AuditType.Delete, currentUser, relationType.Id, "Deleted relation type");
scope.Notifications.Publish(new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification));
scope.Complete();
return await Task.FromResult(Attempt.SucceedWithStatus<IRelationType?, RelationTypeOperationStatus>(RelationTypeOperationStatus.Success, relationType));
return Attempt.FailWithStatus<IRelationType?, RelationTypeOperationStatus>(RelationTypeOperationStatus.NotFound, null);
}
EventMessages eventMessages = EventMessagesFactory.Get();
var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages);
if (scope.Notifications.PublishCancelable(deletingNotification))
{
scope.Complete();
return Attempt.FailWithStatus<IRelationType?, RelationTypeOperationStatus>(RelationTypeOperationStatus.CancelledByNotification, null);
}
_relationTypeRepository.Delete(relationType);
var currentUser = await _userIdKeyResolver.GetAsync(userKey);
Audit(AuditType.Delete, currentUser, relationType.Id, "Deleted relation type");
scope.Notifications.Publish(new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification));
scope.Complete();
return await Task.FromResult(Attempt.SucceedWithStatus<IRelationType?, RelationTypeOperationStatus>(RelationTypeOperationStatus.Success, relationType));
}
/// <inheritdoc />
public void DeleteRelationsOfType(IRelationType relationType)
{
var relations = new List<IRelation>();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using ICoreScope scope = ScopeProvider.CreateCoreScope();
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationType.Id);
var allRelations = _relationRepository.Get(query).ToList();
relations.AddRange(allRelations);
// TODO: N+1, we should be able to do this in a single call
foreach (IRelation relation in relations)
{
IQuery<IRelation> query = Query<IRelation>().Where(x => x.RelationTypeId == relationType.Id);
var allRelations = _relationRepository.Get(query).ToList();
relations.AddRange(allRelations);
// TODO: N+1, we should be able to do this in a single call
foreach (IRelation relation in relations)
{
_relationRepository.Delete(relation);
}
scope.Complete();
scope.Notifications.Publish(new RelationDeletedNotification(relations, EventMessagesFactory.Get()));
_relationRepository.Delete(relation);
}
scope.Complete();
scope.Notifications.Publish(new RelationDeletedNotification(relations, EventMessagesFactory.Get()));
}
/// <inheritdoc />
public bool AreRelated(int parentId, int childId, IRelationType relationType)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelation> query = Query<IRelation>().Where(x =>
x.ParentId == parentId && x.ChildId == childId && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query).Any();
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>().Where(x =>
x.ParentId == parentId && x.ChildId == childId && x.RelationTypeId == relationType.Id);
return _relationRepository.Get(query).Any();
}
/// <inheritdoc />
public IEnumerable<UmbracoObjectTypes> GetAllowedObjectTypes() =>
new[]
{
[
UmbracoObjectTypes.Document,
UmbracoObjectTypes.Media,
UmbracoObjectTypes.Member,
@@ -778,17 +738,15 @@ public class RelationService : RepositoryService, IRelationService
UmbracoObjectTypes.MemberGroup,
UmbracoObjectTypes.ROOT,
UmbracoObjectTypes.RecycleBin,
};
];
#region Private Methods
private IRelationType? GetRelationType(string relationTypeAlias)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IRelationType> query = Query<IRelationType>().Where(x => x.Alias == relationTypeAlias);
return _relationTypeRepository.Get(query).FirstOrDefault();
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelationType> query = Query<IRelationType>().Where(x => x.Alias == relationTypeAlias);
return _relationTypeRepository.Get(query).FirstOrDefault();
}
private IEnumerable<IRelation> GetRelationsByListOfTypeIds(IEnumerable<int> relationTypeIds)

View File

@@ -1,8 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Integration.Attributes;
@@ -12,29 +11,20 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
public partial class ContentEditingServiceTests
{
protected IRelationService RelationService => GetRequiredService<IRelationService>();
public static void ConfigureDisableDeleteWhenReferenced(IUmbracoBuilder builder)
{
builder.Services.Configure<ContentSettings>(config =>
=> builder.Services.Configure<ContentSettings>(config =>
config.DisableDeleteWhenReferenced = true);
}
public void Relate(IContent child, IContent parent)
{
var relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias);
var relation = RelationService.Relate(child.Id, parent.Id, relatedContentRelType);
RelationService.Save(relation);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableDeleteWhenReferenced))]
public async Task Cannot_Delete_Referenced_Content()
public async Task Cannot_Delete_When_Content_Is_Related_As_A_Child_And_Configured_To_Disable_When_Related()
{
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(moveAttempt.Success);
Relate(Subpage, Subpage2);
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage2, Subpage);
var result = await ContentEditingService.DeleteFromRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.CannotDeleteWhenReferenced, result.Status);
@@ -44,6 +34,24 @@ public partial class ContentEditingServiceTests
Assert.IsNotNull(subpage);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableDeleteWhenReferenced))]
public async Task Can_Delete_When_Content_Is_Related_As_A_Parent_And_Configured_To_Disable_When_Related()
{
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(moveAttempt.Success);
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage, Subpage2);
var result = await ContentEditingService.DeleteFromRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);
// re-get and verify deleted
var subpage = await ContentEditingService.GetAsync(Subpage.Key);
Assert.IsNull(subpage);
}
[TestCase(true)]
[TestCase(false)]
public async Task Can_Delete_FromRecycleBin(bool variant)

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
@@ -9,12 +9,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
public partial class ContentEditingServiceTests
{
public static void ConfigureDisableDelete(IUmbracoBuilder builder)
{
builder.Services.Configure<ContentSettings>(config =>
public static new void ConfigureDisableUnpublishWhenReferencedTrue(IUmbracoBuilder builder)
=> builder.Services.Configure<ContentSettings>(config =>
config.DisableUnpublishWhenReferenced = true);
}
[TestCase(true)]
[TestCase(false)]
@@ -33,10 +30,11 @@ public partial class ContentEditingServiceTests
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableDelete))]
public async Task Cannot_Move_To_Recycle_Bin_If_Referenced()
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Cannot_Move_To_Recycle_Bin_When_Content_Is_Related_As_A_Child_And_Configured_To_Disable_When_Related()
{
Relate(Subpage, Subpage2);
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage2, Subpage);
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsFalse(moveAttempt.Success);
Assert.AreEqual(ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced, moveAttempt.Status);
@@ -47,6 +45,22 @@ public partial class ContentEditingServiceTests
Assert.IsFalse(content.Trashed);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Can_Move_To_Recycle_Bin_When_Content_Is_Related_As_A_Parent_And_Configured_To_Disable_When_Related()
{
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage, Subpage2);
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(moveAttempt.Success);
Assert.AreEqual(ContentEditingOperationStatus.Success, moveAttempt.Status);
// re-get and verify moved
var content = await ContentEditingService.GetAsync(Subpage.Key);
Assert.IsNotNull(content);
Assert.IsTrue(content.Trashed);
}
[Test]
public async Task Cannot_Move_Non_Existing_To_Recycle_Bin()
{

View File

@@ -16,6 +16,14 @@ public partial class ContentEditingServiceTests : ContentEditingServiceTestsBase
[SetUp]
public void Setup() => ContentRepositoryBase.ThrowOnWarning = true;
public void Relate(IContent parent, IContent child)
{
var relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias);
var relation = RelationService.Relate(parent.Id, child.Id, relatedContentRelType);
RelationService.Save(relation);
}
protected override void CustomTestSetup(IUmbracoBuilder builder)
=> builder.AddNotificationHandler<ContentCopiedNotification, RelateOnCopyNotificationHandler>();

View File

@@ -1,14 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Integration.Attributes;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
public partial class ContentPublishingServiceTests
{
public static new void ConfigureDisableUnpublishWhenReferencedTrue(IUmbracoBuilder builder)
=> builder.Services.Configure<ContentSettings>(config =>
config.DisableUnpublishWhenReferenced = true);
[Test]
public async Task Can_Unpublish_Root()
{
@@ -92,6 +100,40 @@ public partial class ContentPublishingServiceTests
VerifyIsPublished(Textpage.Key);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Cannot_Unpublish_When_Content_Is_Related_As_A_Child_And_Configured_To_Disable_When_Related()
{
await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(_allCultures), Constants.Security.SuperUserKey);
VerifyIsPublished(Textpage.Key);
// Setup a relation where the page being unpublished is related to another page as a child (e.g. the other page has a picker and has selected this page).
RelationService.Relate(Subpage, Textpage, Constants.Conventions.RelationTypes.RelatedDocumentAlias);
var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, null, Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentPublishingOperationStatus.CannotUnpublishWhenReferenced, result.Result);
VerifyIsPublished(Textpage.Key);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Can_Unpublish_When_Content_Is_Related_As_A_Parent_And_Configured_To_Disable_When_Related()
{
await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(_allCultures), Constants.Security.SuperUserKey);
VerifyIsPublished(Textpage.Key);
// Setup a relation where the page being unpublished is related to another page as a parent (e.g. this page has a picker and has selected the other page).
RelationService.Relate(Textpage, Subpage, Constants.Conventions.RelationTypes.RelatedDocumentAlias);
var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, null, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
VerifyIsNotPublished(Textpage.Key);
}
[Test]
public async Task Can_Unpublish_Single_Culture()
{
@@ -229,8 +271,6 @@ public partial class ContentPublishingServiceTests
Assert.AreEqual(0, content.PublishedCultures.Count());
}
[Test]
public async Task Can_Unpublish_Non_Mandatory_Cultures()
{

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
@@ -20,6 +20,8 @@ public partial class ContentPublishingServiceTests : UmbracoIntegrationTestWithC
{
private IContentPublishingService ContentPublishingService => GetRequiredService<IContentPublishingService>();
private IRelationService RelationService => GetRequiredService<IRelationService>();
private static readonly ISet<string> _allCultures = new HashSet<string>(){ "*" };
private static CultureAndScheduleModel MakeModel(ISet<string> cultures) => new CultureAndScheduleModel()