diff --git a/.vscode/launch.json b/.vscode/launch.json index 33155498..396abc9b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -45,7 +45,7 @@ //"AYANOVA_LOG_LEVEL": "Debug", "AYANOVA_DEFAULT_TRANSLATION": "en", //TRANSLATION MUST BE en for Integration TESTING - "AYANOVA_PERMANENTLY_ERASE_DATABASE": "true", + //"AYANOVA_PERMANENTLY_ERASE_DATABASE": "true", "AYANOVA_DB_CONNECTION": "Server=localhost;Username=postgres;Password=raven;Database=AyaNova;", "AYANOVA_USE_URLS": "http://*:7575;", "AYANOVA_FOLDER_USER_FILES": "c:\\temp\\RavenTestData\\userfiles", diff --git a/server/AyaNova/Controllers/TranslationController.cs b/server/AyaNova/Controllers/TranslationController.cs index f52682fb..f8f7cc2b 100644 --- a/server/AyaNova/Controllers/TranslationController.cs +++ b/server/AyaNova/Controllers/TranslationController.cs @@ -48,7 +48,7 @@ namespace AyaNova.Api.Controllers serverState = apiServerState; } - + /// @@ -84,23 +84,52 @@ namespace AyaNova.Api.Controllers /// - /// Get Translations list - /// - /// List in alphabetical order of all Translations - [HttpGet("list")] - public async Task TranslationList() + /// Put (update) Translation + /// + /// + /// + /// + [HttpPut] + public async Task PutTranslation([FromBody] Translation updatedObject) { - if (serverState.IsClosed) + if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - - //Instantiate the business object handler + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext); - - var l = await biz.GetTranslationListAsync(); - return Ok(ApiOkResponse.Response(l)); + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + var o = await biz.PutAsync(updatedObject);//In future may need to return entire object, for now just concurrency token + if (o == null) + { + if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT)) + return StatusCode(409, new ApiErrorResponse(biz.Errors)); + else + return BadRequest(new ApiErrorResponse(biz.Errors)); + } + return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency })); ; } + + // /// + // /// Get Translations list + // /// + // /// List in alphabetical order of all Translations + // [HttpGet("list")] + // public async Task TranslationList() + // { + // if (serverState.IsClosed) + // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + + // //Instantiate the business object handler + // TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext); + + // var l = await biz.GetTranslationListAsync(); + // return Ok(ApiOkResponse.Response(l)); + // } + + #if (DEBUG) /// /// Get a coverage report of translation keys used versus unused @@ -146,222 +175,32 @@ namespace AyaNova.Api.Controllers } + /// - /// Duplicates an existing translation with a new name + /// Duplicate /// - /// NameIdItem object containing source translation Id and new name + /// Source object id /// From route path - /// Error response or newly created translation - [HttpPost("duplicate")] - public async Task Duplicate([FromBody] NameIdItem inObj, ApiVersion apiVersion) + /// Duplicate + [HttpPost("duplicate/{id}")] + public async Task DuplicateTranslation([FromRoute] long id, ApiVersion apiVersion) { - if (serverState.IsClosed) + if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - - //Instantiate the business object handler TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext); - - var o = await biz.DuplicateAsync(inObj); - + if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + Translation o = await biz.DuplicateAsync(id); if (o == null) - { - //error return return BadRequest(new ApiErrorResponse(biz.Errors)); - } else - { return CreatedAtAction(nameof(TranslationController.GetTranslation), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); - } } - /// - /// Put (UpdateTranslationItemDisplayText) - /// Update a single key with new display text - /// - /// NewText/Id/Concurrency token object. NewText is new display text, Id is TranslationItem Id, concurrency token is required - /// - [HttpPut("updatetranslationitemdisplaytext")] - public async Task PutTranslationItemDisplayText([FromBody] NewTextIdConcurrencyTokenItem inObj) - { - if (serverState.IsClosed) - return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - - if (!ModelState.IsValid) - { - return BadRequest(new ApiErrorResponse(ModelState)); - } - - var oFromDb = await ct.TranslationItem.SingleOrDefaultAsync(z => z.Id == inObj.Id); - - if (oFromDb == null) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - //Now fetch translation for rights and to ensure not stock - var oDbParent = await ct.Translation.SingleOrDefaultAsync(z => z.Id == oFromDb.TranslationId); - if (oDbParent == null) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Translation)) - { - return StatusCode(403, new ApiNotAuthorizedResponse()); - } - - //Instantiate the business object handler - TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext); - - try - { - if (!await biz.PutTranslationItemDisplayTextAsync(oFromDb, inObj, oDbParent)) - { - return BadRequest(new ApiErrorResponse(biz.Errors)); - } - } - catch (DbUpdateConcurrencyException) - { - if (!await biz.TranslationItemExistsAsync(inObj.Id)) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - else - { - //exists but was changed by another user - //I considered returning new and old record, but where would it end? - //Better to let the client decide what to do than to send extra data that is not required - return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); - } - } - - - return Ok(ApiOkResponse.Response(new { Concurrency = oFromDb.Concurrency })); - } - - - - /// - /// Put (UpdateTranslationItemDisplayText) - /// Update a list of items with new display text - /// - /// Array of NewText/Id/Concurrency token objects. NewText is new display text, Id is TranslationItem Id, concurrency token is required - /// - [HttpPut("updatetranslationitemsdisplaytext")] - public async Task PutTranslationItemsDisplayText([FromBody] List inObj) - { - if (serverState.IsClosed) - return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - - if (!ModelState.IsValid) - { - return BadRequest(new ApiErrorResponse(ModelState)); - } - - var oFromDb = await ct.TranslationItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == inObj[0].Id); - - if (oFromDb == null) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - //Now fetch translation for rights and to ensure not stock - var oDbParent = await ct.Translation.SingleOrDefaultAsync(z => z.Id == oFromDb.TranslationId); - if (oDbParent == null) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Translation)) - { - return StatusCode(403, new ApiNotAuthorizedResponse()); - } - - //Instantiate the business object handler - TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext); - - try - { - if (!await biz.PutTranslationItemsDisplayTextAsync(inObj, oDbParent)) - { - return BadRequest(new ApiErrorResponse(biz.Errors)); - } - } - catch (DbUpdateConcurrencyException) - { - - //exists but was changed by another user - //I considered returning new and old record, but where would it end? - //Better to let the client decide what to do than to send extra data that is not required - return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); - } - return NoContent(); - } - - - - /// - /// Put (UpdateTranslationName) - /// Update a translation to change the name (non-stock Translations only) - /// - /// NewText/Id/Concurrency token object. NewText is new translation name, Id is Translation Id, concurrency token is required - /// - [HttpPut("updatetranslationname")] - public async Task PutTranslationName([FromBody] NewTextIdConcurrencyTokenItem inObj) - { - if (serverState.IsClosed) - return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - - if (!ModelState.IsValid) - { - return BadRequest(new ApiErrorResponse(ModelState)); - } - - var oFromDb = await ct.Translation.SingleOrDefaultAsync(z => z.Id == inObj.Id); - - if (oFromDb == null) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - - if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Translation)) - { - return StatusCode(403, new ApiNotAuthorizedResponse()); - } - - //Instantiate the business object handler - TranslationBiz biz = TranslationBiz.GetBiz(ct, HttpContext); - - try - { - if (!await biz.PutTranslationNameAsync(oFromDb, inObj)) - { - return BadRequest(new ApiErrorResponse(biz.Errors)); - } - - } - catch (DbUpdateConcurrencyException) - { - if (!await biz.TranslationExistsAsync(inObj.Id)) - { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - else - { - //exists but was changed by another user - //I considered returning new and old record, but where would it end? - //Better to let the client decide what to do than to send extra data that is not required - return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT)); - } - } - - return Ok(ApiOkResponse.Response(new { Concurrency = oFromDb.Concurrency })); - } - - /// /// Delete Translation /// diff --git a/server/AyaNova/biz/TranslationBiz.cs b/server/AyaNova/biz/TranslationBiz.cs index d9390021..7a1d3948 100644 --- a/server/AyaNova/biz/TranslationBiz.cs +++ b/server/AyaNova/biz/TranslationBiz.cs @@ -32,50 +32,95 @@ namespace AyaNova.Biz 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); + } //////////////////////////////////////////////////////////////////////////////////////////////// - //DUPLICATE - only way to create a new translation - // - internal async Task DuplicateAsync(NameIdItem inObj) + //UPDATE + // + internal async Task PutAsync(Translation putObject) { - - //make sure sourceid exists - if (!await TranslationExistsAsync(inObj.Id)) - AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Id", "Source translation id does not exist"); - - //Ensure name is unique and not too long and not empty - await ValidateAsync(inObj.Name, true); - - if (HasErrors) - return null; - - //fetch the existing translation for duplication - var SourceTranslation = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == inObj.Id); - - //replicate the source to a new dest and save - Translation NewTranslation = new Translation(); - NewTranslation.Name = inObj.Name; - - NewTranslation.Stock = false; - NewTranslation.CjkIndex = false; - foreach (TranslationItem i in SourceTranslation.TranslationItems) + Translation dbObject = await ct.Translation.SingleOrDefaultAsync(z => z.Id == putObject.Id); + if (dbObject == null) { - NewTranslation.TranslationItems.Add(new TranslationItem() { Key = i.Key, Display = i.Display }); + AddError(ApiErrorCode.NOT_FOUND, "id"); + return null; + } + Translation SnapshotOfOriginalDBObj = new Translation(); + CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); + CopyObject.Copy(putObject, dbObject, "Id"); + + ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; + //maybe validate that there are no empty values? + await ValidateAsync(dbObject, SnapshotOfOriginalDBObj); + 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; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DUPLICATE + // + internal async Task DuplicateAsync(long id) + { + Translation dbObject = await ct.Translation.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 }); } - //Add it to the context so the controller can save it - await ct.Translation.AddAsync(NewTranslation); + newObject.Id = 0; + newObject.Concurrency = 0; + await ct.Translation.AddAsync(newObject); await ct.SaveChangesAsync(); - //Log - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, NewTranslation.Id, AyaType.Translation, AyaEvent.Created), ct); - return NewTranslation; - + 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; } + + //////////////////////////////////////////////////////////////////////////////////////////////// /// GET @@ -206,106 +251,6 @@ namespace AyaNova.Biz } - //////////////////////////////////////////////////////////////////////////////////////////////// - //UPDATE - // - - - 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; - } - - - internal async Task PutTranslationNameAsync(Translation dbObj, NewTextIdConcurrencyTokenItem inObj) - { - if (dbObj.Stock == true) - { - AddError(ApiErrorCode.INVALID_OPERATION, "object", "Translation is a Stock translation and cannot be modified"); - return false; - } - - dbObj.Name = 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; - - await ValidateAsync(dbObj.Name, false); - - if (HasErrors) - return false; - - await ct.SaveChangesAsync(); - //Log - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, AyaType.Translation, AyaEvent.Modified), ct); - - return true; - } @@ -333,22 +278,28 @@ namespace AyaNova.Biz // //Can save or update? - private async Task ValidateAsync(string inObjName, bool isNew) + private async Task ValidateAsync(Translation proposedObj, Translation currentObj) { //run validation and biz rules //Name required - if (string.IsNullOrWhiteSpace(inObjName)) + if (string.IsNullOrWhiteSpace(proposedObj.Name)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); //Name must be less than 255 characters - if (inObjName.Length > 255) + if (proposedObj.Name.Length > 255) AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "Name", "255 char max"); //Name must be unique - if (await ct.Translation.AnyAsync(z => z.Name == inObjName)) + if (await ct.Translation.AnyAsync(z => z.Name == proposedObj.Name)) AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); + //Ensure there are no empty keys + if (proposedObj.TranslationItems.Where(z => z.Display.Length < 1).Any()) + { + AddError(ApiErrorCode.VALIDATION_REQUIRED, "Display", "One or more items are missing a display value"); + } + return; }