8012: Add Excel Export functionality to DynamicForms (#8016)

* Add Excel Export functionality to DynamicForms.

* Using the net46-targeted DLL of DocumentFormat.OpenXml instead of net40

* Updating DocumentFormat.OpenXml to latest (3.0.2) version

* Code styling

---------

Co-authored-by: Benedek Farkas <benedek.farkas@lombiq.com>
This commit is contained in:
Thierry Fleury 2024-04-16 16:59:14 +02:00 committed by GitHub
parent 530d2a9221
commit 04e9c73391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 134 additions and 23 deletions

View File

@ -74,5 +74,11 @@ namespace Orchard.DynamicForms.Controllers {
return Redirect(Request.UrlReferrer.ToString()); return Redirect(Request.UrlReferrer.ToString());
} }
public ActionResult Export(string id) =>
File(
_formService.ExportSubmissions(id),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Export.xlsx");
} }
} }

View File

@ -52,6 +52,12 @@
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="DocumentFormat.OpenXml, Version=3.0.2.0, Culture=neutral, PublicKeyToken=8fb06cb64d019a17, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\DocumentFormat.OpenXml.3.0.2\lib\net46\DocumentFormat.OpenXml.dll</HintPath>
</Reference>
<Reference Include="DocumentFormat.OpenXml.Framework, Version=3.0.2.0, Culture=neutral, PublicKeyToken=8fb06cb64d019a17, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\DocumentFormat.OpenXml.Framework.3.0.2\lib\net46\DocumentFormat.OpenXml.Framework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <Reference Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.4.1.0\lib\net472\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll</HintPath> <HintPath>..\..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.4.1.0\lib\net472\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll</HintPath>
</Reference> </Reference>
@ -68,6 +74,7 @@
<RequiredTargetFramework>3.5</RequiredTargetFramework> <RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference> </Reference>
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Web.ApplicationServices" /> <Reference Include="System.Web.ApplicationServices" />
<Reference Include="System.Web.DynamicData" /> <Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Entity" /> <Reference Include="System.Web.Entity" />
@ -96,6 +103,7 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Configuration" /> <Reference Include="System.Configuration" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Assets\CSS\DynamicForms-Admin.css" /> <Content Include="Assets\CSS\DynamicForms-Admin.css" />
@ -597,4 +605,4 @@
</FlavorProperties> </FlavorProperties>
</VisualStudio> </VisualStudio>
</ProjectExtensions> </ProjectExtensions>
</Project> </Project>

View File

@ -2,9 +2,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Data; using System.Data;
using System.IO;
using System.Linq; using System.Linq;
using System.Web; using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using Orchard.Collections; using Orchard.Collections;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData; using Orchard.ContentManagement.MetaData;
@ -21,6 +25,7 @@ using Orchard.Layouts.Models;
using Orchard.Layouts.Services; using Orchard.Layouts.Services;
using Orchard.Localization.Services; using Orchard.Localization.Services;
using Orchard.Services; using Orchard.Services;
using Orchard.Utility.Extensions;
namespace Orchard.DynamicForms.Services { namespace Orchard.DynamicForms.Services {
public class FormService : IFormService { public class FormService : IFormService {
@ -38,16 +43,16 @@ namespace Orchard.DynamicForms.Services {
private readonly ICultureAccessor _cultureAccessor; private readonly ICultureAccessor _cultureAccessor;
public FormService( public FormService(
ILayoutSerializer serializer, ILayoutSerializer serializer,
IClock clock, IClock clock,
IRepository<Submission> submissionRepository, IRepository<Submission> submissionRepository,
IFormElementEventHandler elementHandlers, IFormElementEventHandler elementHandlers,
IContentDefinitionManager contentDefinitionManager, IContentDefinitionManager contentDefinitionManager,
IBindingManager bindingManager, IBindingManager bindingManager,
IDynamicFormEventHandler formEventHandler, IDynamicFormEventHandler formEventHandler,
Lazy<IEnumerable<IElementValidator>> validators, Lazy<IEnumerable<IElementValidator>> validators,
IDateLocalizationServices dateLocalizationServices, IDateLocalizationServices dateLocalizationServices,
IOrchardServices services, IOrchardServices services,
ICultureAccessor cultureAccessor) { ICultureAccessor cultureAccessor) {
_serializer = serializer; _serializer = serializer;
@ -152,6 +157,90 @@ namespace Orchard.DynamicForms.Services {
}; };
} }
public Stream ExportSubmissions(string formName = null) {
var stream = new MemoryStream();
string GetColumnId(int columnNumber) {
string result = "";
do {
result = ((char)((columnNumber - 1) % 26 + (int)'A')).ToString() + result;
columnNumber = (columnNumber - 1) / 26;
} while (columnNumber != 0);
return result;
}
// Create a spreadsheet document.
var spreadsheetDocument = SpreadsheetDocument.Create(stream, SpreadsheetDocumentType.Workbook);
// Add a WorkbookPart to the document.
var workbookpart = spreadsheetDocument.AddWorkbookPart();
workbookpart.Workbook = new Workbook();
// Add a WorksheetPart to the WorkbookPart.
var worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
var sheetData = new SheetData();
worksheetPart.Worksheet = new Worksheet(sheetData);
// Add Sheets to the Workbook.
var sheets = spreadsheetDocument.WorkbookPart.Workbook.AppendChild(new Sheets());
// Fetch submissions
var query = _submissionRepository.Table;
if (!String.IsNullOrWhiteSpace(formName)) {
query = query.Where(x => x.FormName == formName);
}
var submissions = new Orderable<Submission>(query).Desc(x => x.CreatedUtc).Queryable.ToArray();
foreach (var formGroup in submissions.GroupBy(s => s.FormName)) {
// Append a new worksheet and associate it with the workbook.
var sheet = new Sheet() {
Id = spreadsheetDocument.WorkbookPart.GetIdOfPart(worksheetPart),
SheetId = 1,
Name = formGroup.Key
};
sheets.Append(sheet);
var data = GenerateDataTable(formGroup);
uint rowIndex = 1;
var headerRow = new Row { RowIndex = rowIndex };
sheetData.Append(headerRow);
for (int i = 0; i < data.Columns.Count; i++) {
var title = data.Columns[i].ToString().CamelFriendly();
headerRow.Append(new Cell {
CellReference = GetColumnId(i + 1) + rowIndex,
InlineString = new InlineString { Text = new Text(title) },
DataType = new EnumValue<CellValues>(CellValues.InlineString),
});
}
foreach (DataRow dataRow in data.Rows) {
rowIndex++;
var row = new Row { RowIndex = rowIndex };
sheetData.Append(row);
for (int i = 0; i < data.Columns.Count; i++) {
var value = dataRow[data.Columns[i]];
row.Append(new Cell {
CellReference = GetColumnId(i + 1) + rowIndex,
InlineString = new InlineString { Text = new Text(value.ToString()) },
DataType = new EnumValue<CellValues>(CellValues.InlineString),
});
}
}
}
workbookpart.Workbook.Save();
// Close the document.
spreadsheetDocument.Dispose();
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
public void DeleteSubmission(Submission submission) { public void DeleteSubmission(Submission submission) {
_submissionRepository.Delete(submission); _submissionRepository.Delete(submission);
} }
@ -191,10 +280,10 @@ namespace Orchard.DynamicForms.Services {
// Collect any remaining form values not handled by any specific element. // Collect any remaining form values not handled by any specific element.
var requestForm = _services.WorkContext.HttpContext.Request.Form; var requestForm = _services.WorkContext.HttpContext.Request.Form;
var blackList = new[] {"__RequestVerificationToken", "formName", "contentId"}; var blackList = new[] { "__RequestVerificationToken", "formName", "contentId" };
foreach (var key in foreach (var key in
from string key in requestForm from string key in requestForm
where !String.IsNullOrWhiteSpace(key) && !blackList.Contains(key) && values[key] == null where !String.IsNullOrWhiteSpace(key) && !blackList.Contains(key) && values[key] == null
select key) { select key) {
values.Add(key, requestForm[key]); values.Add(key, requestForm[key]);
@ -204,13 +293,14 @@ namespace Orchard.DynamicForms.Services {
} }
public DataTable GenerateDataTable(IEnumerable<Submission> submissions) { public DataTable GenerateDataTable(IEnumerable<Submission> submissions) {
var records = submissions.Select(x => Tuple.Create(x, x.ToNameValues())).ToArray(); var records = submissions.Select(x => System.Tuple.Create(x, x.ToNameValues())).ToArray();
var columnNames = new HashSet<string>(); var columnNames = new HashSet<string>();
var dataTable = new DataTable(); var dataTable = new DataTable();
foreach (var key in foreach (var key in
from record in records from record in records
from string key in record.Item2 where !columnNames.Contains(key) from string key in record.Item2
where !columnNames.Contains(key)
where !String.IsNullOrWhiteSpace(key) where !String.IsNullOrWhiteSpace(key)
select key) { select key) {
columnNames.Add(key); columnNames.Add(key);
@ -282,7 +372,7 @@ namespace Orchard.DynamicForms.Services {
if (form.Publication == "Publish" || !contentTypeSettings.Draftable) { if (form.Publication == "Publish" || !contentTypeSettings.Draftable) {
_contentManager.Publish(contentItem); _contentManager.Publish(contentItem);
} }
return contentItem; return contentItem;
} }
@ -307,8 +397,8 @@ namespace Orchard.DynamicForms.Services {
} }
private static void InvokePartBindings( private static void InvokePartBindings(
ContentItem contentItem, ContentItem contentItem,
IEnumerable<ContentPartBindingDescriptor> lookup, IEnumerable<ContentPartBindingDescriptor> lookup,
PartBindingSettings partBindingSettings, PartBindingSettings partBindingSettings,
string value) { string value) {
@ -348,7 +438,7 @@ namespace Orchard.DynamicForms.Services {
if (field == null) if (field == null)
return; return;
var fieldBindingDescriptorsQuery = var fieldBindingDescriptorsQuery =
from partBindingDescriptor in lookup from partBindingDescriptor in lookup
where partBindingDescriptor.Part.PartDefinition.Name == partBindingSettings.Name where partBindingDescriptor.Part.PartDefinition.Name == partBindingSettings.Name
from fieldBindingDescriptor in partBindingDescriptor.FieldBindings from fieldBindingDescriptor in partBindingDescriptor.FieldBindings

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Data; using System.Data;
using System.IO;
using System.Web.Mvc; using System.Web.Mvc;
using Orchard.Collections; using Orchard.Collections;
using Orchard.ContentManagement; using Orchard.ContentManagement;
@ -20,6 +21,7 @@ namespace Orchard.DynamicForms.Services {
Submission CreateSubmission(Submission submission); Submission CreateSubmission(Submission submission);
Submission GetSubmission(int id); Submission GetSubmission(int id);
IPageOfItems<Submission> GetSubmissions(string formName = null, int? skip = null, int? take = null); IPageOfItems<Submission> GetSubmissions(string formName = null, int? skip = null, int? take = null);
Stream ExportSubmissions(string formName = null);
void DeleteSubmission(Submission submission); void DeleteSubmission(Submission submission);
int DeleteSubmissions(IEnumerable<int> submissionIds); int DeleteSubmissions(IEnumerable<int> submissionIds);
void ReadElementValues(FormElement element, ReadElementValuesContext context); void ReadElementValues(FormElement element, ReadElementValuesContext context);

View File

@ -13,6 +13,9 @@
dataColumns.Add(Model.Submissions.Columns[i]); dataColumns.Add(Model.Submissions.Columns[i]);
} }
} }
<div class="manage">
@Html.ActionLink(T("Export").Text, "Export", "SubmissionAdmin", new { id = Model.FormName, area = "Orchard.DynamicForms" }, new { @class = "button primaryAction" })
</div>
@using (Html.BeginFormAntiForgeryPost()) { @using (Html.BeginFormAntiForgeryPost()) {
<fieldset class="bulk-actions"> <fieldset class="bulk-actions">
<label for="publishActions">@T("Actions:")</label> <label for="publishActions">@T("Actions:")</label>

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="DocumentFormat.OpenXml" version="3.0.2" targetFramework="net48" />
<package id="DocumentFormat.OpenXml.Framework" version="3.0.2" targetFramework="net48" />
<package id="Microsoft.AspNet.Mvc" version="5.2.7" targetFramework="net48" /> <package id="Microsoft.AspNet.Mvc" version="5.2.7" targetFramework="net48" />
<package id="Microsoft.AspNet.Razor" version="3.2.7" targetFramework="net48" /> <package id="Microsoft.AspNet.Razor" version="3.2.7" targetFramework="net48" />
<package id="Microsoft.AspNet.WebPages" version="3.2.7" targetFramework="net48" /> <package id="Microsoft.AspNet.WebPages" version="3.2.7" targetFramework="net48" />
<package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="4.1.0" targetFramework="net48" /> <package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="4.1.0" targetFramework="net48" />
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net48" /> <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net48" />
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net48" /> <package id="Newtonsoft.Json" version="12.0.3" targetFramework="net48" />
</packages> </packages>