From 51941a958e52dd9e1feb3f208bec7852e5ec04cb Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Mon, 3 Dec 2018 19:12:35 +0000 Subject: [PATCH] --- .vscode/launch.json | 4 +- ...-list-graph-datatable-filtering-paging.txt | 4 +- devdocs/todo.txt | 5 +- .../ControllerHelpers/PagingOptions.cs | 10 +- server/AyaNova/Startup.cs | 2 +- server/AyaNova/biz/IFilterableObject.cs | 22 -- server/AyaNova/biz/WidgetBiz.cs | 45 +++- .../DataFilter/DataFilterFilteringLists.cs | 196 ++++++++++++++++++ test/raven-integration/Widget/WidgetLists.cs | 11 +- .../Widget/WidgetValidationTests.cs | 36 ++-- 10 files changed, 270 insertions(+), 65 deletions(-) delete mode 100644 server/AyaNova/biz/IFilterableObject.cs create mode 100644 test/raven-integration/DataFilter/DataFilterFilteringLists.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 49c9bd20..d95867a5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,8 +31,8 @@ }, "env": { "ASPNETCORE_ENVIRONMENT": "Development", - "AYANOVA_LOG_LEVEL": "Info", - //"AYANOVA_LOG_LEVEL": "Debug", + //"AYANOVA_LOG_LEVEL": "Info", + "AYANOVA_LOG_LEVEL": "Debug", "AYANOVA_DEFAULT_LANGUAGE": "en", //LOCALE MUST BE en for Integration TESTING //"AYANOVA_PERMANENTLY_ERASE_DATABASE": "true", diff --git a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt index 96ff6e4c..c3791deb 100644 --- a/devdocs/specs/core-list-graph-datatable-filtering-paging.txt +++ b/devdocs/specs/core-list-graph-datatable-filtering-paging.txt @@ -63,9 +63,7 @@ LIST FILTERING TODO Add test for providing saved filter id to widgetlist and recieving correct filtered / paged / sorted results (test all those) - Requires filter to sql code to be written and changes to the widgetlist route - - This is where it gets real! Happy monday!! :) - - Copy as much from v7 as possible - + - Client side - Implement filter editor dialog and test diff --git a/devdocs/todo.txt b/devdocs/todo.txt index 07f7ed97..d3582d69 100644 --- a/devdocs/todo.txt +++ b/devdocs/todo.txt @@ -12,7 +12,10 @@ SERVER SCHEMA - See datafilter schema code (note that it will increase the index count by one) - http://www.postgresqltutorial.com/postgresql-unique-constraint/ - +SERVER ALL LIST ROUTES + - Forgot to put AsNoTracking in the list routes, no need to track them as they are only returning info, not being modified + - See widgetBiz getmany method for example + - This applies to all picklist and list routes INITIAL TESTING NOTES: - Tested on iPad (safari, chrome), Pixel (chrome), samsung chrome and samsung browser, desktop chrome, firefox browsers, android table (looks good, works but very slow, good for testing) diff --git a/server/AyaNova/ControllerHelpers/PagingOptions.cs b/server/AyaNova/ControllerHelpers/PagingOptions.cs index d9517aa7..a5adf334 100644 --- a/server/AyaNova/ControllerHelpers/PagingOptions.cs +++ b/server/AyaNova/ControllerHelpers/PagingOptions.cs @@ -18,9 +18,13 @@ namespace AyaNova.Api.ControllerHelpers [Range(1, MaxPageSize, ErrorMessage = "Limit must be greater than 0 and less than 100.")] public int? Limit { get; set; } - public string Sort {get;set;} - public bool? Asc {get;set;} - + public string Sort { get; set; } + public bool? Asc { get; set; } + + //Data filter id to use with this list query + //0 or less means no filter + public long DataFilterId { get; set; } + } diff --git a/server/AyaNova/Startup.cs b/server/AyaNova/Startup.cs index e42f09a9..f34e0cf8 100644 --- a/server/AyaNova/Startup.cs +++ b/server/AyaNova/Startup.cs @@ -98,7 +98,7 @@ namespace AyaNova bool LOG_SENSITIVE_DATA = false; #if (DEBUG) - //LOG_SENSITIVE_DATA = true; + LOG_SENSITIVE_DATA = true; #endif diff --git a/server/AyaNova/biz/IFilterableObject.cs b/server/AyaNova/biz/IFilterableObject.cs deleted file mode 100644 index 12d34cbd..00000000 --- a/server/AyaNova/biz/IFilterableObject.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using AyaNova.Models; -using Newtonsoft.Json.Linq; - - -namespace AyaNova.Biz -{ - /// - /// Interface for biz objects that support list filtering - /// - internal interface IFilterableObject - { - - //FILTEROPTIONS COLLECTION - FilterOptions FilterOptions { get; } - - - } - -} \ No newline at end of file diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs index 21cde0df..5dc8974a 100644 --- a/server/AyaNova/biz/WidgetBiz.cs +++ b/server/AyaNova/biz/WidgetBiz.cs @@ -12,7 +12,7 @@ using System.Collections.Generic; namespace AyaNova.Biz { - + internal class WidgetBiz : BizObject, IJobObject { public static FilterOptions FilterOptions(long localizeToLocaleId = 0) @@ -153,14 +153,39 @@ namespace AyaNova.Biz internal async Task> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions) { + //TODO: build a query, run it outside of Entity Framework directly and return the results in models?! + //Or can ef accept a query with criteria + //https://docs.microsoft.com/en-us/ef/core/querying/raw-sql + //If no datafilter then it can just run this regular code which already works, I think? + //no, order by is brokeh + + pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset; pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit; + // var items = await ct.Widget + // .OrderBy(m => m.Id) + // .Skip(pagingOptions.Offset.Value) + // .Take(pagingOptions.Limit.Value) + // .ToArrayAsync(); + + + //BUILD THE QUERY + //base query + var q = "SELECT *, xmin FROM AWIDGET "; + + //BUILD WHERE AND APPEND IT + + //BUILD ORDER BY AND APPEND IT + + var items = await ct.Widget - .OrderBy(m => m.Id) - .Skip(pagingOptions.Offset.Value) - .Take(pagingOptions.Limit.Value) - .ToArrayAsync(); + .AsNoTracking() + .FromSql(q) + .Skip(pagingOptions.Offset.Value) + .Take(pagingOptions.Limit.Value) + .ToArrayAsync(); + var totalRecordCount = await ct.Widget.CountAsync(); var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); @@ -314,11 +339,11 @@ namespace AyaNova.Biz //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"); - } + // //NEW widgets must be active + // if (inObj.Active == null || ((bool)inObj.Active) == false) + // { + // AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active"); + // } } //OwnerId required diff --git a/test/raven-integration/DataFilter/DataFilterFilteringLists.cs b/test/raven-integration/DataFilter/DataFilterFilteringLists.cs new file mode 100644 index 00000000..eee48900 --- /dev/null +++ b/test/raven-integration/DataFilter/DataFilterFilteringLists.cs @@ -0,0 +1,196 @@ +using System; +using Xunit; +using Newtonsoft.Json.Linq; +using FluentAssertions; +using System.Collections.Generic; +using System.Collections.Concurrent; + +namespace raven_integration +{ + + + /* + + Using the widget object test all filtering options + for all data types, all operation types + + This is the supertest to always confirm the filtering code is working as expected. + + + */ + + public class DataFilterFilteringLists + { + public const string OpEquality = "="; + public const string OpGreaterThan = ">"; + public const string OpGreaterThanOrEqualTo = ">="; + public const string OpLessThan = "<"; + public const string OpLessThanOrEqualTo = "<="; + public const string OpNotEqual = "!="; + public const string OpNotLike = "!%"; + public const string OpStartsWith = "%-"; + public const string OpEndsWith = "-%"; + public const string OpContains = "-%-"; + public const string OpNotContains = "!-%-"; + + // public const string AyDataTypeDate = "date"; + // public const string AyDataTypeText = "text"; + // public const string AyDataTypeInteger = "int"; + // public const string AyDataTypeBool = "bool"; + // public const string AyDataTypeDecimal = "decimal"; + + + + /////////////////////// + //DATE + // + + /////////////////////// + //TEXT + // + + /////////////////////// + //INT + // + + /////////////////////// + //BOOL + // + + + + /// + /// + /// + [Fact] + public async void BoolFiltersWork() + { + + //OPS: equal to, not equal to + //values: true, false + + var WidgetNameStart = "BoolDataFilterTest"; + + List ActiveWidgetIdList = new List(); + List NotActiveWidgetIdList = new List(); + + //CREATE 4 TEST WIDGETS + //two active and two non active + + //first active widget + dynamic w = new JObject(); + w.name = Util.Uniquify(WidgetNameStart); + w.active = true; + w.roles = 0; + + ApiResponse a = await Util.PostAsync("Widget", await Util.GetTokenAsync("manager", "l3tm3in"), w.ToString()); + Util.ValidateDataReturnResponseOk(a); + ActiveWidgetIdList.Add(a.ObjectResponse["data"]["id"].Value()); + + //second active widget + w.name = Util.Uniquify(WidgetNameStart); + + a = await Util.PostAsync("Widget", await Util.GetTokenAsync("manager", "l3tm3in"), w.ToString()); + Util.ValidateDataReturnResponseOk(a); + ActiveWidgetIdList.Add(a.ObjectResponse["data"]["id"].Value()); + + + //first NON active widget + w.name = Util.Uniquify(WidgetNameStart); + w.active = false; + + a = await Util.PostAsync("Widget", await Util.GetTokenAsync("manager", "l3tm3in"), w.ToString()); + Util.ValidateDataReturnResponseOk(a); + NotActiveWidgetIdList.Add(a.ObjectResponse["data"]["id"].Value()); + + //second NON active widget + w.name = Util.Uniquify(WidgetNameStart); + w.active = false; + + a = await Util.PostAsync("Widget", await Util.GetTokenAsync("manager", "l3tm3in"), w.ToString()); + Util.ValidateDataReturnResponseOk(a); + NotActiveWidgetIdList.Add(a.ObjectResponse["data"]["id"].Value()); + + + //CREATE FILTER + dynamic d = new JObject(); + d.name = Util.Uniquify("Test BOOL DataFilter"); + // d.ownerId = 1L; + d["public"] = true; + d.listKey = "widget"; + + dynamic dfilter = new JArray(); + + //name starts with filter to constrict to widgets that this test block created only + dynamic DataFilterNameStart = new JObject(); + DataFilterNameStart.fld = "name"; + DataFilterNameStart.op = OpStartsWith; + DataFilterNameStart.value = WidgetNameStart; + dfilter.Add(DataFilterNameStart); + + //active bool test filter + dynamic DataFilterActive = new JObject(); + DataFilterActive.fld = "active"; + DataFilterActive.op = OpEquality; + DataFilterActive.value = true; + dfilter.Add(DataFilterActive); + + d.filter = dfilter.ToString();//it expects it to be a json string, not actual json + + a = await Util.PostAsync("DataFilter", await Util.GetTokenAsync("BizAdminFull"), d.ToString()); + Util.ValidateDataReturnResponseOk(a); + + long DataFilterId = a.ObjectResponse["data"]["id"].Value(); + + //NOW FETCH WIDGET LIST WITH FILTER + a = await Util.GetAsync($"Widget/listwidgets?DataFilterId={DataFilterId.ToString()}", await Util.GetTokenAsync("manager", "l3tm3in")); + Util.ValidateDataReturnResponseOk(a); + Util.ValidateHTTPStatusCode(a, 200); + + //assert aAll contains exactly two records + ((JArray)a.ObjectResponse["data"]).Count.Should().Be(2); + +//TODO: ensure the results match the appropriate matching widgetIDList made earlier + + + + // //UPDATE FILTER TO LIMIT TO INACTIVE + + // //PUT, make private + // d["public"] = false; + // d.name = Util.Uniquify("Put - Test DataFilter (privatized)"); + // d.concurrencyToken = a.ObjectResponse["data"]["concurrencyToken"].Value(); + // a = await Util.PutAsync("DataFilter/" + DataFilterId.ToString(), await Util.GetTokenAsync("BizAdminFull"), d.ToString()); + // Util.ValidateHTTPStatusCode(a, 200); + + // //check PUT worked + // a = await Util.GetAsync("DataFilter/" + DataFilterId.ToString(), await Util.GetTokenAsync("BizAdminFull")); + // Util.ValidateNoErrorInResponse(a); + // a.ObjectResponse["data"]["name"].Value().Should().Be(d.name.ToString()); + + + // //FETCH DISALLOWED + // //Get as alternate user should fail for private filter + // a = await Util.GetAsync("DataFilter/" + DataFilterId.ToString(), await Util.GetTokenAsync("SubContractorLimited")); + // Util.ValidateResponseNotFound(a); + + // // //DELETE + // ApiResponse DELETETestResponse = await Util.DeleteAsync("DataFilter/" + DataFilterId.ToString(), await Util.GetTokenAsync("BizAdminFull")); + // Util.ValidateHTTPStatusCode(DELETETestResponse, 204); + + } + + + + /////////////////////// + //DECIMAL + // + + /////////////////////// + //TAGS + // + + //================================================== + + }//eoc +}//eons diff --git a/test/raven-integration/Widget/WidgetLists.cs b/test/raven-integration/Widget/WidgetLists.cs index 8bb86d39..01e6736f 100644 --- a/test/raven-integration/Widget/WidgetLists.cs +++ b/test/raven-integration/Widget/WidgetLists.cs @@ -11,7 +11,7 @@ namespace raven_integration public class WidgetLists { - + /// /// /// @@ -26,11 +26,11 @@ namespace raven_integration d.active = true; d.roles = 0; - ApiResponse r1 = await Util.PostAsync("Widget", await Util.GetTokenAsync( "manager", "l3tm3in"), d.ToString()); + ApiResponse r1 = await Util.PostAsync("Widget", await Util.GetTokenAsync("manager", "l3tm3in"), d.ToString()); Util.ValidateDataReturnResponseOk(r1); //Get all - ApiResponse a = await Util.GetAsync("Widget/picklist?Offset=2&Limit=3&q=%25of%25", await Util.GetTokenAsync( "InventoryLimited")); + ApiResponse a = await Util.GetAsync("Widget/picklist?Offset=2&Limit=3&q=%25of%25", await Util.GetTokenAsync("InventoryLimited")); Util.ValidateDataReturnResponseOk(a); Util.ValidateHTTPStatusCode(a, 200); ((JArray)a.ObjectResponse["data"]).Count.Should().BeGreaterThan(0); @@ -44,7 +44,7 @@ namespace raven_integration public async void PagingShouldWorkAsExpected() { //Get all - ApiResponse a = await Util.GetAsync("Widget/listwidgets?Offset=2&Limit=3", await Util.GetTokenAsync( "manager", "l3tm3in")); + ApiResponse a = await Util.GetAsync("Widget/listwidgets?Offset=2&Limit=3", await Util.GetTokenAsync("manager", "l3tm3in")); Util.ValidateDataReturnResponseOk(a); Util.ValidateHTTPStatusCode(a, 200); @@ -64,7 +64,7 @@ namespace raven_integration -/// + /// /// /// [Fact] @@ -99,6 +99,7 @@ namespace raven_integration + //================================================== }//eoc diff --git a/test/raven-integration/Widget/WidgetValidationTests.cs b/test/raven-integration/Widget/WidgetValidationTests.cs index 619963e1..e3bd09bd 100644 --- a/test/raven-integration/Widget/WidgetValidationTests.cs +++ b/test/raven-integration/Widget/WidgetValidationTests.cs @@ -11,27 +11,27 @@ namespace raven_integration { - /// - /// Test business rule should be active on new - /// - [Fact] - public async void BusinessRuleNewShouldBeActiveShouldWork() - { - //CREATE attempt with broken rules - dynamic d = new JObject(); - d.name = Util.Uniquify("ServerShouldDisAllowOwnerOnlyRightsUserToDeleteNonOwned TEST WIDGET"); - d.created = DateTime.Now.ToString(); - d.dollarAmount = 1.11m; - d.active = false;//<--- BROKEN RULE new widget must be active = true!! - d.roles = 0; + // /// + // /// Test business rule should be active on new + // /// + // [Fact] + // public async void BusinessRuleNewShouldBeActiveShouldWork() + // { + // //CREATE attempt with broken rules + // dynamic d = new JObject(); + // d.name = Util.Uniquify("ServerShouldDisAllowOwnerOnlyRightsUserToDeleteNonOwned TEST WIDGET"); + // d.created = DateTime.Now.ToString(); + // d.dollarAmount = 1.11m; + // d.active = false;//<--- BROKEN RULE new widget must be active = true!! + // d.roles = 0; - //create via inventory full test user - ApiResponse a = await Util.PostAsync("Widget", await Util.GetTokenAsync("InventoryFull"), d.ToString()); + // //create via inventory full test user + // ApiResponse a = await Util.PostAsync("Widget", await Util.GetTokenAsync("InventoryFull"), d.ToString()); - Util.ValidateErrorCodeResponse(a, 2200, 400); - Util.ShouldContainValidationError(a, "Active", "InvalidValue"); + // Util.ValidateErrorCodeResponse(a, 2200, 400); + // Util.ShouldContainValidationError(a, "Active", "InvalidValue"); - } + // }