This commit is contained in:
2019-01-10 00:15:01 +00:00
parent 4cacf19b65
commit 55266fd87f
11 changed files with 591 additions and 79 deletions

View File

@@ -51,10 +51,12 @@ UI FEATURE - The ability to customize forms:
- Need a FormCustom controller that supports routes for:
- Routes for customization form populating and receiving
- GET (formkey), gets a FormOptions object that combines the data from FormAvailableFields static class with the current custom values if any from the FormCustom table
- GET (formkey), gets a FormOptions object that provides the basis for building the form customization view
- Also required at the client is the current form customizations if any which can be gotten from the other route for day to day ops below
- POST (formkey), accepts a FormCustom object that contains the users customization choices only
- Validation ensures "CORE" fields cannot be customized
- Localization changes are handled here even though they aren't part of the actual FormCustom table, so split off and separately the localized text is updated
- Localization changes are NOT handled here even though it's the same client end form, that is handled at the CLIENT end after it has updated this part separately through regular localized text routes
- To ensure proper separation of concerns
- Routes for the day to day form display purposes that efficiently fetch the form customization to use on demand and caches at the client
- (GET formkey, token): Given a formkey and an *optional* concurrency token, returns one of the following:
@@ -63,7 +65,7 @@ UI FEATURE - The ability to customize forms:
- Basically whatever is in the FormCustom table record for that formkey
- If concurrency token provided and is unchanged then simply returns a code 304 (NOT MODIFIED)
- TESTS for the above!!!
EXISTING v7:

View File

@@ -18,6 +18,8 @@ SERVER
- Resource localization edit all Custom field locale keys for all langauges and change to be 1 to 16 (remove 0 and add 6 more)
- So, for example ClientCustom0 becomes ClientCustom1 and make sure the display values are identical as right now there are "My Custom0" and "Custom Field 8" in the same bunch
- Copy from WidgetCustom1 to 16 if necessary or for reference
- Resource localization edit wherever possible to change "Common*" such as "CommonActive" to remove the "common" part wherever possible.
- Also check if used anywhere in client or at server and rename there too
- CUSTOM FIELDS?!
- CUSTOM FIELDS (case 3426)
@@ -77,7 +79,8 @@ DEVOPS
- Doesn't require docs support as is now changed to a standard file attachment
- SERVER landing page
- I know TTM and all that but the server landing page should look nicer. Maybe a logo and better layout or something.
=-=-=-

View File

@@ -15,12 +15,9 @@ using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
//DOCUMENTATING THE API
//https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/recommended-tags-for-documentation-comments
//https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments
/// <summary>
/// Sample controller class used during development for testing purposes
///
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]

View File

@@ -0,0 +1,273 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
///
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class FormCustomController : Controller
{
private readonly AyContext ct;
private readonly ILogger<FormCustomController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public FormCustomController(AyContext dbcontext, ILogger<FormCustomController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get form customizations for Client form display
///
/// Required roles:
/// Any
///
/// </summary>
/// <param name="formkey"></param>
/// <param name="concurrencyToken"></param>
/// <returns>A single FormCustom</returns>
[HttpGet("{formkey}")]
public async Task<IActionResult> GetFormCustom([FromRoute] string formkey, [FromQuery] uint? concurrencyToken)
{
if (serverState.IsClosed)
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
//Instantiate the business object handler
FormCustomBiz biz = FormCustomBiz.GetBiz(ct, HttpContext);
//Just have to be authenticated for this one
if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, biz.BizType))
return StatusCode(401, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
var o = await biz.GetAsync(formkey);
if (o == null)
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
//If concurrency token specified then check if ours is newer
if (concurrencyToken != null)
{
if (o.ConcurrencyToken != concurrencyToken)
{
//returns a code 304 (NOT MODIFIED)
return StatusCode(304);
}
}
return Ok(new ApiOkResponse(o));
}
/// <summary>
/// Get available fields for form specified
/// Used to build UI for customizing a form
///
/// Required roles:
/// BizAdminFull only has rights to customize forms
///
/// </summary>
/// <param name="formkey"></param>
/// <returns>A single FormCustom</returns>
[HttpGet("AvailableFields/{formkey}")]
public ActionResult GetAvailableFields([FromRoute] string formkey)
{
if (serverState.IsClosed)
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.FormCustom))
return StatusCode(401, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
if (FormAvailableFields.IsValidFormKey(formkey))
{
return Ok(new ApiOkResponse(FormAvailableFields.FormFields(formkey)));
}
else
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
}
// /// <summary>
// /// Get FormCustom pick list
// ///
// /// Required roles: Any
// ///
// /// </summary>
// /// <returns>List of public or owned data filters for listKey provided</returns>
// [HttpGet("PickList", Name = nameof(FormCustomPickList))]
// public async Task<IActionResult> FormCustomPickList([FromQuery] string ListKey)
// {
// if (serverState.IsClosed)
// return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
// if (!ModelState.IsValid)
// return BadRequest(new ApiErrorResponse(ModelState));
// //Instantiate the business object handler
// FormCustomBiz biz = FormCustomBiz.GetBiz(ct, HttpContext);
// var l = await biz.GetPickListAsync(ListKey);
// return Ok(new ApiOkResponse(l));
// }
// /// <summary>
// /// Put (update) FormCustom
// ///
// /// Required roles:
// /// Any (public filter) or owned only (private filter)
// ///
// /// </summary>
// /// <param name="id"></param>
// /// <param name="inObj"></param>
// /// <returns></returns>
// [HttpPut("{id}")]
// public async Task<IActionResult> PutFormCustom([FromRoute] long id, [FromBody] FormCustom inObj)
// {
// if (!serverState.IsOpen)
// return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
// if (!ModelState.IsValid)
// return BadRequest(new ApiErrorResponse(ModelState));
// //Instantiate the business object handler
// FormCustomBiz biz = FormCustomBiz.GetBiz(ct, HttpContext);
// var o = await biz.GetNoLogAsync(id);
// if (o == null)
// return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
// if (!Authorized.IsAuthorizedToModify(HttpContext.Items, biz.BizType, o.OwnerId))
// return StatusCode(401, new ApiNotAuthorizedResponse());
// try
// {
// if (!biz.Put(o, inObj))
// return BadRequest(new ApiErrorResponse(biz.Errors));
// }
// catch (DbUpdateConcurrencyException)
// {
// if (!await biz.ExistsAsync(id))
// return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
// else
// return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
// }
// return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken }));
// }
// /// <summary>
// /// Post FormCustom
// ///
// /// Required roles:
// /// BizAdminFull, InventoryFull, TechFull
// /// </summary>
// /// <param name="inObj"></param>
// /// <returns></returns>
// [HttpPost]
// public async Task<IActionResult> PostFormCustom([FromBody] FormCustom inObj)
// {
// if (!serverState.IsOpen)
// return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
// //Instantiate the business object handler
// FormCustomBiz biz = FormCustomBiz.GetBiz(ct, HttpContext);
// //If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner
// if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, biz.BizType))
// return StatusCode(401, new ApiNotAuthorizedResponse());
// if (!ModelState.IsValid)
// return BadRequest(new ApiErrorResponse(ModelState));
// //Create and validate
// FormCustom o = await biz.CreateAsync(inObj);
// if (o == null)
// return BadRequest(new ApiErrorResponse(biz.Errors));
// else
// return CreatedAtAction("GetFormCustom", new { id = o.Id }, new ApiCreatedResponse(o));
// }
// /// <summary>
// /// Delete FormCustom
// ///
// /// Required roles:
// /// Any if public otherwise creator only
// ///
// /// </summary>
// /// <param name="id"></param>
// /// <returns>Ok</returns>
// [HttpDelete("{id}")]
// public async Task<IActionResult> DeleteFormCustom([FromRoute] long id)
// {
// if (!serverState.IsOpen)
// return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
// if (!ModelState.IsValid)
// return BadRequest(new ApiErrorResponse(ModelState));
// //Instantiate the business object handler
// FormCustomBiz biz = FormCustomBiz.GetBiz(ct, HttpContext);
// var o = await biz.GetNoLogAsync(id);
// if (o == null)
// return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
// if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, biz.BizType, o.OwnerId))
// return StatusCode(401, new ApiNotAuthorizedResponse());
// if (!biz.Delete(o))
// return BadRequest(new ApiErrorResponse(biz.Errors));
// return NoContent();
// }
//------------
}//eoc
}//eons

View File

@@ -32,14 +32,15 @@ namespace AyaNova.Biz
DEPRECATED_REUSELATER_15 = 15,
DEPRECATED_REUSELATER_16 = 16,
FileAttachment = 17,
DataFilter = 18
DataFilter = 18,
FormCustom = 19
//NOTE: New objects added here need to also be added to the following classes:
//AyaNova.Biz.BizObjectExistsInDatabase
//AyaNova.Biz.BizObjectFactory
//AyaNova.Biz.BizRoles
//AyaNova.Biz.BizObjectNameFetcher && BizObjectNameFetcherDIRECT
//AyaNova.Biz.BizObjectNameFetcherDIRECT
}

View File

@@ -43,6 +43,8 @@ namespace AyaNova.Biz
return ct.FileAttachment.Any(m => m.Id == id);
case AyaType.DataFilter:
return ct.DataFilter.Any(m => m.Id == id);
case AyaType.FormCustom:
return ct.FormCustom.Any(m => m.Id == id);

View File

@@ -26,7 +26,7 @@ namespace AyaNova.Biz
case AyaType.User:
return new UserBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, roles);
case AyaType.Widget:
return new WidgetBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, roles);
return new WidgetBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, roles);
case AyaType.JobOperations:
return new JobOperationsBiz(dbcontext, userId, roles);
case AyaType.AyaNova7Import:
@@ -37,6 +37,8 @@ namespace AyaNova.Biz
return new LocaleBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, roles);
case AyaType.DataFilter:
return new DataFilterBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, roles);
case AyaType.FormCustom:
return new FormCustomBiz(dbcontext, userId, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, roles);
default:

View File

@@ -1,67 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
//Turn a type and ID into a displayable name
internal static class BizObjectNameFetcher
{
/*
NOT SURE IF I WILL WANT TO USE THIS OR NOT GOING FORWARD SO KEEPING IT IN PLACE BUT NOT USABLE FOR NOW
*/
internal static string Name(AyaTypeId tid, AyContext ct = null)
{
throw new System.NotSupportedException("BizObjectNameFetcher:: Slow version, did you mean to call this one?");
//return Name(tid.ObjectType, tid.ObjectId, ct);
}
//Returns existance status of object type and id specified in database
internal static string Name(AyaType aytype, long id, AyContext ct = null)
{
throw new System.NotSupportedException("BizObjectNameFetcher:: Slow version, did you mean to call this one?");
// //new up a context??
// if (ct == null)
// {
// ct = ServiceProviderProvider.DBContext;
// }
// switch (aytype)
// {
// case AyaType.User:
// return ct.User.AsNoTracking().Where(m => m.Id == id).Select(m => m.Name).FirstOrDefault();
// case AyaType.Widget:
// return ct.Widget.AsNoTracking().Where(m => m.Id == id).Select(m => m.Name).FirstOrDefault();
// case AyaType.FileAttachment:
// return ct.FileAttachment.AsNoTracking().Where(m => m.Id == id).Select(m => m.DisplayFileName).FirstOrDefault();
// default:
// throw new System.NotSupportedException($"AyaNova.BLL.BizObjectNameFetcher::Name type {aytype.ToString()} is not supported");
// }
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -45,6 +45,10 @@ namespace AyaNova.Biz
case AyaType.DataFilter:
TABLE = "adatafilter";
break;
case AyaType.FormCustom:
TABLE = "aformcustom";
COLUMN = "formkey";
break;
default:
throw new System.NotSupportedException($"AyaNova.BLL.BizObjectNameFetcher::Name type {aytype.ToString()} is not supported");
}

View File

@@ -149,6 +149,17 @@ namespace AyaNova.Biz
ReadFullRecord = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////
//FORMCUSTOM
//
roles.Add(AyaType.FormCustom, new BizRoleSet()
{
//Only BizAdminFull can modify forms
Change = AuthorizationRoles.BizAdminFull,
EditOwn = AuthorizationRoles.NoRole,
ReadFullRecord = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////////////
#endregion all roles init

View File

@@ -0,0 +1,284 @@
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
namespace AyaNova.Biz
{
internal class FormCustomBiz : BizObject
{
internal FormCustomBiz(AyContext dbcontext, long currentUserId, long userLocaleId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserLocaleId = userLocaleId;
CurrentUserRoles = UserRoles;
BizType = AyaType.FormCustom;
}
internal static FormCustomBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext)
{
return new FormCustomBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
}
//Version for internal use
internal static FormCustomBiz GetBizInternal(AyContext ct)
{
return new FormCustomBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.BizAdminFull);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.FormCustom.AnyAsync(e => e.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
internal async Task<FormCustom> GetNoLogAsync(long fetchId)
{
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.FormCustom.SingleOrDefaultAsync(m => m.Id == fetchId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<FormCustom> CreateAsync(FormCustom inObj)
{
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with datafilter
FormCustom outObj = inObj;
outObj.OwnerId = UserId;
await ct.FormCustom.AddAsync(outObj);
await ct.SaveChangesAsync();
//Handle child and associated items:
//EVENT LOG
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
//SEARCH INDEXING
// Search.ProcessNewObjectKeywords(UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.Name);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal FormCustom Create(AyContext TempContext, FormCustom inObj)
{
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with datafilter
FormCustom outObj = inObj;
outObj.OwnerId = UserId;
TempContext.FormCustom.Add(outObj);
TempContext.SaveChanges();
//Handle child and associated items:
//EVENT LOG
EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext);
//SEARCH INDEXING
// Search.ProcessNewObjectKeywords(UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.Name);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<FormCustom> GetAsync(string formKey)
{
var ret = await ct.FormCustom.SingleOrDefaultAsync(m => m.FormKey == formKey);
//Do not log this, it's going to be called a zillion times anyway
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal bool Put(FormCustom dbObj, FormCustom inObj)
{
//preserve the owner ID if none was specified
if (inObj.OwnerId == 0)
inObj.OwnerId = dbObj.OwnerId;
//Replace the db object with the PUT object
CopyObject.Copy(inObj, dbObj, "Id");
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
Validate(dbObj, false);
if (HasErrors)
return false;
//Log modification
EventLogProcessor.LogEventToDatabase(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
//Update keywords
// Search.ProcessUpdatedObjectKeywords(UserLocaleId, dbObj.Id, BizType, dbObj.Name, dbObj.Name);
return true;
}
//NO DELETE, ONLY EDIT
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(FormCustom inObj, bool isNew)
{
//OwnerId required
if (!isNew)
{
if (inObj.OwnerId == 0)
AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId");
}
// //Owner must be current user, there are no exceptions
// if (inObj.OwnerId != UserId)
// AddError(ValidationErrorType.InvalidValue, "OwnerId", "OwnerId must be current user Id");
//FormKey required
if (string.IsNullOrWhiteSpace(inObj.FormKey))
AddError(ValidationErrorType.RequiredPropertyEmpty, "FormKey");
//FormKey must be less than 255 characters
if (inObj.FormKey.Length > 255)
AddError(ValidationErrorType.LengthExceeded, "FormKey", "255 max");
//If name is otherwise OK, check that name is unique
if (!PropertyHasErrors("FormKey"))
{
//Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
if (ct.FormCustom.Any(m => m.FormKey == inObj.FormKey && m.Id != inObj.Id))
{
AddError(ValidationErrorType.NotUnique, "FormKey");
}
}
//Template json must parse
if (!string.IsNullOrWhiteSpace(inObj.Template))
{
try
{
var v = JArray.Parse(inObj.Template);
//TODO: validate the json
// for (int i = 0; i < v.Count; i++)
// {
// var filterItem = v[i];
// if (filterItem["fld"] == null)
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing required \"fld\" property ");
// else
// {
// var fld = filterItem["fld"].Value<string>();
// if (string.IsNullOrWhiteSpace(fld))
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, \"fld\" property is empty and required");
// //validate the field name if we can
// if (ListValidFilterOptions != null)
// {
// if (!ListValidFilterOptions.Flds.Exists(x => x.Fld == fld))
// {
// AddError(ValidationErrorType.InvalidValue, "Filter", $"Filter array item {i}, fld property value \"{fld}\" is not a valid value for ListKey specified");
// }
// }
// }
// if (filterItem["op"] == null)
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing required \"op\" property ");
// else
// {
// var opType = filterItem["op"].Value<string>();
// if (!FilterComparisonOperator.Operators.Contains(opType))
// AddError(ValidationErrorType.InvalidValue, "Filter", $"Filter array item {i}, \"op\" property value of \"{opType}\" is not a valid FilterComparisonOperator type");
// }
// if (filterItem["value"] == null)
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing or is empty the required \"value\" property ");
// else
// {
// if (filterItem["value"].Type == JTokenType.String && string.IsNullOrWhiteSpace(filterItem["value"].Value<string>()))
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing or is empty the required \"value\" property ");
// if (filterItem["value"].Type == JTokenType.Array && filterItem["value"].Count() == 0)
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Filter", $"Filter array item {i}, object is missing or is empty the required \"value\" property ARRAY ");
// }
// //NOTE: value of nothing, null or empty is a valid value so no checking for it here
// }
}
catch (Newtonsoft.Json.JsonReaderException ex)
{
AddError(ValidationErrorType.InvalidValue, "Template", "Template is not valid JSON string: " + ex.Message);
}
}
return;
}
// //Can delete?
// private void ValidateCanDelete(FormCustom inObj)
// {
// //Leaving this off for now
// }
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons