This commit is contained in:
2018-09-06 21:57:30 +00:00
parent 6329b624c5
commit f6256212ef
8 changed files with 677 additions and 2 deletions

View File

@@ -26,7 +26,9 @@ namespace AyaNova.Biz
TrialSeeder = 11,
Metrics = 12,
Locale = 13,
UserOptions=14
UserOptions=14,
TagGroup = 15,
TagGroupMap = 16
}

View File

@@ -112,6 +112,26 @@ 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()
{
Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryFull | AuthorizationRoles.TechFull | AuthorizationRoles.AccountingFull,
EditOwn = AuthorizationRoles.NoRole,
ReadFullRecord = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////
//TAGGROUPMAP - MIRROR TAGMAP
//Any roles can tag objects and remove tags as per their rights to the taggable object type in question
roles.Add(AyaType.TagGroupMap, new BizRoleSet()
{
Change = AuthorizationRoles.AnyRole,
EditOwn = AuthorizationRoles.NoRole,
ReadFullRecord = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////
//OPERATIONS / JOBS

View File

@@ -0,0 +1,426 @@
using System;
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;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace AyaNova.Biz
{
internal class TagGroupBiz : BizObject, IImportAyaNova7Object
{
private readonly AyContext ct;
public readonly long userId;
private readonly AuthorizationRoles userRoles;
public bool V7ValidationImportMode { get; set; }
internal TagGroupBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
V7ValidationImportMode = false;//default
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<TagGroup> CreateAsync(string inObj)
{
inObj = CleanTagGroupName(inObj);
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with TagGroup
TagGroup outObj = new TagGroup()
{
Name = inObj,
OwnerId = userId
};
await ct.TagGroup.AddAsync(outObj);
return outObj;
}
}
private static string CleanTagGroupName(string inObj)
{
//Must be lowercase per rules
//This may be naive when we get international customers but for now supporting utf-8 and it appears it's safe to do this with unicode
inObj = inObj.ToLowerInvariant();
//No spaces in TagGroups, replace with dashes
inObj = inObj.Replace(" ", "-");
//Remove multiple dash sequences
inObj = System.Text.RegularExpressions.Regex.Replace(inObj, "-+", "-");
//Ensure doesn't start or end with a dash
inObj = inObj.Trim('-');
//No longer than 255 characters
inObj = StringUtil.MaxLength(inObj, 255);
return inObj;
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<TagGroup> 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.TagGroup.SingleOrDefaultAsync(m => m.Id == fetchId);
}
//get picklist (paged)
//Unlike most picklists, this one only checks for starts with and wildcards are not supported / treated as part of TagGroup name
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))
{
//TagGroups are allow saved this way so search this way too
q = q.ToLowerInvariant();
items = await ct.TagGroup
//There is some debate on this for efficiency
//I chose this method because I think it escapes potential wildcards in the string automatically
//and I don't want people using wildcards with this, only starts with is supported
//https://stackoverflow.com/questions/45708715/entity-framework-ef-functions-like-vs-string-contains
.Where(m => m.Name.StartsWith(q))
// .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.TagGroup.Where(m => m.Name.StartsWith(q)).CountAsync();
}
else
{
items = await ct.TagGroup
.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.TagGroup.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(TagGroup dbObj, TagGroup inObj)
{
//Ensure it follows the rules
inObj.Name = CleanTagGroupName(inObj.Name);
//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.Name, false);
if (HasErrors)
return false;
return true;
}
//patch
internal bool Patch(TagGroup dbObj, JsonPatchDocument<TagGroup> objectPatch, uint concurrencyToken)
{
//Validate Patch is allowed
if (!ValidateJsonPatch<TagGroup>.Validate(this, objectPatch)) return false;
//Do the patching
objectPatch.ApplyTo(dbObj);
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
//Ensure it follows the rules
dbObj.Name = CleanTagGroupName(dbObj.Name);
Validate(dbObj.Name, false);
if (HasErrors)
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal bool Delete(TagGroup dbObj)
{
//Determine if the object can be deleted, do the deletion tentatively
ValidateCanDelete(dbObj);
if (HasErrors)
return false;
ct.TagGroup.Remove(dbObj);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(string inObj, bool isNew)
{
//run validation and biz rules
//Name required
if (string.IsNullOrWhiteSpace(inObj))
AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
//Name must be less than 255 characters
if (inObj.Length > 255)
AddError(ValidationErrorType.LengthExceeded, "Name", "255 char max");
//Name must be unique
if (ct.TagGroup.Where(m => m.Name == inObj).FirstOrDefault() != null)
AddError(ValidationErrorType.NotUnique, "Name");
return;
}
//Can delete?
private void ValidateCanDelete(TagGroup inObj)
{
//whatever needs to be check to delete this object
//See if any TagGroupmaps exist with this TagGroup in which case it's not deleteable
if (ct.TagGroupMap.Any(e => e.TagGroupId == inObj.Id))
{
AddError(ValidationErrorType.ReferentialIntegrity, "object", "Can't be deleted while has relations");
}
}
/////////////////////////////////////////////////////////////////////
/// IMPORT v7 implementation
public async Task<bool> ImportV7Async(JObject j, List<ImportAyaNova7MapItem> importMap, Guid jobId)
{
//NO TASK TYPE, IT'S ALL THE SAME, KEEPING THIS FOR POSSIBLE FUTURE PURPOSES LIKE APPENDING OBJECT TYPE OR SOMETHING
string SourceType = j["V7_TYPE"].Value<string>();
switch (SourceType)
{
case "GZTW.AyaNova.BLL.Region":
case "GZTW.AyaNova.BLL.UnitModelCategory":
case "GZTW.AyaNova.BLL.UnitServiceType":
case "GZTW.AyaNova.BLL.WorkorderItemType":
case "GZTW.AyaNova.BLL.ClientGroup":
case "GZTW.AyaNova.BLL.WorkorderCategory":
case "GZTW.AyaNova.BLL.PartCategory":
case "GZTW.AyaNova.BLL.DispatchZone":
case "GZTW.AyaNova.BLL.ScheduleableUserGroup":
{
switch (j["IMPORT_TASK"].Value<string>())
{
case "main":
{
#region main import task
var NewTagGroupName = j["Name"].Value<string>();
var ShortTypeName = string.Empty;
switch (SourceType)
{
case "GZTW.AyaNova.BLL.Region":
ShortTypeName = "rgn";
break;
case "GZTW.AyaNova.BLL.UnitModelCategory":
ShortTypeName = "unitmdlctgry";
break;
case "GZTW.AyaNova.BLL.UnitServiceType":
ShortTypeName = "unitsvtyp";
break;
case "GZTW.AyaNova.BLL.WorkorderItemType":
ShortTypeName = "woitemtyp";
break;
case "GZTW.AyaNova.BLL.ClientGroup":
ShortTypeName = "clntgrp";
break;
case "GZTW.AyaNova.BLL.WorkorderCategory":
ShortTypeName = "woctgry";
break;
case "GZTW.AyaNova.BLL.PartCategory":
ShortTypeName = "prtctgry";
break;
case "GZTW.AyaNova.BLL.DispatchZone":
ShortTypeName = "dspchzn";
break;
case "GZTW.AyaNova.BLL.ScheduleableUserGroup":
ShortTypeName = "schdusrgrp";
break;
}
NewTagGroupName += "." + ShortTypeName;
var OldV7Id = new Guid(j["ID"].Value<string>());
//Ensure it follows the rules
NewTagGroupName = CleanTagGroupName(NewTagGroupName);
//There might already be a TagGroup of the same name since so many different types of V7 objects are becoming TagGroups
//Weighed the pros and cons of uniquifying by object type versus just using the same name for different object types:
//it seems to me at this point that people might desire the same exact name because if they used it that way they probably
//intended it that way, so decision is to check if it already exists and then use that ID in the importMap instead
//for matching other objects imported to TagGroups
//Already present?
var ExistingTagGroup = ct.TagGroup.Where(m => m.Name == NewTagGroupName).FirstOrDefault();
if (ExistingTagGroup != null)
{
//map it to the existing TagGroup of same name
var mapItem = new ImportAyaNova7MapItem(OldV7Id, AyaType.TagGroup, ExistingTagGroup.Id);
}
else
{
TagGroup o = await CreateAsync(NewTagGroupName);
if (HasErrors)
{
//If there are any validation errors, log in joblog and move on
JobsBiz.LogJob(jobId, $"TagGroupBiz::ImportV7Async -> import object \"{NewTagGroupName}\" of type \"{SourceType}\" source id {OldV7Id.ToString()} failed validation and was not imported: {GetErrorsAsString()} ", ct);
return false;
}
else
{
await ct.SaveChangesAsync();
var mapItem = new ImportAyaNova7MapItem(OldV7Id, AyaType.TagGroup, o.Id);
importMap.Add(mapItem);
await ImportAyaNova7Biz.LogEventCreatedModifiedEvents(j, importMap, AyaType.TagGroup, ct);
}
}
#endregion
}
break;
case "scheduleableusergroupTagGroups":
{
#region attribute sched user group TagGroups
/*
{
"ID": "871e77b2-979a-4f26-930b-46f7c05fc19f",
"Created": "08/30/2018 08:12 AM",
"Modified": "08/30/2018 08:13 AM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Name": "Yet another test group",
"Active": true,
"Description": "More testing yay!",
"ScheduleableUsers": [
{
"Created": "08/30/2018 08:13 AM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modified": "08/30/2018 08:13 AM",
"Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"ID": "676475be-8301-47d0-bd54-af9dbd1fe7eb",
"ScheduleableUserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3",
"ScheduleableUserGroupID": "871e77b2-979a-4f26-930b-46f7c05fc19f"
},
{
"Created": "08/30/2018 08:13 AM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modified": "08/30/2018 08:13 AM",
"Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"ID": "173499c3-a616-42a0-b08c-74008f8fa352",
"ScheduleableUserID": "42b282bb-100b-4b31-aa14-5c831d7cda66",
"ScheduleableUserGroupID": "871e77b2-979a-4f26-930b-46f7c05fc19f"
},
{
"Created": "08/30/2018 08:13 AM",
"Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"Modified": "08/30/2018 08:13 AM",
"Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed",
"ID": "19c9b3d6-eeb2-44ac-be4e-6ec93d15b02a",
"ScheduleableUserID": "e6ff9bc6-a550-4242-8c41-857f740e2841",
"ScheduleableUserGroupID": "871e77b2-979a-4f26-930b-46f7c05fc19f"
}
]
}
*/
var V7Id = new Guid(j["ID"].Value<string>());
var RavenTagGroupId = importMap.Where(m => m.V7ObjectId == V7Id).First().NewObjectAyaTypeId.ObjectId;
foreach (JToken t in j["ScheduleableUsers"])
{
var techId = new Guid(t["ScheduleableUserID"].Value<String>());
var RavenUserId = importMap.Where(m => m.V7ObjectId == techId).First().NewObjectAyaTypeId.ObjectId;
var Creator = importMap.Where(m => m.V7ObjectId == new Guid(t["Creator"].Value<string>())).First().NewObjectAyaTypeId.ObjectId;
TagGroupMap tm = new TagGroupMap();
tm.TagGroupToObjectId = RavenUserId;
tm.TagGroupToObjectType=AyaType.User;
tm.TagGroupId = RavenTagGroupId;
tm.OwnerId = Creator;
ct.TagGroupMap.Add(tm);
}
ct.SaveChanges();
#endregion
}
break;
}
}
break;
}
//this is the equivalent of returning void for a Task signature with nothing to return
return true;
}
/////////////////////////////////////////////////////////////////////
}//eoc
//
}//eons

View File

@@ -0,0 +1,177 @@
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;
using System.Collections.Generic;
namespace AyaNova.Biz
{
internal class TagGroupMapBiz : BizObject
{
private readonly AyContext ct;
public readonly long userId;
private readonly AuthorizationRoles userRoles;
internal TagGroupMapBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<TagGroupMap> CreateAsync(TagGroupMapInfo inObj)
{
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with TagGroupMap
TagGroupMap outObj = new TagGroupMap()
{
TagId = inObj.TagId,
TagToObjectId = inObj.TagToObjectId,
TagToObjectType = inObj.TagToObjectType,
OwnerId = userId
};
await ct.TagGroupMap.AddAsync(outObj);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<TagGroupMap> 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.TagGroupMap.SingleOrDefaultAsync(m => m.Id == fetchId);
}
internal async Task<List<NameIdItem>> GetTagsOnObjectListAsync(AyaTypeId tid)
{
/*
NOTES: This will be a bit of a "hot" path as every fetch of any main object will involve this
for now, just make it work and later can improve performance
Also is sort going to be adequate, it's supposed to be based on invariant culture
*/
List<NameIdItem> l = new List<NameIdItem>();
//Get the list of tags on the object
var TagGroupMapsOnObject = await ct.TagGroupMap
.Where(m => m.TagToObjectId == tid.ObjectId && m.TagToObjectType == tid.ObjectType)
.Select(m => m.TagId)
.ToListAsync();
foreach (long tagId in TagGroupMapsOnObject)
{
var tagFromDb = await ct.Tag.SingleOrDefaultAsync(m => m.Id == tagId);
if (tagFromDb != null)
{
l.Add(new NameIdItem() { Id = tagFromDb.Id, Name = tagFromDb.Name });
}
}
//Return the list sorted alphabetically
//Note if this is commonly required then maybe make a helper / extension for it
return (l.OrderBy(o => o.Name).ToList());
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal bool Delete(TagGroupMap dbObj)
{
//Determine if the object can be deleted, do the deletion tentatively
ValidateCanDelete(dbObj);
if (HasErrors)
return false;
ct.TagGroupMap.Remove(dbObj);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE ALL TagGroupMapS FOR OBJECT
//
static internal bool DeleteAllForObject(AyaTypeId parentObj, AyContext ct)
{
//Be careful in future, if you put ToString at the end of each object in the string interpolation
//npgsql driver will assume it's a string and put quotes around it triggering an error that a string can't be compared to an int
ct.Database.ExecuteSqlCommand($"delete from aTagGroupMap where tagtoobjectid={parentObj.ObjectId} and tagtoobjecttype={parentObj.ObjectTypeAsInt}");
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(TagGroupMapInfo inObj, bool isNew)
{
//run validation and biz rules
// //Name required
// if (string.IsNullOrWhiteSpace(inObj))
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
// //Name must be less than 255 characters
// if (inObj.Length > 255)
// AddError(ValidationErrorType.LengthExceeded, "Name", "255 char max");
return;
}
//Can delete?
private void ValidateCanDelete(TagGroupMap inObj)
{
//whatever needs to be check to delete this object
//See if any TagGroupMaps exist with this tag in which case it's not deleteable
// if (ct.TagGroupMap.Any(e => e.TagGroupMapId == inObj.Id))
// {
// AddError(ValidationErrorType.ReferentialIntegrity, "object", "Can't be deleted while has relations");
// }
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -16,6 +16,8 @@ namespace AyaNova.Models
public virtual DbSet<FileAttachment> FileAttachment { get; set; }
public virtual DbSet<Tag> Tag { get; set; }
public virtual DbSet<TagMap> TagMap { get; set; }
public virtual DbSet<TagGroup> TagGroup { get; set; }
public virtual DbSet<TagGroupMap> TagGroupMap { get; set; }
public virtual DbSet<OpsJob> OpsJob { get; set; }
public virtual DbSet<OpsJobLog> OpsJobLog { get; set; }
public virtual DbSet<Locale> Locale { get; set; }

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
public partial class TagGroup
{
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
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
public partial class TagGroupMap
{
public long Id { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
[Required]
public long TagId { get; set; }
[Required]
public long TagGroupId { get; set; }
}
}

View File

@@ -144,7 +144,7 @@ namespace AyaNova.Util
"dlkey text, dlkeyexpire timestamp, usertype integer not null, employeenumber varchar(255), notes text, clientid bigint, headofficeid bigint, subvendorid bigint)");
//Add user options table
exec("CREATE TABLE auseroptions (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, "+
exec("CREATE TABLE auseroptions (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, " +
"userid bigint not null, timezoneoffset decimal(19,5) not null default 0, emailaddress text, uicolor int not null default 0)");
@@ -211,6 +211,10 @@ namespace AyaNova.Util
exec("CREATE TABLE atagmap (id BIGSERIAL PRIMARY KEY, ownerid bigint not null," +
"tagid bigint not null REFERENCES atag (id), tagtoobjectid bigint not null, tagtoobjecttype integer not null)");
//Taggroup
exec("CREATE TABLE ataggroup (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, name varchar(255) not null)");
exec("CREATE TABLE ataggroupmap (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, tagid bigint not null REFERENCES atag (id), taggroupid bigint not null)");
setSchemaLevel(++currentSchema);
}