diff --git a/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs b/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs index a393f785..6aeb9588 100644 --- a/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs +++ b/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs @@ -6,10 +6,13 @@ using Disco.Services.Documents; using Disco.Services.Documents.ManagedGroups; using Disco.Services.Expressions; using Disco.Services.Interop.ActiveDirectory; +using Disco.Services.Tasks; using iTextSharp.text.pdf; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; +using System.Data.Entity; using System.Drawing; using System.IO; using System.Linq; @@ -60,14 +63,6 @@ namespace Disco.BI.Extensions return dt.PdfExpressionsFromCache(Database).Values.OrderBy(e => e.Ordinal).ToList(); } - public static Stream GeneratePdfBulk(this DocumentTemplate dt, DiscoDataContext Database, User CreatorUser, DateTime Timestamp, bool InsertBlankPages, params string[] DataObjectsIds) - { - return Interop.Pdf.PdfGenerator.GenerateBulkFromTemplate(dt, Database, CreatorUser, Timestamp, InsertBlankPages, DataObjectsIds); - } - public static Stream GeneratePdfBulk(this DocumentTemplate dt, DiscoDataContext Database, User CreatorUser, DateTime Timestamp, bool InsertBlankPages, params IAttachmentTarget[] DataObjects) - { - return Interop.Pdf.PdfGenerator.GenerateBulkFromTemplate(dt, Database, CreatorUser, Timestamp, InsertBlankPages, DataObjects); - } public static Stream GeneratePdf(this DocumentTemplate dt, DiscoDataContext Database, IAttachmentTarget Target, User CreatorUser, DateTime TimeStamp, DocumentState State, bool FlattenFields = false) { bool generateExpression = !string.IsNullOrEmpty(dt.OnGenerateExpression); diff --git a/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs b/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs index e6db591c..3c6f3aa8 100644 --- a/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs +++ b/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs @@ -7,6 +7,7 @@ using Disco.Services; using Disco.Services.Documents; using Disco.Services.Expressions; using Disco.Services.Interop.ActiveDirectory; +using Disco.Services.Tasks; using Disco.Services.Users; using iTextSharp.text.pdf; using iTextSharp.text.pdf.codec; @@ -120,15 +121,19 @@ namespace Disco.BI.Interop.Pdf return bulkPdf; } } - public static Stream GenerateBulkFromTemplate(DocumentTemplate dt, DiscoDataContext Database, User CreatorUser, DateTime Timestamp, bool InsertBlankPages, params IAttachmentTarget[] DataObjects) + public static Stream GenerateBulkFromTemplate(DocumentTemplate dt, DiscoDataContext Database, User CreatorUser, DateTime Timestamp, bool InsertBlankPages, List DataObjects, IScheduledTaskStatus taskStatus) { - if (DataObjects.Length > 0) + if (DataObjects.Count > 0) { - List generatedPdfs = new List(DataObjects.Length); + List generatedPdfs = new List(DataObjects.Count); + var progressPerDoc = 80d / DataObjects.Count; + var progressDoc = 10d; using (var state = DocumentState.DefaultState()) { + taskStatus.UpdateStatus(10, "Rendering", "Starting"); foreach (var d in DataObjects) { + taskStatus.UpdateStatus(progressDoc += progressPerDoc, $"Rendering {d.AttachmentReferenceId}"); generatedPdfs.Add(dt.GeneratePdf(Database, d, CreatorUser, Timestamp, state, true)); state.SequenceNumber++; state.FlushScopeCache(); @@ -140,6 +145,7 @@ namespace Disco.BI.Interop.Pdf } else { + taskStatus.UpdateStatus(90, "Merging", "Merging documents"); Stream bulkPdf = Utilities.JoinPdfs(InsertBlankPages, generatedPdfs); foreach (Stream singlePdf in generatedPdfs) singlePdf.Dispose(); @@ -149,35 +155,36 @@ namespace Disco.BI.Interop.Pdf return null; } - public static Stream GenerateBulkFromTemplate(DocumentTemplate dt, DiscoDataContext Database, User CreatorUser, DateTime Timestamp, bool InsertBlankPages, params string[] DataObjectsIds) + public static Stream GenerateBulkFromTemplate(DocumentTemplate dt, DiscoDataContext Database, User CreatorUser, DateTime Timestamp, bool InsertBlankPages, List DataObjectsIds, IScheduledTaskStatus taskStatus) { - IAttachmentTarget[] DataObjects; + List DataObjects; + + taskStatus.UpdateStatus(0, "Resolving targets", "Resolving render targets"); switch (dt.Scope) { case DocumentTemplate.DocumentTemplateScopes.Device: - DataObjects = Database.Devices.Where(d => DataObjectsIds.Contains(d.SerialNumber)).ToArray(); + DataObjects = Database.Devices.Where(d => DataObjectsIds.Contains(d.SerialNumber)).Cast().ToList(); break; case DocumentTemplate.DocumentTemplateScopes.Job: - int[] intDataObjectsIds = DataObjectsIds.Select(i => int.Parse(i)).ToArray(); - DataObjects = Database.Jobs.Where(j => intDataObjectsIds.Contains(j.Id)).ToArray(); + var intDataObjectsIds = DataObjectsIds.Select(i => int.Parse(i)).ToList(); + DataObjects = Database.Jobs.Where(j => intDataObjectsIds.Contains(j.Id)).Cast().ToList(); break; case DocumentTemplate.DocumentTemplateScopes.User: - DataObjects = new IAttachmentTarget[DataObjectsIds.Length]; - for (int idIndex = 0; idIndex < DataObjectsIds.Length; idIndex++) + DataObjects = new List(DataObjectsIds.Count); + foreach (var userId in DataObjectsIds) { - string dataObjectId = DataObjectsIds[idIndex]; - - DataObjects[idIndex] = UserService.GetUser(ActiveDirectory.ParseDomainAccountId(dataObjectId), Database, true); - if (DataObjects[idIndex] == null) - throw new Exception($"Unknown Username specified: {dataObjectId}"); + var user = UserService.GetUser(ActiveDirectory.ParseDomainAccountId(userId), Database, true); + if (user == null) + throw new Exception($"Unknown Username specified: {userId}"); + DataObjects.Add(user); } break; default: throw new InvalidOperationException("Invalid DocumentType Scope"); } - return GenerateBulkFromTemplate(dt, Database, CreatorUser, Timestamp, InsertBlankPages, DataObjects); + return GenerateBulkFromTemplate(dt, Database, CreatorUser, Timestamp, InsertBlankPages, DataObjects, taskStatus); } public static Stream GenerateFromTemplate(DocumentTemplate dt, DiscoDataContext Database, IAttachmentTarget Data, User CreatorUser, DateTime TimeStamp, DocumentState State, bool FlattenFields = false) diff --git a/Disco.Models/UI/Config/DocumentTemplate/ConfigDocumentTemplateShowModel.cs b/Disco.Models/UI/Config/DocumentTemplate/ConfigDocumentTemplateShowModel.cs index 70c41456..c3cbb1c7 100644 --- a/Disco.Models/UI/Config/DocumentTemplate/ConfigDocumentTemplateShowModel.cs +++ b/Disco.Models/UI/Config/DocumentTemplate/ConfigDocumentTemplateShowModel.cs @@ -8,6 +8,8 @@ namespace Disco.Models.UI.Config.DocumentTemplate int StoredInstanceCount { get; set; } List TemplatePagesHaveAttachmentId { get; set; } int TemplatePageCount { get; } + string BulkGenerateDownloadId { get; } + string BulkGenerateDownloadFilename { get; } List JobTypes { get; set; } diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index 760c450a..8a148960 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -289,6 +289,7 @@ + diff --git a/Disco.Services/Documents/DocumentBulkGenerateTask.cs b/Disco.Services/Documents/DocumentBulkGenerateTask.cs new file mode 100644 index 00000000..0f26ecf3 --- /dev/null +++ b/Disco.Services/Documents/DocumentBulkGenerateTask.cs @@ -0,0 +1,96 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.Services.Tasks; +using Quartz; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Disco.Services.Documents +{ + public class DocumentBulkGenerateTask : ScheduledTask + { + private const string JobDataMapContext = "Context"; + + public override string TaskName { get; } = "Document Template - Bulk Generate"; + public override bool SingleInstanceTask { get; } = false; + public override bool CancelInitiallySupported { get; } = false; + + public override void InitalizeScheduledTask(DiscoDataContext Database) + { + // clear cache + var cachePath = GetCachePath(Database); + + if (Directory.Exists(cachePath)) + Directory.Delete(cachePath, true); + } + + public static Stream GetCached(DiscoDataContext database, string id) + { + var cachePath = GetCachePath(database); + var path = Path.Combine(cachePath, $"{id}.pdf"); + if (File.Exists(path)) + return File.OpenRead(path); + else + throw new FileNotFoundException(); + } + + public static ScheduledTaskStatus ScheduleNow(Func, IScheduledTaskStatus, Stream> generateDelegate, DocumentTemplate documentTemplate, User creatorUser, DateTime timestamp, bool insertBlankPages, List dataObjectsIds) + { + var context = new DocumentBulkGenerateContext() + { + GenerateDelegate = generateDelegate, + DocumentTemplate = documentTemplate, + CreatorUser = creatorUser, + Timestamp = timestamp, + InsertBlankPages = insertBlankPages, + DataObjectsIds = dataObjectsIds, + }; + + // Build Data Map + var task = new DocumentBulkGenerateTask(); + JobDataMap taskData = new JobDataMap() { { JobDataMapContext, context } }; + + // Schedule Task + var status = task.ScheduleTask(taskData); + context.Id = status.SessionId; + return status; + } + + protected override void ExecuteTask() + { + var context = (DocumentBulkGenerateContext)ExecutionContext.JobDetail.JobDataMap[JobDataMapContext]; + + using (var database = new DiscoDataContext()) + { + var cachePath = GetCachePath(database); + if (!Directory.Exists(cachePath)) + Directory.CreateDirectory(cachePath); + + var filePath = Path.Combine(cachePath, $"{Status.SessionId}.pdf"); + + var stream = context.GenerateDelegate(context.DocumentTemplate, database, context.CreatorUser, context.Timestamp, context.InsertBlankPages, context.DataObjectsIds, Status); + + using (var cacheStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) + stream.CopyTo(cacheStream); + + } + Status.UpdateStatus(100); + Status.Finished("Generated Document"); + } + + private static string GetCachePath(DiscoDataContext database) + => Path.Combine(database.DiscoConfiguration.DataStoreLocation, @"DocumentTemplates\BulkGenerateCache"); + + private class DocumentBulkGenerateContext + { + public Func, IScheduledTaskStatus, Stream> GenerateDelegate { get; set; } + public string Id { get; set; } + public DocumentTemplate DocumentTemplate { get; set; } + public User CreatorUser { get; set; } + public DateTime Timestamp { get; set; } + public bool InsertBlankPages { get; set; } + public List DataObjectsIds { get; set; } + } + } +} diff --git a/Disco.Services/amd64/pdfium.dll b/Disco.Services/amd64/pdfium.dll deleted file mode 100644 index 8969400e..00000000 Binary files a/Disco.Services/amd64/pdfium.dll and /dev/null differ diff --git a/Disco.Web/Areas/API/Controllers/DocumentTemplateController.cs b/Disco.Web/Areas/API/Controllers/DocumentTemplateController.cs index fc008764..4fbda51b 100644 --- a/Disco.Web/Areas/API/Controllers/DocumentTemplateController.cs +++ b/Disco.Web/Areas/API/Controllers/DocumentTemplateController.cs @@ -685,19 +685,32 @@ namespace Disco.Web.Areas.API.Controllers throw new InvalidOperationException("Unknown DocumentType Scope"); } - var dataIds = DataIds.Split(new string[] { Environment.NewLine, ",", ";" }, StringSplitOptions.RemoveEmptyEntries).Select(d => d.Trim()).Where(d => !string.IsNullOrEmpty(d)).ToArray(); + var dataIds = DataIds.Split(new string[] { Environment.NewLine, ",", ";" }, StringSplitOptions.RemoveEmptyEntries).Select(d => d.Trim()).Where(d => !string.IsNullOrEmpty(d)).ToList(); var timeStamp = DateTime.Now; - var pdf = documentTemplate.GeneratePdfBulk(Database, UserService.CurrentUser, timeStamp, InsertBlankPage, dataIds); - return File(pdf, "application/pdf", string.Format("{0}_Bulk_{1:yyyyMMdd-HHmmss}.pdf", documentTemplate.Id, timeStamp)); + var taskStatus = DocumentBulkGenerateTask.ScheduleNow(BI.Interop.Pdf.PdfGenerator.GenerateBulkFromTemplate, documentTemplate, UserService.CurrentUser, timeStamp, InsertBlankPage, dataIds); + + var fileName = $"{documentTemplate.Id}_Bulk_{timeStamp:yyyyMMdd-HHmmss}.pdf"; + taskStatus.SetFinishedUrl(Url.Action(MVC.Config.DocumentTemplate.Index(documentTemplate.Id, taskStatus.SessionId, fileName))); + + if (!taskStatus.WaitUntilFinished(TimeSpan.FromSeconds(1))) + return RedirectToAction(MVC.Config.Logging.TaskStatus(taskStatus.SessionId)); + + var stream = DocumentBulkGenerateTask.GetCached(Database, taskStatus.SessionId); + return File(stream, "application/pdf", fileName); } public virtual ActionResult Generate(string id, string TargetId) + [DiscoAuthorize(Claims.Config.DocumentTemplate.BulkGenerate)] + public virtual ActionResult BulkGenerateDownload(string id, string fileName) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); if (string.IsNullOrWhiteSpace(TargetId)) throw new ArgumentNullException(nameof(TargetId)); + var stream = DocumentBulkGenerateTask.GetCached(Database, id); + return File(stream, "application/pdf", fileName); + } // get template var template = Database.DocumentTemplates.Find(id); diff --git a/Disco.Web/Areas/Config/Controllers/DocumentTemplateController.cs b/Disco.Web/Areas/Config/Controllers/DocumentTemplateController.cs index 8ba972e1..960dd320 100644 --- a/Disco.Web/Areas/Config/Controllers/DocumentTemplateController.cs +++ b/Disco.Web/Areas/Config/Controllers/DocumentTemplateController.cs @@ -18,7 +18,7 @@ namespace Disco.Web.Areas.Config.Controllers public partial class DocumentTemplateController : AuthorizedDatabaseController { [DiscoAuthorize(Claims.Config.DocumentTemplate.Show)] - public virtual ActionResult Index(string id) + public virtual ActionResult Index(string id, string bulkGenerateId = null, string bulkGenerateFilename = null) { if (string.IsNullOrEmpty(id)) { @@ -63,6 +63,9 @@ namespace Disco.Web.Areas.Config.Controllers if (DocumentTemplateUsersManagedGroup.TryGetManagedGroup(m.DocumentTemplate, out usersManagedGroup)) m.UsersLinkedGroup = usersManagedGroup; + m.BulkGenerateDownloadId = bulkGenerateId; + m.BulkGenerateDownloadFilename = bulkGenerateFilename; + // UI Extensions UIExtensions.ExecuteExtensions(this.ControllerContext, m); diff --git a/Disco.Web/Areas/Config/Models/DocumentTemplate/ShowModel.cs b/Disco.Web/Areas/Config/Models/DocumentTemplate/ShowModel.cs index 2f82094b..2dc15616 100644 --- a/Disco.Web/Areas/Config/Models/DocumentTemplate/ShowModel.cs +++ b/Disco.Web/Areas/Config/Models/DocumentTemplate/ShowModel.cs @@ -31,6 +31,10 @@ namespace Disco.Web.Areas.Config.Models.DocumentTemplate public DocumentTemplateDevicesManagedGroup DevicesLinkedGroup { get; set; } public DocumentTemplateUsersManagedGroup UsersLinkedGroup { get; set; } + public string BulkGenerateDownloadId { get; set; } + + public string BulkGenerateDownloadFilename { get; set; } + public void UpdateModel(DiscoDataContext Database) { diff --git a/Disco.Web/Areas/Config/Views/DocumentTemplate/Show.cshtml b/Disco.Web/Areas/Config/Views/DocumentTemplate/Show.cshtml index e4320c39..2db3175c 100644 --- a/Disco.Web/Areas/Config/Views/DocumentTemplate/Show.cshtml +++ b/Disco.Web/Areas/Config/Views/DocumentTemplate/Show.cshtml @@ -774,103 +774,114 @@ } @if (canBulkGenerate) { - Bulk Generate -
-
- @switch (Model.DocumentTemplate.Scope) + if (Model.DocumentTemplate.Scope == DocumentTemplate.DocumentTemplateScopes.User) + { + @Html.ActionLinkButton("Bulk Generate", MVC.Config.DocumentTemplate.BulkGenerate(Model.DocumentTemplate.Id)) + } + else + { + Bulk Generate +
+
+ @switch (Model.DocumentTemplate.Scope) + { + case "Device": +
+ Enter multiple Device Serial Numbers separated by <new line>, commas (,) or semicolons (;). +
+
+

Examples:

+
+ 01234567
+ ABCD9876
+ 8VQ6G2R +
+
01234567,ABCD9876,8VQ6G2R
+
01234567;ABCD9876;8VQ6G2R
+
+ break; + case "Job": +
+ Enter multiple Job Ids separated by <new line>, commas (,) or semicolons (;). +
+
+

Examples:

+
+ 86
+ 99
+ 44 +
+
86,99,44
+
86;99;44
+
+ break; + } +
+ @using (Html.BeginForm(MVC.API.DocumentTemplate.BulkGenerate(Model.DocumentTemplate.Id), FormMethod.Post)) { - case "Device": -
- Enter multiple Device Serial Numbers separated by <new line>, commas (,) or semicolons (;). +
+ + if (Model.TemplatePageCount > 1 && Model.TemplatePageCount % 2 != 0) + { +
+
-
-

Examples:

-
- 01234567
- ABCD9876
- 8VQ6G2R -
-
01234567,ABCD9876,8VQ6G2R
-
01234567;ABCD9876;8VQ6G2R
-
- break; - case "Job": -
- Enter multiple Job Ids separated by <new line>, commas (,) or semicolons (;). -
-
-

Examples:

-
- 86
- 99
- 44 -
-
86,99,44
-
86;99;44
-
- break; - case "User": -
- Enter multiple User Ids separated by <new line>, commas (,) or semicolons (;). -
-
-

Examples:

-
- user6
- smi0099
@(ActiveDirectory.Context.PrimaryDomain.NetBiosName)\rsmith -
-
user6,smi0099,@(ActiveDirectory.Context.PrimaryDomain.NetBiosName)\rsmith
-
user6;smi0099;@(ActiveDirectory.Context.PrimaryDomain.NetBiosName)\rsmith
-
- break; + } }
- @using (Html.BeginForm(MVC.API.DocumentTemplate.BulkGenerate(Model.DocumentTemplate.Id), FormMethod.Post)) - { -
- - if (Model.TemplatePageCount > 1 && Model.TemplatePageCount % 2 != 0) - { -
- -
- } - } -
- + + } } @if (Authorization.Has(Claims.Config.DocumentTemplate.Delete)) { @Html.ActionLinkButton("Delete", MVC.API.DocumentTemplate.Delete(Model.DocumentTemplate.Id, true), "buttonDelete") }
+@if (!string.IsNullOrWhiteSpace(Model.BulkGenerateDownloadId)) +{ + + +} \ No newline at end of file