diff --git a/server/AyaNova/AyaNova.csproj b/server/AyaNova/AyaNova.csproj
index 469a5ec3..08d34c58 100644
--- a/server/AyaNova/AyaNova.csproj
+++ b/server/AyaNova/AyaNova.csproj
@@ -4,8 +4,8 @@
true
- 8.0.0-alpha.86
- 8.0.0.86
+ 8.0.0-alpha.87
+ 8.0.0.87
bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
1591
diff --git a/server/AyaNova/Controllers/ExportController.cs b/server/AyaNova/Controllers/ExportController.cs
new file mode 100644
index 00000000..cdaf6fbb
--- /dev/null
+++ b/server/AyaNova/Controllers/ExportController.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.EntityFrameworkCore;
+using AyaNova.Models;
+using AyaNova.Api.ControllerHelpers;
+using AyaNova.Biz;
+using AyaNova.Util;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace AyaNova.Api.Controllers
+{
+ [ApiController]
+ [ApiVersion("8.0")]
+ [Route("api/v{version:apiVersion}/export")]
+ [Produces("application/json")]
+ [Authorize]
+ public class ExportController : ControllerBase
+ {
+ private readonly AyContext ct;
+ private readonly ILogger log;
+ private readonly ApiServerState serverState;
+
+ ///
+ /// ctor
+ ///
+ ///
+ ///
+ ///
+ public ExportController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState)
+ {
+ ct = dbcontext;
+ log = logger;
+ serverState = apiServerState;
+ }
+
+
+ ///
+ /// Export to file
+ ///
+ /// Valid values are: "csv","json"
+ ///
+
+ /// downloadable export file name
+ [HttpPost("render")]
+ public async Task RenderExport([FromRoute] string format, [FromBody] DataListSelection dataListSelection)
+ {
+
+ if (!serverState.IsOpen)
+ return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
+ if (!ModelState.IsValid)
+ return BadRequest(new ApiErrorResponse(ModelState));
+
+ if (dataListSelection.IsEmpty)
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, null, "DataListSelection is required"));
+
+ if (!dataListSelection.ObjectType.HasAttribute(typeof(CoreBizObjectAttribute)))
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Not a taggable object type"));
+
+ if (!Authorized.HasReadFullRole(HttpContext.Items, dataListSelection.ObjectType))
+ return StatusCode(403, new ApiNotAuthorizedResponse());
+
+ if (string.IsNullOrWhiteSpace(format))
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, null, "format required"));
+
+ if (format != "csv" && format != "json")
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, "format not valid, must be 'csv' or 'json'"));
+
+ await dataListSelection.RehydrateIdList(ct, UserRolesFromContext.Roles(HttpContext.Items), log);
+ if (dataListSelection.SelectedRowIds.Length == 0)
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, null, "List of ids"));
+
+
+ log.LogDebug($"Instantiating biz object handler for {dataListSelection.ObjectType}");
+ var biz = BizObjectFactory.GetBizObject(dataListSelection.ObjectType, ct);
+ log.LogDebug($"Fetching data for {dataListSelection.SelectedRowIds.Length} {dataListSelection.ObjectType} items");
+ var TheData = await ((IExportAbleObject)biz).GetExportData(dataListSelection.SelectedRowIds);
+
+ string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf";
+ string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName);
+ log.LogDebug($"Calling render export data to file");
+ //TODO: RENDER DATA TO FILE HERE await page.PdfAsync(outputFullPath, PdfOptions);
+
+ log.LogDebug($"Completed, returning results");
+ return Ok(ApiOkResponse.Response(outputFileName));
+
+ // var httpConnectionFeature = HttpContext.Features.Get();
+ // var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/";
+ // try
+ // {
+ // var result = await biz.RenderExport(ExportParam, API_URL);
+
+ // if (string.IsNullOrWhiteSpace(result))
+ // return BadRequest(new ApiErrorResponse(biz.Errors));
+ // else
+ // return Ok(ApiOkResponse.Response(result));
+ // }
+ // catch (System.Exception ex)
+ // {
+ // //The Javascript evaluation stack trace can be in the message making it long and internalized,
+ // //however the info is useful as it can indicate exactly which function failed etc so sending it all back is best
+ // return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message));
+ // }
+ }
+
+
+ ///
+ /// Download a rendered Export
+ ///
+ ///
+ /// download token
+ ///
+ [HttpGet("download/{fileName}")]
+ [AllowAnonymous]
+ public async Task DownloadAsync([FromRoute] string fileName, [FromQuery] string t)
+ {
+ if (!serverState.IsOpen)
+ return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
+
+ if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
+ {
+ await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
+ return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
+ }
+
+ if (!FileUtil.TemporaryFileExists(fileName))
+ {
+ await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+
+ var FilePath = FileUtil.GetFullPathForTemporaryFile(fileName);
+ return PhysicalFile(FilePath, "application/pdf");
+ }
+
+
+
+ ///
+ /// Download Export template
+ ///
+ /// Export id
+ /// download token
+ /// A single Export template as a file
+ [AllowAnonymous]
+ [HttpGet("export/{id}")]
+ public async Task DownloadTemplate([FromRoute] long id, [FromQuery] string t)
+ {
+ if (serverState.IsClosed)
+ return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
+
+ if (!ModelState.IsValid)
+ return BadRequest(new ApiErrorResponse(ModelState));
+
+ if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
+ {
+ await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
+ return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
+ }
+
+ var o = await ct.Export.SingleOrDefaultAsync(z => z.Id == id);
+ //turn into correct format and then send as file
+ if (o == null)
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+ var asText = Newtonsoft.Json.JsonConvert.SerializeObject(
+ o,
+ Newtonsoft.Json.Formatting.None,
+ new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id" }) });
+ var bytes = System.Text.Encoding.UTF8.GetBytes(asText);
+ var file = new FileContentResult(bytes, "application/octet-stream");
+ file.FileDownloadName = Util.FileUtil.StringToSafeFileName(o.Name) + ".ayrt";
+ return file;
+ }
+
+
+
+
+ ///
+ /// Upload Reprot template export file
+ /// Max 15mb total
+ ///
+ /// Accepted
+ [Authorize]
+ [HttpPost("upload")]
+ [DisableFormValueModelBinding]
+ [RequestSizeLimit(15000000)]//currently the largest v7 export for a Export template is 828kb, I'm guessing 15mb is more than enough
+ public async Task UploadAsync()
+ {
+ //Adapted from the example found here: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
+
+ if (!serverState.IsOpen)
+ return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
+
+
+ // AyaTypeId attachToObject = null;
+ ApiUploadProcessor.ApiUploadedFilesResult uploadFormData = null;
+ try
+ {
+ if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, $"Expected a multipart request, but got {Request.ContentType}"));
+
+ //Save uploads to disk under temporary file names until we decide how to handle them
+ uploadFormData = await ApiUploadProcessor.ProcessUploadAsync(HttpContext);
+
+
+
+ List FileData = new List();
+
+ if (!uploadFormData.FormFieldData.ContainsKey("FileData"))//only filedata is required
+ {
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Missing required FormFieldData value: FileData"));
+ }
+
+ //fileData in JSON stringify format
+ FileData = Newtonsoft.Json.JsonConvert.DeserializeObject>(uploadFormData.FormFieldData["FileData"].ToString());
+
+ //Instantiate the business object handler
+ ExportBiz biz = ExportBiz.GetBiz(ct, HttpContext);
+
+
+ //We have our files now can parse and insert into db
+ if (uploadFormData.UploadedFiles.Count > 0)
+ {
+ //deserialize each file and import
+ foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
+ {
+ JObject o = JObject.Parse(System.IO.File.ReadAllText(a.InitialUploadedPathName));
+ if (!await biz.ImportAsync(o))
+ {
+ //delete all the files temporarily uploaded and return bad request
+ DeleteTempUploadFile(uploadFormData);
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+ }
+ }
+ }
+ catch (System.IO.InvalidDataException ex)
+ {
+ return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message));
+ }
+ finally
+ {
+ //delete all the files temporarily uploaded and return bad request
+
+ DeleteTempUploadFile(uploadFormData);
+ }
+
+ //Return the list of attachment ids and filenames
+ return Accepted();
+ }
+
+ private static void DeleteTempUploadFile(ApiUploadProcessor.ApiUploadedFilesResult uploadFormData)
+ {
+ if (uploadFormData.UploadedFiles.Count > 0)
+ {
+ foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
+ {
+ System.IO.File.Delete(a.InitialUploadedPathName);
+ }
+ }
+ }
+ //-----------------------------------------
+
+
+
+
+
+ }//eoc
+}//eons
\ No newline at end of file
diff --git a/server/AyaNova/biz/IExportAbleObject.cs b/server/AyaNova/biz/IExportAbleObject.cs
new file mode 100644
index 00000000..b5c81a01
--- /dev/null
+++ b/server/AyaNova/biz/IExportAbleObject.cs
@@ -0,0 +1,18 @@
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+namespace AyaNova.Biz
+{
+ ///
+ /// Interface for biz objects that support exporting
+ ///
+ internal interface IExportAbleObject
+ {
+
+ //Get items indicated in id list in exportable format
+ //called by ExportBiz rendering code
+ Task GetExportData(long[] idList);
+ const int EXPORT_DATA_BATCH_SIZE = 100;
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs
index bae11b1f..752a5fab 100644
--- a/server/AyaNova/biz/WidgetBiz.cs
+++ b/server/AyaNova/biz/WidgetBiz.cs
@@ -11,7 +11,7 @@ using System.Collections.Generic;
namespace AyaNova.Biz
{
- internal class WidgetBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject
+ internal class WidgetBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject
{
@@ -305,14 +305,22 @@ namespace AyaNova.Biz
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
foreach (Widget w in orderedList)
{
- var jo=JObject.FromObject(w);
- jo["CustomFields"]=JObject.Parse((string)jo["CustomFields"]);
+ var jo = JObject.FromObject(w);
+ jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
}
return ReportData;
}
+ //Get data for export
+ public async Task GetExportData(long[] idList)
+ {
+ //for now just re-use the report data code
+ //this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
+ return await GetReportData(idList);
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
@@ -389,6 +397,8 @@ namespace AyaNova.Biz
+
+
/////////////////////////////////////////////////////////////////////
}//eoc
diff --git a/server/AyaNova/util/AyaNovaVersion.cs b/server/AyaNova/util/AyaNovaVersion.cs
index c06e3677..1fda47aa 100644
--- a/server/AyaNova/util/AyaNovaVersion.cs
+++ b/server/AyaNova/util/AyaNovaVersion.cs
@@ -5,7 +5,7 @@ namespace AyaNova.Util
///
internal static class AyaNovaVersion
{
- public const string VersionString = "8.0.0-alpha.86";
+ public const string VersionString = "8.0.0-alpha.87";
public const string FullNameAndVersion = "AyaNova server " + VersionString;
}//eoc
}//eons
\ No newline at end of file