diff --git a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt
index abbe0a54..f93ba107 100644
--- a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt
+++ b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt
@@ -12,8 +12,8 @@ Filter is constructed from an FILTEROPTIONS object fetched from the server list
- e.g.: {list:"widget",fields:[{fld:"name",lt:"WidgetName",type:"text"},{fld:"dollarAmount",lt:"WidgetDollarAmount",type:"currency"}]}
Certain types have extended abilities, for example dates have the classic floating AyaNova date ranges pre-defined or specific dates
Filters are saved to the database:
- - Filter: Name, UserId, List, Filter (Json string) (column names to be determined)
- - i.e. "My widget filter", 1, "widget", "[{fld:"name",comparisonoperator:"Like",value:"Bob*"},{fld:"tags",comparisonoperator:"Eq",value:"[23,456,54]"}]
+ - Filter: Name, OwnerId, Public, ListKey, Filter (Json string) (column names to be determined)
+ - i.e. "My widget filter", 1, true, "widget", "[{fld:"name",comparisonoperator:"Like",value:"Bob*"},{fld:"tags",comparisonoperator:"Eq",value:"[23,456,54]"}]
- means all widgets that start with the name "Bob" and are tagged with tags with id values 23, 456 and 54
Upon user selecting a filter to use the list query string has the regular paging info but also the filter id as a query parameter
- Server loads the filter if it's public or has the user ID if it's personal only
@@ -25,9 +25,15 @@ Upon user selecting a filter to use the list query string has the regular paging
LIST FILTERING TODO
- Implement this with widget list first
- - Add Filter table and models and route to save, edit
- - Add API route to widget list that returns the FILTEROPTIONS object as outlined above
+ - Add DataFilter table and models and route to save, edit
+ - Add listkey property to list??
+ - Add API route to widget list that returns the FILTEROPTIONS object as outlined above
+ - Just a dynamic object really, no need for objects to model it I don't think as it's a one way server to client thing and doesn't need parsing or anything
- Add a list route that accepts the current paging options and also a filter id
- A method that can take the list of field filter values and the field types and construct an sql query from it
+ - Tests for all of the above (particularly the query builder because that was a bitch with v7)
+
+ - Client side
+ - Implement filter editor dialog
diff --git a/devdocs/todo.txt b/devdocs/todo.txt
index 41500e81..c525fe17 100644
--- a/devdocs/todo.txt
+++ b/devdocs/todo.txt
@@ -5,7 +5,10 @@ Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTQyNjY5Njc3IiwiZXhwIjoi
## IMMEDIATE ITEMS
-
+SERVER SCHEMA
+ - Add unique constraint to all name columns in all tables in ayschema and run tests (how did I miss that before??)
+ - See datafilter schema code (note that it will increase the index count by one)
+ - http://www.postgresqltutorial.com/postgresql-unique-constraint/
@@ -75,8 +78,6 @@ INITIAL TESTING NOTES:
-
-
diff --git a/server/AyaNova/Controllers/DataFilterController.cs b/server/AyaNova/Controllers/DataFilterController.cs
new file mode 100644
index 00000000..74d6d30a
--- /dev/null
+++ b/server/AyaNova/Controllers/DataFilterController.cs
@@ -0,0 +1,227 @@
+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
+{
+ //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
+
+ ///
+ /// Sample controller class used during development for testing purposes
+ ///
+ [ApiVersion("8.0")]
+ [Route("api/v{version:apiVersion}/[controller]")]
+ [Produces("application/json")]
+ [Authorize]
+ public class DataFilterController : Controller
+ {
+ private readonly AyContext ct;
+ private readonly ILogger log;
+ private readonly ApiServerState serverState;
+
+
+ ///
+ /// ctor
+ ///
+ ///
+ ///
+ ///
+ public DataFilterController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState)
+ {
+ ct = dbcontext;
+ log = logger;
+ serverState = apiServerState;
+ }
+
+
+ ///
+ /// Get full DataFilter object
+ ///
+ /// Required roles:
+ /// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
+ ///
+ ///
+ /// A single DataFilter
+ [HttpGet("{id}")]
+ public async Task GetDataFilter([FromRoute] long id)
+ {
+ if (serverState.IsClosed)
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+
+ //Instantiate the business object handler
+ DataFilterBiz biz = DataFilterBiz.GetBiz(ct, HttpContext);
+
+ 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(id);
+ if (o == null)
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+
+ return Ok(new ApiOkResponse(o));
+ }
+
+
+
+ ///
+ /// Get DataFilter pick list
+ ///
+ /// Required roles: Any
+ ///
+ ///
+ /// Paged id/name collection of DataFilters with paging data
+ [HttpGet("PickList", Name = nameof(DataFilterPickList))]
+ public async Task DataFilterPickList([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
+ DataFilterBiz biz = DataFilterBiz.GetBiz(ct, HttpContext);
+
+ var l = await biz.GetPickListAsync(ListKey);
+ return Ok(new ApiOkResponse(l));
+
+ }
+
+
+ ///
+ /// Put (update) DataFilter
+ ///
+ /// Required roles:
+ /// BizAdminFull, InventoryFull
+ /// TechFull (owned only)
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPut("{id}")]
+ public async Task PutDataFilter([FromRoute] long id, [FromBody] DataFilter 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
+ DataFilterBiz biz = DataFilterBiz.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 }));
+ }
+
+
+ ///
+ /// Post DataFilter
+ ///
+ /// Required roles:
+ /// BizAdminFull, InventoryFull, TechFull
+ ///
+ ///
+ ///
+ [HttpPost]
+ public async Task PostDataFilter([FromBody] DataFilter inObj)
+ {
+ if (!serverState.IsOpen)
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+
+ //Instantiate the business object handler
+ DataFilterBiz biz = DataFilterBiz.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
+ DataFilter o = await biz.CreateAsync(inObj);
+ if (o == null)
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ else
+ return CreatedAtAction("GetDataFilter", new { id = o.Id }, new ApiCreatedResponse(o));
+
+ }
+
+
+
+ ///
+ /// Delete DataFilter
+ ///
+ /// Required roles:
+ /// BizAdminFull, InventoryFull
+ /// TechFull (owned only)
+ ///
+ ///
+ ///
+ /// Ok
+ [HttpDelete("{id}")]
+ public async Task DeleteDataFilter([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
+ DataFilterBiz biz = DataFilterBiz.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
\ No newline at end of file
diff --git a/server/AyaNova/biz/AyaType.cs b/server/AyaNova/biz/AyaType.cs
index 3b85811b..cd47c34c 100644
--- a/server/AyaNova/biz/AyaType.cs
+++ b/server/AyaNova/biz/AyaType.cs
@@ -31,7 +31,8 @@ namespace AyaNova.Biz
UserOptions = 14,
TagGroup = 15,
TagGroupMap = 16,
- FileAttachment = 17
+ FileAttachment = 17,
+ DataFilter = 18
//NOTE: New objects added here need to also be added to the following classes:
diff --git a/server/AyaNova/biz/BizObjectExistsInDatabase.cs b/server/AyaNova/biz/BizObjectExistsInDatabase.cs
index 04fe6316..d9c1e91b 100644
--- a/server/AyaNova/biz/BizObjectExistsInDatabase.cs
+++ b/server/AyaNova/biz/BizObjectExistsInDatabase.cs
@@ -45,7 +45,8 @@ namespace AyaNova.Biz
return ct.TagGroup.Any(m => m.Id == id);
case AyaType.FileAttachment:
return ct.FileAttachment.Any(m => m.Id == id);
-
+ case AyaType.DataFilter:
+ return ct.DataFilter.Any(m => m.Id == id);
diff --git a/server/AyaNova/biz/BizObjectFactory.cs b/server/AyaNova/biz/BizObjectFactory.cs
index 3c19ec23..189f8695 100644
--- a/server/AyaNova/biz/BizObjectFactory.cs
+++ b/server/AyaNova/biz/BizObjectFactory.cs
@@ -39,6 +39,8 @@ namespace AyaNova.Biz
return new TrialBiz(dbcontext, userId, roles);
case AyaType.Locale:
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);
default:
diff --git a/server/AyaNova/biz/BizObjectNameFetcherDirect.cs b/server/AyaNova/biz/BizObjectNameFetcherDirect.cs
index 4c893d3b..42befc62 100644
--- a/server/AyaNova/biz/BizObjectNameFetcherDirect.cs
+++ b/server/AyaNova/biz/BizObjectNameFetcherDirect.cs
@@ -47,6 +47,9 @@ namespace AyaNova.Biz
TABLE = "afileattachment";
COLUMN = "displayfilename";
break;
+ case AyaType.DataFilter:
+ TABLE = "adatafilter";
+ break;
default:
throw new System.NotSupportedException($"AyaNova.BLL.BizObjectNameFetcher::Name type {aytype.ToString()} is not supported");
}
diff --git a/server/AyaNova/biz/BizRoles.cs b/server/AyaNova/biz/BizRoles.cs
index d723450b..fddad9f5 100644
--- a/server/AyaNova/biz/BizRoles.cs
+++ b/server/AyaNova/biz/BizRoles.cs
@@ -49,7 +49,7 @@ namespace AyaNova.Biz
ReadFullRecord = AuthorizationRoles.BizAdminLimited
});
-
+
////////////////////////////////////////////////////////////
//WIDGET
@@ -112,7 +112,7 @@ namespace AyaNova.Biz
ReadFullRecord = AuthorizationRoles.AnyRole
});
- ////////////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////
//TAGGROUP - MIRROR TAGS
//Full roles can make new tags and can edit or delete existing tags
roles.Add(AyaType.TagGroup, new BizRoleSet()
@@ -177,7 +177,15 @@ namespace AyaNova.Biz
});
-
+ ////////////////////////////////////////////////////////////
+ //DATAFILTER
+ //
+ roles.Add(AyaType.DataFilter, new BizRoleSet()
+ {
+ Change = AuthorizationRoles.BizAdminFull,
+ EditOwn = AuthorizationRoles.AnyRole,
+ ReadFullRecord = AuthorizationRoles.AnyRole
+ });
////////////////////////////////////////////////////////////////////
#endregion all roles init
diff --git a/server/AyaNova/biz/DataFilterBiz.cs b/server/AyaNova/biz/DataFilterBiz.cs
new file mode 100644
index 00000000..1e19ed90
--- /dev/null
+++ b/server/AyaNova/biz/DataFilterBiz.cs
@@ -0,0 +1,407 @@
+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
+{
+
+
+ internal class DataFilterBiz : BizObject, IJobObject
+ {
+
+ internal DataFilterBiz(AyContext dbcontext, long currentUserId, long userLocaleId, AuthorizationRoles UserRoles)
+ {
+ ct = dbcontext;
+ UserId = currentUserId;
+ UserLocaleId = userLocaleId;
+ CurrentUserRoles = UserRoles;
+ BizType = AyaType.DataFilter;
+ }
+
+ internal static DataFilterBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext)
+ {
+ return new DataFilterBiz(ct, UserIdFromContext.Id(httpContext.Items), UserLocaleIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
+ }
+
+ //Version for internal use
+ internal static DataFilterBiz GetBizInternal(AyContext ct)
+ {
+ return new DataFilterBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.BizAdminFull);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //EXISTS
+ internal async Task ExistsAsync(long id)
+ {
+ return await ct.DataFilter.AnyAsync(e => e.Id == id);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ /// GET
+ internal async Task 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.DataFilter.SingleOrDefaultAsync(m => m.Id == fetchId);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //CREATE
+ internal async Task CreateAsync(DataFilter inObj)
+ {
+ Validate(inObj, true);
+ if (HasErrors)
+ return null;
+ else
+ {
+ //do stuff with widget
+ DataFilter outObj = inObj;
+ outObj.OwnerId = UserId;
+
+ //Test get serial id visible id number from generator
+ outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();
+
+
+ await ct.DataFilter.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.Notes, outObj.Name, outObj.Serial.ToString());
+
+ return outObj;
+
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //CREATE
+ internal DataFilter Create(AyContext TempContext, DataFilter inObj)
+ {
+ Validate(inObj, true);
+ if (HasErrors)
+ return null;
+ else
+ {
+ //do stuff with widget
+ DataFilter outObj = inObj;
+ outObj.OwnerId = UserId;
+ //Test get serial id visible id number from generator
+ outObj.Serial = ServerBootConfig.WIDGET_SERIAL.GetNext();
+
+ TempContext.DataFilter.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.Notes, outObj.Name, outObj.Serial.ToString());
+
+ return outObj;
+
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ /// GET
+
+ //Get one
+ internal async Task GetAsync(long fetchId)
+ {
+ //This is simple so nothing more here, but often will be copying to a different output object or some other ops
+ var ret = await ct.DataFilter.SingleOrDefaultAsync(m => m.Id == fetchId);
+ if (ret != null)
+ {
+ //Log
+ EventLogProcessor.LogEventToDatabase(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct);
+ }
+ return ret;
+ }
+
+ //get many (paged)
+ internal async Task> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions)
+ {
+
+ pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
+ pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
+
+ var items = await ct.DataFilter
+ .OrderBy(m => m.Id)
+ .Skip(pagingOptions.Offset.Value)
+ .Take(pagingOptions.Limit.Value)
+ .ToArrayAsync();
+
+ var totalRecordCount = await ct.DataFilter.CountAsync();
+ var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
+
+ ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks);
+ return pr;
+ }
+
+
+ //get picklist (paged)
+ internal async Task> GetPickListAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions, string q)
+ {
+ pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
+ pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
+
+ NameIdItem[] items;
+ int totalRecordCount = 0;
+
+ if (!string.IsNullOrWhiteSpace(q))
+ {
+ items = await ct.DataFilter
+ .Where(m => EF.Functions.ILike(m.Name, q))
+ .OrderBy(m => m.Name)
+ .Skip(pagingOptions.Offset.Value)
+ .Take(pagingOptions.Limit.Value)
+ .Select(m => new NameIdItem()
+ {
+ Id = m.Id,
+ Name = m.Name
+ }).ToArrayAsync();
+
+ totalRecordCount = await ct.DataFilter.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync();
+ }
+ else
+ {
+ items = await ct.DataFilter
+ .OrderBy(m => m.Name)
+ .Skip(pagingOptions.Offset.Value)
+ .Take(pagingOptions.Limit.Value)
+ .Select(m => new NameIdItem()
+ {
+ Id = m.Id,
+ Name = m.Name
+ }).ToArrayAsync();
+
+ totalRecordCount = await ct.DataFilter.CountAsync();
+ }
+
+
+
+ var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
+
+ ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks);
+ return pr;
+ }
+
+
+
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //UPDATE
+ //
+
+ //put
+ internal bool Put(DataFilter dbObj, DataFilter inObj)
+ {
+
+ //Replace the db object with the PUT object
+ CopyObject.Copy(inObj, dbObj, "Id,Serial");
+ //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.Notes, dbObj.Name, dbObj.Serial.ToString());
+
+ return true;
+ }
+
+ //patch
+ internal bool Patch(DataFilter dbObj, JsonPatchDocument objectPatch, uint concurrencyToken)
+ {
+ //Validate Patch is allowed
+ //Note: Id, OwnerId and Serial are all checked for and disallowed in the validate code by default
+ if (!ValidateJsonPatch.Validate(this, objectPatch)) return false;
+
+ //Do the patching
+ objectPatch.ApplyTo(dbObj);
+ ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = 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.Notes, dbObj.Name, dbObj.Serial.ToString());
+
+ return true;
+ }
+
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //DELETE
+ //
+ internal bool Delete(DataFilter dbObj)
+ {
+ //Determine if the object can be deleted, do the deletion tentatively
+ //Probably also in here deal with tags and associated search text etc
+
+ ValidateCanDelete(dbObj);
+ if (HasErrors)
+ return false;
+ ct.DataFilter.Remove(dbObj);
+ ct.SaveChanges();
+
+ //Delete sibling objects
+
+ //Event log process delete
+ EventLogProcessor.DeleteObject(UserId, BizType, dbObj.Id, dbObj.Name, ct);
+ ct.SaveChanges();
+
+ //Delete search index
+ Search.ProcessDeletedObjectKeywords(dbObj.Id, BizType);
+
+ //TAGS
+ TagMapBiz.DeleteAllForObject(new AyaTypeId(BizType, dbObj.Id), ct);
+ ct.SaveChanges();
+
+ return true;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //VALIDATION
+ //
+
+ //Can save or update?
+ private void Validate(DataFilter inObj, bool isNew)
+ {
+ //run validation and biz rules
+ if (isNew)
+ {
+ //NEW widgets must be active
+ if (inObj.Active == null || ((bool)inObj.Active) == false)
+ {
+ AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active");
+ }
+ }
+
+ //OwnerId required
+ if (!isNew)
+ {
+ if (inObj.OwnerId == 0)
+ AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId");
+ }
+
+ //Name required
+ if (string.IsNullOrWhiteSpace(inObj.Name))
+ AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
+
+ //Name must be less than 255 characters
+ if (inObj.Name.Length > 255)
+ AddError(ValidationErrorType.LengthExceeded, "Name", "255 max");
+
+ //If name is otherwise OK, check that name is unique
+ if (!PropertyHasErrors("Name"))
+ {
+ //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
+ if (ct.DataFilter.Any(m => m.Name == inObj.Name && m.Id != inObj.Id))
+ {
+ AddError(ValidationErrorType.NotUnique, "Name");
+ }
+ }
+
+ //Start date AND end date must both be null or both contain values
+ if (inObj.StartDate == null && inObj.EndDate != null)
+ AddError(ValidationErrorType.RequiredPropertyEmpty, "StartDate");
+
+ if (inObj.StartDate != null && inObj.EndDate == null)
+ AddError(ValidationErrorType.RequiredPropertyEmpty, "EndDate");
+
+ //Start date before end date
+ if (inObj.StartDate != null && inObj.EndDate != null)
+ if (inObj.StartDate > inObj.EndDate)
+ AddError(ValidationErrorType.StartDateMustComeBeforeEndDate, "StartDate");
+
+ //Enum is valid value
+
+ if (!inObj.Roles.IsValid())
+ {
+ AddError(ValidationErrorType.InvalidValue, "Roles");
+ }
+
+
+ return;
+ }
+
+
+ //Can delete?
+ private void ValidateCanDelete(DataFilter inObj)
+ {
+ //whatever needs to be check to delete this object
+ }
+
+
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //JOB / OPERATIONS
+ //
+ public async Task HandleJobAsync(OpsJob job)
+ {
+ //Hand off the particular job to the corresponding processing code
+ //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so
+ //basically any error condition during job processing should throw up an exception if it can't be handled
+ switch (job.JobType)
+ {
+ case JobType.TestDataFilterJob:
+ await ProcessTestJobAsync(job);
+ break;
+ default:
+ throw new System.ArgumentOutOfRangeException($"DataFilterBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
+ }
+ }
+
+
+ ///
+ /// /// Handle the test job
+ ///
+ ///
+ private async Task ProcessTestJobAsync(OpsJob job)
+ {
+ var sleepTime = 30 * 1000;
+ //Simulate a long running job here
+ JobsBiz.UpdateJobStatus(job.GId, JobStatus.Running, ct);
+ JobsBiz.LogJob(job.GId, $"DataFilterBiz::ProcessTestJob started, sleeping for {sleepTime} seconds...", ct);
+ //Uncomment this to test if the job prevents other routes from running
+ //result is NO it doesn't prevent other requests, so we are a-ok for now
+ await Task.Delay(sleepTime);
+ JobsBiz.LogJob(job.GId, "DataFilterBiz::ProcessTestJob done sleeping setting job to finished", ct);
+ JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct);
+
+ }
+
+ //Other job handlers here...
+
+
+ /////////////////////////////////////////////////////////////////////
+
+ }//eoc
+
+
+}//eons
+
diff --git a/server/AyaNova/models/AyContext.cs b/server/AyaNova/models/AyContext.cs
index 083a7f33..923a8ac4 100644
--- a/server/AyaNova/models/AyContext.cs
+++ b/server/AyaNova/models/AyContext.cs
@@ -24,6 +24,7 @@ namespace AyaNova.Models
public virtual DbSet OpsJobLog { get; set; }
public virtual DbSet Locale { get; set; }
public virtual DbSet LocaleItem { get; set; }
+ public virtual DbSet DataFilter { get; set; }
//Note: had to add this constructor to work with the code in startup.cs that gets the connection string from the appsettings.json file
//and commented out the above on configuring
diff --git a/server/AyaNova/models/DataFilter.cs b/server/AyaNova/models/DataFilter.cs
new file mode 100644
index 00000000..01bc4498
--- /dev/null
+++ b/server/AyaNova/models/DataFilter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using AyaNova.Biz;
+
+using System.ComponentModel.DataAnnotations;
+
+namespace AyaNova.Models
+{
+
+ public partial class DataFilter
+ {
+ public long Id { get; set; }
+ public uint ConcurrencyToken { get; set; }
+
+ [Required]
+ public long OwnerId { get; set; }
+ [Required, MaxLength(255)]
+ public string Name { get; set; }//max 255 characters ascii set
+ [Required]
+ public bool Public { get; set; }
+ [Required, MaxLength(255)]
+ public string ListKey { get; set; }//max 255 characters ascii set
+ public string Filter { get; set; }//can be empty I guess meaning nothing selected in it
+
+ }
+}
diff --git a/server/AyaNova/util/AySchema.cs b/server/AyaNova/util/AySchema.cs
index 1d95b7a0..3e59f913 100644
--- a/server/AyaNova/util/AySchema.cs
+++ b/server/AyaNova/util/AySchema.cs
@@ -22,8 +22,8 @@ namespace AyaNova.Util
//!!!!WARNING: BE SURE TO UPDATE THE DbUtil::PrepareDatabaseForSeeding WHEN NEW TABLES ADDED!!!!
private const int DESIRED_SCHEMA_LEVEL = 9;
- internal const long EXPECTED_COLUMN_COUNT = 100;
- internal const long EXPECTED_INDEX_COUNT = 22;
+ internal const long EXPECTED_COLUMN_COUNT = 106;
+ internal const long EXPECTED_INDEX_COUNT = 24;
//!!!!WARNING: BE SURE TO UPDATE THE DbUtil::PrepareDatabaseForSeeding WHEN NEW TABLES ADDED!!!!
@@ -144,7 +144,7 @@ namespace AyaNova.Util
//LOOKAT: this index is periodically being violated during testing
exec("CREATE UNIQUE INDEX asearchdictionary_word_idx ON asearchdictionary (word);");
-
+
exec("CREATE TABLE asearchkey (id BIGSERIAL PRIMARY KEY, wordid bigint not null REFERENCES asearchdictionary (id), objectid bigint not null, objecttype integer not null, inname bool not null)");
//create locale text tables
@@ -290,7 +290,17 @@ namespace AyaNova.Util
+ //////////////////////////////////////////////////
+ //DATAFILTER table
+ if (currentSchema < 9)
+ {
+ LogUpdateMessage(log);
+
+ exec("CREATE TABLE adatafilter (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, name varchar(255) not null, public bool not null," +
+ "listkey varchar(255) not null, filter text, UNIQUE(name))");
+ setSchemaLevel(++currentSchema);
+ }
diff --git a/server/AyaNova/util/DbUtil.cs b/server/AyaNova/util/DbUtil.cs
index 1ffbc03d..2c451bd0 100644
--- a/server/AyaNova/util/DbUtil.cs
+++ b/server/AyaNova/util/DbUtil.cs
@@ -280,6 +280,7 @@ namespace AyaNova.Util
EraseTable("afileattachment", conn);
EraseTable("awidget", conn);
EraseTable("aevent", conn);
+ EraseTable("adatafilter", conn);
conn.Close();
}