From ca1cfe752f0c8cdd40bbf256baf4dadf4b19cbe1 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Thu, 29 Nov 2018 01:10:29 +0000 Subject: [PATCH] --- ...-list-graph-datatable-filtering-paging.txt | 14 +- devdocs/todo.txt | 7 +- .../Controllers/DataFilterController.cs | 227 ++++++++++ server/AyaNova/biz/AyaType.cs | 3 +- .../AyaNova/biz/BizObjectExistsInDatabase.cs | 3 +- server/AyaNova/biz/BizObjectFactory.cs | 2 + .../AyaNova/biz/BizObjectNameFetcherDirect.cs | 3 + server/AyaNova/biz/BizRoles.cs | 14 +- server/AyaNova/biz/DataFilterBiz.cs | 407 ++++++++++++++++++ server/AyaNova/models/AyContext.cs | 1 + server/AyaNova/models/DataFilter.cs | 26 ++ server/AyaNova/util/AySchema.cs | 16 +- server/AyaNova/util/DbUtil.cs | 1 + 13 files changed, 709 insertions(+), 15 deletions(-) create mode 100644 server/AyaNova/Controllers/DataFilterController.cs create mode 100644 server/AyaNova/biz/DataFilterBiz.cs create mode 100644 server/AyaNova/models/DataFilter.cs 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(); }