Files
raven/server/AyaNova/biz/UserBiz.cs
2020-04-19 23:10:09 +00:00

813 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
namespace AyaNova.Biz
{
internal class UserBiz : BizObject, IJobObject, IImportAyaNova7Object, ISearchAbleObject
{
public bool SeedOrImportRelaxedRulesMode { get; set; }
internal UserBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles userRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = userRoles;
BizType = AyaType.User;
SeedOrImportRelaxedRulesMode = false;//default
}
//This is where active tech license consumers are accounted for
internal static async Task<long> ActiveCountAsync()
{
var ct = ServiceProviderProvider.DBContext;
var ret = await ct.User.Where(x => x.Active == true && (x.UserType == UserType.Schedulable || x.UserType == UserType.Subcontractor)).LongCountAsync();
return ret;
}
internal static UserBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new UserBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else//when called internally for internal ops there will be no context so need to set default values for that
return new UserBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<User> CreateAsync(User inObj)
{
//This is a new user so it will have been posted with a password in plaintext which needs to be salted and hashed
inObj.Salt = Hasher.GenerateSalt();
inObj.Password = Hasher.hash(inObj.Salt, inObj.Password);
inObj.Tags = TagUtil.NormalizeTags(inObj.Tags);
inObj.CustomFields = JsonUtil.CompactJson(inObj.CustomFields);
//Seeder sets user options in advance so no need to create them here in that case
if (inObj.UserOptions == null)
{
inObj.UserOptions = new UserOptions();
//todo: for now defaulting to server boot config but might need to add this to the route as an option
//now that it's not in the actual user record itself anymore as it's kind of critical
//revisit when get to client ui
inObj.UserOptions.TranslationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID;
}
await ValidateAsync(inObj, null);
if (HasErrors)
return null;
else
{
await ct.User.AddAsync(inObj);
//save to get Id
await ct.SaveChangesAsync();
//Handle child and associated items
//Log event
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, inObj.Id, BizType, AyaEvent.Created), ct);
//SEARCH INDEXING
await SearchIndexAsync(inObj, true);
//TAGS
await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, inObj.Tags, null);
return inObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// 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
var ret = await ct.User.SingleOrDefaultAsync(m => m.Id == fetchId);
if (ret != null)
{
//Log
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal async Task<bool> PutAsync(User dbObj, User inObj)
{
//Get a snapshot of the original db value object before changes
User SnapshotOfOriginalDBObj = new User();
CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj);
//Update the db object with the PUT object values
CopyObject.Copy(inObj, dbObj, "Id, Salt");
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields);
//Is the user updating the password?
if (!string.IsNullOrWhiteSpace(inObj.Password) && SnapshotOfOriginalDBObj.Password != inObj.Password)
{
//YES password is being updated:
dbObj.Password = Hasher.hash(SnapshotOfOriginalDBObj.Salt, inObj.Password);
}
else
{
//No, use the snapshot password value
dbObj.Password = SnapshotOfOriginalDBObj.Password;
dbObj.Salt = SnapshotOfOriginalDBObj.Salt;
}
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
await ValidateAsync(dbObj, SnapshotOfOriginalDBObj);
if (HasErrors)
return false;
await ct.SaveChangesAsync();
//Log modification and save context
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
await SearchIndexAsync(dbObj, false);
await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);
return true;
}
//patch
internal async Task<bool> PatchAsync(User dbObj, JsonPatchDocument<User> objectPatch, uint concurrencyToken)
{
//Validate Patch is allowed
if (!ValidateJsonPatch<User>.Validate(this, objectPatch)) return false;
//make a snapshot of the original for validation but update the original to preserve workflow
User SnapshotOfOriginalDBObj = new User();
CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj);
//Do the patching
objectPatch.ApplyTo(dbObj);
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields);
//Is the user patching the password?
if (!string.IsNullOrWhiteSpace(dbObj.Password) && dbObj.Password != SnapshotOfOriginalDBObj.Password)
{
//YES password is being updated:
dbObj.Password = Hasher.hash(dbObj.Salt, dbObj.Password);
}
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
await ValidateAsync(dbObj, SnapshotOfOriginalDBObj);
if (HasErrors)
return false;
await ct.SaveChangesAsync();
//Log modification and save context
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
await SearchIndexAsync(dbObj, false);
await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags);
return true;
}
//put
internal async Task<bool> ChangePasswordAsync(long userId, string newPassword)
{
User dbObj = await ct.User.FirstOrDefaultAsync(m => m.Id == userId);
dbObj.Password = Hasher.hash(dbObj.Salt, newPassword);
await ct.SaveChangesAsync();
//Log modification and save context
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct);
return true;
}
private async Task SearchIndexAsync(User obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.EmployeeNumber).AddText(obj.Tags).AddText(obj.Wiki).AddCustomFields(obj.CustomFields);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id)
{
var obj = await ct.User.SingleOrDefaultAsync(m => m.Id == id);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Notes).AddText(obj.Name).AddText(obj.EmployeeNumber).AddText(obj.Tags).AddText(obj.Wiki).AddCustomFields(obj.CustomFields);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(User dbObj)
{
await ValidateCanDelete(dbObj);
if (HasErrors)
return false;
//Remove the object
ct.User.Remove(dbObj);
await ct.SaveChangesAsync();
//Delete sibling objects
//USEROPTIONS
await ct.Database.ExecuteSqlInterpolatedAsync($"delete from auseroptions where userid = {dbObj.Id}");
//personal datalistview
await ct.Database.ExecuteSqlInterpolatedAsync($"delete from adatalistview where public = {false} and userid = {dbObj.Id}");
await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObj.Id, dbObj.Name, ct);
await Search.ProcessDeletedObjectKeywordsAsync(dbObj.Id, BizType);
await TagUtil.ProcessDeleteTagsInRepositoryAsync(ct, dbObj.Tags);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private async Task ValidateAsync(User proposedObj, User currentObj)
{
//run validation and biz rules
bool isNew = currentObj == null;
//do we need to check the license situation?
if (proposedObj.IsTech && proposedObj.Active)
{
//Yes, it might be affected depending on things
long CurrentActiveCount = await UserBiz.ActiveCountAsync();
long LicensedUserCount = AyaNova.Core.License.ActiveKey.ActiveNumber;
if (isNew)
{
//This operation is about to consume one more license, check that we are not at the limit already
CheckActiveForValidation(CurrentActiveCount, LicensedUserCount);
}
else
{
//did anything that might affect licensing change?
if (!currentObj.IsTech || (!currentObj.Active))//currently not a tech or if it is it's not active
{
//going from non tech to tech and active
//Yes, this is about to consume one more license, check that we are not at the limit already
CheckActiveForValidation(CurrentActiveCount, LicensedUserCount);
}
}
}
// TODO: check user count if not new to see if affected that way
//also check user count in general to see if it's exceeded
//And maybe check it in login as well as a good central spot or wherever makes sense
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//Name must be less than 255 characters
if (proposedObj.Name.Length > 255)
AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "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 (await ct.User.AnyAsync(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
}
}
//TODO: Validation rules that require future other objects that aren't present yet:
/*
//Don't allow to go from non scheduleable if there are any scheduled workorder items because
//it would damage the history
BrokenRules.Assert("UserType","User.Label.MustBeScheduleable","UserType",(mUserType==UserTypes.Schedulable) && (ScheduledUserCount(this.mID,false)>0));
mUserType = value;
BrokenRules.Assert("UserTypeInvalid","Error.Object.FieldValueNotBetween,User.Label.UserType,1,7","UserType",((int)value<1 || (int)value>6));
bool bOutOfLicenses=OutOfLicenses;
BrokenRules.Assert("ActiveLicense","Error.Security.UserCapacity","Active",bOutOfLicenses);
BrokenRules.Assert("UserTypeLicense","Error.Security.UserCapacity","UserType",bOutOfLicenses);
//Case 850
BrokenRules.Assert("ClientIDInvalid", "Error.Object.RequiredFieldEmpty,O.Client", "ClientID",
(mUserType== UserTypes.Client && mClientID==Guid.Empty));
BrokenRules.Assert("HeadOfficeIDInvalid", "Error.Object.RequiredFieldEmpty,O.HeadOffice", "HeadOfficeID",
(mUserType == UserTypes.HeadOffice && mHeadOfficeID == Guid.Empty));
ACTIVE: need to check user count against license when re-activating a user
need to check open workorders and any other critical items when de-activating a user
*/
if (!proposedObj.UserType.IsValid())
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UserType");
}
//Validate customer type user
if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.Customer)
{
if (proposedObj.CustomerId == null || proposedObj.CustomerId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "CustomerId");
}
else
{
//verify that client exists
//TODO WHEN CLIENT OBJECT MADE if(!ct.Client.any(m))
}
}
//Validate headoffice type user
if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.HeadOffice)
{
if (proposedObj.HeadOfficeId == null || proposedObj.HeadOfficeId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "HeadOfficeId");
}
else
{
//verify that HeadOfficeId exists
//TODO WHEN HEADOFFICE OBJECT MADE if(!ct.HeadOffice.any(m))
}
}
//Validate headoffice type user
if (!SeedOrImportRelaxedRulesMode && proposedObj.UserType == UserType.Subcontractor)
{
if (proposedObj.SubVendorId == null || proposedObj.SubVendorId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "SubVendorId");
}
else
{
//verify that VENDOR SubVendorId exists
//TODO WHEN VENDOR OBJECT MADE if(!ct.Vendor.any(m))
}
}
if (!proposedObj.Roles.IsValid())
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Roles");
}
//Optional employee number field must be less than 255 characters
if (!string.IsNullOrWhiteSpace(proposedObj.EmployeeNumber) && proposedObj.EmployeeNumber.Length > 255)
AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "EmployeeNumber", "255 max");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.SingleOrDefaultAsync(x => x.FormKey == AyaType.User.ToString());
if (FormCustomization != null)
{
//Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required
//validate users choices for required non custom fields
RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);
//validate custom fields
CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
return;
}
private void CheckActiveForValidation(long CurrentActiveCount, long LicensedUserCount)
{
if (CurrentActiveCount >= LicensedUserCount)
{
AddError(ApiErrorCode.INVALID_OPERATION, null, "LT:ErrorSecurityUserCapacity");
}
}
//Can delete?
private async Task ValidateCanDelete(User inObj)
{
//To make this simple and avoid a whole host of issues and work
//I've decided that a user can't be deleted if they have *any* activity in the event log
//this way a newly created user can be deleted before they do any real work still to cover a scenario where a user
//makes a user but then doesn't need it or did it wrong
//This avoids the whole issues related to having to check every table everywhere for their work and
//the associated fuckery with trying to back them out of those tables without knock-on effects
//They can always make any user inactive to get rid of them and it will mean referential integrity issues are not there
//There's only one rule - have they done anything eventlog worthy yet?
//if (await ct.Event.Select(m => m).Where(m => m.UserId == inObj.Id).Count() > 0)
if (await ct.Event.AnyAsync(m => m.UserId == inObj.Id))
{
AddError(ApiErrorCode.INVALID_OPERATION, "user", "LT:ErrorDBForeignKeyViolation");
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Utilities
//
internal static object CleanUserForReturn(User o)
{
return new
{
Id = o.Id,
ConcurrencyToken = o.ConcurrencyToken,
Active = o.Active,
Name = o.Name,
Roles = o.Roles,
TranslationId = o.UserOptions.TranslationId,
UserType = o.UserType,
EmployeeNumber = o.EmployeeNumber,
Notes = o.Notes,
CustomerId = o.CustomerId,
HeadOfficeId = o.HeadOfficeId,
SubVendorId = o.SubVendorId
};
}
////////////////////////////////////////////////////////////////////////////////////////////////
// JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
//just to hide compiler warning for now
await Task.CompletedTask;
//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)
{
default:
throw new System.ArgumentOutOfRangeException($"UserBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
public async Task<bool> ImportV7Async(JObject j, List<ImportAyaNova7MapItem> importMap, Guid jobId, Dictionary<string, Dictionary<Guid, string>> tagLists)
{
//NEEDS 3 of the tag lists
//TODO: Some of these items will need to be imported in future USEROPTIONS object that doesn't exist yet
#region V7 record format
/*
{
"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,//This is not actually a feature in v7, you can just pick a vendorId for subcontractor but still be of type technician
"DefaultWarehouseID": "d45eab37-b6e6-4ad2-9163-66d7ba83a98c",
"Custom1": "",
"Custom2": "",
"Custom3": "",
"Custom4": "",
"Custom5": "",
"Custom6": "",
"Custom7": "",
"Custom8": "",
"Custom9": "",
"Custom0": "",
"ScheduleBackColor": -2097216,
"TimeZoneOffset": null
}
*/
#endregion v7 record format
SeedOrImportRelaxedRulesMode = true;
//some types need to import from more than one source hence the seemingly redundant switch statement for futureproofing
switch (j["IMPORT_TASK"].Value<string>())
{
case "main":
{
#region Main import
var V7Id = new Guid(j["ID"].Value<string>());
//skip the administrator account but add it to the map for all the other import code that requires it
if (V7Id == new Guid("2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed"))
{
var mapItem = new ImportAyaNova7MapItem(V7Id, BizType, 1);
importMap.Add(mapItem);
return true;
}
//Copy values
User i = new User();
i.Name = j["FirstName"].Value<string>() + " " + j["LastName"].Value<string>();
var Temp = j["UserType"].Value<int>();
i.UserType = (UserType)Temp;
//If there is a vendorId set then this user is actually a subcontractor in v7 so set accordingly
var VendorId = new Guid(j["VendorID"].Value<string>());
if (VendorId != Guid.Empty)
{
i.UserType = UserType.Subcontractor;
}
i.Active = false;//Ignore incoming value and set all imports to false so that there's no chance of licensing getting circumvented; users all need to be edited anyway for pw and login
i.EmployeeNumber = j["EmployeeNumber"].Value<string>();
i.Notes = j["Notes"].Value<string>();
//TAGS
var MemberOfGroupId = new Guid(j["MemberOfGroup"].Value<string>());
if (MemberOfGroupId != Guid.Empty)
{
string sTag = string.Empty;
if (tagLists["ScheduleableUserGroup"].TryGetValue(MemberOfGroupId, out sTag))
{
i.Tags.Add(sTag);
}
}
var RegionID = new Guid(j["RegionID"].Value<string>());
if (RegionID != Guid.Empty)
{
string sTag = string.Empty;
if (tagLists["Region"].TryGetValue(RegionID, out sTag))
{
i.Tags.Add(sTag);
}
}
var DispatchZoneID = new Guid(j["DispatchZoneID"].Value<string>());
if (DispatchZoneID != Guid.Empty)
{
string sTag = string.Empty;
if (tagLists["DispatchZone"].TryGetValue(DispatchZoneID, out sTag))
{
i.Tags.Add(sTag);
}
}
//User options
i.UserOptions = new UserOptions();
//TimeZone Offset
//NOT IMPORTED / SUPPORTED
//Email address
i.UserOptions.EmailAddress = j["EmailAddress"].Value<string>();
//UI colour
//TODO: this needs to be tested now that it's a web friendly value in the dbdump file
/*
Hexadecimal notation: #RGB[A]
R (red), G (green), B (blue), and A (alpha) are hexadecimal characters (09, AF). A is optional. The three-digit notation (#RGB) is a shorter version of the six-digit form (#RRGGBB). For example, #f09 is the same color as #ff0099. Likewise, the four-digit RGB notation (#RGBA) is a shorter version of the eight-digit form (#RRGGBBAA). For example, #0f38 is the same color as #00ff3388.
*/
// This is untested as of now, but should work, maybe if it doesn't the selector needs to be combined with dot notation instead of two array notations?
i.UserOptions.UiColor = j["jextra"]["hexaScheduleBackColor"].Value<string>();
//Set unusable random login credentials
i.Salt = Hasher.GenerateSalt();
i.Login = Hasher.GenerateSalt();
i.Password = Hasher.hash(i.Salt, Hasher.GenerateSalt());
//No rights
i.Roles = AuthorizationRoles.NoRole;
//temporary translation id to satisfy db settings
i.UserOptions.TranslationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID;
User o = await CreateAsync(i);
if (HasErrors)
{
//If there are any validation errors, log in joblog and move on
await JobsBiz.LogJobAsync(jobId, $" -> import object \"{i.Name}\" source id {V7Id.ToString()} failed validation and was not imported: {GetErrorsAsString()} ", ct);
//This is a fundamental problem with the import as users are required for many things so bomb out entirely
//other things might be able to work around but this is too serious
throw new System.SystemException("UserBiz::ImportV7Async - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE WITHOUT ALL USERS BEING IMPORTED, SEE JOB ERROR LOG FOR DETAILS");
// return false;
}
else
{
await ct.SaveChangesAsync();
var mapItem = new ImportAyaNova7MapItem(V7Id, BizType, o.Id);
importMap.Add(mapItem);
}
#endregion
}
break;
case "eventlog":
{
await ImportAyaNova7Biz.LogEventCreatedModifiedEventsAsync(j, importMap, BizType, ct);
}
break;
case "translation":
{
#region set translation
//get the userId
//----
var V7Id = new Guid(j["ID"].Value<string>());
var MapItem = importMap.Where(m => m.V7ObjectId == V7Id).FirstOrDefault();
if (MapItem == null)
{
throw new System.Exception("UserBiz::ImportV7Async-translation - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE USER NOT FOUND IN IMPORTMAP");
}
var NewId = MapItem.NewObjectAyaTypeId.ObjectId;
User u = ct.User.Where(m => m.Id == NewId).FirstOrDefault();
if (u == null)
{
throw new System.Exception("UserBiz::ImportV7Async-translation - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE USER NOT FOUND IN DATABASE");
}
//handle translation entries for users now that we have the Translations created
var V7Translation = j["DefaultLanguage"].Value<string>();
//Get new translation name
var NewTranslationName = string.Empty;
switch (V7Translation)
{
case "Français":
NewTranslationName = "fr";
break;
case "Español":
NewTranslationName = "es";
break;
case "Deutsch":
NewTranslationName = "de";
break;
case "English":
NewTranslationName = "en";
break;
default:
{
//It's a custom translation, translate it from v7 original format to imported name format
//make lower and replace spaces with dashes
NewTranslationName = V7Translation.ToLowerInvariant().Replace(" ", "-");
//ensure each character is a valid path character
foreach (char c in System.IO.Path.GetInvalidFileNameChars())//is this kosher on linux? Original code was windows
{
NewTranslationName = NewTranslationName.Replace(c, '_');
}
}
break;
}
u.UserOptions.TranslationId = await TranslationBiz.TranslationNameToIdStaticAsync(NewTranslationName, ct);
ct.SaveChanges();
#endregion set translation
}
break;
case "clientid":
{
var V7Id = new Guid(j["ID"].Value<string>());
//handle setting client id for user client login
//throw new System.NotImplementedException();
}
break;
case "headofficeid":
{
var V7Id = new Guid(j["ID"].Value<string>());
//handle setting ho id for user headoffice login
//throw new System.NotImplementedException();
}
break;
}
//this is the equivalent of returning void for a Task signature with nothing to return
return true;
}
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons