This commit is contained in:
2018-08-29 22:52:14 +00:00
parent 35e5183543
commit c91ed15689
7 changed files with 507 additions and 7 deletions

View File

@@ -2,6 +2,15 @@
Main case is 3373 https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3373
TODO: TAG-GROUP - bag of tags
Modify the below and coded and tests and tables to now have feature for tag groups
They will be in their own table with a dictionary table of tag ids and tag group id's to link
They are used for read purposes, you never tag an item with a tag group, only query by group.
If a group is deleted tags it contains are NOT deleted or changed
If a tag is deleted it must be removed from any tag groups and if there are no other items in that tag group then the tag group must be deleted as well??
- Or maybe is just an empty group with a warning symbol in ui
Tag groups will become a major way to save filters for things because a lot of filtering will be by tags so this should be a solid feature.
FORMAT
=-=-=-
Copied from stack overflow
@@ -38,9 +47,9 @@ Add a tag to an object type and id, start typing and a selection list fills in t
- Don't allow the same tag more than once
- Create a tag if not present (rights?)
Show tags on an object
Find any type or specific type items by a set of tags to include and a set of tags to exclude (i.e. has "red, yellow" but not "green")
Search for text must allow specifying tags to refine
Reporting will require filtering sources of report data by tags
Find any type or specific type items by a set of tags (and/or tag groups) to include and a set of tags to exclude (i.e. has "red, yellow" but not "green")
Search for text must allow specifying tags (and/or tag groups) to refine
Reporting will require filtering sources of report data by tags (and/or tag groups)
METHODS REQUIRED IN TAG CONTROLLER

View File

@@ -0,0 +1,10 @@
DISPATCH ZONE SPECS
CASES
3472, 3473, 3439
REQUIREMENTS
Dispatch zones will be a collection now for users and also will replace scheduleable user groups

View File

@@ -23,6 +23,7 @@ Overall plan for now: anything standing in the way of making the initial client
- v7importusers
- User route and controller and biz object
- v7import once have users imported then proper attribution in eventlog of creator,modifier, created,modified
- Tag groups (modify tags already coded)
- Localized text
- Search and search text indexing
- Auto visible id number assigning code

View File

@@ -0,0 +1,315 @@
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 UserBiz : BizObject, IJobObject
{
private readonly AyContext ct;
public readonly long userId;
private readonly AuthorizationRoles userRoles;
internal UserBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<User> CreateAsync(User inObj)
{
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with User
User outObj = inObj;
outObj.OwnerId = userId;
//SearchHelper(break down text fields, save to db)
//TagHelper(collection of tags??)
await ct.User.AddAsync(outObj);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<User> GetAsync(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.User.SingleOrDefaultAsync(m => m.Id == fetchId);
}
//get many (paged)
internal async Task<ApiPagedResponse<User>> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions)
{
pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
var items = await ct.User
.OrderBy(m => m.Id)
.Skip(pagingOptions.Offset.Value)
.Take(pagingOptions.Limit.Value)
.ToArrayAsync();
var totalRecordCount = await ct.User.CountAsync();
var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
ApiPagedResponse<User> pr = new ApiPagedResponse<User>(items, pageLinks);
return pr;
}
//get picklist (paged)
internal async Task<ApiPagedResponse<NameIdItem>> 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.User
.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.User.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync();
}
else
{
items = await ct.User
.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.User.CountAsync();
}
var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
ApiPagedResponse<NameIdItem> pr = new ApiPagedResponse<NameIdItem>(items, pageLinks);
return pr;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal bool Put(User dbObj, User inObj)
{
//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;
return true;
}
//patch
internal bool Patch(User dbObj, JsonPatchDocument<User> objectPatch, uint concurrencyToken)
{
//Do the patching
objectPatch.ApplyTo(dbObj);
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
Validate(dbObj, false);
if (HasErrors)
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal bool Delete(User 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.User.Remove(dbObj);
return true;
}
/// <summary>
/// Delete child objects like tags and attachments and etc
/// </summary>
/// <param name="dbObj"></param>
internal void DeleteChildren(User dbObj)
{
//TAGS
TagMapBiz.DeleteAllForObject(new AyaTypeId(AyaType.User, dbObj.Id), ct);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(User inObj, bool isNew)
{
//run validation and biz rules
if (isNew)
{
//NEW Users must be active
if (inObj.Active == null || ((bool)inObj.Active) == false)
{
AddError(ValidationErrorType.InvalidValue, "Active", "New User 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.User.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(User 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.TestUserJob:
await ProcessTestJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"UserBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
/// <summary>
/// /// Handle the test job
/// </summary>
/// <param name="job"></param>
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, $"UserBiz::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, "UserBiz::ProcessTestJob done sleeping setting job to finished", ct);
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct);
}
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,72 @@
namespace AyaNova.Biz
{
/// <summary>
/// All AyaNova user types
/// </summary>
public enum UserType : int
{
Administrator = 1,
Schedulable = 2,
NonSchedulable = 3,
Client = 4,
HeadOffice = 5,
Utility = 6,
Subcontractor = 7
}
}//eons
/*
///////////////////////////////////////////////////////////
// UserTypes.cs
// Implementation of Class UserTypes
// CSLA type: enumeration
// Created on: 07-Jun-2004 8:41:44 AM
// Object design: Joyce
///////////////////////////////////////////////////////////
using System.ComponentModel;
namespace GZTW.AyaNova.BLL {
/// <summary>
/// Variations of <see cref="User"/> types
/// </summary>
public enum UserTypes : int {
/// <summary>
/// This is the special account used only for updates, for customizing layout, etc
/// </summary>
[Description("LT:UserTypes.Label.Administrator")] Administrator = 1,
/// <summary>
/// This is a user that can be assigned to a workorder or schedule and consumes an AyaNova license
/// </summary>
[Description("LT:UserTypes.Label.Schedulable")] Schedulable = 2,
/// <summary>
/// Standard user that can use all aspects of AyaNova but can not be chosen to assign to a work order or schedule
/// Does not consume an AyaNova license
/// </summary>
[Description("LT:UserTypes.Label.NonSchedulable")] NonSchedulable = 3,
/// <summary>
/// Client's login user account for WBI
/// </summary>
[Description("LT:UserTypes.Label.Client")] Client = 4,
/// <summary>
/// HeadOffice's login user account for WBI
/// </summary>
[Description("LT:UserTypes.Label.HeadOffice")] HeadOffice = 5,
/// <summary>
/// Utility user type for internal processing with limited functionality
/// ATTN. API users: Do not create or set a user to this type in your code, you will be dissapointed and will break things. :)
/// </summary>
[Description("UTILITY")] Utility = 6
//*** IF MORE ADDED USER OBJECT USERTYPE PROPERTIES
// BROKEN RULES MUST BE UPDATED AND SQL SERVER USER TABLE CHECK CONSTRAINT
// FOR USERTYPE VALUE ***
}//end UserTypes
}//end namespace GZTW.AyaNova.BLL
*/

View File

@@ -11,14 +11,107 @@ namespace AyaNova.Models
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
[Required]
public bool Active { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Login { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string Salt { get; set; }
public AuthorizationRoles Roles { get; set; }
[Required]
public AuthorizationRoles Roles { get; set; }
[Required]
public long LocaleId { get; set; }
public string DlKey { get; set; }
public DateTime? DlKeyExpire { get; set; }
public long LocaleId { get; set; }
[Required]
public UserType UserType { get; set; }
public string EmployeeNumber { get; set; }
public string Notes { get; set; }
public long? ClientId { get; set; }
public long? HeadOfficeId { get; set; }
public long? SubVendorId { get; set; }
}
}
/*
v7 export record sample
{
"DefaultLanguage": "Custom English",
"DefaultServiceTemplateID": "ca83a7b8-4e5f-4a7b-a02b-9cf78d5f983f",
"UserType": 2,
"Active": true,
"ClientID": "00000000-0000-0000-0000-000000000000",
"HeadOfficeID": "00000000-0000-0000-0000-000000000000",
"MemberOfGroup": "0f8a80ff-4b03-4114-ae51-2d13b812dd65",
"Created": "03/21/2005 07:19 AM",
"Modified": "09/15/2015 12:22 PM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modifier": "1d859264-3f32-462a-9b0c-a67dddfdf4d3",
"ID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3",
"FirstName": "Hank",
"LastName": "Rearden",
"Initials": "HR",
"EmployeeNumber": "EMP1236",
"PageAddress": "",
"PageMaxText": 24,
"Phone1": "",
"Phone2": "",
"EmailAddress": "",
"UserCertifications": [
{
"Created": "12/22/2005 02:07 PM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modified": "12/22/2005 02:08 PM",
"Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"ID": "4492360c-43e4-4209-9f33-30691b0808ed",
"UserCertificationID": "b2f26359-7c42-4218-923a-e949f3ef1f85",
"UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3",
"ValidStartDate": "2005-10-11T00:00:00-07:00",
"ValidStopDate": "2006-10-11T00:00:00-07:00"
}
],
"UserSkills": [
{
"Created": "12/22/2005 02:06 PM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modified": "12/22/2005 02:08 PM",
"Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"ID": "1dc5ce96-f411-4885-856e-5bdb3ad79728",
"UserSkillID": "2e6f8b65-594c-4f6c-9cd6-e14a562daba8",
"UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3"
},
{
"Created": "12/22/2005 02:06 PM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modified": "12/22/2005 02:08 PM",
"Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"ID": "88e476d3-7526-45f5-a0dd-706c8053a63f",
"UserSkillID": "47a4ee94-b0e9-41b5-afe5-4b4f2c981877",
"UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3"
}
],
"Notes": "",
"VendorID": "06e502c2-69ba-4e88-8efb-5b53c1687740",
"RegionID": "f856423a-d468-4344-b7b8-121e466738c6",
"DispatchZoneID": "00000000-0000-0000-0000-000000000000",
"SubContractor": false,
"DefaultWarehouseID": "d45eab37-b6e6-4ad2-9163-66d7ba83a98c",
"Custom1": "",
"Custom2": "",
"Custom3": "",
"Custom4": "",
"Custom5": "",
"Custom6": "",
"Custom7": "",
"Custom8": "",
"Custom9": "",
"Custom0": "",
"ScheduleBackColor": -2097216,
"TimeZoneOffset": null
}
*/

View File

@@ -139,8 +139,8 @@ namespace AyaNova.Util
AyaNova.Biz.PrimeData.PrimeLocales(ct);
//Add user table
exec("CREATE TABLE auser (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, name varchar(255) not null, " +
"login text not null, password text not null, salt text not null, roles integer not null, localeid bigint REFERENCES alocale (id), " +
exec("CREATE TABLE auser (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, active bool not null, name varchar(255) not null, " +
"login text not null, password text not null, salt text not null, roles integer not null, localeid bigint not null REFERENCES alocale (id), " +
"dlkey text, dlkeyexpire timestamp)");
//Prime the db with the default MANAGER account