feature #180: bulk download document attachment instances
This commit is contained in:
@@ -20,6 +20,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data.Entity;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
@@ -1880,6 +1881,82 @@ namespace Disco.Web.Areas.API.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[DiscoAuthorize(Claims.Config.DocumentTemplate.Configure)]
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public virtual ActionResult BulkDownload([Required] string id, bool? latestOnly = null, DateTime? threshold = null)
|
||||
{
|
||||
var template = Database.DocumentTemplates.FirstOrDefault(t => t.Id == id)
|
||||
?? throw new ArgumentException("Unknown document template", nameof(id));
|
||||
|
||||
var attachments = BulkDownloadRetrieveAttachments(template, latestOnly ?? false, threshold);
|
||||
|
||||
var responseStream = new MemoryStream();
|
||||
using (var archive = new ZipArchive(responseStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
var repoFileName = attachment.RepositoryFilename(Database);
|
||||
if (System.IO.File.Exists(repoFileName))
|
||||
{
|
||||
var entry = archive.CreateEntry($"{attachment.Reference.ToString().Replace('\\', '_')}-{attachment.Timestamp:yyyyMMdd-HHmmss}_{attachment.Filename}", CompressionLevel.Fastest);
|
||||
entry.LastWriteTime = attachment.Timestamp;
|
||||
using (var entryStream = entry.Open())
|
||||
{
|
||||
using (var attachmentStream = System.IO.File.OpenRead(repoFileName))
|
||||
{
|
||||
attachmentStream.CopyTo(entryStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responseStream.Position = 0;
|
||||
return File(responseStream, "application/zip", $"{template.Id}_Attachments_{DateTime.Now:yyyyMMdd-HHmmss}.zip");
|
||||
}
|
||||
private List<IAttachment> BulkDownloadRetrieveAttachments(DocumentTemplate template, bool latestOnly, DateTime? threshold)
|
||||
{
|
||||
List<IAttachment> attachments;
|
||||
|
||||
switch (template.Scope)
|
||||
{
|
||||
case DocumentTemplate.DocumentTemplateScopes.Device:
|
||||
Authorization.Require(Claims.Device.ShowAttachments);
|
||||
var deviceQuery = Database.DeviceAttachments
|
||||
.Where(a => a.DocumentTemplateId == template.Id);
|
||||
if (threshold.HasValue)
|
||||
deviceQuery = deviceQuery.Where(a => a.Timestamp >= threshold.Value);
|
||||
attachments = deviceQuery.OrderBy(a => a.Timestamp).ToList<IAttachment>();
|
||||
break;
|
||||
case DocumentTemplate.DocumentTemplateScopes.Job:
|
||||
Authorization.Require(Claims.Job.ShowAttachments);
|
||||
var jobQuery = Database.JobAttachments
|
||||
.Where(a => a.DocumentTemplateId == template.Id);
|
||||
if (threshold.HasValue)
|
||||
jobQuery = jobQuery.Where(a => a.Timestamp >= threshold.Value);
|
||||
attachments = jobQuery.OrderBy(a => a.Timestamp).ToList<IAttachment>();
|
||||
break;
|
||||
case DocumentTemplate.DocumentTemplateScopes.User:
|
||||
Authorization.Require(Claims.User.ShowAttachments);
|
||||
var userQuery = Database.UserAttachments
|
||||
.Where(a => a.DocumentTemplateId == template.Id);
|
||||
if (threshold.HasValue)
|
||||
userQuery = userQuery.Where(a => a.Timestamp >= threshold.Value);
|
||||
attachments = userQuery.OrderBy(a => a.Timestamp).ToList<IAttachment>();
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
if (latestOnly)
|
||||
{
|
||||
attachments.Reverse();
|
||||
attachments = attachments.GroupBy(a => a.Reference).Select(a => a.First()).OrderBy(a => a.Timestamp).ToList();
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Handlers
|
||||
|
||||
@@ -16,26 +16,25 @@
|
||||
Model.TemplateExpressions.All(e => e.All(p => !p.ParseError) &&
|
||||
!Model.OnImportUserFlagRules.Any());
|
||||
|
||||
#region Can Bulk Generate
|
||||
var canBulkGenerate = Authorization.Has(Claims.Config.DocumentTemplate.BulkGenerate);
|
||||
if (canBulkGenerate)
|
||||
var canBulkGenerate = false;
|
||||
var canBulkDownload = false;
|
||||
switch (Model.DocumentTemplate.Scope)
|
||||
{
|
||||
switch (Model.DocumentTemplate.Scope)
|
||||
{
|
||||
case DocumentTemplate.DocumentTemplateScopes.Device:
|
||||
canBulkGenerate = Authorization.Has(Claims.Device.Actions.GenerateDocuments);
|
||||
break;
|
||||
case DocumentTemplate.DocumentTemplateScopes.Job:
|
||||
canBulkGenerate = Authorization.Has(Claims.Job.Actions.GenerateDocuments);
|
||||
break;
|
||||
case DocumentTemplate.DocumentTemplateScopes.User:
|
||||
canBulkGenerate = Authorization.Has(Claims.User.Actions.GenerateDocuments);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid DocumentType Scope");
|
||||
}
|
||||
case DocumentTemplate.DocumentTemplateScopes.Device:
|
||||
canBulkGenerate = Authorization.Has(Claims.Config.DocumentTemplate.BulkGenerate) && Authorization.Has(Claims.Device.Actions.GenerateDocuments);
|
||||
canBulkDownload = Authorization.Has(Claims.Device.ShowAttachments) && Model.StoredInstanceCount > 0;
|
||||
break;
|
||||
case DocumentTemplate.DocumentTemplateScopes.Job:
|
||||
canBulkGenerate = Authorization.Has(Claims.Config.DocumentTemplate.BulkGenerate) && Authorization.Has(Claims.Job.Actions.GenerateDocuments);
|
||||
canBulkDownload = Authorization.Has(Claims.Job.ShowAttachments) && Model.StoredInstanceCount > 0;
|
||||
break;
|
||||
case DocumentTemplate.DocumentTemplateScopes.User:
|
||||
canBulkGenerate = Authorization.Has(Claims.Config.DocumentTemplate.BulkGenerate) && Authorization.Has(Claims.User.Actions.GenerateDocuments);
|
||||
canBulkDownload = Authorization.Has(Claims.User.ShowAttachments) && Model.StoredInstanceCount > 0;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid DocumentType Scope");
|
||||
}
|
||||
#endregion
|
||||
|
||||
ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Document Templates", MVC.Config.DocumentTemplate.Index(null), Model.DocumentTemplate.Description);
|
||||
|
||||
@@ -1037,6 +1036,66 @@
|
||||
{
|
||||
@Html.ActionLinkButton("Export Instances", MVC.Config.DocumentTemplate.Export(Model.DocumentTemplate.Id, null))
|
||||
}
|
||||
@if (canBulkDownload)
|
||||
{
|
||||
<button id="dialogBulkDownloadButton" type="button" class="button">Download Instances</button>
|
||||
<div id="dialogBulkDownload" class="dialog" title="Download Instances: @(Model.DocumentTemplate.Id)">
|
||||
@using (Html.BeginForm(MVC.API.DocumentTemplate.BulkDownload(Model.DocumentTemplate.Id)))
|
||||
{
|
||||
@Html.AntiForgeryToken()
|
||||
<h3>Scope</h3>
|
||||
<ul class="none">
|
||||
<li>
|
||||
<label><input type="radio" name="latestOnly" value="True" checked /> Latest @Model.DocumentTemplate.Scope Attachment</label>
|
||||
</li>
|
||||
<li>
|
||||
<label><input type="radio" name="latestOnly" value="False" /> All @Model.DocumentTemplate.Scope Attachments</label>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<h3>Threshold</h3>
|
||||
<div>
|
||||
<label>Only On or After <input type="date" name="threshold" value="@DateTime.Now.ToString("yyyy")-01-01" /></label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
let dialog;
|
||||
$('#dialogBulkDownloadButton').on('click', function () {
|
||||
if (!dialog) {
|
||||
dialog = $('#dialogBulkDownload').dialog({
|
||||
resizable: false,
|
||||
modal: true,
|
||||
autoOpen: false,
|
||||
width: 460,
|
||||
buttons: {
|
||||
Close: function () {
|
||||
$(this).dialog("close");
|
||||
},
|
||||
"Download Instances": function () {
|
||||
const $this = $(this);
|
||||
const $form = $this.find('form');
|
||||
$form.trigger('submit');
|
||||
$form.find('input').prop('disabled', true);
|
||||
$this.closest('.ui-dialog').find('.ui-dialog-buttonset button').prop('disabled', true).addClass('ui-state-disabled');
|
||||
window.setTimeout(function () {
|
||||
$this.dialog("close");
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dialog.dialog('open');
|
||||
dialog.find('form').find('input').prop('disabled', false);
|
||||
dialog.closest('.ui-dialog').find('.ui-dialog-buttonset button').prop('disabled', false).removeClass('ui-state-disabled');
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@if (canBulkGenerate)
|
||||
{
|
||||
if (Model.DocumentTemplate.Scope == DocumentTemplate.DocumentTemplateScopes.User || Model.DocumentTemplate.Scope == DocumentTemplate.DocumentTemplateScopes.Device)
|
||||
@@ -1045,7 +1104,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<a id="buttonBulkGenerate" href="#" class="button">Bulk Generate</a>
|
||||
<button id="buttonBulkGenerate" type="button" class="button">Bulk Generate</button>
|
||||
<div id="dialogBulkGenerate" class="dialog dialog-bulk-generate" title="Bulk Generate: @(Model.DocumentTemplate.Id)">
|
||||
<div class="brief">
|
||||
@switch (Model.DocumentTemplate.Scope)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -224,7 +224,7 @@
|
||||
<div>
|
||||
Add all devices in the selected batch
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
@using (Html.BeginForm(MVC.API.DocumentTemplate.BulkGenerateAddDeviceBatch()))
|
||||
{
|
||||
|
||||
@@ -1287,10 +1287,11 @@ WriteLiteral(" <div>\r\n Add all devices in th
|
||||
|
||||
#line 227 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
}
|
||||
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral(")\r\n </div>\r\n");
|
||||
WriteLiteral(" </div>\r\n");
|
||||
|
||||
|
||||
#line 229 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
@@ -1336,15 +1337,15 @@ WriteLiteral(">\r\n");
|
||||
#line hidden
|
||||
WriteLiteral(" <div");
|
||||
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 9870), Tuple.Create("\"", 9922)
|
||||
, Tuple.Create(Tuple.Create("", 9878), Tuple.Create("item", 9878), true)
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 9869), Tuple.Create("\"", 9921)
|
||||
, Tuple.Create(Tuple.Create("", 9877), Tuple.Create("item", 9877), true)
|
||||
|
||||
#line 235 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create(" ", 9882), Tuple.Create<System.Object, System.Int32>(batch.Count == 0 ? "disabled" : null
|
||||
, Tuple.Create(Tuple.Create(" ", 9881), Tuple.Create<System.Object, System.Int32>(batch.Count == 0 ? "disabled" : null
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 9883), false)
|
||||
, 9882), false)
|
||||
);
|
||||
|
||||
WriteLiteral(" data-id=\"");
|
||||
@@ -1413,14 +1414,14 @@ WriteLiteral(" type=\"hidden\"");
|
||||
|
||||
WriteLiteral(" name=\"scope\"");
|
||||
|
||||
WriteAttribute("value", Tuple.Create(" value=\"", 10226), Tuple.Create("\"", 10240)
|
||||
WriteAttribute("value", Tuple.Create(" value=\"", 10225), Tuple.Create("\"", 10239)
|
||||
|
||||
#line 240 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 10234), Tuple.Create<System.Object, System.Int32>(scope
|
||||
, Tuple.Create(Tuple.Create("", 10233), Tuple.Create<System.Object, System.Int32>(scope
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 10234), false)
|
||||
, 10233), false)
|
||||
);
|
||||
|
||||
WriteLiteral(" />\r\n");
|
||||
@@ -1471,19 +1472,19 @@ WriteLiteral(" id=\"DocumentTemplate_BulkGenerate_Dialog_AddDocumentAttachment\"
|
||||
|
||||
WriteLiteral(" class=\"dialog dialog-bulk-generate\"");
|
||||
|
||||
WriteAttribute("title", Tuple.Create(" title=\"", 10476), Tuple.Create("\"", 10549)
|
||||
WriteAttribute("title", Tuple.Create(" title=\"", 10475), Tuple.Create("\"", 10548)
|
||||
|
||||
#line 248 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 10484), Tuple.Create<System.Object, System.Int32>(Model.DocumentTemplate.Description
|
||||
, Tuple.Create(Tuple.Create("", 10483), Tuple.Create<System.Object, System.Int32>(Model.DocumentTemplate.Description
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 10484), false)
|
||||
, Tuple.Create(Tuple.Create("", 10521), Tuple.Create(":", 10521), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10522), Tuple.Create("Add", 10523), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10526), Tuple.Create("by", 10527), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10529), Tuple.Create("Document", 10530), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10538), Tuple.Create("Attachment", 10539), true)
|
||||
, 10483), false)
|
||||
, Tuple.Create(Tuple.Create("", 10520), Tuple.Create(":", 10520), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10521), Tuple.Create("Add", 10522), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10525), Tuple.Create("by", 10526), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10528), Tuple.Create("Document", 10529), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 10537), Tuple.Create("Attachment", 10538), true)
|
||||
);
|
||||
|
||||
WriteLiteral(">\r\n <div");
|
||||
@@ -1581,15 +1582,15 @@ WriteLiteral(">\r\n");
|
||||
#line hidden
|
||||
WriteLiteral(" <div");
|
||||
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 11364), Tuple.Create("\"", 11419)
|
||||
, Tuple.Create(Tuple.Create("", 11372), Tuple.Create("item", 11372), true)
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 11363), Tuple.Create("\"", 11418)
|
||||
, Tuple.Create(Tuple.Create("", 11371), Tuple.Create("item", 11371), true)
|
||||
|
||||
#line 269 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create(" ", 11376), Tuple.Create<System.Object, System.Int32>(template.Count == 0 ? "disabled" : null
|
||||
, Tuple.Create(Tuple.Create(" ", 11375), Tuple.Create<System.Object, System.Int32>(template.Count == 0 ? "disabled" : null
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 11377), false)
|
||||
, 11376), false)
|
||||
);
|
||||
|
||||
WriteLiteral(" data-id=\"");
|
||||
@@ -1686,14 +1687,14 @@ WriteLiteral(" type=\"hidden\"");
|
||||
|
||||
WriteLiteral(" name=\"scope\"");
|
||||
|
||||
WriteAttribute("value", Tuple.Create(" value=\"", 12106), Tuple.Create("\"", 12120)
|
||||
WriteAttribute("value", Tuple.Create(" value=\"", 12105), Tuple.Create("\"", 12119)
|
||||
|
||||
#line 278 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 12114), Tuple.Create<System.Object, System.Int32>(scope
|
||||
, Tuple.Create(Tuple.Create("", 12113), Tuple.Create<System.Object, System.Int32>(scope
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 12114), false)
|
||||
, 12113), false)
|
||||
);
|
||||
|
||||
WriteLiteral(" />\r\n");
|
||||
@@ -1744,19 +1745,19 @@ WriteLiteral(" id=\"DocumentTemplate_BulkGenerate_Dialog_AddUserDetail\"");
|
||||
|
||||
WriteLiteral(" class=\"dialog dialog-bulk-generate\"");
|
||||
|
||||
WriteAttribute("title", Tuple.Create(" title=\"", 12342), Tuple.Create("\"", 12407)
|
||||
WriteAttribute("title", Tuple.Create(" title=\"", 12341), Tuple.Create("\"", 12406)
|
||||
|
||||
#line 286 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 12350), Tuple.Create<System.Object, System.Int32>(Model.DocumentTemplate.Description
|
||||
, Tuple.Create(Tuple.Create("", 12349), Tuple.Create<System.Object, System.Int32>(Model.DocumentTemplate.Description
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 12350), false)
|
||||
, Tuple.Create(Tuple.Create("", 12387), Tuple.Create(":", 12387), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12388), Tuple.Create("Add", 12389), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12392), Tuple.Create("by", 12393), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12395), Tuple.Create("User", 12396), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12400), Tuple.Create("Detail", 12401), true)
|
||||
, 12349), false)
|
||||
, Tuple.Create(Tuple.Create("", 12386), Tuple.Create(":", 12386), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12387), Tuple.Create("Add", 12388), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12391), Tuple.Create("by", 12392), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12394), Tuple.Create("User", 12395), true)
|
||||
, Tuple.Create(Tuple.Create(" ", 12399), Tuple.Create("Detail", 12400), true)
|
||||
);
|
||||
|
||||
WriteLiteral(">\r\n <div");
|
||||
@@ -1874,15 +1875,15 @@ WriteLiteral(">\r\n");
|
||||
#line hidden
|
||||
WriteLiteral(" <div");
|
||||
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 13278), Tuple.Create("\"", 13328)
|
||||
, Tuple.Create(Tuple.Create("", 13286), Tuple.Create("item", 13286), true)
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 13277), Tuple.Create("\"", 13327)
|
||||
, Tuple.Create(Tuple.Create("", 13285), Tuple.Create("item", 13285), true)
|
||||
|
||||
#line 309 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create(" ", 13290), Tuple.Create<System.Object, System.Int32>(key.Count == 0 ? "disabled" : null
|
||||
, Tuple.Create(Tuple.Create(" ", 13289), Tuple.Create<System.Object, System.Int32>(key.Count == 0 ? "disabled" : null
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 13291), false)
|
||||
, 13290), false)
|
||||
);
|
||||
|
||||
WriteLiteral(" data-id=\"");
|
||||
@@ -1951,14 +1952,14 @@ WriteLiteral(" type=\"hidden\"");
|
||||
|
||||
WriteLiteral(" name=\"scope\"");
|
||||
|
||||
WriteAttribute("value", Tuple.Create(" value=\"", 13635), Tuple.Create("\"", 13649)
|
||||
WriteAttribute("value", Tuple.Create(" value=\"", 13634), Tuple.Create("\"", 13648)
|
||||
|
||||
#line 314 "..\..\Areas\Config\Views\DocumentTemplate\_BulkGenerateShared.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 13643), Tuple.Create<System.Object, System.Int32>(scope
|
||||
, Tuple.Create(Tuple.Create("", 13642), Tuple.Create<System.Object, System.Int32>(scope
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 13643), false)
|
||||
, 13642), false)
|
||||
);
|
||||
|
||||
WriteLiteral(" />\r\n");
|
||||
|
||||
Reference in New Issue
Block a user