Skip to content

Commit

Permalink
Archive Later functionality (#10935)
Browse files Browse the repository at this point in the history
  • Loading branch information
hishamco committed Sep 3, 2022
1 parent 4b7f286 commit 83f7028
Show file tree
Hide file tree
Showing 20 changed files with 436 additions and 0 deletions.
7 changes: 7 additions & 0 deletions OrchardCore.sln
Expand Up @@ -470,6 +470,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.FileStorage.Ama
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Media.AmazonS3", "src\OrchardCore.Modules\OrchardCore.Media.AmazonS3\OrchardCore.Media.AmazonS3.csproj", "{FF1C550C-6D30-499A-AF11-68DE7C8B6869}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.ArchiveLater", "src\OrchardCore.Modules\OrchardCore.ArchiveLater\OrchardCore.ArchiveLater.csproj", "{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1242,6 +1244,10 @@ Global
{FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Release|Any CPU.Build.0 = Release|Any CPU
{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1459,6 +1465,7 @@ Global
{25C50837-551F-433C-9CED-DD0732523D3D} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{38F43FA0-5BA8-4D6B-8F66-C708D590EF76} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{FF1C550C-6D30-499A-AF11-68DE7C8B6869} = {90030E85-0C4F-456F-B879-443E8A3F220D}
{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC} = {90030E85-0C4F-456F-B879-443E8A3F220D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341}
Expand Down
9 changes: 9 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.ArchiveLater/Assets.json
@@ -0,0 +1,9 @@
[
{
"generateSourceMaps": false,
"inputs": [
"Assets/scss/archive-later.scss"
],
"output": "wwwroot/Styles/archive-later.css"
}
]
@@ -0,0 +1,3 @@
.btn-archive-later {
white-space: nowrap;
}
@@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using OrchardCore.ArchiveLater.Models;
using OrchardCore.ArchiveLater.ViewModels;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.Display.Models;
using OrchardCore.Contents;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Modules;

namespace OrchardCore.ArchiveLater.Drivers;

public class ArchiveLaterPartDisplayDriver : ContentPartDisplayDriver<ArchiveLaterPart>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAuthorizationService _authorizationService;
private readonly ILocalClock _localClock;

public ArchiveLaterPartDisplayDriver(
IHttpContextAccessor httpContextAccessor,
IAuthorizationService authorizationService,
ILocalClock localClock)
{
_httpContextAccessor = httpContextAccessor;
_authorizationService = authorizationService;
_localClock = localClock;
}

public override IDisplayResult Display(ArchiveLaterPart part, BuildPartDisplayContext context)
=> Initialize<ArchiveLaterPartViewModel>(
$"{nameof(ArchiveLaterPart)}_SummaryAdmin",
model => PopulateViewModel(part, model)).Location("SummaryAdmin", "Meta:25");

public override IDisplayResult Edit(ArchiveLaterPart part, BuildPartEditorContext context)
=> Initialize<ArchiveLaterPartViewModel>(
GetEditorShapeType(context),
model => PopulateViewModel(part, model)).Location("Actions:10");

public override async Task<IDisplayResult> UpdateAsync(ArchiveLaterPart part, IUpdateModel updater, UpdatePartEditorContext context)
{
var httpContext = _httpContextAccessor.HttpContext;

if (await _authorizationService.AuthorizeAsync(httpContext?.User, CommonPermissions.PublishContent, part.ContentItem))
{
var viewModel = new ArchiveLaterPartViewModel();

await updater.TryUpdateModelAsync(viewModel, Prefix);

if (viewModel.ScheduledArchiveLocalDateTime == null || httpContext.Request.Form["submit.Save"] == "submit.CancelArchiveLater")
{
part.ScheduledArchiveUtc = null;
}
else
{
part.ScheduledArchiveUtc = await _localClock.ConvertToUtcAsync(viewModel.ScheduledArchiveLocalDateTime.Value);
}
}

return Edit(part, context);
}

private async ValueTask PopulateViewModel(ArchiveLaterPart part, ArchiveLaterPartViewModel viewModel)
{
viewModel.ContentItem = part.ContentItem;
viewModel.ScheduledArchiveUtc = part.ScheduledArchiveUtc;
viewModel.ScheduledArchiveLocalDateTime = part.ScheduledArchiveUtc.HasValue
? (await _localClock.ConvertToLocalAsync(part.ScheduledArchiveUtc.Value)).DateTime
: (DateTime?)null;
}
}
@@ -0,0 +1,15 @@
using System;
using YesSql.Indexes;

namespace OrchardCore.ArchiveLater.Indexes;

public class ArchiveLaterPartIndex : MapIndex
{
public string ContentItemId { get; set; }

public DateTime? ScheduledArchiveDateTimeUtc { get; set; }

public bool Published { get; set; }

public bool Latest { get; set; }
}
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.ArchiveLater.Models;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Handlers;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.Data;
using YesSql.Indexes;

namespace OrchardCore.ArchiveLater.Indexes;

public class ArchiveLaterPartIndexProvider : ContentHandlerBase, IIndexProvider, IScopedIndexProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly HashSet<string> _partRemoved = new();
private IContentDefinitionManager _contentDefinitionManager;

public ArchiveLaterPartIndexProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public override Task UpdatedAsync(UpdateContentContext context)
{
var part = context.ContentItem.As<ArchiveLaterPart>();

if (part != null)
{
_contentDefinitionManager ??= _serviceProvider.GetRequiredService<IContentDefinitionManager>();

var contentTypeDefinition = _contentDefinitionManager.GetTypeDefinition(context.ContentItem.ContentType);
if (!contentTypeDefinition.Parts.Any(pd => pd.Name == nameof(ArchiveLaterPart)))
{
context.ContentItem.Remove<ArchiveLaterPart>();
_partRemoved.Add(context.ContentItem.ContentItemId);
}
}

return Task.CompletedTask;
}

public string CollectionName { get; set; }

public Type ForType() => typeof(ContentItem);

public void Describe(IDescriptor context) => Describe((DescribeContext<ContentItem>)context);

public void Describe(DescribeContext<ContentItem> context)
{
context
.For<ArchiveLaterPartIndex>()
.When(contentItem => contentItem.Has<ArchiveLaterPart>() || _partRemoved.Contains(contentItem.ContentItemId))
.Map(contentItem =>
{
if (!contentItem.Published || !contentItem.Latest)
{
return null;
}
var part = contentItem.As<ArchiveLaterPart>();
if (part == null || !part.ScheduledArchiveUtc.HasValue)
{
return null;
}
return new ArchiveLaterPartIndex
{
ContentItemId = part.ContentItem.ContentItemId,
ScheduledArchiveDateTimeUtc = part.ScheduledArchiveUtc,
Published = part.ContentItem.Published,
Latest = part.ContentItem.Latest
};
});
}
}
11 changes: 11 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.ArchiveLater/Manifest.cs
@@ -0,0 +1,11 @@
using OrchardCore.Modules.Manifest;

[assembly: Module(
Name = "Archive Later",
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion,
Description = "The Archive Later module adds the ability to schedule content items to be archived at a given future date and time.",
Dependencies = new[] { "OrchardCore.Contents" },
Category = "Content Management"
)]
46 changes: 46 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.ArchiveLater/Migrations.cs
@@ -0,0 +1,46 @@
using System;
using OrchardCore.ArchiveLater.Indexes;
using OrchardCore.ArchiveLater.Models;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.ContentManagement.Metadata.Settings;
using OrchardCore.ContentManagement.Records;
using OrchardCore.Data.Migration;
using YesSql.Sql;

namespace OrchardCore.ArchiveLater;

public class Migrations : DataMigration
{
private readonly IContentDefinitionManager _contentDefinitionManager;

public Migrations(IContentDefinitionManager contentDefinitionManager)
{
_contentDefinitionManager = contentDefinitionManager;
}

public int Create()
{
_contentDefinitionManager.AlterPartDefinition(nameof(ArchiveLaterPart), builder => builder
.Attachable()
.WithDescription("Adds the ability to schedule content items to be archived at a given future date and time."));

SchemaBuilder.CreateMapIndexTable<ArchiveLaterPartIndex>(table => table
.Column<string>(nameof(ArchiveLaterPartIndex.ContentItemId))
.Column<DateTime>(nameof(ArchiveLaterPartIndex.ScheduledArchiveDateTimeUtc))
.Column<bool>(nameof(ArchiveLaterPartIndex.Published))
.Column<bool>(nameof(ArchiveLaterPartIndex.Latest))
);

SchemaBuilder.AlterIndexTable<ArchiveLaterPartIndex>(table => table
.CreateIndex($"IDX_{nameof(ArchiveLaterPartIndex)}_{nameof(ContentItemIndex.DocumentId)}",
"Id",
nameof(ContentItemIndex.DocumentId),
nameof(ArchiveLaterPartIndex.ContentItemId),
nameof(ArchiveLaterPartIndex.ScheduledArchiveDateTimeUtc),
nameof(ArchiveLaterPartIndex.Published),
nameof(ArchiveLaterPartIndex.Latest))
);

return 1;
}
}
@@ -0,0 +1,9 @@
using System;
using OrchardCore.ContentManagement;

namespace OrchardCore.ArchiveLater.Models;

public class ArchiveLaterPart : ContentPart
{
public DateTime? ScheduledArchiveUtc { get; set; }
}
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<!-- NuGet properties-->
<Title>OrchardCore AchiveLater</Title>
<Description>$(OCCMSDescription)

The Archive Later module adds the ability to schedule content items to be archived at a given future date and time.</Description>
<PackageTags>$(PackageTags) OrchardCoreCMS</PackageTags>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentManagement\OrchardCore.ContentManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentManagement.Display\OrchardCore.ContentManagement.Display.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Contents.Core\OrchardCore.Contents.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Data.Abstractions\OrchardCore.Data.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Data.YesSql\OrchardCore.Data.YesSql.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.DisplayManagement\OrchardCore.DisplayManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ResourceManagement\OrchardCore.ResourceManagement.csproj" />
</ItemGroup>

</Project>
@@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OrchardCore.ArchiveLater.Indexes;
using OrchardCore.ArchiveLater.Models;
using OrchardCore.BackgroundTasks;
using OrchardCore.ContentManagement;
using OrchardCore.Entities;
using OrchardCore.Modules;
using YesSql;

namespace OrchardCore.ArchiveLater.Services;

[BackgroundTask(Schedule = "* * * * *", Description = "Archives content items when their scheduled archive date time arrives.")]
public class ScheduledArchivingBackgroundTask : IBackgroundTask
{
private readonly ILogger<ScheduledArchivingBackgroundTask> _logger;
private readonly IClock _clock;

public ScheduledArchivingBackgroundTask(ILogger<ScheduledArchivingBackgroundTask> logger, IClock clock)
{
_logger = logger;
_clock = clock;
}

public async Task DoWorkAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var itemsToArchive = await serviceProvider
.GetRequiredService<ISession>()
.QueryIndex<ArchiveLaterPartIndex>(index => index.Latest && index.Published && index.ScheduledArchiveDateTimeUtc < _clock.UtcNow)
.ListAsync();

if (!itemsToArchive.Any())
{
return;
}

var contentManager = serviceProvider.GetRequiredService<IContentManager>();

foreach (var item in itemsToArchive)
{
var contentItem = await contentManager.GetAsync(item.ContentItemId);

var part = contentItem.As<ArchiveLaterPart>();
if (part != null)
{
part.ScheduledArchiveUtc = null;
}

_logger.LogDebug("Archiving scheduled content item {ContentItemId}.", contentItem.ContentItemId);

await contentManager.UnpublishAsync(contentItem);
}
}
}

0 comments on commit 83f7028

Please sign in to comment.