Skip to content

Commit e425f0b

Browse files
Migaroezbergmania
andauthored
Improve document schedule (#17535)
* Expose schedule date for on document get endpoint * typo fix * stupid stuff * Enable content scheduling features in the publishing service * Replace obsoleted non async calls * Add content scheduling test * Publush and schedule combination test * More invariantCulture notation allignment and more tests * Link up api with updated document scheduling * More invariant culture notation allignment * Fix breaking change * Return expected status codes. * Fix constructor * Forward Default implementation to actual core implementation Co-authored-by: Bjarke Berg <[email protected]> * Forward default implementation to core implementation Co-authored-by: Bjarke Berg <[email protected]> * Make content with scheduling retrieval scope safe --------- Co-authored-by: Bjarke Berg <[email protected]>
1 parent 413398a commit e425f0b

File tree

26 files changed

+2045
-40
lines changed

26 files changed

+2045
-40
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
using Microsoft.AspNetCore.Authorization;
33
using Microsoft.AspNetCore.Http;
44
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.Extensions.DependencyInjection;
56
using Umbraco.Cms.Api.Management.Factories;
6-
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
77
using Umbraco.Cms.Api.Management.ViewModels.Document;
88
using Umbraco.Cms.Core.Actions;
9-
using Umbraco.Cms.Core.Models;
9+
using Umbraco.Cms.Core.DependencyInjection;
1010
using Umbraco.Cms.Core.Security.Authorization;
1111
using Umbraco.Cms.Core.Services;
12+
using Umbraco.Cms.Core.Services.Querying;
1213
using Umbraco.Cms.Web.Common.Authorization;
1314
using Umbraco.Extensions;
1415

@@ -18,17 +19,42 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
1819
public class ByKeyDocumentController : DocumentControllerBase
1920
{
2021
private readonly IAuthorizationService _authorizationService;
21-
private readonly IContentEditingService _contentEditingService;
2222
private readonly IDocumentPresentationFactory _documentPresentationFactory;
23+
private readonly IContentQueryService _contentQueryService;
2324

25+
[Obsolete("Scheduled for removal in v17")]
2426
public ByKeyDocumentController(
2527
IAuthorizationService authorizationService,
2628
IContentEditingService contentEditingService,
2729
IDocumentPresentationFactory documentPresentationFactory)
2830
{
2931
_authorizationService = authorizationService;
30-
_contentEditingService = contentEditingService;
3132
_documentPresentationFactory = documentPresentationFactory;
33+
_contentQueryService = StaticServiceProvider.Instance.GetRequiredService<IContentQueryService>();
34+
}
35+
36+
// needed for greedy selection until other constructor remains in v17
37+
[Obsolete("Scheduled for removal in v17")]
38+
public ByKeyDocumentController(
39+
IAuthorizationService authorizationService,
40+
IContentEditingService contentEditingService,
41+
IDocumentPresentationFactory documentPresentationFactory,
42+
IContentQueryService contentQueryService)
43+
{
44+
_authorizationService = authorizationService;
45+
_documentPresentationFactory = documentPresentationFactory;
46+
_contentQueryService = contentQueryService;
47+
}
48+
49+
[ActivatorUtilitiesConstructor]
50+
public ByKeyDocumentController(
51+
IAuthorizationService authorizationService,
52+
IDocumentPresentationFactory documentPresentationFactory,
53+
IContentQueryService contentQueryService)
54+
{
55+
_authorizationService = authorizationService;
56+
_documentPresentationFactory = documentPresentationFactory;
57+
_contentQueryService = contentQueryService;
3258
}
3359

3460
[HttpGet("{id:guid}")]
@@ -47,13 +73,16 @@ public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid
4773
return Forbidden();
4874
}
4975

50-
IContent? content = await _contentEditingService.GetAsync(id);
51-
if (content == null)
76+
var contentWithScheduleAttempt = await _contentQueryService.GetWithSchedulesAsync(id);
77+
78+
if (contentWithScheduleAttempt.Success == false)
5279
{
53-
return DocumentNotFound();
80+
return ContentQueryOperationStatusResult(contentWithScheduleAttempt.Status);
5481
}
5582

56-
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content);
83+
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(
84+
contentWithScheduleAttempt.Result!.Content,
85+
contentWithScheduleAttempt.Result.Schedules);
5786
return Ok(model);
5887
}
5988
}

src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,15 @@ protected IActionResult PublicAccessOperationStatusResult(PublicAccessOperationS
170170
.WithTitle("Unknown content operation status.")
171171
.Build()),
172172
});
173+
174+
protected IActionResult ContentQueryOperationStatusResult(ContentQueryOperationStatus status)
175+
=> OperationStatusResult(status, problemDetailsBuilder => status switch
176+
{
177+
ContentQueryOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
178+
.WithTitle("The document could not be found")
179+
.Build()),
180+
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
181+
.WithTitle("Unknown content query status.")
182+
.Build()),
183+
});
173184
}

src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ public async Task<IActionResult> Publish(CancellationToken cancellationToken, Gu
4646
{
4747
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
4848
User,
49-
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x=>x.Culture is not null).Select(x=>x.Culture!)),
49+
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x => x.Culture is not null).Select(x=>x.Culture!)),
5050
AuthorizationPolicies.ContentPermissionByResource);
5151

5252
if (!authorizationResult.Succeeded)
5353
{
5454
return Forbidden();
5555
}
5656

57-
Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCultureAndScheduleModel(requestModel);
57+
Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCulturePublishScheduleModels(requestModel);
5858

5959
if (modelResult.Success is false)
6060
{

src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public DocumentPresentationFactory(
4040
_idKeyMap = idKeyMap;
4141
}
4242

43+
[Obsolete("Schedule for removal in v17")]
4344
public async Task<DocumentResponseModel> CreateResponseModelAsync(IContent content)
4445
{
4546
DocumentResponseModel responseModel = _umbracoMapper.Map<DocumentResponseModel>(content)!;
@@ -74,6 +75,24 @@ public async Task<PublishedDocumentResponseModel> CreatePublishedResponseModelAs
7475
return responseModel;
7576
}
7677

78+
public async Task<DocumentResponseModel> CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule)
79+
{
80+
DocumentResponseModel responseModel = _umbracoMapper.Map<DocumentResponseModel>(content)!;
81+
_umbracoMapper.Map(schedule, responseModel);
82+
83+
responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content);
84+
85+
Guid? templateKey = content.TemplateId.HasValue
86+
? _templateService.GetAsync(content.TemplateId.Value).Result?.Key
87+
: null;
88+
89+
responseModel.Template = templateKey.HasValue
90+
? new ReferenceByIdModel { Id = templateKey.Value }
91+
: null;
92+
93+
return responseModel;
94+
}
95+
7796
public DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity)
7897
{
7998
Attempt<Guid> parentKeyAttempt = _idKeyMap.GetKeyForId(entity.ParentId, UmbracoObjectTypes.Document);
@@ -135,6 +154,7 @@ public IEnumerable<DocumentVariantItemResponseModel> CreateVariantsItemResponseM
135154
public DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IDocumentEntitySlim entity)
136155
=> _umbracoMapper.Map<DocumentTypeReferenceResponseModel>(entity)!;
137156

157+
[Obsolete("Use CreateCulturePublishScheduleModels instead. Scheduled for removal in v17")]
138158
public Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> CreateCultureAndScheduleModel(PublishDocumentRequestModel requestModel)
139159
{
140160
var contentScheduleCollection = new ContentScheduleCollection();
@@ -143,7 +163,7 @@ public Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> Create
143163
{
144164
if (cultureAndScheduleRequestModel.Schedule is null || (cultureAndScheduleRequestModel.Schedule.PublishTime is null && cultureAndScheduleRequestModel.Schedule.UnpublishTime is null))
145165
{
146-
culturesToPublishImmediately.Add(cultureAndScheduleRequestModel.Culture ?? "*"); // API have `null` for invariant, but service layer has "*".
166+
culturesToPublishImmediately.Add(cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture); // API have `null` for invariant, but service layer has "*".
147167
continue;
148168
}
149169

@@ -159,7 +179,7 @@ public Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> Create
159179
}
160180

161181
contentScheduleCollection.Add(new ContentSchedule(
162-
cultureAndScheduleRequestModel.Culture ?? "*",
182+
cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture,
163183
cultureAndScheduleRequestModel.Schedule.PublishTime.Value.UtcDateTime,
164184
ContentScheduleAction.Release));
165185
}
@@ -184,7 +204,7 @@ public Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> Create
184204
}
185205

186206
contentScheduleCollection.Add(new ContentSchedule(
187-
cultureAndScheduleRequestModel.Culture ?? "*",
207+
cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture,
188208
cultureAndScheduleRequestModel.Schedule.UnpublishTime.Value.UtcDateTime,
189209
ContentScheduleAction.Expire));
190210
}
@@ -195,4 +215,51 @@ public Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> Create
195215
CulturesToPublishImmediately = culturesToPublishImmediately,
196216
});
197217
}
218+
219+
public Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels(PublishDocumentRequestModel requestModel)
220+
{
221+
var model = new List<CulturePublishScheduleModel>();
222+
223+
foreach (CultureAndScheduleRequestModel cultureAndScheduleRequestModel in requestModel.PublishSchedules)
224+
{
225+
if (cultureAndScheduleRequestModel.Schedule is null)
226+
{
227+
model.Add(new CulturePublishScheduleModel
228+
{
229+
Culture = cultureAndScheduleRequestModel.Culture
230+
?? Constants.System.InvariantCulture // API have `null` for invariant, but service layer has "*".
231+
});
232+
continue;
233+
}
234+
235+
if (cultureAndScheduleRequestModel.Schedule.PublishTime is not null
236+
&& cultureAndScheduleRequestModel.Schedule.PublishTime <= _timeProvider.GetUtcNow())
237+
{
238+
return Attempt.FailWithStatus(ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture, model);
239+
}
240+
241+
if (cultureAndScheduleRequestModel.Schedule.UnpublishTime is not null
242+
&& cultureAndScheduleRequestModel.Schedule.UnpublishTime <= _timeProvider.GetUtcNow())
243+
{
244+
return Attempt.FailWithStatus(ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture, model);
245+
}
246+
247+
if (cultureAndScheduleRequestModel.Schedule.UnpublishTime <= cultureAndScheduleRequestModel.Schedule.PublishTime)
248+
{
249+
return Attempt.FailWithStatus(ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime, model);
250+
}
251+
252+
model.Add(new CulturePublishScheduleModel
253+
{
254+
Culture = cultureAndScheduleRequestModel.Culture,
255+
Schedule = new ContentScheduleModel
256+
{
257+
PublishDate = cultureAndScheduleRequestModel.Schedule.PublishTime,
258+
UnpublishDate = cultureAndScheduleRequestModel.Schedule.UnpublishTime,
259+
},
260+
});
261+
}
262+
263+
return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model);
264+
}
198265
}

src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
using Umbraco.Cms.Api.Management.ViewModels.Document;
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Umbraco.Cms.Api.Management.ViewModels.Document;
23
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
34
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint.Item;
45
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
56
using Umbraco.Cms.Core;
7+
using Umbraco.Cms.Core.DependencyInjection;
68
using Umbraco.Cms.Core.Models;
79
using Umbraco.Cms.Core.Models.ContentPublishing;
810
using Umbraco.Cms.Core.Models.Entities;
@@ -12,10 +14,17 @@ namespace Umbraco.Cms.Api.Management.Factories;
1214

1315
public interface IDocumentPresentationFactory
1416
{
17+
[Obsolete("Schedule for removal in v17")]
1518
Task<DocumentResponseModel> CreateResponseModelAsync(IContent content);
1619

1720
Task<PublishedDocumentResponseModel> CreatePublishedResponseModelAsync(IContent content);
1821

22+
Task<DocumentResponseModel> CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule)
23+
#pragma warning disable CS0618 // Type or member is obsolete
24+
// Remove when obsolete CreateResponseModelAsync is removed
25+
=> CreateResponseModelAsync(content);
26+
#pragma warning restore CS0618 // Type or member is obsolete
27+
1928
DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity);
2029

2130
DocumentBlueprintItemResponseModel CreateBlueprintItemResponseModel(IDocumentEntitySlim entity);
@@ -24,5 +33,55 @@ public interface IDocumentPresentationFactory
2433

2534
DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IDocumentEntitySlim entity);
2635

36+
[Obsolete("Use CreateCulturePublishScheduleModels instead. Scheduled for removal in v17")]
2737
Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> CreateCultureAndScheduleModel(PublishDocumentRequestModel requestModel);
38+
39+
Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels(
40+
PublishDocumentRequestModel requestModel)
41+
{
42+
// todo remove default implementation when obsolete method is removed
43+
var model = new List<CulturePublishScheduleModel>();
44+
45+
foreach (CultureAndScheduleRequestModel cultureAndScheduleRequestModel in requestModel.PublishSchedules)
46+
{
47+
if (cultureAndScheduleRequestModel.Schedule is null)
48+
{
49+
model.Add(new CulturePublishScheduleModel
50+
{
51+
Culture = cultureAndScheduleRequestModel.Culture
52+
?? Constants.System.InvariantCulture
53+
});
54+
continue;
55+
}
56+
57+
if (cultureAndScheduleRequestModel.Schedule.PublishTime is not null
58+
&& cultureAndScheduleRequestModel.Schedule.PublishTime <= StaticServiceProvider.Instance.GetRequiredService<TimeProvider>().GetUtcNow())
59+
{
60+
return Attempt.FailWithStatus(ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture, model);
61+
}
62+
63+
if (cultureAndScheduleRequestModel.Schedule.UnpublishTime is not null
64+
&& cultureAndScheduleRequestModel.Schedule.UnpublishTime <= StaticServiceProvider.Instance.GetRequiredService<TimeProvider>().GetUtcNow())
65+
{
66+
return Attempt.FailWithStatus(ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture, model);
67+
}
68+
69+
if (cultureAndScheduleRequestModel.Schedule.UnpublishTime <= cultureAndScheduleRequestModel.Schedule.PublishTime)
70+
{
71+
return Attempt.FailWithStatus(ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime, model);
72+
}
73+
74+
model.Add(new CulturePublishScheduleModel
75+
{
76+
Culture = cultureAndScheduleRequestModel.Culture,
77+
Schedule = new ContentScheduleModel
78+
{
79+
PublishDate = cultureAndScheduleRequestModel.Schedule.PublishTime,
80+
UnpublishDate = cultureAndScheduleRequestModel.Schedule.UnpublishTime,
81+
},
82+
});
83+
}
84+
85+
return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model);
86+
}
2887
}

src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public void DefineMaps(IUmbracoMapper mapper)
2525
mapper.Define<IContent, PublishedDocumentResponseModel>((_, _) => new PublishedDocumentResponseModel(), Map);
2626
mapper.Define<IContent, DocumentCollectionResponseModel>((_, _) => new DocumentCollectionResponseModel(), Map);
2727
mapper.Define<IContent, DocumentBlueprintResponseModel>((_, _) => new DocumentBlueprintResponseModel(), Map);
28+
mapper.Define<ContentScheduleCollection, DocumentResponseModel>(Map);
2829
}
2930

3031
// Umbraco.Code.MapAll -Urls -Template
@@ -113,4 +114,26 @@ private void Map(IContent source, DocumentBlueprintResponseModel target, MapperC
113114
documentVariantViewModel.State = DocumentVariantState.Draft;
114115
});
115116
}
117+
118+
private void Map(ContentScheduleCollection source, DocumentResponseModel target, MapperContext context)
119+
{
120+
foreach (ContentSchedule schedule in source.FullSchedule)
121+
{
122+
DocumentVariantResponseModel? variant = target.Variants.FirstOrDefault(v => v.Culture == schedule.Culture || (v.Culture.IsNullOrWhiteSpace() && schedule.Culture.IsNullOrWhiteSpace()));
123+
if (variant is null)
124+
{
125+
continue;
126+
}
127+
128+
switch (schedule.Action)
129+
{
130+
case ContentScheduleAction.Release:
131+
variant.ScheduledPublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero);
132+
break;
133+
case ContentScheduleAction.Expire:
134+
variant.ScheduledUnpublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero);
135+
break;
136+
}
137+
}
138+
}
116139
}

src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ public class DocumentVariantResponseModel : VariantResponseModelBase
77
public DocumentVariantState State { get; set; }
88

99
public DateTimeOffset? PublishDate { get; set; }
10+
11+
public DateTimeOffset? ScheduledPublishDate { get; set; }
12+
13+
public DateTimeOffset? ScheduledUnpublishDate { get; set; }
1014
}

src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public class CultureAndScheduleRequestModel
1717
/// Gets or sets the schedule of publishing. Null means immediately.
1818
/// </summary>
1919
public ScheduleRequestModel? Schedule { get; set; }
20-
2120
}
2221

2322

src/Umbraco.Core/Constants-System.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,7 @@ public static class System
8989
/// The DataDirectory placeholder.
9090
/// </summary>
9191
public const string DataDirectoryPlaceholder = "|DataDirectory|";
92+
93+
public const string InvariantCulture = "*";
9294
}
9395
}

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
using Umbraco.Cms.Core.Services.FileSystem;
4040
using Umbraco.Cms.Core.Services.ImportExport;
4141
using Umbraco.Cms.Core.Services.Navigation;
42+
using Umbraco.Cms.Core.Services.Querying;
4243
using Umbraco.Cms.Core.Services.Querying.RecycleBin;
4344
using Umbraco.Cms.Core.Sync;
4445
using Umbraco.Cms.Core.Telemetry;
@@ -418,6 +419,7 @@ private void AddCoreServices()
418419
// Add Query services
419420
Services.AddUnique<IDocumentRecycleBinQueryService, DocumentRecycleBinQueryService>();
420421
Services.AddUnique<IMediaRecycleBinQueryService, MediaRecycleBinQueryService>();
422+
Services.AddUnique<IContentQueryService, ContentQueryService>();
421423

422424
// Authorizers
423425
Services.AddSingleton<IAuthorizationHelper, AuthorizationHelper>();

0 commit comments

Comments
 (0)