using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; using System.Collections.Generic; using Newtonsoft.Json.Linq; namespace AyaNova.Biz { internal class TranslationBiz : BizObject { internal TranslationBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles userRoles) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = userRoles; BizType = AyaType.Translation; } internal static TranslationBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) { if (httpContext != null) return new TranslationBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); else return new TranslationBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.Translation.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PutAsync(Translation putObject) { Translation dbObject = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == putObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } //No tags and no validation of prior state required so no snapshot required CopyObject.Copy(putObject, dbObject, "Id");//note: won't update the child collection has to be done independently foreach (TranslationItem ti in putObject.TranslationItems) { dbObject.TranslationItems.Where(z => z.Id == ti.Id).First().Display = ti.Display; } ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; await ValidateAsync(dbObject); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); return dbObject; } // internal async Task PutTranslationItemDisplayTextAsync(TranslationItem dbObj, NewTextIdConcurrencyTokenItem inObj, Translation dbParent) // { // if (dbParent.Stock == true) // { // AddError(ApiErrorCode.INVALID_OPERATION, "object", "TranslationItem is from a Stock translation and cannot be modified"); // return false; // } // //Replace the db object with the PUT object // //CopyObject.Copy(inObj, dbObj, "Id"); // dbObj.Display = inObj.NewText; // //Set "original" value of concurrency token to input token // //this will allow EF to check it out // ct.Entry(dbObj).OriginalValues["Concurrency"] = inObj.Concurrency; // //Only thing to validate is if it has data at all in it // if (string.IsNullOrWhiteSpace(inObj.NewText)) // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Display (NewText)"); // if (HasErrors) // return false; // await ct.SaveChangesAsync(); // //Log // await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbParent.Id, AyaType.Translation, AyaEvent.Modified), ct); // return true; // } internal async Task PutTranslationItemsDisplayTextAsync(List inObj, Translation dbParent) { if (dbParent.Stock == true) { AddError(ApiErrorCode.INVALID_OPERATION, "object", "TranslationItem is from a Stock translation and cannot be modified"); return false; } foreach (NewTextIdConcurrencyTokenItem tit in inObj) { var titem = await ct.TranslationItem.SingleOrDefaultAsync(z => z.Id == tit.Id); if (titem == null) { AddError(ApiErrorCode.NOT_FOUND, $"Translation item ID {tit.Id}"); return false; } //Replace the db object with the PUT object //CopyObject.Copy(inObj, dbObj, "Id"); titem.Display = tit.NewText; //Set "original" value of concurrency token to input token //this will allow EF to check it out ct.Entry(titem).OriginalValues["Concurrency"] = tit.Concurrency; //Only thing to validate is if it has data at all in it if (string.IsNullOrWhiteSpace(tit.NewText)) AddError(ApiErrorCode.VALIDATION_REQUIRED, $"Display (NewText) for Id: {tit.Id}"); } if (HasErrors) return false; await ct.SaveChangesAsync(); //Log await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbParent.Id, AyaType.Translation, AyaEvent.Modified), ct); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //DUPLICATE // internal async Task DuplicateAsync(long id) { Translation dbObject = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } Translation newObject = new Translation(); //CopyObject.Copy(dbObject, newObject, "Id, Salt, Login, Password, CurrentAuthToken, DlKey, DlKeyExpire, Wiki, Serial"); string newUniqueName = string.Empty; bool NotUnique = true; long l = 1; do { newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObject.Name, l++, 255); NotUnique = await ct.Translation.AnyAsync(z => z.Name == newUniqueName); } while (NotUnique); newObject.Name = newUniqueName; newObject.Stock = false; newObject.CjkIndex = false; foreach (TranslationItem i in dbObject.TranslationItems) { newObject.TranslationItems.Add(new TranslationItem() { Key = i.Key, Display = i.Display }); } newObject.Id = 0; newObject.Concurrency = 0; await ct.Translation.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); // await SearchIndexAsync(newObject, true); // await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); return newObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //IMPORT // internal async Task ImportAsync(JObject o) { Translation t = new Translation(); var proposedName = (string)o["Name"]; string newUniqueName = proposedName; bool NotUnique = true; long l = 1; do { NotUnique = await ct.Translation.AnyAsync(z => z.Name == newUniqueName); if (NotUnique) newUniqueName = Util.StringUtil.UniqueNameBuilder(proposedName, l++, 255); } while (NotUnique); t.Name = newUniqueName; t.CjkIndex = (bool)o["CjkIndex"]; t.Stock = false; Translation sample = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == 1); int ExpectedKeyCount = sample.TranslationItems.Count(); JArray tItems = (JArray)o["TranslationItems"]; if (tItems.Count() < ExpectedKeyCount) { AddError(ApiErrorCode.VALIDATION_FAILED, null, $"TranslationItems incomplete, expected {ExpectedKeyCount} but found {tItems.Count()}"); return false; } foreach (JObject j in tItems) { var key = (string)j["Key"]; var display = (string)j["Display"]; if (null == sample.TranslationItems.Where(z => z.Key == key).FirstOrDefault()) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, null, $"TranslationItems key {key} is not valid"); return false; } t.TranslationItems.Add(new TranslationItem { Key = key, Display = display }); } await ct.Translation.AddAsync(t); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, t.Id, BizType, AyaEvent.Created), ct); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// /// GET //Get entire translation 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 return await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == fetchId); } //get list (simple non-paged) internal async Task> GetTranslationListAsync() { List l = new List(); l = await ct.Translation .AsNoTracking() .OrderBy(z => z.Name) .Select(z => new NameIdItem() { Id = z.Id, Name = z.Name }).ToListAsync(); return l; } #if (DEBUG) internal async Task TranslationKeyCoverageAsync() { AyaNova.Api.Controllers.TranslationController.TranslationCoverageInfo L = new AyaNova.Api.Controllers.TranslationController.TranslationCoverageInfo(); L.RequestedKeys = ServerBootConfig.TranslationKeysRequested; L.RequestedKeys.Sort(); var AllKeys = await GetKeyListAsync(); foreach (string StockKey in AllKeys) { if (!L.RequestedKeys.Contains(StockKey)) { L.NotRequestedKeys.Add(StockKey); } } L.NotRequestedKeys.Sort(); L.RequestedKeyCount = L.RequestedKeys.Count; L.NotRequestedKeyCount = L.NotRequestedKeys.Count; return L; } //Track requests for keys so we can determine which are being used and which are not //TODO: Ideally this should be paired with tests that either directly request each key that are def. being used //or the UI needs to be tested in a way that triggers every key to be used even errors etc internal static void TrackRequestedKey(string key) { if (!ServerBootConfig.TranslationKeysRequested.Contains(key)) ServerBootConfig.TranslationKeysRequested.Add(key); } internal static void TrackRequestedKey(List keys) { foreach (string Key in keys) { if (!ServerBootConfig.TranslationKeysRequested.Contains(Key)) ServerBootConfig.TranslationKeysRequested.Add(Key); } } #endif ///////////////////////////////////////////////////////////////// // Get subset for currently logged in user's translation id // Standard used by translationcontroller for logged in user // // internal async Task>> GetSubsetAsync(List param) { #if (DEBUG) TrackRequestedKey(param); #endif var ret = await ct.TranslationItem.Where(z => z.TranslationId == UserTranslationId && param.Contains(z.Key)).AsNoTracking().ToDictionaryAsync(z => z.Key, z => z.Display); return ret.ToList(); } ///////////////////////////////////////////////////////////////// // Get subset for specified translation ID // called from controller and Used when user is not logged in // e.g. when resetting their password // ## NOTE: NO other use for this other than the reset password at this point internal static async Task>> GetSpecifiedTranslationSubsetStaticAsync(List param, long translationId) { #if (DEBUG) TrackRequestedKey(param); #endif using (AyContext ct = ServiceProviderProvider.DBContext) { if (!await ct.Translation.AnyAsync(e => e.Id == translationId)) translationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID; var ret = await ct.TranslationItem.Where(z => z.TranslationId == translationId && param.Contains(z.Key)).AsNoTracking().ToDictionaryAsync(z => z.Key, z => z.Display); return ret.ToList(); } } ///////////////////////////////////////////////////////////////// // Get subset for specified translation ID statically // called from internal code differs from // GetSpecifiedTranslationSubsetStaticAsync above only in return signature // and used for internal classes to call // internal static async Task> GetSubsetStaticAsync(List param, long translationId) { #if (DEBUG) TrackRequestedKey(param); #endif using (AyContext ct = ServiceProviderProvider.DBContext) { if (!await ct.Translation.AnyAsync(e => e.Id == translationId)) translationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID; var ret = await ct.TranslationItem.Where(z => z.TranslationId == translationId && param.Contains(z.Key)).AsNoTracking().ToDictionaryAsync(z => z.Key, z => z.Display); return ret; } } ///////////////////////////////////////////////////////////////// // Get subset for specified user (looks up translation id) statically // called from internal code (e.g. notification processing) // internal static async Task> GetSubsetForUserStaticAsync(List param, long userId) { long translationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID; using (AyContext ct = ServiceProviderProvider.DBContext) { translationId = await ct.UserOptions.AsNoTracking().Where(z => z.UserId == userId).Select(z => z.TranslationId).SingleAsync(); } return await GetSubsetStaticAsync(param, translationId); } ///////////////////////////////////////////////////////////////// // Get single item for specified user (looks up translation id) statically // called from internal code (e.g. notification processing) // internal static async Task GetTranslationForUserStaticAsync(string translationKey, long userId) { long translationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID; using (AyContext ct = ServiceProviderProvider.DBContext) { translationId = await ct.UserOptions.AsNoTracking().Where(z => z.UserId == userId).Select(z => z.TranslationId).SingleAsync(); } var param = new List() { translationKey }; var ret = await GetSubsetStaticAsync(param, translationId); if (ret.Count > 0) return ret[translationKey]; return $"??{translationKey}"; } //used by internal notification and other processes i.e. "Server" in all languages for server based notifications internal static async Task> GetAllTranslationsForKey(string translationKey) { #if (DEBUG) TrackRequestedKey(translationKey); #endif using (AyContext ct = ServiceProviderProvider.DBContext) { return await ct.TranslationItem.Where(z => z.Key == translationKey).AsNoTracking().ToDictionaryAsync(z => z.TranslationId, z => z.Display); } } //Get the CJKIndex value for the translation specified internal static async Task GetCJKIndexAsync(long translationId, AyContext ct) { var ret = await ct.Translation.Where(z => z.Id == translationId).AsNoTracking().Select(z => z.CjkIndex).SingleOrDefaultAsync(); return ret; } //DEPRECATED // /// // /// Get the value of the key provided in the default translation chosen // /// // /// // /// // internal static async Task GetDefaultTranslationAsync(string key) // { // if (string.IsNullOrWhiteSpace(key)) // return "ERROR: GetDefaultTranslation NO KEY VALUE SPECIFIED"; // #if (DEBUG) // TrackRequestedKey(key); // #endif // using (AyContext ct = ServiceProviderProvider.DBContext) // return await ct.TranslationItem.Where(z => z.TranslationId == ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID && z.Key == key).Select(z => z.Display).AsNoTracking().FirstOrDefaultAsync(); // } // //Get all stock keys that are valid (used for key coverage reporting) // internal static async Task> GetKeyListAsync() // { // using (AyContext ct = ServiceProviderProvider.DBContext) // return await ct.TranslationItem.Where(z => z.TranslationId == 1).OrderBy(z => z.Key).Select(z => z.Key).AsNoTracking().ToListAsync(); // } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task DeleteAsync(Translation dbObject) { //Determine if the object can be deleted, do the deletion tentatively await ValidateCanDeleteAsync(dbObject); if (HasErrors) return false; ct.Translation.Remove(dbObject); await ct.SaveChangesAsync(); //Log await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.Translation, dbObject.Id, dbObject.Name, ct); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private async Task ValidateAsync(Translation proposedObj) { //run validation and biz rules //Name required if (string.IsNullOrWhiteSpace(proposedObj.Name)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); //Name must be unique if (await ct.Translation.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id)) AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); //Ensure there are no empty keys or too long ones //fixing them up here rather than at the client as it's a bit of fuckery //to try to validate or fix an item edited inside a data table with vuetify //rather than try to deal with that just fix it here foreach (var item in proposedObj.TranslationItems.Where(z => z.Display.Length < 1)) { item.Display = item.Key; } foreach (var item in proposedObj.TranslationItems.Where(z => z.Display.Length > 255)) { item.Display = item.Display.Substring(0, 255); } return; } //Can delete? private async Task ValidateCanDeleteAsync(Translation inObj) { //Decided to short circuit these; if there is one issue then return immediately (fail fast rule) //Ensure it's not a stock translation if (inObj.Stock == true) { AddError(ApiErrorCode.INVALID_OPERATION, null, "Translation is a Stock translation and cannot be deleted"); return; } if (inObj.Id == ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID) { AddError(ApiErrorCode.INVALID_OPERATION, null, "Translation is set as the default server translation (AYANOVA_DEFAULT_LANGUAGE_ID) and can not be deleted"); return; } //See if any users exist with this translation selected in which case it's not deleteable if (await ct.UserOptions.AnyAsync(e => e.TranslationId == inObj.Id)) { AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, null, "Can't be deleted in use by one or more Users"); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// //UTILITIES // public async Task TranslationNameToIdAsync(string translationName) { var v = await ct.Translation.AsNoTracking().FirstOrDefaultAsync(z => z.Name == translationName); if (v == null) return 0; return v.Id; } public static async Task TranslationNameToIdStaticAsync(string translationName) { using (AyContext ct = ServiceProviderProvider.DBContext) { var v = await ct.Translation.AsNoTracking().FirstOrDefaultAsync(z => z.Name == translationName); if (v == null) return ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID; return v.Id; } } public async Task TranslationExistsAsync(string translationName) { return await ct.Translation.AnyAsync(z => z.Name == translationName); } public async Task TranslationExistsAsync(long id) { return await ct.Translation.AnyAsync(z => z.Id == id); } //this is only called by Search.cs to cache a local cjk and stopwords, no one else calls it currently public static async Task ReturnSpecifiedTranslationIdIfExistsOrDefaultTranslationId(long id, AyContext ct) { if (!await ct.Translation.AnyAsync(z => z.Id == id)) return ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID; return id; } public async Task TranslationItemExistsAsync(long id) { return await ct.TranslationItem.AnyAsync(z => z.Id == id); } /// /// Ensure stock Translations and setup defaults /// Called by boot preflight check code AFTER it has already ensured the translation is a two letter code if stock one was chosen /// public async Task ValidateTranslationsAsync() { //Ensure default translations are present and that there is a server default translation that exists if (!await TranslationExistsAsync("en")) { throw new System.Exception($"E1015: stock translation English (en) not found in database!"); } if (!await TranslationExistsAsync("es")) { throw new System.Exception($"E1015: stock translation Spanish (es) not found in database!"); } if (!await TranslationExistsAsync("de")) { throw new System.Exception($"E1015: stock translation German (de) not found in database!"); } if (!await TranslationExistsAsync("fr")) { throw new System.Exception($"E1015: stock translation French (fr) not found in database!"); } //Ensure chosen default translation exists switch (ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION) { case "en": case "es": case "de": case "fr": break; default: if (!await TranslationExistsAsync(ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION)) { throw new System.Exception($"E1015: stock translation {ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION} not found in database!"); } break; } //Put the default translation ID number into the ServerBootConfig for later use ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID = await TranslationNameToIdAsync(ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION); } ///////////////////////////////////////////////////////////////////// }//eoc // }//eons